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.
Files changed (45) hide show
  1. package/lib/commonjs/actions/queryParams/QueryParamsGetActionRequester.d.ts +1 -0
  2. package/lib/commonjs/actions/queryParams/QueryParamsGetActionRequester.js +1 -0
  3. package/lib/commonjs/actions/queryParams/QueryParamsGetActionRequester.js.map +1 -1
  4. package/lib/commonjs/actions/queryParams/QueryParamsGetAllActionRequester.d.ts +1 -0
  5. package/lib/commonjs/actions/queryParams/QueryParamsGetAllActionRequester.js +1 -0
  6. package/lib/commonjs/actions/queryParams/QueryParamsGetAllActionRequester.js.map +1 -1
  7. package/lib/commonjs/actions/queryParams/QueryParamsSetActionRequester.d.ts +5 -0
  8. package/lib/commonjs/actions/queryParams/QueryParamsSetActionRequester.js +5 -0
  9. package/lib/commonjs/actions/queryParams/QueryParamsSetActionRequester.js.map +1 -1
  10. package/lib/commonjs/actions/window/WindowGetLocationActionRequester.d.ts +1 -0
  11. package/lib/commonjs/actions/window/WindowGetLocationActionRequester.js +1 -0
  12. package/lib/commonjs/actions/window/WindowGetLocationActionRequester.js.map +1 -1
  13. package/lib/commonjs/federation/forceReloadFederatedRemote.js +5 -5
  14. package/lib/commonjs/federation/forceReloadFederatedRemote.js.map +1 -1
  15. package/lib/commonjs/services/WebsocketService.d.ts +9 -4
  16. package/lib/commonjs/services/WebsocketService.js +31 -28
  17. package/lib/commonjs/services/WebsocketService.js.map +1 -1
  18. package/lib/commonjs/tools/timeAgo.js +1 -1
  19. package/lib/commonjs/tools/timeAgo.js.map +1 -1
  20. package/lib/commonjs/tools/uniqueBy.d.ts +2 -1
  21. package/lib/commonjs/tools/uniqueBy.js +1 -0
  22. package/lib/commonjs/tools/uniqueBy.js.map +1 -1
  23. package/lib/esm/actions/queryParams/QueryParamsGetActionRequester.d.ts +1 -0
  24. package/lib/esm/actions/queryParams/QueryParamsGetActionRequester.js +1 -0
  25. package/lib/esm/actions/queryParams/QueryParamsGetActionRequester.js.map +1 -1
  26. package/lib/esm/actions/queryParams/QueryParamsGetAllActionRequester.d.ts +1 -0
  27. package/lib/esm/actions/queryParams/QueryParamsGetAllActionRequester.js +1 -0
  28. package/lib/esm/actions/queryParams/QueryParamsGetAllActionRequester.js.map +1 -1
  29. package/lib/esm/actions/queryParams/QueryParamsSetActionRequester.d.ts +5 -0
  30. package/lib/esm/actions/queryParams/QueryParamsSetActionRequester.js +5 -0
  31. package/lib/esm/actions/queryParams/QueryParamsSetActionRequester.js.map +1 -1
  32. package/lib/esm/actions/window/WindowGetLocationActionRequester.d.ts +1 -0
  33. package/lib/esm/actions/window/WindowGetLocationActionRequester.js +1 -0
  34. package/lib/esm/actions/window/WindowGetLocationActionRequester.js.map +1 -1
  35. package/lib/esm/federation/forceReloadFederatedRemote.js +5 -5
  36. package/lib/esm/federation/forceReloadFederatedRemote.js.map +1 -1
  37. package/lib/esm/services/WebsocketService.d.ts +9 -4
  38. package/lib/esm/services/WebsocketService.js +29 -28
  39. package/lib/esm/services/WebsocketService.js.map +1 -1
  40. package/lib/esm/tools/timeAgo.js +1 -1
  41. package/lib/esm/tools/timeAgo.js.map +1 -1
  42. package/lib/esm/tools/uniqueBy.d.ts +2 -1
  43. package/lib/esm/tools/uniqueBy.js +1 -0
  44. package/lib/esm/tools/uniqueBy.js.map +1 -1
  45. 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":";;AAGA,8CAEC;AALD,mEAAgE;AAGhE,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\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":";;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"]}
@@ -1,2 +1,3 @@
1
1
  import { QueryParamsGetAllActionRequester } from './QueryParamsGetAllActionRequesterTypes';
2
+ /** Reads the whole query string of the current url as key -> all values for that key. */
2
3
  export declare function askQueryParamsGetAll(): QueryParamsGetAllActionRequester;
