quidproquo-web 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/actions/queryParams/QueryParamsGetActionRequester.d.ts +1 -0
- package/lib/commonjs/actions/queryParams/QueryParamsGetActionRequester.js +1 -0
- package/lib/commonjs/actions/queryParams/QueryParamsGetActionRequester.js.map +1 -1
- package/lib/commonjs/actions/queryParams/QueryParamsGetAllActionRequester.d.ts +1 -0
- package/lib/commonjs/actions/queryParams/QueryParamsGetAllActionRequester.js +1 -0
- package/lib/commonjs/actions/queryParams/QueryParamsGetAllActionRequester.js.map +1 -1
- package/lib/commonjs/actions/queryParams/QueryParamsSetActionRequester.d.ts +5 -0
- package/lib/commonjs/actions/queryParams/QueryParamsSetActionRequester.js +5 -0
- package/lib/commonjs/actions/queryParams/QueryParamsSetActionRequester.js.map +1 -1
- package/lib/commonjs/actions/window/WindowGetLocationActionRequester.d.ts +1 -0
- package/lib/commonjs/actions/window/WindowGetLocationActionRequester.js +1 -0
- package/lib/commonjs/actions/window/WindowGetLocationActionRequester.js.map +1 -1
- package/lib/commonjs/federation/forceReloadFederatedRemote.js +5 -5
- package/lib/commonjs/federation/forceReloadFederatedRemote.js.map +1 -1
- package/lib/commonjs/services/WebsocketService.d.ts +9 -4
- package/lib/commonjs/services/WebsocketService.js +31 -28
- package/lib/commonjs/services/WebsocketService.js.map +1 -1
- package/lib/commonjs/tools/timeAgo.js +1 -1
- package/lib/commonjs/tools/timeAgo.js.map +1 -1
- package/lib/commonjs/tools/uniqueBy.d.ts +2 -1
- package/lib/commonjs/tools/uniqueBy.js +1 -0
- package/lib/commonjs/tools/uniqueBy.js.map +1 -1
- package/lib/esm/actions/queryParams/QueryParamsGetActionRequester.d.ts +1 -0
- package/lib/esm/actions/queryParams/QueryParamsGetActionRequester.js +1 -0
- package/lib/esm/actions/queryParams/QueryParamsGetActionRequester.js.map +1 -1
- package/lib/esm/actions/queryParams/QueryParamsGetAllActionRequester.d.ts +1 -0
- package/lib/esm/actions/queryParams/QueryParamsGetAllActionRequester.js +1 -0
- package/lib/esm/actions/queryParams/QueryParamsGetAllActionRequester.js.map +1 -1
- package/lib/esm/actions/queryParams/QueryParamsSetActionRequester.d.ts +5 -0
- package/lib/esm/actions/queryParams/QueryParamsSetActionRequester.js +5 -0
- package/lib/esm/actions/queryParams/QueryParamsSetActionRequester.js.map +1 -1
- package/lib/esm/actions/window/WindowGetLocationActionRequester.d.ts +1 -0
- package/lib/esm/actions/window/WindowGetLocationActionRequester.js +1 -0
- package/lib/esm/actions/window/WindowGetLocationActionRequester.js.map +1 -1
- package/lib/esm/federation/forceReloadFederatedRemote.js +5 -5
- package/lib/esm/federation/forceReloadFederatedRemote.js.map +1 -1
- package/lib/esm/services/WebsocketService.d.ts +9 -4
- package/lib/esm/services/WebsocketService.js +29 -28
- package/lib/esm/services/WebsocketService.js.map +1 -1
- package/lib/esm/tools/timeAgo.js +1 -1
- package/lib/esm/tools/timeAgo.js.map +1 -1
- package/lib/esm/tools/uniqueBy.d.ts +2 -1
- package/lib/esm/tools/uniqueBy.js +1 -0
- package/lib/esm/tools/uniqueBy.js.map +1 -1
- package/package.json +6 -5
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { QueryParamsGetActionRequester } from './QueryParamsGetActionRequesterTypes';
|
|
2
|
+
/** Reads ALL values of a query-string key from the current url (repeated keys are legal, hence string[]). Empty array when absent. */
|
|
2
3
|
export declare function askQueryParamsGet(key: string): QueryParamsGetActionRequester;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.askQueryParamsGet = askQueryParamsGet;
|
|
4
4
|
const QueryParamsActionType_1 = require("./QueryParamsActionType");
|
|
5
|
+
/** Reads ALL values of a query-string key from the current url (repeated keys are legal, hence string[]). Empty array when absent. */
|
|
5
6
|
function* askQueryParamsGet(key) {
|
|
6
7
|
return yield { type: QueryParamsActionType_1.QueryParamsActionType.Get, payload: { key } };
|
|
7
8
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueryParamsGetActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsGetActionRequester.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"QueryParamsGetActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsGetActionRequester.ts"],"names":[],"mappings":";;AAIA,8CAEC;AAND,mEAAgE;AAGhE,sIAAsI;AACtI,QAAe,CAAC,CAAC,iBAAiB,CAAC,GAAW;IAC5C,OAAO,MAAM,EAAE,IAAI,EAAE,6CAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC;AACrE,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsGetActionRequester } from './QueryParamsGetActionRequesterTypes';\n\n/** Reads ALL values of a query-string key from the current url (repeated keys are legal, hence string[]). Empty array when absent. */\nexport function* askQueryParamsGet(key: string): QueryParamsGetActionRequester {\n return yield { type: QueryParamsActionType.Get, payload: { key } };\n}\n"]}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.askQueryParamsGetAll = askQueryParamsGetAll;
|
|
4
4
|
const QueryParamsActionType_1 = require("./QueryParamsActionType");
|
|
5
|
+
/** Reads the whole query string of the current url as key -> all values for that key. */
|
|
5
6
|
function* askQueryParamsGetAll() {
|
|
6
7
|
return yield { type: QueryParamsActionType_1.QueryParamsActionType.GetAll };
|
|
7
8
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueryParamsGetAllActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsGetAllActionRequester.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"QueryParamsGetAllActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsGetAllActionRequester.ts"],"names":[],"mappings":";;AAIA,oDAEC;AAND,mEAAgE;AAGhE,yFAAyF;AACzF,QAAe,CAAC,CAAC,oBAAoB;IACnC,OAAO,MAAM,EAAE,IAAI,EAAE,6CAAqB,CAAC,MAAM,EAAE,CAAC;AACtD,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsGetAllActionRequester } from './QueryParamsGetAllActionRequesterTypes';\n\n/** Reads the whole query string of the current url as key -> all values for that key. */\nexport function* askQueryParamsGetAll(): QueryParamsGetAllActionRequester {\n return yield { type: QueryParamsActionType.GetAll };\n}\n"]}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { QueryParamsSetActionRequester } from './QueryParamsSetActionRequesterTypes';
|
|
2
|
+
/**
|
|
3
|
+
* Replaces a query-string key's values in the current url; an empty array removes
|
|
4
|
+
* the key. By default the url is swapped in place (replaceState); pass
|
|
5
|
+
* createHistoryEntry to make the change a back-button stop (pushState).
|
|
6
|
+
*/
|
|
2
7
|
export declare function askQueryParamsSet(key: string, values: string[], createHistoryEntry?: boolean): QueryParamsSetActionRequester;
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.askQueryParamsSet = askQueryParamsSet;
|
|
4
4
|
const QueryParamsActionType_1 = require("./QueryParamsActionType");
|
|
5
|
+
/**
|
|
6
|
+
* Replaces a query-string key's values in the current url; an empty array removes
|
|
7
|
+
* the key. By default the url is swapped in place (replaceState); pass
|
|
8
|
+
* createHistoryEntry to make the change a back-button stop (pushState).
|
|
9
|
+
*/
|
|
5
10
|
function* askQueryParamsSet(key, values, createHistoryEntry = false) {
|
|
6
11
|
return yield { type: QueryParamsActionType_1.QueryParamsActionType.Set, payload: { key, values, createHistoryEntry } };
|
|
7
12
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueryParamsSetActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsSetActionRequester.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"QueryParamsSetActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsSetActionRequester.ts"],"names":[],"mappings":";;AAQA,8CAEC;AAVD,mEAAgE;AAGhE;;;;GAIG;AACH,QAAe,CAAC,CAAC,iBAAiB,CAAC,GAAW,EAAE,MAAgB,EAAE,qBAA8B,KAAK;IACnG,OAAO,MAAM,EAAE,IAAI,EAAE,6CAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC;AACjG,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsSetActionRequester } from './QueryParamsSetActionRequesterTypes';\n\n/**\n * Replaces a query-string key's values in the current url; an empty array removes\n * the key. By default the url is swapped in place (replaceState); pass\n * createHistoryEntry to make the change a back-button stop (pushState).\n */\nexport function* askQueryParamsSet(key: string, values: string[], createHistoryEntry: boolean = false): QueryParamsSetActionRequester {\n return yield { type: QueryParamsActionType.Set, payload: { key, values, createHistoryEntry } };\n}\n"]}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { WindowGetLocationActionRequester } from './WindowGetLocationActionRequesterTypes';
|
|
2
|
+
/** Reads the current `window.location` as a plain serializable snapshot (never the live Location object). */
|
|
2
3
|
export declare function askWindowGetLocation(): WindowGetLocationActionRequester;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.askWindowGetLocation = askWindowGetLocation;
|
|
4
4
|
const WindowActionType_1 = require("./WindowActionType");
|
|
5
|
+
/** Reads the current `window.location` as a plain serializable snapshot (never the live Location object). */
|
|
5
6
|
function* askWindowGetLocation() {
|
|
6
7
|
return yield { type: WindowActionType_1.WindowActionType.GetLocation };
|
|
7
8
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WindowGetLocationActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/window/WindowGetLocationActionRequester.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"WindowGetLocationActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/window/WindowGetLocationActionRequester.ts"],"names":[],"mappings":";;AAIA,oDAEC;AAND,yDAAsD;AAGtD,6GAA6G;AAC7G,QAAe,CAAC,CAAC,oBAAoB;IACnC,OAAO,MAAM,EAAE,IAAI,EAAE,mCAAgB,CAAC,WAAW,EAAE,CAAC;AACtD,CAAC","sourcesContent":["import { WindowActionType } from './WindowActionType';\nimport { WindowGetLocationActionRequester } from './WindowGetLocationActionRequesterTypes';\n\n/** Reads the current `window.location` as a plain serializable snapshot (never the live Location object). */\nexport function* askWindowGetLocation(): WindowGetLocationActionRequester {\n return yield { type: WindowActionType.GetLocation };\n}\n"]}
|
|
@@ -8,25 +8,25 @@
|
|
|
8
8
|
// with a nonce'd entry URL clears them and guarantees a fresh manifest fetch.
|
|
9
9
|
// 2. The container global: loadEntryScript short-circuits on `window[globalName]`
|
|
10
10
|
// BEFORE fetching the entry, silently reusing the old container. Cleared by
|
|
11
|
-
// ASSIGNING undefined, not `delete
|
|
11
|
+
// ASSIGNING undefined, not `delete`: the global is a top-level `var` from a
|
|
12
12
|
// classic script, so the window property is non-configurable and delete throws;
|
|
13
13
|
// the runtime's short-circuit is a truthiness check, so undefined is enough.
|
|
14
14
|
// 3. The bundler's chunk-loading global (`rspackChunk<name>` / `webpackChunk<name>`):
|
|
15
|
-
// shared by BUILD NAME
|
|
15
|
+
// shared by BUILD NAME: every chunk file of every previously loaded copy of the
|
|
16
16
|
// remote pushed its modules into it, and a fresh container drains that array on
|
|
17
17
|
// boot. Chunk ids are stable across builds, so without a reset the new container
|
|
18
|
-
// sees the old build's chunks as already loaded and NEVER fetches its own files
|
|
18
|
+
// sees the old build's chunks as already loaded and NEVER fetches its own files:
|
|
19
19
|
// the swap re-executes the OLD code under a new identity. The old container keeps
|
|
20
20
|
// its captured reference to the old array, so it is unaffected.
|
|
21
21
|
// 4. The DOM script-tag dedup: both the MF sdk and the chunk loader reuse an existing
|
|
22
22
|
// <script> tag whose src matches (never re-executing it), so every one of the
|
|
23
23
|
// remote's tags (entry AND chunks) is swept.
|
|
24
24
|
// 5. The browser HTTP cache of the fixed-name remoteEntry.js: a createScript runtime
|
|
25
|
-
// plugin serves the entry script with the reload nonce on its src
|
|
25
|
+
// plugin serves the entry script with the reload nonce on its src, a never-seen
|
|
26
26
|
// URL that no cache layer can answer.
|
|
27
27
|
//
|
|
28
28
|
// The MF runtime bindings are INJECTED (see FederationRuntimeApi): they must be the
|
|
29
|
-
// host bundle's own instance
|
|
29
|
+
// host bundle's own instance. The bundler aliases '@module-federation/enhanced/runtime'
|
|
30
30
|
// onto the runtime it embeds, and importing a second copy here would target the wrong
|
|
31
31
|
// (or no) instance. Injection also keeps this package dependency-free.
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forceReloadFederatedRemote.js","sourceRoot":"","sources":["../../../src/federation/forceReloadFederatedRemote.ts"],"names":[],"mappings":";AAAA,iFAAiF;AACjF,oFAAoF;AACpF,mFAAmF;AACnF,iCAAiC;AACjC,EAAE;AACF,qFAAqF;AACrF,mFAAmF;AACnF,oFAAoF;AACpF,iFAAiF;AACjF,
|
|
1
|
+
{"version":3,"file":"forceReloadFederatedRemote.js","sourceRoot":"","sources":["../../../src/federation/forceReloadFederatedRemote.ts"],"names":[],"mappings":"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","sourcesContent":["// Hot-swap support for a module-federation remote: make the NEXT loadRemote of a\n// given remote genuinely re-fetch and re-execute its current deployment, instead of\n// serving any of the FIVE stale layers a naive force re-registration leaves behind\n// (each one found the hard way):\n//\n// 1. The MF runtime caches (moduleCache, manifest snapshot): force re-registration\n// with a nonce'd entry URL clears them and guarantees a fresh manifest fetch.\n// 2. The container global: loadEntryScript short-circuits on `window[globalName]`\n// BEFORE fetching the entry, silently reusing the old container. Cleared by\n// ASSIGNING undefined, not `delete`: the global is a top-level `var` from a\n// classic script, so the window property is non-configurable and delete throws;\n// the runtime's short-circuit is a truthiness check, so undefined is enough.\n// 3. The bundler's chunk-loading global (`rspackChunk<name>` / `webpackChunk<name>`):\n// shared by BUILD NAME: every chunk file of every previously loaded copy of the\n// remote pushed its modules into it, and a fresh container drains that array on\n// boot. Chunk ids are stable across builds, so without a reset the new container\n// sees the old build's chunks as already loaded and NEVER fetches its own files:\n// the swap re-executes the OLD code under a new identity. The old container keeps\n// its captured reference to the old array, so it is unaffected.\n// 4. The DOM script-tag dedup: both the MF sdk and the chunk loader reuse an existing\n// <script> tag whose src matches (never re-executing it), so every one of the\n// remote's tags (entry AND chunks) is swept.\n// 5. The browser HTTP cache of the fixed-name remoteEntry.js: a createScript runtime\n// plugin serves the entry script with the reload nonce on its src, a never-seen\n// URL that no cache layer can answer.\n//\n// The MF runtime bindings are INJECTED (see FederationRuntimeApi): they must be the\n// host bundle's own instance. The bundler aliases '@module-federation/enhanced/runtime'\n// onto the runtime it embeds, and importing a second copy here would target the wrong\n// (or no) instance. Injection also keeps this package dependency-free.\n\n// Universal function supertype: any concrete MF runtime function is assignable.\ntype InjectedFunction = (...args: never[]) => unknown;\n\nexport type FederationRuntimeApi = {\n getInstance: InjectedFunction;\n registerRemotes: InjectedFunction;\n registerPlugins: InjectedFunction;\n};\n\ntype FederatedRemote = { name: string; alias?: string; entry: string };\n\ntype ResolvedFederationRuntime = {\n getInstance: () => { options: { remotes: (FederatedRemote | { name: string })[] } } | null | undefined;\n registerRemotes: (remotes: FederatedRemote[], options?: { force?: boolean }) => void;\n registerPlugins: (\n plugins: { name: string; createScript: (args: { url: string; remoteInfo?: { name: string } }) => HTMLScriptElement | undefined }[],\n ) => void;\n};\n\n// Remotes with a reload in flight: mf container name -> current reload nonce.\n// Module scope is safe: quidproquo-web is a module-federation shared singleton, so\n// one copy serves the page.\nconst remoteReloadNonces = new Map<string, number>();\n\nlet cacheBustPluginRegistered = false;\n\nconst ensureCacheBustPlugin = (registerPlugins: ResolvedFederationRuntime['registerPlugins']): void => {\n if (cacheBustPluginRegistered) {\n return;\n }\n cacheBustPluginRegistered = true;\n\n registerPlugins([\n {\n name: 'qpq-federated-reload-cache-bust',\n createScript(args: { url: string; remoteInfo?: { name: string } }) {\n const reloadNonce = args.remoteInfo ? remoteReloadNonces.get(args.remoteInfo.name) : undefined;\n\n if (!reloadNonce) {\n return undefined;\n }\n\n const script = document.createElement('script');\n script.src = `${args.url.split('?')[0]}?reload=${reloadNonce}`;\n return script;\n },\n },\n ]);\n};\n\n/**\n * Prepare a federated remote for a hot-swap: after this call, the next\n * loadRemote() of the remote fetches and executes its CURRENT deployment.\n * `reloadNonce` must be a fresh value per swap (a per-remote counter).\n * Returns false (with a console warning) when the remote is not registered.\n */\nexport const forceReloadFederatedRemote = (runtimeApi: FederationRuntimeApi, remoteName: string, reloadNonce: number): boolean => {\n const { getInstance, registerRemotes, registerPlugins } = runtimeApi as ResolvedFederationRuntime;\n\n const remote = getInstance()?.options.remotes.find(\n (candidate) => ('alias' in candidate && candidate.alias === remoteName) || candidate.name === remoteName,\n );\n\n if (!remote || !('entry' in remote)) {\n console.warn(`[qpq-federated-reload] remote \"${remoteName}\" not found in MF runtime options - a reload will serve the cached bundle`);\n return false;\n }\n\n ensureCacheBustPlugin(registerPlugins);\n remoteReloadNonces.set(remote.name, reloadNonce);\n\n // Layer 2: the container global.\n (globalThis as Record<string, unknown>)[remote.name] = undefined;\n\n // Layer 3: the bundler chunk-loading globals (rspack and webpack builds).\n (globalThis as Record<string, unknown>)[`rspackChunk${remote.name}`] = undefined;\n (globalThis as Record<string, unknown>)[`webpackChunk${remote.name}`] = undefined;\n\n const baseEntry = remote.entry.split('?')[0];\n const entryDir = baseEntry.slice(0, baseEntry.lastIndexOf('/'));\n\n // Layer 4: sweep ALL of this remote's script tags (entry AND chunks).\n document.querySelectorAll<HTMLScriptElement>('script[src]').forEach((scriptTag) => {\n const src = scriptTag.getAttribute('src') ?? '';\n if (src.startsWith(entryDir)) {\n scriptTag.remove();\n }\n });\n\n // Layer 1: force re-registration with the nonce'd entry.\n registerRemotes([{ ...remote, entry: `${baseEntry}?reload=${reloadNonce}` }], { force: true });\n\n return true;\n};\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyEventMessage,
|
|
1
|
+
import { AnyEventMessage, Nullable } from 'quidproquo-core';
|
|
2
2
|
export declare enum WebsocketServiceEvent {
|
|
3
3
|
OPEN = "open",
|
|
4
4
|
CLOSE = "close",
|
|
@@ -8,9 +8,14 @@ export declare enum WebsocketServiceEvent {
|
|
|
8
8
|
export type SubscriptionHandle = {
|
|
9
9
|
type: WebsocketServiceEvent;
|
|
10
10
|
};
|
|
11
|
-
export type WebSocketServiceSubscriptionFunction = (websocketService: WebsocketService, event?: Event) =>
|
|
11
|
+
export type WebSocketServiceSubscriptionFunction = (websocketService: WebsocketService, event?: Event) => void;
|
|
12
12
|
type WebsocketSendPayload = string | ArrayBufferLike | Blob | ArrayBufferView;
|
|
13
13
|
export type WebSocketServiceEventSubscriptionFunction<E extends AnyEventMessage> = (websocketService: WebsocketService, event: E) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Browser WebSocket wrapper that reconnects automatically (exponential backoff
|
|
16
|
+
* with jitter), queues sends while disconnected, and fans events out to
|
|
17
|
+
* subscribers. Call destroy() to stop reconnecting and release the socket.
|
|
18
|
+
*/
|
|
14
19
|
export declare class WebsocketService {
|
|
15
20
|
readonly url: string;
|
|
16
21
|
private socket;
|
|
@@ -26,7 +31,7 @@ export declare class WebsocketService {
|
|
|
26
31
|
close(): void;
|
|
27
32
|
isConnected(): boolean;
|
|
28
33
|
hasBeenDestroyed(): boolean;
|
|
29
|
-
getSocket(): WebSocket
|
|
34
|
+
getSocket(): Nullable<WebSocket>;
|
|
30
35
|
subscribe(subscriptionType: WebsocketServiceEvent, callback: WebSocketServiceSubscriptionFunction): SubscriptionHandle;
|
|
31
36
|
subscribeToEvent<E extends AnyEventMessage>(subscriptionType: E['type'], callback: WebSocketServiceEventSubscriptionFunction<E>): SubscriptionHandle;
|
|
32
37
|
unsubscribe(subscriptionHandle: SubscriptionHandle): void;
|
|
@@ -41,6 +46,6 @@ export declare class WebsocketService {
|
|
|
41
46
|
private onError;
|
|
42
47
|
private notifySubscribers;
|
|
43
48
|
send(data: WebsocketSendPayload): void;
|
|
44
|
-
sendEvent<E extends
|
|
49
|
+
sendEvent<E extends AnyEventMessage>(event: E): void;
|
|
45
50
|
}
|
|
46
51
|
export {};
|
|
@@ -8,6 +8,11 @@ var WebsocketServiceEvent;
|
|
|
8
8
|
WebsocketServiceEvent["MESSAGE"] = "message";
|
|
9
9
|
WebsocketServiceEvent["ERROR"] = "error";
|
|
10
10
|
})(WebsocketServiceEvent || (exports.WebsocketServiceEvent = WebsocketServiceEvent = {}));
|
|
11
|
+
/**
|
|
12
|
+
* Browser WebSocket wrapper that reconnects automatically (exponential backoff
|
|
13
|
+
* with jitter), queues sends while disconnected, and fans events out to
|
|
14
|
+
* subscribers. Call destroy() to stop reconnecting and release the socket.
|
|
15
|
+
*/
|
|
11
16
|
class WebsocketService {
|
|
12
17
|
constructor(url) {
|
|
13
18
|
this.socket = null;
|
|
@@ -52,29 +57,30 @@ class WebsocketService {
|
|
|
52
57
|
return subscriptionHandle;
|
|
53
58
|
}
|
|
54
59
|
subscribeToEvent(subscriptionType, callback) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Must have been some other message format / type do nothing.
|
|
60
|
+
// Forwards only MESSAGE frames that parse as json and carry the subscribed
|
|
61
|
+
// type; anything else on the socket is some other protocol's traffic.
|
|
62
|
+
const forwardMatchingMessage = (websocketService, event) => {
|
|
63
|
+
if (!event) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const parsedEvent = JSON.parse(event.data);
|
|
68
|
+
if (parsedEvent.type === subscriptionType) {
|
|
69
|
+
callback(websocketService, parsedEvent);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
|
-
|
|
72
|
+
catch (_a) {
|
|
73
|
+
// Not json: ignore.
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return this.subscribe(WebsocketServiceEvent.MESSAGE, forwardMatchingMessage);
|
|
69
77
|
}
|
|
70
78
|
unsubscribe(subscriptionHandle) {
|
|
71
79
|
this.subscriptions[subscriptionHandle.type].delete(subscriptionHandle);
|
|
72
80
|
}
|
|
73
81
|
unsubscribeAll() {
|
|
74
|
-
for (const
|
|
75
|
-
|
|
76
|
-
this.subscriptions[subscriptionType].clear();
|
|
77
|
-
}
|
|
82
|
+
for (const subscriptionMap of Object.values(this.subscriptions)) {
|
|
83
|
+
subscriptionMap.clear();
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
connect() {
|
|
@@ -86,21 +92,19 @@ class WebsocketService {
|
|
|
86
92
|
this.addEventListener(WebsocketServiceEvent.ERROR, this.onError.bind(this));
|
|
87
93
|
}
|
|
88
94
|
addEventListener(event, listener) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
this.eventListeners[event].push(listener);
|
|
95
|
+
var _a;
|
|
96
|
+
var _b;
|
|
97
|
+
if (!this.socket) {
|
|
98
|
+
return;
|
|
95
99
|
}
|
|
100
|
+
this.socket.addEventListener(event, listener);
|
|
101
|
+
((_a = (_b = this.eventListeners)[event]) !== null && _a !== void 0 ? _a : (_b[event] = [])).push(listener);
|
|
96
102
|
}
|
|
97
103
|
removeAllEventListeners() {
|
|
98
104
|
if (this.socket) {
|
|
99
|
-
for (const event
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.socket.removeEventListener(event, listener);
|
|
103
|
-
}
|
|
105
|
+
for (const [event, listeners] of Object.entries(this.eventListeners)) {
|
|
106
|
+
for (const listener of listeners) {
|
|
107
|
+
this.socket.removeEventListener(event, listener);
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
}
|
|
@@ -112,7 +116,6 @@ class WebsocketService {
|
|
|
112
116
|
const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);
|
|
113
117
|
const delay = Math.max(0, baseDelay + jitter);
|
|
114
118
|
this.reconnectAttempts++;
|
|
115
|
-
console.log('Delay: ', delay);
|
|
116
119
|
setTimeout(() => {
|
|
117
120
|
if (!this.isDestroyed) {
|
|
118
121
|
this.connect();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsocketService.js","sourceRoot":"","sources":["../../../src/services/WebsocketService.ts"],"names":[],"mappings":"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","sourcesContent":["import { AnyEventMessage, EventMessage } from 'quidproquo-core';\n\n// Private / internal types\ntype WebSocketEventListenerFunction = (this: WebSocket, ev: Event) => any;\n\nexport enum WebsocketServiceEvent {\n OPEN = 'open',\n CLOSE = 'close',\n MESSAGE = 'message',\n ERROR = 'error',\n}\n\nexport type SubscriptionHandle = {\n type: WebsocketServiceEvent;\n};\n\nexport type WebSocketServiceSubscriptionFunction = (websocketService: WebsocketService, event?: Event) => any;\n\ntype SubscriptionMap = Map<SubscriptionHandle, WebSocketServiceSubscriptionFunction>;\n\ntype Subscriptions = {\n [key in WebsocketServiceEvent]: SubscriptionMap;\n};\n\ntype WebsocketSendPayload = string | ArrayBufferLike | Blob | ArrayBufferView;\n\nexport type WebSocketServiceEventSubscriptionFunction<E extends AnyEventMessage> = (websocketService: WebsocketService, event: E) => void;\n\nexport class WebsocketService {\n public readonly url: string;\n private socket: WebSocket | null = null;\n private eventListeners: { [key: string]: WebSocketEventListenerFunction[] } = {};\n private isDestroyed: boolean = false;\n private pendingMessages: WebsocketSendPayload[] = [];\n\n private reconnectAttempts: number = 0;\n private static readonly BASE_RECONNECT_MS = 1000;\n private static readonly MAX_RECONNECT_MS = 60_000;\n\n private subscriptions: Subscriptions = {\n [WebsocketServiceEvent.OPEN]: new Map(),\n [WebsocketServiceEvent.CLOSE]: new Map(),\n [WebsocketServiceEvent.MESSAGE]: new Map(),\n [WebsocketServiceEvent.ERROR]: new Map(),\n };\n\n constructor(url: string) {\n this.url = url;\n\n this.connect();\n }\n\n public destroy() {\n this.isDestroyed = true;\n this.unsubscribeAll();\n this.socket?.close();\n }\n\n public close() {\n this.socket?.close();\n }\n\n public isConnected() {\n return this.socket?.readyState === WebSocket.OPEN;\n }\n\n public hasBeenDestroyed() {\n return this.isDestroyed;\n }\n\n public getSocket() {\n return this.socket;\n }\n\n public subscribe(subscriptionType: WebsocketServiceEvent, callback: WebSocketServiceSubscriptionFunction): SubscriptionHandle {\n const subscriptionHandle: SubscriptionHandle = {\n type: subscriptionType,\n };\n this.subscriptions[subscriptionType].set(subscriptionHandle, callback);\n\n return subscriptionHandle;\n }\n\n public subscribeToEvent<E extends AnyEventMessage>(\n subscriptionType: E['type'],\n callback: WebSocketServiceEventSubscriptionFunction<E>,\n ): SubscriptionHandle {\n return this.subscribe(WebsocketServiceEvent.MESSAGE, (websocketService: WebsocketService, event?: Event) => {\n if (event) {\n const data = (event as MessageEvent).data;\n try {\n const parsedEvent: E = JSON.parse(data);\n if (parsedEvent.type === subscriptionType) {\n callback(websocketService, parsedEvent);\n }\n } catch (e) {\n // Must have been some other message format / type do nothing.\n }\n }\n });\n }\n\n public unsubscribe(subscriptionHandle: SubscriptionHandle) {\n this.subscriptions[subscriptionHandle.type].delete(subscriptionHandle);\n }\n\n public unsubscribeAll() {\n for (const subscriptionType in this.subscriptions) {\n if (Object.prototype.hasOwnProperty.call(this.subscriptions, subscriptionType)) {\n this.subscriptions[subscriptionType as WebsocketServiceEvent].clear();\n }\n }\n }\n\n private connect() {\n this.removeAllEventListeners();\n\n this.socket = new WebSocket(this.url);\n\n this.addEventListener(WebsocketServiceEvent.OPEN, this.onConnect.bind(this));\n this.addEventListener(WebsocketServiceEvent.CLOSE, this.onClose.bind(this));\n this.addEventListener(WebsocketServiceEvent.MESSAGE, this.onMessage.bind(this));\n this.addEventListener(WebsocketServiceEvent.ERROR, this.onError.bind(this));\n }\n\n private addEventListener(event: WebsocketServiceEvent, listener: WebSocketEventListenerFunction) {\n if (this.socket) {\n this.socket.addEventListener(event, listener);\n\n if (!this.eventListeners[event]) {\n this.eventListeners[event] = [];\n }\n\n this.eventListeners[event].push(listener);\n }\n }\n\n private removeAllEventListeners() {\n if (this.socket) {\n for (const event in this.eventListeners) {\n if (Object.prototype.hasOwnProperty.call(this.eventListeners, event)) {\n for (const listener of this.eventListeners[event]) {\n this.socket.removeEventListener(event, listener);\n }\n }\n }\n }\n\n this.eventListeners = {};\n }\n\n private reconnectIfNotDestroyed() {\n const baseDelay = Math.min(WebsocketService.BASE_RECONNECT_MS * Math.pow(2, this.reconnectAttempts), WebsocketService.MAX_RECONNECT_MS);\n\n // Add jitter: ±25% of base delay to prevent thundering herd\n const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);\n const delay = Math.max(0, baseDelay + jitter);\n\n this.reconnectAttempts++;\n\n console.log('Delay: ', delay);\n\n setTimeout(() => {\n if (!this.isDestroyed) {\n this.connect();\n }\n }, delay);\n }\n\n private onConnect() {\n this.reconnectAttempts = 0;\n this.notifySubscribers(WebsocketServiceEvent.OPEN);\n const messages = this.pendingMessages;\n this.pendingMessages = [];\n messages.forEach((message) => {\n this.send(message);\n });\n }\n\n private onClose() {\n this.removeAllEventListeners();\n this.reconnectIfNotDestroyed();\n this.notifySubscribers(WebsocketServiceEvent.CLOSE);\n }\n\n private onMessage(event: Event) {\n this.notifySubscribers(WebsocketServiceEvent.MESSAGE, event);\n }\n\n private onError(event: Event) {\n this.notifySubscribers(WebsocketServiceEvent.ERROR, event);\n }\n\n private notifySubscribers(subType: WebsocketServiceEvent, event?: Event) {\n this.subscriptions[subType].forEach((callback) => {\n callback(this, event);\n });\n }\n\n public send(data: WebsocketSendPayload) {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(data);\n } else {\n this.pendingMessages.push(data);\n }\n }\n\n public sendEvent<E extends EventMessage<any, string | number>>(event: E) {\n this.send(JSON.stringify(event));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WebsocketService.js","sourceRoot":"","sources":["../../../src/services/WebsocketService.ts"],"names":[],"mappings":"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","sourcesContent":["import { AnyEventMessage, Nullable } from 'quidproquo-core';\n\ntype WebSocketEventListenerFunction = (this: WebSocket, ev: Event) => void;\n\nexport enum WebsocketServiceEvent {\n OPEN = 'open',\n CLOSE = 'close',\n MESSAGE = 'message',\n ERROR = 'error',\n}\n\nexport type SubscriptionHandle = {\n type: WebsocketServiceEvent;\n};\n\nexport type WebSocketServiceSubscriptionFunction = (websocketService: WebsocketService, event?: Event) => void;\n\ntype SubscriptionMap = Map<SubscriptionHandle, WebSocketServiceSubscriptionFunction>;\n\ntype Subscriptions = {\n [key in WebsocketServiceEvent]: SubscriptionMap;\n};\n\ntype WebsocketSendPayload = string | ArrayBufferLike | Blob | ArrayBufferView;\n\nexport type WebSocketServiceEventSubscriptionFunction<E extends AnyEventMessage> = (websocketService: WebsocketService, event: E) => void;\n\n/**\n * Browser WebSocket wrapper that reconnects automatically (exponential backoff\n * with jitter), queues sends while disconnected, and fans events out to\n * subscribers. Call destroy() to stop reconnecting and release the socket.\n */\nexport class WebsocketService {\n public readonly url: string;\n private socket: Nullable<WebSocket> = null;\n private eventListeners: Partial<Record<WebsocketServiceEvent, WebSocketEventListenerFunction[]>> = {};\n private isDestroyed: boolean = false;\n private pendingMessages: WebsocketSendPayload[] = [];\n\n private reconnectAttempts: number = 0;\n private static readonly BASE_RECONNECT_MS = 1000;\n private static readonly MAX_RECONNECT_MS = 60_000;\n\n private subscriptions: Subscriptions = {\n [WebsocketServiceEvent.OPEN]: new Map(),\n [WebsocketServiceEvent.CLOSE]: new Map(),\n [WebsocketServiceEvent.MESSAGE]: new Map(),\n [WebsocketServiceEvent.ERROR]: new Map(),\n };\n\n constructor(url: string) {\n this.url = url;\n\n this.connect();\n }\n\n public destroy() {\n this.isDestroyed = true;\n this.unsubscribeAll();\n this.socket?.close();\n }\n\n public close() {\n this.socket?.close();\n }\n\n public isConnected() {\n return this.socket?.readyState === WebSocket.OPEN;\n }\n\n public hasBeenDestroyed() {\n return this.isDestroyed;\n }\n\n public getSocket() {\n return this.socket;\n }\n\n public subscribe(subscriptionType: WebsocketServiceEvent, callback: WebSocketServiceSubscriptionFunction): SubscriptionHandle {\n const subscriptionHandle: SubscriptionHandle = {\n type: subscriptionType,\n };\n this.subscriptions[subscriptionType].set(subscriptionHandle, callback);\n\n return subscriptionHandle;\n }\n\n public subscribeToEvent<E extends AnyEventMessage>(\n subscriptionType: E['type'],\n callback: WebSocketServiceEventSubscriptionFunction<E>,\n ): SubscriptionHandle {\n // Forwards only MESSAGE frames that parse as json and carry the subscribed\n // type; anything else on the socket is some other protocol's traffic.\n const forwardMatchingMessage: WebSocketServiceSubscriptionFunction = (websocketService, event) => {\n if (!event) {\n return;\n }\n\n try {\n const parsedEvent: E = JSON.parse((event as MessageEvent).data);\n if (parsedEvent.type === subscriptionType) {\n callback(websocketService, parsedEvent);\n }\n } catch {\n // Not json: ignore.\n }\n };\n\n return this.subscribe(WebsocketServiceEvent.MESSAGE, forwardMatchingMessage);\n }\n\n public unsubscribe(subscriptionHandle: SubscriptionHandle) {\n this.subscriptions[subscriptionHandle.type].delete(subscriptionHandle);\n }\n\n public unsubscribeAll() {\n for (const subscriptionMap of Object.values(this.subscriptions)) {\n subscriptionMap.clear();\n }\n }\n\n private connect() {\n this.removeAllEventListeners();\n\n this.socket = new WebSocket(this.url);\n\n this.addEventListener(WebsocketServiceEvent.OPEN, this.onConnect.bind(this));\n this.addEventListener(WebsocketServiceEvent.CLOSE, this.onClose.bind(this));\n this.addEventListener(WebsocketServiceEvent.MESSAGE, this.onMessage.bind(this));\n this.addEventListener(WebsocketServiceEvent.ERROR, this.onError.bind(this));\n }\n\n private addEventListener(event: WebsocketServiceEvent, listener: WebSocketEventListenerFunction) {\n if (!this.socket) {\n return;\n }\n\n this.socket.addEventListener(event, listener);\n (this.eventListeners[event] ??= []).push(listener);\n }\n\n private removeAllEventListeners() {\n if (this.socket) {\n for (const [event, listeners] of Object.entries(this.eventListeners)) {\n for (const listener of listeners) {\n this.socket.removeEventListener(event, listener);\n }\n }\n }\n\n this.eventListeners = {};\n }\n\n private reconnectIfNotDestroyed() {\n const baseDelay = Math.min(WebsocketService.BASE_RECONNECT_MS * Math.pow(2, this.reconnectAttempts), WebsocketService.MAX_RECONNECT_MS);\n\n // Add jitter: ±25% of base delay to prevent thundering herd\n const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);\n const delay = Math.max(0, baseDelay + jitter);\n\n this.reconnectAttempts++;\n\n setTimeout(() => {\n if (!this.isDestroyed) {\n this.connect();\n }\n }, delay);\n }\n\n private onConnect() {\n this.reconnectAttempts = 0;\n this.notifySubscribers(WebsocketServiceEvent.OPEN);\n const messages = this.pendingMessages;\n this.pendingMessages = [];\n messages.forEach((message) => {\n this.send(message);\n });\n }\n\n private onClose() {\n this.removeAllEventListeners();\n this.reconnectIfNotDestroyed();\n this.notifySubscribers(WebsocketServiceEvent.CLOSE);\n }\n\n private onMessage(event: Event) {\n this.notifySubscribers(WebsocketServiceEvent.MESSAGE, event);\n }\n\n private onError(event: Event) {\n this.notifySubscribers(WebsocketServiceEvent.ERROR, event);\n }\n\n private notifySubscribers(subType: WebsocketServiceEvent, event?: Event) {\n this.subscriptions[subType].forEach((callback) => {\n callback(this, event);\n });\n }\n\n public send(data: WebsocketSendPayload) {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(data);\n } else {\n this.pendingMessages.push(data);\n }\n }\n\n public sendEvent<E extends AnyEventMessage>(event: E) {\n this.send(JSON.stringify(event));\n }\n}\n"]}
|
|
@@ -18,7 +18,7 @@ const DIVISIONS = [
|
|
|
18
18
|
*/
|
|
19
19
|
const formatTimeAgo = (date, now = new Date(), locale) => {
|
|
20
20
|
// An invalid date (e.g. new Date('')) has a NaN time, which Intl.RelativeTimeFormat
|
|
21
|
-
// rejects
|
|
21
|
+
// rejects: guard so callers never have to.
|
|
22
22
|
if (Number.isNaN(date.getTime())) {
|
|
23
23
|
return '';
|
|
24
24
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeAgo.js","sourceRoot":"","sources":["../../../src/tools/timeAgo.ts"],"names":[],"mappings":";;;AAKA,uEAAuE;AACvE,MAAM,SAAS,GAAe;IAC5B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;IAC7B,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE;IAC3B,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;IAClC,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC9B,EAAE,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE;CACpD,CAAC;AAEF;;;;GAIG;AACI,MAAM,aAAa,GAAG,CAAC,IAAU,EAAE,MAAY,IAAI,IAAI,EAAE,EAAE,MAA0B,EAAU,EAAE;IACtG,oFAAoF;IACpF,
|
|
1
|
+
{"version":3,"file":"timeAgo.js","sourceRoot":"","sources":["../../../src/tools/timeAgo.ts"],"names":[],"mappings":";;;AAKA,uEAAuE;AACvE,MAAM,SAAS,GAAe;IAC5B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;IAC7B,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE;IAC3B,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;IAClC,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC9B,EAAE,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE;CACpD,CAAC;AAEF;;;;GAIG;AACI,MAAM,aAAa,GAAG,CAAC,IAAU,EAAE,MAAY,IAAI,IAAI,EAAE,EAAE,MAA0B,EAAU,EAAE;IACtG,oFAAoF;IACpF,2CAA2C;IAC3C,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAE3E,wDAAwD;IACxD,IAAI,QAAQ,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;IAEvD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;YACzC,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC/D,CAAC;QACD,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AACzD,CAAC,CAAC;AApBW,QAAA,aAAa,iBAoBxB;AAEF;;;GAGG;AACI,MAAM,0BAA0B,GAAG,CAAC,IAAU,EAAE,MAAY,IAAI,IAAI,EAAE,EAAU,EAAE;IACvF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAElE,IAAI,OAAO,GAAG,EAAE,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC,CAAC,oBAAoB;IACnC,CAAC;IACD,IAAI,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;QACtB,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC,oBAAoB;IACxC,CAAC;IACD,IAAI,OAAO,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;QAC3B,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,kBAAkB;IAC3C,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,kBAAkB;AAChD,CAAC,CAAC;AAbW,QAAA,0BAA0B,8BAarC","sourcesContent":["type Division = {\n amount: number;\n unit: Intl.RelativeTimeFormatUnit;\n};\n\n// Each amount is \"how many of this unit make up one of the next unit\".\nconst DIVISIONS: Division[] = [\n { amount: 60, unit: 'seconds' },\n { amount: 60, unit: 'minutes' },\n { amount: 24, unit: 'hours' },\n { amount: 7, unit: 'days' },\n { amount: 4.34524, unit: 'weeks' },\n { amount: 12, unit: 'months' },\n { amount: Number.POSITIVE_INFINITY, unit: 'years' },\n];\n\n/**\n * Formats the distance between `date` and `now` as a relative phrase\n * (e.g. \"5 minutes ago\", \"in 2 hours\") using the native Intl.RelativeTimeFormat.\n * Past dates are negative, future dates are positive.\n */\nexport const formatTimeAgo = (date: Date, now: Date = new Date(), locale?: string | string[]): string => {\n // An invalid date (e.g. new Date('')) has a NaN time, which Intl.RelativeTimeFormat\n // rejects: guard so callers never have to.\n if (Number.isNaN(date.getTime())) {\n return '';\n }\n\n const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });\n\n // Seconds between the two dates; negative for the past.\n let duration = (date.getTime() - now.getTime()) / 1000;\n\n for (const division of DIVISIONS) {\n if (Math.abs(duration) < division.amount) {\n return formatter.format(Math.round(duration), division.unit);\n }\n duration /= division.amount;\n }\n\n return formatter.format(Math.round(duration), 'years');\n};\n\n/**\n * How long until the relative phrase could next change, so callers can\n * re-render no more often than necessary.\n */\nexport const getTimeAgoUpdateIntervalMs = (date: Date, now: Date = new Date()): number => {\n const seconds = Math.abs((date.getTime() - now.getTime()) / 1000);\n\n if (seconds < 60) {\n return 1000; // seconds tick over\n }\n if (seconds < 60 * 60) {\n return 60 * 1000; // minutes tick over\n }\n if (seconds < 60 * 60 * 24) {\n return 60 * 60 * 1000; // hours tick over\n }\n return 60 * 60 * 24 * 1000; // days or coarser\n};\n"]}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** Returns the array with duplicates removed, keeping the FIRST item for each key (SameValueZero key equality). */
|
|
2
|
+
export declare function uniqueBy<T>(array: T[], keyFn: (item: T) => unknown): T[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.uniqueBy = uniqueBy;
|
|
4
|
+
/** Returns the array with duplicates removed, keeping the FIRST item for each key (SameValueZero key equality). */
|
|
4
5
|
function uniqueBy(array, keyFn) {
|
|
5
6
|
const seen = new Set();
|
|
6
7
|
return array.filter((item) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uniqueBy.js","sourceRoot":"","sources":["../../../src/tools/uniqueBy.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"uniqueBy.js","sourceRoot":"","sources":["../../../src/tools/uniqueBy.ts"],"names":[],"mappings":";;AACA,4BAUC;AAXD,mHAAmH;AACnH,SAAgB,QAAQ,CAAI,KAAU,EAAE,KAA2B;IACjE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAW,CAAC;IAChC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAC3B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAClB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/** Returns the array with duplicates removed, keeping the FIRST item for each key (SameValueZero key equality). */\nexport function uniqueBy<T>(array: T[], keyFn: (item: T) => unknown): T[] {\n const seen = new Set<unknown>();\n return array.filter((item) => {\n const key = keyFn(item);\n if (seen.has(key)) {\n return false;\n }\n seen.add(key);\n return true;\n });\n}\n"]}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { QueryParamsGetActionRequester } from './QueryParamsGetActionRequesterTypes';
|
|
2
|
+
/** Reads ALL values of a query-string key from the current url (repeated keys are legal, hence string[]). Empty array when absent. */
|
|
2
3
|
export declare function askQueryParamsGet(key: string): QueryParamsGetActionRequester;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { QueryParamsActionType } from './QueryParamsActionType';
|
|
2
|
+
/** Reads ALL values of a query-string key from the current url (repeated keys are legal, hence string[]). Empty array when absent. */
|
|
2
3
|
export function* askQueryParamsGet(key) {
|
|
3
4
|
return yield { type: QueryParamsActionType.Get, payload: { key } };
|
|
4
5
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueryParamsGetActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsGetActionRequester.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,MAAM,SAAS,CAAC,CAAC,iBAAiB,CAAC,GAAW;IAC5C,OAAO,MAAM,EAAE,IAAI,EAAE,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC;AACrE,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsGetActionRequester } from './QueryParamsGetActionRequesterTypes';\n\nexport function* askQueryParamsGet(key: string): QueryParamsGetActionRequester {\n return yield { type: QueryParamsActionType.Get, payload: { key } };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"QueryParamsGetActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsGetActionRequester.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,sIAAsI;AACtI,MAAM,SAAS,CAAC,CAAC,iBAAiB,CAAC,GAAW;IAC5C,OAAO,MAAM,EAAE,IAAI,EAAE,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC;AACrE,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsGetActionRequester } from './QueryParamsGetActionRequesterTypes';\n\n/** Reads ALL values of a query-string key from the current url (repeated keys are legal, hence string[]). Empty array when absent. */\nexport function* askQueryParamsGet(key: string): QueryParamsGetActionRequester {\n return yield { type: QueryParamsActionType.Get, payload: { key } };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueryParamsGetAllActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsGetAllActionRequester.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,MAAM,SAAS,CAAC,CAAC,oBAAoB;IACnC,OAAO,MAAM,EAAE,IAAI,EAAE,qBAAqB,CAAC,MAAM,EAAE,CAAC;AACtD,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsGetAllActionRequester } from './QueryParamsGetAllActionRequesterTypes';\n\nexport function* askQueryParamsGetAll(): QueryParamsGetAllActionRequester {\n return yield { type: QueryParamsActionType.GetAll };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"QueryParamsGetAllActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsGetAllActionRequester.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,yFAAyF;AACzF,MAAM,SAAS,CAAC,CAAC,oBAAoB;IACnC,OAAO,MAAM,EAAE,IAAI,EAAE,qBAAqB,CAAC,MAAM,EAAE,CAAC;AACtD,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsGetAllActionRequester } from './QueryParamsGetAllActionRequesterTypes';\n\n/** Reads the whole query string of the current url as key -> all values for that key. */\nexport function* askQueryParamsGetAll(): QueryParamsGetAllActionRequester {\n return yield { type: QueryParamsActionType.GetAll };\n}\n"]}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { QueryParamsSetActionRequester } from './QueryParamsSetActionRequesterTypes';
|
|
2
|
+
/**
|
|
3
|
+
* Replaces a query-string key's values in the current url; an empty array removes
|
|
4
|
+
* the key. By default the url is swapped in place (replaceState); pass
|
|
5
|
+
* createHistoryEntry to make the change a back-button stop (pushState).
|
|
6
|
+
*/
|
|
2
7
|
export declare function askQueryParamsSet(key: string, values: string[], createHistoryEntry?: boolean): QueryParamsSetActionRequester;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { QueryParamsActionType } from './QueryParamsActionType';
|
|
2
|
+
/**
|
|
3
|
+
* Replaces a query-string key's values in the current url; an empty array removes
|
|
4
|
+
* the key. By default the url is swapped in place (replaceState); pass
|
|
5
|
+
* createHistoryEntry to make the change a back-button stop (pushState).
|
|
6
|
+
*/
|
|
2
7
|
export function* askQueryParamsSet(key, values, createHistoryEntry = false) {
|
|
3
8
|
return yield { type: QueryParamsActionType.Set, payload: { key, values, createHistoryEntry } };
|
|
4
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueryParamsSetActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsSetActionRequester.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,MAAM,SAAS,CAAC,CAAC,iBAAiB,CAAC,GAAW,EAAE,MAAgB,EAAE,qBAA8B,KAAK;IACnG,OAAO,MAAM,EAAE,IAAI,EAAE,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC;AACjG,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsSetActionRequester } from './QueryParamsSetActionRequesterTypes';\n\nexport function* askQueryParamsSet(key: string, values: string[], createHistoryEntry: boolean = false): QueryParamsSetActionRequester {\n return yield { type: QueryParamsActionType.Set, payload: { key, values, createHistoryEntry } };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"QueryParamsSetActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/queryParams/QueryParamsSetActionRequester.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAGhE;;;;GAIG;AACH,MAAM,SAAS,CAAC,CAAC,iBAAiB,CAAC,GAAW,EAAE,MAAgB,EAAE,qBAA8B,KAAK;IACnG,OAAO,MAAM,EAAE,IAAI,EAAE,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC;AACjG,CAAC","sourcesContent":["import { QueryParamsActionType } from './QueryParamsActionType';\nimport { QueryParamsSetActionRequester } from './QueryParamsSetActionRequesterTypes';\n\n/**\n * Replaces a query-string key's values in the current url; an empty array removes\n * the key. By default the url is swapped in place (replaceState); pass\n * createHistoryEntry to make the change a back-button stop (pushState).\n */\nexport function* askQueryParamsSet(key: string, values: string[], createHistoryEntry: boolean = false): QueryParamsSetActionRequester {\n return yield { type: QueryParamsActionType.Set, payload: { key, values, createHistoryEntry } };\n}\n"]}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { WindowGetLocationActionRequester } from './WindowGetLocationActionRequesterTypes';
|
|
2
|
+
/** Reads the current `window.location` as a plain serializable snapshot (never the live Location object). */
|
|
2
3
|
export declare function askWindowGetLocation(): WindowGetLocationActionRequester;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WindowGetLocationActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/window/WindowGetLocationActionRequester.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,MAAM,SAAS,CAAC,CAAC,oBAAoB;IACnC,OAAO,MAAM,EAAE,IAAI,EAAE,gBAAgB,CAAC,WAAW,EAAE,CAAC;AACtD,CAAC","sourcesContent":["import { WindowActionType } from './WindowActionType';\nimport { WindowGetLocationActionRequester } from './WindowGetLocationActionRequesterTypes';\n\nexport function* askWindowGetLocation(): WindowGetLocationActionRequester {\n return yield { type: WindowActionType.GetLocation };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WindowGetLocationActionRequester.js","sourceRoot":"","sources":["../../../../src/actions/window/WindowGetLocationActionRequester.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGtD,6GAA6G;AAC7G,MAAM,SAAS,CAAC,CAAC,oBAAoB;IACnC,OAAO,MAAM,EAAE,IAAI,EAAE,gBAAgB,CAAC,WAAW,EAAE,CAAC;AACtD,CAAC","sourcesContent":["import { WindowActionType } from './WindowActionType';\nimport { WindowGetLocationActionRequester } from './WindowGetLocationActionRequesterTypes';\n\n/** Reads the current `window.location` as a plain serializable snapshot (never the live Location object). */\nexport function* askWindowGetLocation(): WindowGetLocationActionRequester {\n return yield { type: WindowActionType.GetLocation };\n}\n"]}
|
|
@@ -7,25 +7,25 @@
|
|
|
7
7
|
// with a nonce'd entry URL clears them and guarantees a fresh manifest fetch.
|
|
8
8
|
// 2. The container global: loadEntryScript short-circuits on `window[globalName]`
|
|
9
9
|
// BEFORE fetching the entry, silently reusing the old container. Cleared by
|
|
10
|
-
// ASSIGNING undefined, not `delete
|
|
10
|
+
// ASSIGNING undefined, not `delete`: the global is a top-level `var` from a
|
|
11
11
|
// classic script, so the window property is non-configurable and delete throws;
|
|
12
12
|
// the runtime's short-circuit is a truthiness check, so undefined is enough.
|
|
13
13
|
// 3. The bundler's chunk-loading global (`rspackChunk<name>` / `webpackChunk<name>`):
|
|
14
|
-
// shared by BUILD NAME
|
|
14
|
+
// shared by BUILD NAME: every chunk file of every previously loaded copy of the
|
|
15
15
|
// remote pushed its modules into it, and a fresh container drains that array on
|
|
16
16
|
// boot. Chunk ids are stable across builds, so without a reset the new container
|
|
17
|
-
// sees the old build's chunks as already loaded and NEVER fetches its own files
|
|
17
|
+
// sees the old build's chunks as already loaded and NEVER fetches its own files:
|
|
18
18
|
// the swap re-executes the OLD code under a new identity. The old container keeps
|
|
19
19
|
// its captured reference to the old array, so it is unaffected.
|
|
20
20
|
// 4. The DOM script-tag dedup: both the MF sdk and the chunk loader reuse an existing
|
|
21
21
|
// <script> tag whose src matches (never re-executing it), so every one of the
|
|
22
22
|
// remote's tags (entry AND chunks) is swept.
|
|
23
23
|
// 5. The browser HTTP cache of the fixed-name remoteEntry.js: a createScript runtime
|
|
24
|
-
// plugin serves the entry script with the reload nonce on its src
|
|
24
|
+
// plugin serves the entry script with the reload nonce on its src, a never-seen
|
|
25
25
|
// URL that no cache layer can answer.
|
|
26
26
|
//
|
|
27
27
|
// The MF runtime bindings are INJECTED (see FederationRuntimeApi): they must be the
|
|
28
|
-
// host bundle's own instance
|
|
28
|
+
// host bundle's own instance. The bundler aliases '@module-federation/enhanced/runtime'
|
|
29
29
|
// onto the runtime it embeds, and importing a second copy here would target the wrong
|
|
30
30
|
// (or no) instance. Injection also keeps this package dependency-free.
|
|
31
31
|
// Remotes with a reload in flight: mf container name -> current reload nonce.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forceReloadFederatedRemote.js","sourceRoot":"","sources":["../../../src/federation/forceReloadFederatedRemote.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,oFAAoF;AACpF,mFAAmF;AACnF,iCAAiC;AACjC,EAAE;AACF,qFAAqF;AACrF,mFAAmF;AACnF,oFAAoF;AACpF,iFAAiF;AACjF,
|
|
1
|
+
{"version":3,"file":"forceReloadFederatedRemote.js","sourceRoot":"","sources":["../../../src/federation/forceReloadFederatedRemote.ts"],"names":[],"mappings":"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","sourcesContent":["// Hot-swap support for a module-federation remote: make the NEXT loadRemote of a\n// given remote genuinely re-fetch and re-execute its current deployment, instead of\n// serving any of the FIVE stale layers a naive force re-registration leaves behind\n// (each one found the hard way):\n//\n// 1. The MF runtime caches (moduleCache, manifest snapshot): force re-registration\n// with a nonce'd entry URL clears them and guarantees a fresh manifest fetch.\n// 2. The container global: loadEntryScript short-circuits on `window[globalName]`\n// BEFORE fetching the entry, silently reusing the old container. Cleared by\n// ASSIGNING undefined, not `delete`: the global is a top-level `var` from a\n// classic script, so the window property is non-configurable and delete throws;\n// the runtime's short-circuit is a truthiness check, so undefined is enough.\n// 3. The bundler's chunk-loading global (`rspackChunk<name>` / `webpackChunk<name>`):\n// shared by BUILD NAME: every chunk file of every previously loaded copy of the\n// remote pushed its modules into it, and a fresh container drains that array on\n// boot. Chunk ids are stable across builds, so without a reset the new container\n// sees the old build's chunks as already loaded and NEVER fetches its own files:\n// the swap re-executes the OLD code under a new identity. The old container keeps\n// its captured reference to the old array, so it is unaffected.\n// 4. The DOM script-tag dedup: both the MF sdk and the chunk loader reuse an existing\n// <script> tag whose src matches (never re-executing it), so every one of the\n// remote's tags (entry AND chunks) is swept.\n// 5. The browser HTTP cache of the fixed-name remoteEntry.js: a createScript runtime\n// plugin serves the entry script with the reload nonce on its src, a never-seen\n// URL that no cache layer can answer.\n//\n// The MF runtime bindings are INJECTED (see FederationRuntimeApi): they must be the\n// host bundle's own instance. The bundler aliases '@module-federation/enhanced/runtime'\n// onto the runtime it embeds, and importing a second copy here would target the wrong\n// (or no) instance. Injection also keeps this package dependency-free.\n\n// Universal function supertype: any concrete MF runtime function is assignable.\ntype InjectedFunction = (...args: never[]) => unknown;\n\nexport type FederationRuntimeApi = {\n getInstance: InjectedFunction;\n registerRemotes: InjectedFunction;\n registerPlugins: InjectedFunction;\n};\n\ntype FederatedRemote = { name: string; alias?: string; entry: string };\n\ntype ResolvedFederationRuntime = {\n getInstance: () => { options: { remotes: (FederatedRemote | { name: string })[] } } | null | undefined;\n registerRemotes: (remotes: FederatedRemote[], options?: { force?: boolean }) => void;\n registerPlugins: (\n plugins: { name: string; createScript: (args: { url: string; remoteInfo?: { name: string } }) => HTMLScriptElement | undefined }[],\n ) => void;\n};\n\n// Remotes with a reload in flight: mf container name -> current reload nonce.\n// Module scope is safe: quidproquo-web is a module-federation shared singleton, so\n// one copy serves the page.\nconst remoteReloadNonces = new Map<string, number>();\n\nlet cacheBustPluginRegistered = false;\n\nconst ensureCacheBustPlugin = (registerPlugins: ResolvedFederationRuntime['registerPlugins']): void => {\n if (cacheBustPluginRegistered) {\n return;\n }\n cacheBustPluginRegistered = true;\n\n registerPlugins([\n {\n name: 'qpq-federated-reload-cache-bust',\n createScript(args: { url: string; remoteInfo?: { name: string } }) {\n const reloadNonce = args.remoteInfo ? remoteReloadNonces.get(args.remoteInfo.name) : undefined;\n\n if (!reloadNonce) {\n return undefined;\n }\n\n const script = document.createElement('script');\n script.src = `${args.url.split('?')[0]}?reload=${reloadNonce}`;\n return script;\n },\n },\n ]);\n};\n\n/**\n * Prepare a federated remote for a hot-swap: after this call, the next\n * loadRemote() of the remote fetches and executes its CURRENT deployment.\n * `reloadNonce` must be a fresh value per swap (a per-remote counter).\n * Returns false (with a console warning) when the remote is not registered.\n */\nexport const forceReloadFederatedRemote = (runtimeApi: FederationRuntimeApi, remoteName: string, reloadNonce: number): boolean => {\n const { getInstance, registerRemotes, registerPlugins } = runtimeApi as ResolvedFederationRuntime;\n\n const remote = getInstance()?.options.remotes.find(\n (candidate) => ('alias' in candidate && candidate.alias === remoteName) || candidate.name === remoteName,\n );\n\n if (!remote || !('entry' in remote)) {\n console.warn(`[qpq-federated-reload] remote \"${remoteName}\" not found in MF runtime options - a reload will serve the cached bundle`);\n return false;\n }\n\n ensureCacheBustPlugin(registerPlugins);\n remoteReloadNonces.set(remote.name, reloadNonce);\n\n // Layer 2: the container global.\n (globalThis as Record<string, unknown>)[remote.name] = undefined;\n\n // Layer 3: the bundler chunk-loading globals (rspack and webpack builds).\n (globalThis as Record<string, unknown>)[`rspackChunk${remote.name}`] = undefined;\n (globalThis as Record<string, unknown>)[`webpackChunk${remote.name}`] = undefined;\n\n const baseEntry = remote.entry.split('?')[0];\n const entryDir = baseEntry.slice(0, baseEntry.lastIndexOf('/'));\n\n // Layer 4: sweep ALL of this remote's script tags (entry AND chunks).\n document.querySelectorAll<HTMLScriptElement>('script[src]').forEach((scriptTag) => {\n const src = scriptTag.getAttribute('src') ?? '';\n if (src.startsWith(entryDir)) {\n scriptTag.remove();\n }\n });\n\n // Layer 1: force re-registration with the nonce'd entry.\n registerRemotes([{ ...remote, entry: `${baseEntry}?reload=${reloadNonce}` }], { force: true });\n\n return true;\n};\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyEventMessage,
|
|
1
|
+
import { AnyEventMessage, Nullable } from 'quidproquo-core';
|
|
2
2
|
export declare enum WebsocketServiceEvent {
|
|
3
3
|
OPEN = "open",
|
|
4
4
|
CLOSE = "close",
|
|
@@ -8,9 +8,14 @@ export declare enum WebsocketServiceEvent {
|
|
|
8
8
|
export type SubscriptionHandle = {
|
|
9
9
|
type: WebsocketServiceEvent;
|
|
10
10
|
};
|
|
11
|
-
export type WebSocketServiceSubscriptionFunction = (websocketService: WebsocketService, event?: Event) =>
|
|
11
|
+
export type WebSocketServiceSubscriptionFunction = (websocketService: WebsocketService, event?: Event) => void;
|
|
12
12
|
type WebsocketSendPayload = string | ArrayBufferLike | Blob | ArrayBufferView;
|
|
13
13
|
export type WebSocketServiceEventSubscriptionFunction<E extends AnyEventMessage> = (websocketService: WebsocketService, event: E) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Browser WebSocket wrapper that reconnects automatically (exponential backoff
|
|
16
|
+
* with jitter), queues sends while disconnected, and fans events out to
|
|
17
|
+
* subscribers. Call destroy() to stop reconnecting and release the socket.
|
|
18
|
+
*/
|
|
14
19
|
export declare class WebsocketService {
|
|
15
20
|
readonly url: string;
|
|
16
21
|
private socket;
|
|
@@ -26,7 +31,7 @@ export declare class WebsocketService {
|
|
|
26
31
|
close(): void;
|
|
27
32
|
isConnected(): boolean;
|
|
28
33
|
hasBeenDestroyed(): boolean;
|
|
29
|
-
getSocket(): WebSocket
|
|
34
|
+
getSocket(): Nullable<WebSocket>;
|
|
30
35
|
subscribe(subscriptionType: WebsocketServiceEvent, callback: WebSocketServiceSubscriptionFunction): SubscriptionHandle;
|
|
31
36
|
subscribeToEvent<E extends AnyEventMessage>(subscriptionType: E['type'], callback: WebSocketServiceEventSubscriptionFunction<E>): SubscriptionHandle;
|
|
32
37
|
unsubscribe(subscriptionHandle: SubscriptionHandle): void;
|
|
@@ -41,6 +46,6 @@ export declare class WebsocketService {
|
|
|
41
46
|
private onError;
|
|
42
47
|
private notifySubscribers;
|
|
43
48
|
send(data: WebsocketSendPayload): void;
|
|
44
|
-
sendEvent<E extends
|
|
49
|
+
sendEvent<E extends AnyEventMessage>(event: E): void;
|
|
45
50
|
}
|
|
46
51
|
export {};
|
|
@@ -5,6 +5,11 @@ export var WebsocketServiceEvent;
|
|
|
5
5
|
WebsocketServiceEvent["MESSAGE"] = "message";
|
|
6
6
|
WebsocketServiceEvent["ERROR"] = "error";
|
|
7
7
|
})(WebsocketServiceEvent || (WebsocketServiceEvent = {}));
|
|
8
|
+
/**
|
|
9
|
+
* Browser WebSocket wrapper that reconnects automatically (exponential backoff
|
|
10
|
+
* with jitter), queues sends while disconnected, and fans events out to
|
|
11
|
+
* subscribers. Call destroy() to stop reconnecting and release the socket.
|
|
12
|
+
*/
|
|
8
13
|
export class WebsocketService {
|
|
9
14
|
url;
|
|
10
15
|
socket = null;
|
|
@@ -49,29 +54,30 @@ export class WebsocketService {
|
|
|
49
54
|
return subscriptionHandle;
|
|
50
55
|
}
|
|
51
56
|
subscribeToEvent(subscriptionType, callback) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// Must have been some other message format / type do nothing.
|
|
57
|
+
// Forwards only MESSAGE frames that parse as json and carry the subscribed
|
|
58
|
+
// type; anything else on the socket is some other protocol's traffic.
|
|
59
|
+
const forwardMatchingMessage = (websocketService, event) => {
|
|
60
|
+
if (!event) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const parsedEvent = JSON.parse(event.data);
|
|
65
|
+
if (parsedEvent.type === subscriptionType) {
|
|
66
|
+
callback(websocketService, parsedEvent);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
|
-
|
|
69
|
+
catch {
|
|
70
|
+
// Not json: ignore.
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return this.subscribe(WebsocketServiceEvent.MESSAGE, forwardMatchingMessage);
|
|
66
74
|
}
|
|
67
75
|
unsubscribe(subscriptionHandle) {
|
|
68
76
|
this.subscriptions[subscriptionHandle.type].delete(subscriptionHandle);
|
|
69
77
|
}
|
|
70
78
|
unsubscribeAll() {
|
|
71
|
-
for (const
|
|
72
|
-
|
|
73
|
-
this.subscriptions[subscriptionType].clear();
|
|
74
|
-
}
|
|
79
|
+
for (const subscriptionMap of Object.values(this.subscriptions)) {
|
|
80
|
+
subscriptionMap.clear();
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
connect() {
|
|
@@ -83,21 +89,17 @@ export class WebsocketService {
|
|
|
83
89
|
this.addEventListener(WebsocketServiceEvent.ERROR, this.onError.bind(this));
|
|
84
90
|
}
|
|
85
91
|
addEventListener(event, listener) {
|
|
86
|
-
if (this.socket) {
|
|
87
|
-
|
|
88
|
-
if (!this.eventListeners[event]) {
|
|
89
|
-
this.eventListeners[event] = [];
|
|
90
|
-
}
|
|
91
|
-
this.eventListeners[event].push(listener);
|
|
92
|
+
if (!this.socket) {
|
|
93
|
+
return;
|
|
92
94
|
}
|
|
95
|
+
this.socket.addEventListener(event, listener);
|
|
96
|
+
(this.eventListeners[event] ??= []).push(listener);
|
|
93
97
|
}
|
|
94
98
|
removeAllEventListeners() {
|
|
95
99
|
if (this.socket) {
|
|
96
|
-
for (const event
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.socket.removeEventListener(event, listener);
|
|
100
|
-
}
|
|
100
|
+
for (const [event, listeners] of Object.entries(this.eventListeners)) {
|
|
101
|
+
for (const listener of listeners) {
|
|
102
|
+
this.socket.removeEventListener(event, listener);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
}
|
|
@@ -109,7 +111,6 @@ export class WebsocketService {
|
|
|
109
111
|
const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);
|
|
110
112
|
const delay = Math.max(0, baseDelay + jitter);
|
|
111
113
|
this.reconnectAttempts++;
|
|
112
|
-
console.log('Delay: ', delay);
|
|
113
114
|
setTimeout(() => {
|
|
114
115
|
if (!this.isDestroyed) {
|
|
115
116
|
this.connect();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsocketService.js","sourceRoot":"","sources":["../../../src/services/WebsocketService.ts"],"names":[],"mappings":"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","sourcesContent":["import { AnyEventMessage, EventMessage } from 'quidproquo-core';\n\n// Private / internal types\ntype WebSocketEventListenerFunction = (this: WebSocket, ev: Event) => any;\n\nexport enum WebsocketServiceEvent {\n OPEN = 'open',\n CLOSE = 'close',\n MESSAGE = 'message',\n ERROR = 'error',\n}\n\nexport type SubscriptionHandle = {\n type: WebsocketServiceEvent;\n};\n\nexport type WebSocketServiceSubscriptionFunction = (websocketService: WebsocketService, event?: Event) => any;\n\ntype SubscriptionMap = Map<SubscriptionHandle, WebSocketServiceSubscriptionFunction>;\n\ntype Subscriptions = {\n [key in WebsocketServiceEvent]: SubscriptionMap;\n};\n\ntype WebsocketSendPayload = string | ArrayBufferLike | Blob | ArrayBufferView;\n\nexport type WebSocketServiceEventSubscriptionFunction<E extends AnyEventMessage> = (websocketService: WebsocketService, event: E) => void;\n\nexport class WebsocketService {\n public readonly url: string;\n private socket: WebSocket | null = null;\n private eventListeners: { [key: string]: WebSocketEventListenerFunction[] } = {};\n private isDestroyed: boolean = false;\n private pendingMessages: WebsocketSendPayload[] = [];\n\n private reconnectAttempts: number = 0;\n private static readonly BASE_RECONNECT_MS = 1000;\n private static readonly MAX_RECONNECT_MS = 60_000;\n\n private subscriptions: Subscriptions = {\n [WebsocketServiceEvent.OPEN]: new Map(),\n [WebsocketServiceEvent.CLOSE]: new Map(),\n [WebsocketServiceEvent.MESSAGE]: new Map(),\n [WebsocketServiceEvent.ERROR]: new Map(),\n };\n\n constructor(url: string) {\n this.url = url;\n\n this.connect();\n }\n\n public destroy() {\n this.isDestroyed = true;\n this.unsubscribeAll();\n this.socket?.close();\n }\n\n public close() {\n this.socket?.close();\n }\n\n public isConnected() {\n return this.socket?.readyState === WebSocket.OPEN;\n }\n\n public hasBeenDestroyed() {\n return this.isDestroyed;\n }\n\n public getSocket() {\n return this.socket;\n }\n\n public subscribe(subscriptionType: WebsocketServiceEvent, callback: WebSocketServiceSubscriptionFunction): SubscriptionHandle {\n const subscriptionHandle: SubscriptionHandle = {\n type: subscriptionType,\n };\n this.subscriptions[subscriptionType].set(subscriptionHandle, callback);\n\n return subscriptionHandle;\n }\n\n public subscribeToEvent<E extends AnyEventMessage>(\n subscriptionType: E['type'],\n callback: WebSocketServiceEventSubscriptionFunction<E>,\n ): SubscriptionHandle {\n return this.subscribe(WebsocketServiceEvent.MESSAGE, (websocketService: WebsocketService, event?: Event) => {\n if (event) {\n const data = (event as MessageEvent).data;\n try {\n const parsedEvent: E = JSON.parse(data);\n if (parsedEvent.type === subscriptionType) {\n callback(websocketService, parsedEvent);\n }\n } catch (e) {\n // Must have been some other message format / type do nothing.\n }\n }\n });\n }\n\n public unsubscribe(subscriptionHandle: SubscriptionHandle) {\n this.subscriptions[subscriptionHandle.type].delete(subscriptionHandle);\n }\n\n public unsubscribeAll() {\n for (const subscriptionType in this.subscriptions) {\n if (Object.prototype.hasOwnProperty.call(this.subscriptions, subscriptionType)) {\n this.subscriptions[subscriptionType as WebsocketServiceEvent].clear();\n }\n }\n }\n\n private connect() {\n this.removeAllEventListeners();\n\n this.socket = new WebSocket(this.url);\n\n this.addEventListener(WebsocketServiceEvent.OPEN, this.onConnect.bind(this));\n this.addEventListener(WebsocketServiceEvent.CLOSE, this.onClose.bind(this));\n this.addEventListener(WebsocketServiceEvent.MESSAGE, this.onMessage.bind(this));\n this.addEventListener(WebsocketServiceEvent.ERROR, this.onError.bind(this));\n }\n\n private addEventListener(event: WebsocketServiceEvent, listener: WebSocketEventListenerFunction) {\n if (this.socket) {\n this.socket.addEventListener(event, listener);\n\n if (!this.eventListeners[event]) {\n this.eventListeners[event] = [];\n }\n\n this.eventListeners[event].push(listener);\n }\n }\n\n private removeAllEventListeners() {\n if (this.socket) {\n for (const event in this.eventListeners) {\n if (Object.prototype.hasOwnProperty.call(this.eventListeners, event)) {\n for (const listener of this.eventListeners[event]) {\n this.socket.removeEventListener(event, listener);\n }\n }\n }\n }\n\n this.eventListeners = {};\n }\n\n private reconnectIfNotDestroyed() {\n const baseDelay = Math.min(WebsocketService.BASE_RECONNECT_MS * Math.pow(2, this.reconnectAttempts), WebsocketService.MAX_RECONNECT_MS);\n\n // Add jitter: ±25% of base delay to prevent thundering herd\n const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);\n const delay = Math.max(0, baseDelay + jitter);\n\n this.reconnectAttempts++;\n\n console.log('Delay: ', delay);\n\n setTimeout(() => {\n if (!this.isDestroyed) {\n this.connect();\n }\n }, delay);\n }\n\n private onConnect() {\n this.reconnectAttempts = 0;\n this.notifySubscribers(WebsocketServiceEvent.OPEN);\n const messages = this.pendingMessages;\n this.pendingMessages = [];\n messages.forEach((message) => {\n this.send(message);\n });\n }\n\n private onClose() {\n this.removeAllEventListeners();\n this.reconnectIfNotDestroyed();\n this.notifySubscribers(WebsocketServiceEvent.CLOSE);\n }\n\n private onMessage(event: Event) {\n this.notifySubscribers(WebsocketServiceEvent.MESSAGE, event);\n }\n\n private onError(event: Event) {\n this.notifySubscribers(WebsocketServiceEvent.ERROR, event);\n }\n\n private notifySubscribers(subType: WebsocketServiceEvent, event?: Event) {\n this.subscriptions[subType].forEach((callback) => {\n callback(this, event);\n });\n }\n\n public send(data: WebsocketSendPayload) {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(data);\n } else {\n this.pendingMessages.push(data);\n }\n }\n\n public sendEvent<E extends EventMessage<any, string | number>>(event: E) {\n this.send(JSON.stringify(event));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WebsocketService.js","sourceRoot":"","sources":["../../../src/services/WebsocketService.ts"],"names":[],"mappings":"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","sourcesContent":["import { AnyEventMessage, Nullable } from 'quidproquo-core';\n\ntype WebSocketEventListenerFunction = (this: WebSocket, ev: Event) => void;\n\nexport enum WebsocketServiceEvent {\n OPEN = 'open',\n CLOSE = 'close',\n MESSAGE = 'message',\n ERROR = 'error',\n}\n\nexport type SubscriptionHandle = {\n type: WebsocketServiceEvent;\n};\n\nexport type WebSocketServiceSubscriptionFunction = (websocketService: WebsocketService, event?: Event) => void;\n\ntype SubscriptionMap = Map<SubscriptionHandle, WebSocketServiceSubscriptionFunction>;\n\ntype Subscriptions = {\n [key in WebsocketServiceEvent]: SubscriptionMap;\n};\n\ntype WebsocketSendPayload = string | ArrayBufferLike | Blob | ArrayBufferView;\n\nexport type WebSocketServiceEventSubscriptionFunction<E extends AnyEventMessage> = (websocketService: WebsocketService, event: E) => void;\n\n/**\n * Browser WebSocket wrapper that reconnects automatically (exponential backoff\n * with jitter), queues sends while disconnected, and fans events out to\n * subscribers. Call destroy() to stop reconnecting and release the socket.\n */\nexport class WebsocketService {\n public readonly url: string;\n private socket: Nullable<WebSocket> = null;\n private eventListeners: Partial<Record<WebsocketServiceEvent, WebSocketEventListenerFunction[]>> = {};\n private isDestroyed: boolean = false;\n private pendingMessages: WebsocketSendPayload[] = [];\n\n private reconnectAttempts: number = 0;\n private static readonly BASE_RECONNECT_MS = 1000;\n private static readonly MAX_RECONNECT_MS = 60_000;\n\n private subscriptions: Subscriptions = {\n [WebsocketServiceEvent.OPEN]: new Map(),\n [WebsocketServiceEvent.CLOSE]: new Map(),\n [WebsocketServiceEvent.MESSAGE]: new Map(),\n [WebsocketServiceEvent.ERROR]: new Map(),\n };\n\n constructor(url: string) {\n this.url = url;\n\n this.connect();\n }\n\n public destroy() {\n this.isDestroyed = true;\n this.unsubscribeAll();\n this.socket?.close();\n }\n\n public close() {\n this.socket?.close();\n }\n\n public isConnected() {\n return this.socket?.readyState === WebSocket.OPEN;\n }\n\n public hasBeenDestroyed() {\n return this.isDestroyed;\n }\n\n public getSocket() {\n return this.socket;\n }\n\n public subscribe(subscriptionType: WebsocketServiceEvent, callback: WebSocketServiceSubscriptionFunction): SubscriptionHandle {\n const subscriptionHandle: SubscriptionHandle = {\n type: subscriptionType,\n };\n this.subscriptions[subscriptionType].set(subscriptionHandle, callback);\n\n return subscriptionHandle;\n }\n\n public subscribeToEvent<E extends AnyEventMessage>(\n subscriptionType: E['type'],\n callback: WebSocketServiceEventSubscriptionFunction<E>,\n ): SubscriptionHandle {\n // Forwards only MESSAGE frames that parse as json and carry the subscribed\n // type; anything else on the socket is some other protocol's traffic.\n const forwardMatchingMessage: WebSocketServiceSubscriptionFunction = (websocketService, event) => {\n if (!event) {\n return;\n }\n\n try {\n const parsedEvent: E = JSON.parse((event as MessageEvent).data);\n if (parsedEvent.type === subscriptionType) {\n callback(websocketService, parsedEvent);\n }\n } catch {\n // Not json: ignore.\n }\n };\n\n return this.subscribe(WebsocketServiceEvent.MESSAGE, forwardMatchingMessage);\n }\n\n public unsubscribe(subscriptionHandle: SubscriptionHandle) {\n this.subscriptions[subscriptionHandle.type].delete(subscriptionHandle);\n }\n\n public unsubscribeAll() {\n for (const subscriptionMap of Object.values(this.subscriptions)) {\n subscriptionMap.clear();\n }\n }\n\n private connect() {\n this.removeAllEventListeners();\n\n this.socket = new WebSocket(this.url);\n\n this.addEventListener(WebsocketServiceEvent.OPEN, this.onConnect.bind(this));\n this.addEventListener(WebsocketServiceEvent.CLOSE, this.onClose.bind(this));\n this.addEventListener(WebsocketServiceEvent.MESSAGE, this.onMessage.bind(this));\n this.addEventListener(WebsocketServiceEvent.ERROR, this.onError.bind(this));\n }\n\n private addEventListener(event: WebsocketServiceEvent, listener: WebSocketEventListenerFunction) {\n if (!this.socket) {\n return;\n }\n\n this.socket.addEventListener(event, listener);\n (this.eventListeners[event] ??= []).push(listener);\n }\n\n private removeAllEventListeners() {\n if (this.socket) {\n for (const [event, listeners] of Object.entries(this.eventListeners)) {\n for (const listener of listeners) {\n this.socket.removeEventListener(event, listener);\n }\n }\n }\n\n this.eventListeners = {};\n }\n\n private reconnectIfNotDestroyed() {\n const baseDelay = Math.min(WebsocketService.BASE_RECONNECT_MS * Math.pow(2, this.reconnectAttempts), WebsocketService.MAX_RECONNECT_MS);\n\n // Add jitter: ±25% of base delay to prevent thundering herd\n const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);\n const delay = Math.max(0, baseDelay + jitter);\n\n this.reconnectAttempts++;\n\n setTimeout(() => {\n if (!this.isDestroyed) {\n this.connect();\n }\n }, delay);\n }\n\n private onConnect() {\n this.reconnectAttempts = 0;\n this.notifySubscribers(WebsocketServiceEvent.OPEN);\n const messages = this.pendingMessages;\n this.pendingMessages = [];\n messages.forEach((message) => {\n this.send(message);\n });\n }\n\n private onClose() {\n this.removeAllEventListeners();\n this.reconnectIfNotDestroyed();\n this.notifySubscribers(WebsocketServiceEvent.CLOSE);\n }\n\n private onMessage(event: Event) {\n this.notifySubscribers(WebsocketServiceEvent.MESSAGE, event);\n }\n\n private onError(event: Event) {\n this.notifySubscribers(WebsocketServiceEvent.ERROR, event);\n }\n\n private notifySubscribers(subType: WebsocketServiceEvent, event?: Event) {\n this.subscriptions[subType].forEach((callback) => {\n callback(this, event);\n });\n }\n\n public send(data: WebsocketSendPayload) {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(data);\n } else {\n this.pendingMessages.push(data);\n }\n }\n\n public sendEvent<E extends AnyEventMessage>(event: E) {\n this.send(JSON.stringify(event));\n }\n}\n"]}
|
package/lib/esm/tools/timeAgo.js
CHANGED
|
@@ -15,7 +15,7 @@ const DIVISIONS = [
|
|
|
15
15
|
*/
|
|
16
16
|
export const formatTimeAgo = (date, now = new Date(), locale) => {
|
|
17
17
|
// An invalid date (e.g. new Date('')) has a NaN time, which Intl.RelativeTimeFormat
|
|
18
|
-
// rejects
|
|
18
|
+
// rejects: guard so callers never have to.
|
|
19
19
|
if (Number.isNaN(date.getTime())) {
|
|
20
20
|
return '';
|
|
21
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeAgo.js","sourceRoot":"","sources":["../../../src/tools/timeAgo.ts"],"names":[],"mappings":"AAKA,uEAAuE;AACvE,MAAM,SAAS,GAAe;IAC5B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;IAC7B,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE;IAC3B,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;IAClC,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC9B,EAAE,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE;CACpD,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAU,EAAE,MAAY,IAAI,IAAI,EAAE,EAAE,MAA0B,EAAU,EAAE;IACtG,oFAAoF;IACpF,
|
|
1
|
+
{"version":3,"file":"timeAgo.js","sourceRoot":"","sources":["../../../src/tools/timeAgo.ts"],"names":[],"mappings":"AAKA,uEAAuE;AACvE,MAAM,SAAS,GAAe;IAC5B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/B,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;IAC7B,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE;IAC3B,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;IAClC,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC9B,EAAE,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE;CACpD,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAU,EAAE,MAAY,IAAI,IAAI,EAAE,EAAE,MAA0B,EAAU,EAAE;IACtG,oFAAoF;IACpF,2CAA2C;IAC3C,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAE3E,wDAAwD;IACxD,IAAI,QAAQ,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;IAEvD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;YACzC,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC/D,CAAC;QACD,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;AACzD,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,IAAU,EAAE,MAAY,IAAI,IAAI,EAAE,EAAU,EAAE;IACvF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAElE,IAAI,OAAO,GAAG,EAAE,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC,CAAC,oBAAoB;IACnC,CAAC;IACD,IAAI,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;QACtB,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC,oBAAoB;IACxC,CAAC;IACD,IAAI,OAAO,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;QAC3B,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,kBAAkB;IAC3C,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,kBAAkB;AAChD,CAAC,CAAC","sourcesContent":["type Division = {\n amount: number;\n unit: Intl.RelativeTimeFormatUnit;\n};\n\n// Each amount is \"how many of this unit make up one of the next unit\".\nconst DIVISIONS: Division[] = [\n { amount: 60, unit: 'seconds' },\n { amount: 60, unit: 'minutes' },\n { amount: 24, unit: 'hours' },\n { amount: 7, unit: 'days' },\n { amount: 4.34524, unit: 'weeks' },\n { amount: 12, unit: 'months' },\n { amount: Number.POSITIVE_INFINITY, unit: 'years' },\n];\n\n/**\n * Formats the distance between `date` and `now` as a relative phrase\n * (e.g. \"5 minutes ago\", \"in 2 hours\") using the native Intl.RelativeTimeFormat.\n * Past dates are negative, future dates are positive.\n */\nexport const formatTimeAgo = (date: Date, now: Date = new Date(), locale?: string | string[]): string => {\n // An invalid date (e.g. new Date('')) has a NaN time, which Intl.RelativeTimeFormat\n // rejects: guard so callers never have to.\n if (Number.isNaN(date.getTime())) {\n return '';\n }\n\n const formatter = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });\n\n // Seconds between the two dates; negative for the past.\n let duration = (date.getTime() - now.getTime()) / 1000;\n\n for (const division of DIVISIONS) {\n if (Math.abs(duration) < division.amount) {\n return formatter.format(Math.round(duration), division.unit);\n }\n duration /= division.amount;\n }\n\n return formatter.format(Math.round(duration), 'years');\n};\n\n/**\n * How long until the relative phrase could next change, so callers can\n * re-render no more often than necessary.\n */\nexport const getTimeAgoUpdateIntervalMs = (date: Date, now: Date = new Date()): number => {\n const seconds = Math.abs((date.getTime() - now.getTime()) / 1000);\n\n if (seconds < 60) {\n return 1000; // seconds tick over\n }\n if (seconds < 60 * 60) {\n return 60 * 1000; // minutes tick over\n }\n if (seconds < 60 * 60 * 24) {\n return 60 * 60 * 1000; // hours tick over\n }\n return 60 * 60 * 24 * 1000; // days or coarser\n};\n"]}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** Returns the array with duplicates removed, keeping the FIRST item for each key (SameValueZero key equality). */
|
|
2
|
+
export declare function uniqueBy<T>(array: T[], keyFn: (item: T) => unknown): T[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uniqueBy.js","sourceRoot":"","sources":["../../../src/tools/uniqueBy.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,QAAQ,CAAI,KAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"uniqueBy.js","sourceRoot":"","sources":["../../../src/tools/uniqueBy.ts"],"names":[],"mappings":"AAAA,mHAAmH;AACnH,MAAM,UAAU,QAAQ,CAAI,KAAU,EAAE,KAA2B;IACjE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAW,CAAC;IAChC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAC3B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAClB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/** Returns the array with duplicates removed, keeping the FIRST item for each key (SameValueZero key equality). */\nexport function uniqueBy<T>(array: T[], keyFn: (item: T) => unknown): T[] {\n const seen = new Set<unknown>();\n return array.filter((item) => {\n const key = keyFn(item);\n if (seen.has(key)) {\n return false;\n }\n seen.add(key);\n return true;\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quidproquo-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/esm/index.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"lib/**/*"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
14
15
|
"clean": "npx rimraf lib && npx rimraf node_modules",
|
|
15
16
|
"build": "npm run clean && npm run build:esm && npm run build:cjs",
|
|
16
17
|
"watch": "tsc -p tsconfig.esm.json -w",
|
|
@@ -32,9 +33,9 @@
|
|
|
32
33
|
},
|
|
33
34
|
"homepage": "https://github.com/joe-coady/quidproquo#readme",
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"quidproquo-core": "0.1.
|
|
36
|
-
"quidproquo-tsconfig": "0.1.
|
|
37
|
-
"quidproquo-webserver": "0.1.
|
|
36
|
+
"quidproquo-core": "0.1.13",
|
|
37
|
+
"quidproquo-tsconfig": "0.1.13",
|
|
38
|
+
"quidproquo-webserver": "0.1.13",
|
|
38
39
|
"typescript": "^5.8.2"
|
|
39
40
|
}
|
|
40
41
|
}
|