New source found from dndbeyond.com
This commit is contained in:
+421
@@ -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
|
||||
+8
@@ -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
|
||||
+33
@@ -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
|
||||
+8
@@ -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
|
||||
Generated
Vendored
+76
@@ -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
|
||||
+56
@@ -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
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export var EventStatus;
|
||||
(function (EventStatus) {
|
||||
EventStatus["Pending"] = "pending";
|
||||
EventStatus["Fulfilled"] = "fulfilled";
|
||||
EventStatus["Rejected"] = "rejected";
|
||||
})(EventStatus || (EventStatus = {}));
|
||||
//# sourceMappingURL=types.js.map
|
||||
Reference in New Issue
Block a user