@@ -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":";;AAGA,oDAEC;AALD,mEAAgE;AAGhE,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\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":";;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":";;AAGA,8CAEC;AALD,mEAAgE;AAGhE,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\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":";;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":";;AAGA,oDAEC;AALD,yDAAsD;AAGtD,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\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":";;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` the global is a top-level `var` from a
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 every chunk file of every previously loaded copy of the
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 a never-seen
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 the bundler aliases '@module-federation/enhanced/runtime'
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":"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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
+ {"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, EventMessage } from 'quidproquo-core';
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) => any;
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 | null;
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 EventMessage<any, string | number>>(event: E): void;
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
- return this.subscribe(WebsocketServiceEvent.MESSAGE, (websocketService, event) => {
56
- if (event) {
57
- const data = event.data;
58
- try {
59
- const parsedEvent = JSON.parse(data);
60
- if (parsedEvent.type === subscriptionType) {
61
- callback(websocketService, parsedEvent);
62
- }
63
- }
64
- catch (e) {
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 subscriptionType in this.subscriptions) {
75
- if (Object.prototype.hasOwnProperty.call(this.subscriptions, subscriptionType)) {
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
- if (this.socket) {
90
- this.socket.addEventListener(event, listener);
91
- if (!this.eventListeners[event]) {
92
- this.eventListeners[event] = [];
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 in this.eventListeners) {
100
- if (Object.prototype.hasOwnProperty.call(this.eventListeners, event)) {
101
- for (const listener of this.eventListeners[event]) {
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 guard so callers never have to.
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,4CAA4C;IAC5C,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
+ {"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
- export declare function uniqueBy<T>(array: T[], keyFn: (item: T) => any): T[];
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":";;AAAA,4BAUC;AAVD,SAAgB,QAAQ,CAAI,KAAU,EAAE,KAAuB;IAC7D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAO,CAAC;IAC5B,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":["export function uniqueBy<T>(array: T[], keyFn: (item: T) => any): T[] {\n const seen = new Set<any>();\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
+ {"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,2 +1,3 @@
1
1
  import { QueryParamsGetAllActionRequester } from './QueryParamsGetAllActionRequesterTypes';
2
+ /** Reads the whole query string of the current url as key -> all values for that key. */
2
3
  export declare function askQueryParamsGetAll(): QueryParamsGetAllActionRequester;
@@ -1,4 +1,5 @@
1
1
  import { QueryParamsActionType } from './QueryParamsActionType';
2
+ /** Reads the whole query string of the current url as key -> all values for that key. */
2
3
  export function* askQueryParamsGetAll() {
3
4
  return yield { type: QueryParamsActionType.GetAll };
4
5
  }
@@ -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,4 +1,5 @@
1
1
  import { WindowActionType } from './WindowActionType';
2
+ /** Reads the current `window.location` as a plain serializable snapshot (never the live Location object). */
2
3
  export function* askWindowGetLocation() {
3
4
  return yield { type: WindowActionType.GetLocation };
4
5
  }
@@ -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` the global is a top-level `var` from a
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 every chunk file of every previously loaded copy of the
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 a never-seen
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 the bundler aliases '@module-federation/enhanced/runtime'
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":"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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
+ {"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, EventMessage } from 'quidproquo-core';
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) => any;
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 | null;
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 EventMessage<any, string | number>>(event: E): void;
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
- return this.subscribe(WebsocketServiceEvent.MESSAGE, (websocketService, event) => {
53
- if (event) {
54
- const data = event.data;
55
- try {
56
- const parsedEvent = JSON.parse(data);
57
- if (parsedEvent.type === subscriptionType) {
58
- callback(websocketService, parsedEvent);
59
- }
60
- }
61
- catch (e) {
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 subscriptionType in this.subscriptions) {
72
- if (Object.prototype.hasOwnProperty.call(this.subscriptions, subscriptionType)) {
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
- this.socket.addEventListener(event, listener);
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 in this.eventListeners) {
97
- if (Object.prototype.hasOwnProperty.call(this.eventListeners, event)) {
98
- for (const listener of this.eventListeners[event]) {
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"]}
@@ -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 guard so callers never have to.
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,4CAA4C;IAC5C,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
+ {"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
- export declare function uniqueBy<T>(array: T[], keyFn: (item: T) => any): T[];
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,3 +1,4 @@
1
+ /** Returns the array with duplicates removed, keeping the FIRST item for each key (SameValueZero key equality). */
1
2
  export function uniqueBy(array, keyFn) {
2
3
  const seen = new Set();
3
4
  return array.filter((item) => {
@@ -1 +1 @@
1
- {"version":3,"file":"uniqueBy.js","sourceRoot":"","sources":["../../../src/tools/uniqueBy.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,QAAQ,CAAI,KAAU,EAAE,KAAuB;IAC7D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAO,CAAC;IAC5B,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":["export function uniqueBy<T>(array: T[], keyFn: (item: T) => any): T[] {\n const seen = new Set<any>();\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
+ {"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.11",
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": "echo \"Error: no test specified\" && exit 1",
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.11",
36
- "quidproquo-tsconfig": "0.1.11",
37
- "quidproquo-webserver": "0.1.11",
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
  }