New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
@@ -0,0 +1,421 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _MessageBroker_gameId, _MessageBroker_userId, _MessageBroker_source, _MessageBroker_connectUrl, _MessageBroker_getMessagesUrl, _MessageBroker_evt, _MessageBroker_queue, _MessageBroker_ws, _MessageBroker_getShortTermToken, _MessageBroker_isAwaitingStt, _MessageBroker_isAwaitingMessages, _MessageBroker_stt, _MessageBroker_pingId, _MessageBroker_oldest, _MessageBroker_newest, _MessageBroker_posted, _MessageBroker_hasMoreMessages;
import { Evt } from 'evt';
import { v4 as uuid } from 'uuid';
import { byDateTime, href, isClosed, isClosing, isConnecting, isOpen, isValidGameId, toKey, ws, } from './helpers';
const MAX_HISTORY_CATCHUP_PAGES = 10;
const FIVE_MINUTES = 1000 * // milliseconds
60 * // seconds
5; // minutes
export var MessageBrokerStatus;
(function (MessageBrokerStatus) {
MessageBrokerStatus["Disconnected"] = "disconnected";
MessageBrokerStatus["Connecting"] = "connecting";
MessageBrokerStatus["Open"] = "open";
MessageBrokerStatus["Closing"] = "closing";
MessageBrokerStatus["Closed"] = "closed";
})(MessageBrokerStatus || (MessageBrokerStatus = {}));
const MESSAGE_BROKER_RESET_REASON = '@dndbeyond/message-broker-lib#reset';
/**
* this class makes use of the (at time of writing) stage 3 class fields
* proposal, specifically private fields to make internal state truly invisible
* to consumers
*
* @see https://github.com/tc39/proposal-class-fields#private-fields
*/
export class MessageBroker {
get gameId() {
return __classPrivateFieldGet(this, _MessageBroker_gameId, "f");
}
set gameId(nextGameId) {
if (__classPrivateFieldGet(this, _MessageBroker_gameId, "f") === nextGameId) {
return;
}
__classPrivateFieldSet(this, _MessageBroker_gameId, nextGameId, "f");
this.reset();
__classPrivateFieldGet(this, _MessageBroker_evt, "f").post(this.composeMessage({
eventType: 'replace/messageBroker/gameId',
}));
if (isValidGameId(__classPrivateFieldGet(this, _MessageBroker_gameId, "f")) && __classPrivateFieldGet(this, _MessageBroker_evt, "f").getHandlers().length) {
// if the new game id is valid and we've already got some subscribers
// listening, we should attempt to reconnect immediately
this.connect();
}
}
get userId() {
return __classPrivateFieldGet(this, _MessageBroker_userId, "f");
}
set userId(nextUserId) {
if (__classPrivateFieldGet(this, _MessageBroker_userId, "f") === nextUserId) {
return;
}
__classPrivateFieldSet(this, _MessageBroker_userId, nextUserId, "f");
this.reset();
}
get source() {
return __classPrivateFieldGet(this, _MessageBroker_source, "f");
}
get connectUrl() {
return __classPrivateFieldGet(this, _MessageBroker_connectUrl, "f");
}
get getMessagesUrl() {
return __classPrivateFieldGet(this, _MessageBroker_getMessagesUrl, "f");
}
get status() {
if (__classPrivateFieldGet(this, _MessageBroker_ws, "f") === undefined) {
return MessageBrokerStatus.Disconnected;
}
if (isConnecting(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
return MessageBrokerStatus.Connecting;
}
if (isOpen(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
return MessageBrokerStatus.Open;
}
if (isClosing(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
return MessageBrokerStatus.Closing;
}
if (isClosed(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
return MessageBrokerStatus.Closed;
}
/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
*/
throw new Error(`Unexpected web socket \`readyState\` (${__classPrivateFieldGet(this, _MessageBroker_ws, "f")}) exists but is not 'connecting', 'open', 'closing', or 'closed'`);
}
constructor({ gameId, userId, source, connectUrl, getMessagesUrl, getShortTermToken, }) {
_MessageBroker_gameId.set(this, void 0);
_MessageBroker_userId.set(this, void 0);
_MessageBroker_source.set(this, void 0);
_MessageBroker_connectUrl.set(this, void 0);
_MessageBroker_getMessagesUrl.set(this, void 0);
/**
* we use an Evt ("EventEmitter's typesafe replacement") instance to stream
* messages to subscribers
*
* @see https://docs.evt.land/
*/
_MessageBroker_evt.set(this, Evt.create());
_MessageBroker_queue.set(this, []);
_MessageBroker_ws.set(this, void 0);
_MessageBroker_getShortTermToken.set(this, void 0);
_MessageBroker_isAwaitingStt.set(this, false);
_MessageBroker_isAwaitingMessages.set(this, false);
_MessageBroker_stt.set(this, '');
_MessageBroker_pingId.set(this, -1);
/**
* `#oldest` and `#newest` are strings in the key format used by the lambdas,
* that is `"<message.dateTime>-<message.eventType>-<message.userId>"`
*
* @see @dndbeyond/game-log-lambdas/src/interfaces/MessageLastEvaluatedKey.ts
* @see ./helpers.ts#toKey
*/
_MessageBroker_oldest.set(this, void 0);
_MessageBroker_newest.set(this, void 0);
_MessageBroker_posted.set(this, new Set());
_MessageBroker_hasMoreMessages.set(this, true);
__classPrivateFieldSet(this, _MessageBroker_gameId, gameId, "f");
__classPrivateFieldSet(this, _MessageBroker_userId, userId, "f");
__classPrivateFieldSet(this, _MessageBroker_source, source, "f");
__classPrivateFieldSet(this, _MessageBroker_connectUrl, connectUrl, "f");
__classPrivateFieldSet(this, _MessageBroker_getMessagesUrl, getMessagesUrl, "f");
__classPrivateFieldSet(this, _MessageBroker_getShortTermToken, getShortTermToken, "f");
Evt.setDefaultMaxHandlers(50);
Evt.from(window, 'online').attach(() => this.connect());
Evt.from(window, 'offline').attach(() => this.reset());
Evt.from(window, 'beforeunload').attach(() => this.reset());
Evt.from(window, 'focus').attach(() => this.handleFocusReconnect());
Evt.from(document, 'visibilitychange').attach(() => this.handleVisibilityReconnect());
if (process.env.NODE_ENV === 'test') {
Object.defineProperties(this, {
ws: { get: () => __classPrivateFieldGet(this, _MessageBroker_ws, "f") },
queue: { get: () => __classPrivateFieldGet(this, _MessageBroker_queue, "f") },
});
}
}
subscribe(callback) {
const ctx = Evt.newCtx();
__classPrivateFieldGet(this, _MessageBroker_evt, "f").attach(ctx, callback);
this.connect();
/**
* Detach, from the `Evt` instances they are attached to, all Handlers bound
* to the context.
* @see https://docs.evt.land/api/ctx#ctx-done-result
*/
return () => ctx.done();
}
dispatch(partial) {
const serializedMessage = JSON.stringify(this.composeMessage(partial));
// post immediately to the Evt instance (message stream) for "optimistic
// updates" ... alerts local subscribers right away
this.post(JSON.parse(serializedMessage));
if (__classPrivateFieldGet(this, _MessageBroker_gameId, "f")) {
// queue the message for sending over the socket and try to connect, the
// queue is flushed when the socket opens, or in the last condition of the
// connect method
__classPrivateFieldGet(this, _MessageBroker_queue, "f").push(serializedMessage);
this.connect();
}
}
fetchHistory() {
return __awaiter(this, void 0, void 0, function* () {
if (!__classPrivateFieldGet(this, _MessageBroker_gameId, "f") || __classPrivateFieldGet(this, _MessageBroker_isAwaitingStt, "f") || __classPrivateFieldGet(this, _MessageBroker_isAwaitingMessages, "f")) {
// if we're already fetching an `stt` successive calls to `fetchHistory`
// should just bail early
return;
}
yield this.validateStt();
this.getMessages(__classPrivateFieldGet(this, _MessageBroker_oldest, "f")).then((messages) => messages.forEach((message) => this.post(message)));
});
}
composeMessage(partial) {
return Object.assign({ id: uuid(),
/**
* The static Date.now() method returns the number of milliseconds elapsed
* since January 1, 1970 00:00:00 UTC.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
*/
dateTime: `${Date.now()}`, gameId: this.gameId, userId: this.userId, source: this.source }, partial);
}
post(message) {
if (__classPrivateFieldGet(this, _MessageBroker_posted, "f").has(message.id)) {
// if we've already posted this message, don't repost it ... I guess I
// just realized there's kind of a weird edge case where a user looses
// network connectivity for more than a "pages' worth" of messages and
// thereby winds up with a gap in their message history
return;
}
__classPrivateFieldGet(this, _MessageBroker_posted, "f").add(message.id);
/**
* this works because `message.dateTime` is a stringified number of
* milliseconds since Unix epoch, and messages are keyed off this string
* that has that same `dateTime` as its prefix
*
* @see @dndbeyond/game-log-lambdas/src/interfaces/MessageLastEvaluatedKey.ts
*/
if (!__classPrivateFieldGet(this, _MessageBroker_oldest, "f") || __classPrivateFieldGet(this, _MessageBroker_oldest, "f") > message.dateTime) {
__classPrivateFieldSet(this, _MessageBroker_oldest, toKey(message), "f");
}
if (!__classPrivateFieldGet(this, _MessageBroker_newest, "f") || __classPrivateFieldGet(this, _MessageBroker_newest, "f") < message.dateTime) {
__classPrivateFieldSet(this, _MessageBroker_newest, toKey(message), "f");
}
__classPrivateFieldGet(this, _MessageBroker_evt, "f").post(message);
}
reset() {
if (__classPrivateFieldGet(this, _MessageBroker_ws, "f") && !isClosed(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
__classPrivateFieldGet(this, _MessageBroker_ws, "f").close(1000, MESSAGE_BROKER_RESET_REASON);
}
// drop any queued messages, this is a "full" reset
__classPrivateFieldGet(this, _MessageBroker_queue, "f").length = 0;
__classPrivateFieldSet(this, _MessageBroker_ws, undefined, "f");
// reset any notion of message history
__classPrivateFieldSet(this, _MessageBroker_oldest, undefined, "f");
__classPrivateFieldSet(this, _MessageBroker_newest, undefined, "f");
__classPrivateFieldGet(this, _MessageBroker_posted, "f").clear();
__classPrivateFieldSet(this, _MessageBroker_hasMoreMessages, true, "f");
}
connect() {
return __awaiter(this, void 0, void 0, function* () {
if (!isValidGameId(__classPrivateFieldGet(this, _MessageBroker_gameId, "f"))) {
return;
}
if (__classPrivateFieldGet(this, _MessageBroker_isAwaitingStt, "f")) {
// if we're already fetching an `stt` successive calls to `connect` should
// just bail early
return;
}
if (!__classPrivateFieldGet(this, _MessageBroker_ws, "f")) {
yield this.validateStt();
if (!__classPrivateFieldGet(this, _MessageBroker_stt, "f")) {
// stt validation failed, bail early / try again on next connect
return;
}
if (__classPrivateFieldGet(this, _MessageBroker_gameId, "f")) {
this.getMessages().then((messages) => messages.forEach((message) => this.post(message)));
}
// if we don't already have a socket instance create one now
__classPrivateFieldSet(this, _MessageBroker_ws, ws(__classPrivateFieldGet(this, _MessageBroker_connectUrl, "f"), Object.assign(Object.assign({}, (__classPrivateFieldGet(this, _MessageBroker_gameId, "f") !== undefined && { gameId: __classPrivateFieldGet(this, _MessageBroker_gameId, "f") })), { userId: __classPrivateFieldGet(this, _MessageBroker_userId, "f"), stt: __classPrivateFieldGet(this, _MessageBroker_stt, "f") })), "f");
// wire up "socket lifecycle" handlers, flush anything in the queue when
// the instance opens, reset if we loose the connection
Evt.from(__classPrivateFieldGet(this, _MessageBroker_ws, "f"), 'open').attachOnce(() => this.flushQueue());
Evt.merge([
Evt.from(__classPrivateFieldGet(this, _MessageBroker_ws, "f"), 'close'),
Evt.from(__classPrivateFieldGet(this, _MessageBroker_ws, "f"), 'error'),
]).attachOnce((event) => !(event instanceof CloseEvent &&
event.reason === MESSAGE_BROKER_RESET_REASON) && event.target === __classPrivateFieldGet(this, _MessageBroker_ws, "f"), () => {
this.reset();
this.connect();
});
// stream messages from the socket to the evt instance
Evt.from(__classPrivateFieldGet(this, _MessageBroker_ws, "f"), 'message').attach((event) => event.data !== 'pong', (event) => {
this.post(JSON.parse(event.data));
});
return;
}
// if we have a socket instance, but it's already closing or closed, just
// reset and try again (should hop back up to that first condition above)
if (isClosing(__classPrivateFieldGet(this, _MessageBroker_ws, "f")) || isClosed(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
this.reset();
this.connect();
return;
}
// it's already connecting just do nothing/wait
if (isConnecting(__classPrivateFieldGet(this, _MessageBroker_ws, "f"))) {
return;
}
// if we tried to connect, but we're already connected/open, flush the queue
if (isOpen(__classPrivateFieldGet(this, _MessageBroker_ws, "f")) && __classPrivateFieldGet(this, _MessageBroker_queue, "f").length) {
this.flushQueue();
}
});
}
validateStt() {
return __awaiter(this, void 0, void 0, function* () {
try {
__classPrivateFieldSet(this, _MessageBroker_isAwaitingStt, true, "f");
__classPrivateFieldSet(this, _MessageBroker_stt, yield __classPrivateFieldGet(this, _MessageBroker_getShortTermToken, "f").call(this), "f");
}
catch (error) {
if (process.env.NODE_ENV !== 'test') {
console.error('Failed to get short term token', error);
}
}
finally {
__classPrivateFieldSet(this, _MessageBroker_isAwaitingStt, false, "f");
}
});
}
getMessages(lastEvaluatedKey) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
if (!(isValidGameId(__classPrivateFieldGet(this, _MessageBroker_gameId, "f")) && __classPrivateFieldGet(this, _MessageBroker_hasMoreMessages, "f"))) {
return [];
}
let response;
try {
__classPrivateFieldSet(this, _MessageBroker_isAwaitingMessages, true, "f");
response = yield fetch(href(__classPrivateFieldGet(this, _MessageBroker_getMessagesUrl, "f"), Object.assign(Object.assign(Object.assign({}, (lastEvaluatedKey && { lastEvaluatedKey })), (__classPrivateFieldGet(this, _MessageBroker_gameId, "f") !== undefined && { gameId: __classPrivateFieldGet(this, _MessageBroker_gameId, "f") })), { userId: __classPrivateFieldGet(this, _MessageBroker_userId, "f") })), {
headers: {
Authorization: `Bearer ${__classPrivateFieldGet(this, _MessageBroker_stt, "f")}`,
'X-Source-Url': (_b = (_a = globalThis === null || globalThis === void 0 ? void 0 : globalThis.location) === null || _a === void 0 ? void 0 : _a.href) !== null && _b !== void 0 ? _b : 'unknown',
},
});
}
catch (error) {
if (process.env.NODE_ENV !== 'test') {
// this is a network error, not a server error
// warn instead of error, so it doesn't fill up RUM logs
console.warn(`Failed to get messages`, error);
}
// `fetch` will only throw if there's a network error, so I think the
// right thing to do is reset and try again on next dispatch
this.reset();
return [];
}
if (!response.ok) {
if (process.env.NODE_ENV !== 'test') {
console.error(`Failed to get messages, server responded with:`, {
status: response.status,
statusText: response.statusText,
});
}
return [];
}
const { data, lastKey, } = yield response.json();
__classPrivateFieldSet(this, _MessageBroker_hasMoreMessages, lastKey !== null, "f");
let nextData = data;
/**
* if we're not fetching history (e.g. `lastEvaluatedKey` is `undefined`)
* and we've got (or had) an active socket (e.g. we've seen some message(s)
* come through and we have a `#newest` key) then we're in a 'reconnecting'
* mode and want to serially fetch pages of messages until we hit a page
* that contains our `#newest` key, or `MAX_HISTORY_CATCHUP_PAGES` (10)
* pages, or a page comes back empty (e.g. `keys[0]` is undefined) at which
* point we just quit
*/
if (!lastEvaluatedKey && __classPrivateFieldGet(this, _MessageBroker_newest, "f")) {
let pagesFetched = 1; // the fetch above counts as the first page
let keys = nextData.map(toKey).sort();
while (!keys.includes(__classPrivateFieldGet(this, _MessageBroker_newest, "f")) &&
pagesFetched++ < MAX_HISTORY_CATCHUP_PAGES &&
keys[0] !== undefined) {
const messages = yield this.getMessages(keys[0]);
nextData = nextData.concat(messages);
keys = nextData.map(toKey).sort();
}
// TODO: if we didn't catch up our previous `#newest` message after
// fetching 10 pages we should signal `game-log-client` (or other
// consumers) that they should drop messages older than `keys[0]` and
// re-fetch history if necessary (or *something*)
}
__classPrivateFieldSet(this, _MessageBroker_isAwaitingMessages, false, "f");
return nextData.sort(byDateTime);
});
}
flushQueue() {
if (!__classPrivateFieldGet(this, _MessageBroker_ws, "f")) {
this.connect();
return;
}
while (__classPrivateFieldGet(this, _MessageBroker_queue, "f").length) {
const serializedMessage = __classPrivateFieldGet(this, _MessageBroker_queue, "f").shift();
serializedMessage && __classPrivateFieldGet(this, _MessageBroker_ws, "f").send(serializedMessage);
}
clearTimeout(__classPrivateFieldGet(this, _MessageBroker_pingId, "f"));
__classPrivateFieldSet(this, _MessageBroker_pingId, setTimeout(() => this.ping(), FIVE_MINUTES), "f");
}
ping() {
if (!__classPrivateFieldGet(this, _MessageBroker_ws, "f")) {
this.connect();
return;
}
/**
* just trying to keep this as light as possible, should be equivalent to:
* `JSON.stringify({ data: 'ping' })`
*
* @see packages/game-log-lambdas/src/gamelog-sendmessage/index.ts
*/
__classPrivateFieldGet(this, _MessageBroker_ws, "f").send('{"data":"ping"}');
clearTimeout(__classPrivateFieldGet(this, _MessageBroker_pingId, "f"));
__classPrivateFieldSet(this, _MessageBroker_pingId, setTimeout(() => this.ping(), FIVE_MINUTES), "f");
}
/**
* Handle window focus events to reconnect if disconnected
* This helps recover from browser resource optimization that may have closed the connection
*/
handleFocusReconnect() {
if (this.status === MessageBrokerStatus.Disconnected) {
this.connect();
}
}
/**
* Handle document visibility change events to reconnect if disconnected when tab becomes visible
* This helps recover when browser throttling/suspension closes connections in background tabs
*/
handleVisibilityReconnect() {
if (!document.hidden && this.status === MessageBrokerStatus.Disconnected) {
this.connect();
}
}
}
_MessageBroker_gameId = new WeakMap(), _MessageBroker_userId = new WeakMap(), _MessageBroker_source = new WeakMap(), _MessageBroker_connectUrl = new WeakMap(), _MessageBroker_getMessagesUrl = new WeakMap(), _MessageBroker_evt = new WeakMap(), _MessageBroker_queue = new WeakMap(), _MessageBroker_ws = new WeakMap(), _MessageBroker_getShortTermToken = new WeakMap(), _MessageBroker_isAwaitingStt = new WeakMap(), _MessageBroker_isAwaitingMessages = new WeakMap(), _MessageBroker_stt = new WeakMap(), _MessageBroker_pingId = new WeakMap(), _MessageBroker_oldest = new WeakMap(), _MessageBroker_newest = new WeakMap(), _MessageBroker_posted = new WeakMap(), _MessageBroker_hasMoreMessages = new WeakMap();
//# sourceMappingURL=MessageBroker.js.map
@@ -0,0 +1,8 @@
/**
* TODO: swap `'@dndbeyond/message-broker-lib'` for `name` once downstream
* builds (e.g. @dndbeyond/dice) can get off Rollup ... `import { name } from
* '../../package.json';` causes `[!] Error: Unexpected token (Note that you
* need @rollup/plugin-json to import JSON files)`
*/
export const key = Symbol.for('@dndbeyond/message-broker-lib');
//# sourceMappingURL=constants.js.map
@@ -0,0 +1,33 @@
import { key } from './constants';
import { isMobileApp, messageBrokerFacade as mobileAppFacade, } from './helpers/mobileApp';
import { isVttView, messageBrokerFacade as vttViewFacade, } from './helpers/vttView';
export const getMessageBroker = () => new Promise((resolve, reject) => (function findMessageBroker() {
if (process.env.NODE_ENV === 'development') {
console.log(`looking for window[${String(key)}]`);
}
const messageBroker = isMobileApp
? mobileAppFacade
: isVttView
? vttViewFacade
: window[key];
if (messageBroker !== undefined) {
if (process.env.NODE_ENV === 'development') {
console.log(`found window[${String(key)}]:`, window[key]);
}
if (!window[key]) {
window[key] = messageBroker;
}
resolve(messageBroker);
return;
}
if (document.readyState === 'complete' ||
document.readyState === 'interactive') {
if (process.env.NODE_ENV === 'development') {
console.log(`never found window[${String(key)}]`);
}
reject(new Error('A message broker was never instantiated in this document'));
return;
}
setTimeout(findMessageBroker, 100);
})());
//# sourceMappingURL=getMessageBroker.js.map
@@ -0,0 +1,8 @@
export * from './mobileApp';
export * from './webSocket';
// gameId is now optional, because the new update diceSet message needs to
// support characters that aren't in a game to still receive the new setId
export const isValidGameId = (gameId) => gameId !== '0';
export const toKey = ({ dateTime, eventType, userId }) => `${dateTime}-${eventType}-${userId}`;
export const byDateTime = ({ dateTime: a }, { dateTime: b }) => (a > b ? -1 : a === b ? 0 : 1);
//# sourceMappingURL=index.js.map
@@ -0,0 +1,76 @@
var _a, _b;
/**
* get the "right" `postMessage`, fallback to a dev log if none is available
*
* n.b. assigning `<whatever>.mobileApp?.postMessage` to a variable won't work
* because it loses its "this binding" when passed as a reference
*
* @see https://stackoverflow.com/questions/59158951/java-bridge-method-cant-be-invoked-on-a-non-injected-object
* @see https://github.com/react-native-webview/react-native-webview/issues/323#issuecomment-511824940
* @see https://developer.android.com/reference/android/webkit/WebView#addJavascriptInterface(java.lang.Object,%20java.lang.String)
*/
const postMessage = (message) => {
var _a, _b, _c, _d, _e, _f;
if ((_b = (_a = window.webkit) === null || _a === void 0 ? void 0 : _a.messageHandlers.mobileApp) === null || _b === void 0 ? void 0 : _b.postMessage) {
(_d = (_c = window.webkit) === null || _c === void 0 ? void 0 : _c.messageHandlers.mobileApp) === null || _d === void 0 ? void 0 : _d.postMessage(message);
return;
}
if ((_e = window.mobileApp) === null || _e === void 0 ? void 0 : _e.postMessage) {
(_f = window.mobileApp) === null || _f === void 0 ? void 0 : _f.postMessage(message);
return;
}
if (process.env.NODE_ENV === 'development') {
console.log(`posting to mobile app with message:\n${JSON.stringify(JSON.parse(message), null, 2)}`);
}
};
/**
* the mobile app loads `tactical-map-client` as a WebView with search params:
*
* - `&disableMb=true` disables rendering of the message-broker-client
* app-portal in app-shell
* - `&disableLayout=true` disables rendering web layout (header, footer, nav)
* - `&gameId=<game id>` the game id
* - `&userId=<user id>` the current user id
*
* @see https://wikia-inc.atlassian.net/wiki/spaces/MOB/pages/1548877853/Access+tactical+maps+in+the+app
* @see https://github.com/DnDBeyond/ddb-app-shell/pull/67
*/
const urlSearchParams = new URLSearchParams(location.search);
export const isMobileApp = urlSearchParams.get('disableMb') === 'true' &&
urlSearchParams.get('disableLayout') === 'true' &&
urlSearchParams.has('userId'); // might not need this?
// TODO: throw if `userId` is missing? decode it from JWT?
window.mobileApp || (window.mobileApp = {});
window.mobileApp.subscribers || (window.mobileApp.subscribers = new Set());
window.mobileApp.gameLogCallback ||
(window.mobileApp.gameLogCallback = (message) => {
var _a, _b;
return (_b = (_a = window.mobileApp) === null || _a === void 0 ? void 0 : _a.subscribers) === null || _b === void 0 ? void 0 : _b.forEach((subscription) => subscription(message));
});
export const messageBrokerFacade = {
gameId: (_a = urlSearchParams.get('gameId')) !== null && _a !== void 0 ? _a : '0',
userId: (_b = urlSearchParams.get('userId')) !== null && _b !== void 0 ? _b : '0',
source: '',
connectUrl: '',
getMessagesUrl: '',
fetchHistory: () => Promise.resolve(),
subscribe: (callback) => {
var _a, _b;
(_b = (_a = window.mobileApp) === null || _a === void 0 ? void 0 : _a.subscribers) === null || _b === void 0 ? void 0 : _b.add(callback);
return () => { var _a, _b; return (_b = (_a = window.mobileApp) === null || _a === void 0 ? void 0 : _a.subscribers) === null || _b === void 0 ? void 0 : _b.delete(callback); };
},
dispatch: (partial) =>
/**
* in `GameLogContextProvider` we have an `isMessage` type predicate that
* "duck types" a message as being anything that includes all of these keys:
* `entityId`, `entityType`, `persist`, `messageScope`, `messageTarget`, and
* `data`, so I think it's important we preserve that shape here. The
* additional `type` key here is (I belive) just for the mobile apps to
* filter messages against
*
* @see https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
* @see packages/game-log-components/src/components/GameLogContextProvider/GameLogContextProvider.tsx
*/
postMessage(JSON.stringify(Object.assign({ type: 'gameLog' }, partial))),
};
//# sourceMappingURL=mobileApp.js.map
@@ -0,0 +1,56 @@
var _a, _b;
const safeParse = (text, reviver) => {
try {
return JSON.parse(text, reviver);
// eslint-disable-next-line no-empty
}
catch (e) { }
};
/**
* @see https://github.com/DnDBeyond/game-log/blob/main/packages/game-log-components/src/components/GameLogContextProvider/GameLogContextProvider.tsx#L184-L190
*/
const isMessage = (x) => !!x &&
typeof x === 'object' &&
'entityId' in x &&
'entityType' in x &&
'persist' in x &&
'messageScope' in x &&
'messageTarget' in x &&
'data' in x;
const isProd = process.env.NODE_ENV === 'production' &&
location.host === 'www.dndbeyond.com';
const postMessage = (message) => {
window.parent.postMessage(message, isProd ? location.origin : '*');
if (process.env.NODE_ENV === 'development') {
console.log(`posting to feywild with message:\n${JSON.stringify(JSON.parse(message), null, 2)}`);
}
};
/**
* the character sheet loads in Feywild as an iframe with `?view=vtt`
*/
const urlSearchParams = new URLSearchParams(location.search);
export const isVttView = urlSearchParams.get('view') === 'vtt';
export const messageBrokerFacade = {
gameId: (_a = urlSearchParams.get('gameId')) !== null && _a !== void 0 ? _a : '0',
userId: (_b = urlSearchParams.get('userId')) !== null && _b !== void 0 ? _b : '0',
source: '',
connectUrl: '',
getMessagesUrl: '',
fetchHistory: () => Promise.resolve(),
subscribe: (callback) => {
const onMessage = ({ data, origin }) => {
if ((isProd && origin !== location.origin) || typeof data !== 'string') {
return;
}
const message = safeParse(data);
if (!isMessage(message)) {
return;
}
callback(message);
};
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
},
dispatch: (partial) => postMessage(JSON.stringify(partial)),
};
//# sourceMappingURL=vttView.js.map
@@ -0,0 +1,7 @@
export var EventStatus;
(function (EventStatus) {
EventStatus["Pending"] = "pending";
EventStatus["Fulfilled"] = "fulfilled";
EventStatus["Rejected"] = "rejected";
})(EventStatus || (EventStatus = {}));
//# sourceMappingURL=types.js.map