@venizia/ignis-worker 0.2.0-2 → 0.2.0-3

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 (33) hide show
  1. package/dist/cjs/transport/fetch-bridge.d.ts +35 -0
  2. package/dist/cjs/transport/fetch-bridge.d.ts.map +1 -0
  3. package/dist/cjs/transport/fetch-bridge.js +95 -0
  4. package/dist/cjs/transport/fetch-bridge.js.map +1 -0
  5. package/dist/cjs/transport/index.d.ts +2 -0
  6. package/dist/cjs/transport/index.d.ts.map +1 -1
  7. package/dist/cjs/transport/index.js +2 -0
  8. package/dist/cjs/transport/index.js.map +1 -1
  9. package/dist/cjs/transport/shared.d.ts +102 -0
  10. package/dist/cjs/transport/shared.d.ts.map +1 -0
  11. package/dist/cjs/transport/shared.js +396 -0
  12. package/dist/cjs/transport/shared.js.map +1 -0
  13. package/dist/cjs/transport/worker.d.ts +16 -2
  14. package/dist/cjs/transport/worker.d.ts.map +1 -1
  15. package/dist/cjs/transport/worker.js +65 -7
  16. package/dist/cjs/transport/worker.js.map +1 -1
  17. package/dist/esm/transport/fetch-bridge.d.ts +35 -0
  18. package/dist/esm/transport/fetch-bridge.d.ts.map +1 -0
  19. package/dist/esm/transport/fetch-bridge.js +91 -0
  20. package/dist/esm/transport/fetch-bridge.js.map +1 -0
  21. package/dist/esm/transport/index.d.ts +2 -0
  22. package/dist/esm/transport/index.d.ts.map +1 -1
  23. package/dist/esm/transport/index.js +2 -0
  24. package/dist/esm/transport/index.js.map +1 -1
  25. package/dist/esm/transport/shared.d.ts +102 -0
  26. package/dist/esm/transport/shared.d.ts.map +1 -0
  27. package/dist/esm/transport/shared.js +391 -0
  28. package/dist/esm/transport/shared.js.map +1 -0
  29. package/dist/esm/transport/worker.d.ts +16 -2
  30. package/dist/esm/transport/worker.d.ts.map +1 -1
  31. package/dist/esm/transport/worker.js +65 -7
  32. package/dist/esm/transport/worker.js.map +1 -1
  33. package/package.json +3 -3
@@ -0,0 +1,91 @@
1
+ import { getError } from '@venizia/ignis-helpers/core';
2
+ /**
3
+ * Marks the patched `fetch` so a second install is refused rather than silently stacking. `Symbol.for`
4
+ * and not a private symbol: two copies of this package in one page must still see each other's mark.
5
+ */
6
+ const BRIDGE_MARKER = Symbol.for('@venizia/ignis-worker/bff-fetch');
7
+ /**
8
+ * Resolves a request's absolute URL WITHOUT constructing a `Request`.
9
+ *
10
+ * This is the whole reason this helper exists rather than one obvious line. `new Request(input)`,
11
+ * where `input` is a `Request` carrying a body, marks that body disturbed - so building one just to
12
+ * read `.url` breaks the very pass-through it is deciding about: the next line hands the same, now
13
+ * unreadable, request to the network.
14
+ *
15
+ * Measured, because the runtimes disagree. Chromium:
16
+ *
17
+ * original.bodyUsed // false
18
+ * new Request(original)
19
+ * original.bodyUsed // true
20
+ * await original.text() // TypeError: body stream already read
21
+ *
22
+ * Bun does NOT disturb it, so the unit tests pass either way and cannot guard this. The browser is
23
+ * where this code runs, so the browser is the behaviour to write for.
24
+ */
25
+ const resolveRequestUrl = (input) => {
26
+ if (typeof input === 'string') {
27
+ return new URL(input, globalThis.location?.href).href;
28
+ }
29
+ if (input instanceof URL) {
30
+ return input.href;
31
+ }
32
+ return input.url;
33
+ };
34
+ const toPrefixList = (basePath) => {
35
+ const prefixes = (Array.isArray(basePath) ? basePath : [basePath]).filter(prefix => prefix.length > 0);
36
+ if (prefixes.length === 0) {
37
+ throw getError({
38
+ message: '[installBffFetch] Invalid basePath | At least one non-empty path prefix is required',
39
+ });
40
+ }
41
+ return prefixes;
42
+ };
43
+ /**
44
+ * Routes the page's own `fetch` into a BFF transport for anything under `basePath`, and leaves every
45
+ * other call on the network untouched.
46
+ *
47
+ * This is the seam that lets an existing HTTP client talk to an in-browser IGNIS application without
48
+ * knowing one exists. A data provider, an SDK or a generated client reaches the network through the
49
+ * global `fetch` and usually accepts no custom fetcher, so answering that `fetch` is the only place
50
+ * an in-browser backend becomes a drop-in swap rather than a fork of the client.
51
+ *
52
+ * Install it BEFORE the application that will use it boots: a request issued while the original
53
+ * `fetch` is still in place leaves the page and 404s against whatever is serving it.
54
+ *
55
+ * @returns The uninstall function. It restores the previous `fetch` only if this bridge is still the
56
+ * installed one - if something patched `fetch` afterwards, restoring would silently discard that.
57
+ */
58
+ export const installBffFetch = (opts) => {
59
+ const { transport, basePath } = opts;
60
+ const carrier = opts.carrier ?? globalThis;
61
+ const prefixes = toPrefixList(basePath);
62
+ const previousFetch = carrier.fetch;
63
+ if (Object.getOwnPropertyDescriptor(previousFetch, BRIDGE_MARKER)) {
64
+ throw getError({
65
+ message: '[installBffFetch] A BFF fetch bridge is already installed | Uninstall it first, or pass every prefix to a single install',
66
+ });
67
+ }
68
+ const networkFetch = previousFetch.bind(carrier);
69
+ const bridged = async (input, init) => {
70
+ const { pathname } = new URL(resolveRequestUrl(input));
71
+ if (!prefixes.some(prefix => pathname.startsWith(prefix))) {
72
+ return networkFetch(input, init);
73
+ }
74
+ // Constructed only now, on the branch that owns the request - so the pass-through above never
75
+ // disturbs a body it is about to hand to the network.
76
+ return transport.fetch({ request: new Request(input, init) });
77
+ };
78
+ // Whatever the runtime hung on `fetch` comes along - Bun puts `preconnect` there, and a
79
+ // replacement that dropped it would break any caller reaching for it. Copying the descriptors is
80
+ // also what makes this satisfy `typeof fetch` rather than merely resembling it.
81
+ Object.defineProperties(bridged, Object.getOwnPropertyDescriptors(previousFetch));
82
+ Object.defineProperty(bridged, BRIDGE_MARKER, { value: true });
83
+ carrier.fetch = bridged;
84
+ return () => {
85
+ if (carrier.fetch !== bridged) {
86
+ return;
87
+ }
88
+ carrier.fetch = previousFetch;
89
+ };
90
+ };
91
+ //# sourceMappingURL=fetch-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-bridge.js","sourceRoot":"","sources":["../../../src/transport/fetch-bridge.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,6 @@
1
1
  export * from './common/index.js';
2
+ export * from './fetch-bridge.js';
2
3
  export * from './in-process.js';
4
+ export * from './shared.js';
3
5
  export * from './worker.js';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transport/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/transport/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
@@ -1,4 +1,6 @@
1
1
  export * from './common/index.js';
2
+ export * from './fetch-bridge.js';
2
3
  export * from './in-process.js';
4
+ export * from './shared.js';
3
5
  export * from './worker.js';
4
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/transport/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/transport/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
@@ -0,0 +1,102 @@
1
+ import { BaseHelper } from '@venizia/ignis-helpers/core';
2
+ import type { TConstValue } from '@venizia/ignis-helpers/common';
3
+ import type { IBffTransport } from './common/types.js';
4
+ /** Which part this tab plays. `ELECTING` is the brief window before the lock answers. */
5
+ export declare class BffRoles {
6
+ static readonly ELECTING = "electing";
7
+ static readonly LEADER = "leader";
8
+ static readonly FOLLOWER = "follower";
9
+ }
10
+ export type TBffRole = TConstValue<typeof BffRoles>;
11
+ export interface ISharedBffTransportOptions {
12
+ /** Builds the Worker. Called ONLY by whichever tab wins the election - the others never start one. */
13
+ createWorker: () => Worker;
14
+ /** Distinguishes independent BFFs on one origin. The lock name is derived from it. */
15
+ channelName?: string;
16
+ timeoutMs?: number;
17
+ scope?: string;
18
+ }
19
+ /**
20
+ * One BFF for every tab of an origin, instead of one per tab.
21
+ *
22
+ * The problem it solves is not a framework limitation, it is a storage one. PGlite in
23
+ * `opfs-ahp://` mode holds an exclusive access handle on the database file, and OPFS access handles
24
+ * are exclusive PER ORIGIN. Measured in Chromium with two tabs of the same page: the first tab
25
+ * works, and the second never boots its database -
26
+ * `Failed to execute 'createSyncAccessHandle' on 'FileSystemFileHandle': Access Handles cannot be
27
+ * created if there is another open Access Handle or Writable stream associated with the same file`.
28
+ * The first tab is unaffected, and closing it lets the second recover on reload - so the failure is
29
+ * contained and self-healing, but the second tab is simply dead until then.
30
+ *
31
+ * So exactly one tab may own the database. This transport elects that tab with the Web Locks API,
32
+ * gives it the Worker, and forwards every other tab's request to it over a `BroadcastChannel`,
33
+ * carried in the same envelope the Worker itself speaks. When the leader's tab closes, the lock is
34
+ * released by the browser and a follower is promoted automatically - no heartbeat, no timeout, no
35
+ * stale-leader window to reason about.
36
+ *
37
+ * A host with no `navigator.locks` runs single-tab, exactly as before. That is not a compromise:
38
+ * measured on a plain-http origin, `navigator.locks` and `navigator.storage.getDirectory` are BOTH
39
+ * undefined, because both are secure-context only. Wherever OPFS works the lock exists, and where it
40
+ * does not the database could not have started either.
41
+ */
42
+ export declare class SharedBffTransport extends BaseHelper implements IBffTransport {
43
+ private readonly createWorker;
44
+ private readonly channel;
45
+ private readonly lockName;
46
+ private readonly timeoutMs;
47
+ private readonly requestIdGenerator;
48
+ private readonly pendingRequests;
49
+ private role;
50
+ private leaderTransport?;
51
+ private isClosed;
52
+ /**
53
+ * Held because THIS class created it. `WorkerBffTransport.close()` deliberately leaves a worker
54
+ * running - it did not create the one it was handed - so without this reference the worker would
55
+ * outlive `close()` still holding the exclusive OPFS access handle, while the lock has already
56
+ * gone to another tab that opens the same database.
57
+ */
58
+ private leaderWorker?;
59
+ /** Set when `createWorker()` throws, so callers get that failure instead of parking forever. */
60
+ private leaderStartupError?;
61
+ /** Resolving this releases the Web Lock, which is what promotes a follower. */
62
+ private releaseLeadership?;
63
+ /** Drops this tab out of the lock QUEUE on close, so a closed transport cannot be promoted. */
64
+ private readonly leadershipQueueAbort;
65
+ /** Callers parked in `fetch()` while the election is still running. */
66
+ private roleWaiters;
67
+ constructor(opts: ISharedBffTransportOptions);
68
+ getRole(): TBffRole;
69
+ fetch(opts: {
70
+ request: Request;
71
+ }): Promise<Response>;
72
+ close(): void;
73
+ private elect;
74
+ private becomeLeader;
75
+ private becomeFollower;
76
+ private heldUntilClosed;
77
+ private whenRoleSettled;
78
+ private settleRoleWaiters;
79
+ /**
80
+ * The caller's `AbortSignal` is honoured HERE as well as in `WorkerBffTransport`. Without it the
81
+ * same page code behaves differently depending on which tab won the lock: abort works in the
82
+ * leader and silently does nothing in every follower, so a data provider that aborts on unmount
83
+ * leaves promises pending for the full timeout in every tab but one.
84
+ *
85
+ * Aborting settles the CALLER. It does not cancel the leader's work - there is no cancel message,
86
+ * and a write already dispatched cannot be taken back.
87
+ */
88
+ private fetchViaLeader;
89
+ /** `signal.reason` first, the way `globalThis.fetch` rejects, so a custom abort reason survives. */
90
+ private toAbortError;
91
+ private takePending;
92
+ private rejectAllPending;
93
+ /**
94
+ * A `BroadcastChannel` never delivers a message to the instance that sent it, so a leader does not
95
+ * hear its own answers and a follower does not hear its own requests. Every tab does hear every
96
+ * OTHER tab, which is why a response is matched by id and silently ignored when it belongs to
97
+ * someone else.
98
+ */
99
+ private readonly handleChannelMessage;
100
+ private serveAsLeader;
101
+ }
102
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/transport/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAgC,MAAM,6BAA6B,CAAC;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAOjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAMpD,yFAAyF;AACzF,qBAAa,QAAQ;IACnB,MAAM,CAAC,QAAQ,CAAC,QAAQ,cAAc;IACtC,MAAM,CAAC,QAAQ,CAAC,MAAM,YAAY;IAClC,MAAM,CAAC,QAAQ,CAAC,QAAQ,cAAc;CACvC;AAED,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,QAAQ,CAAC,CAAC;AA0BpD,MAAM,WAAW,0BAA0B;IACzC,sGAAsG;IACtG,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,kBAAmB,SAAQ,UAAW,YAAW,aAAa;IACzE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsC;IAEtE,OAAO,CAAC,IAAI,CAA+B;IAC3C,OAAO,CAAC,eAAe,CAAC,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;OAKG;IACH,OAAO,CAAC,YAAY,CAAC,CAAS;IAE9B,gGAAgG;IAChG,OAAO,CAAC,kBAAkB,CAAC,CAAU;IAErC,+EAA+E;IAC/E,OAAO,CAAC,iBAAiB,CAAC,CAAa;IAEvC,+FAA+F;IAC/F,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAyB;IAE9D,uEAAuE;IACvE,OAAO,CAAC,WAAW,CAAyB;gBAEhC,IAAI,EAAE,0BAA0B;IAkC5C,OAAO,IAAI,QAAQ;IAIb,KAAK,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkC1D,KAAK,IAAI,IAAI;YA+BC,KAAK;IA4DnB,OAAO,CAAC,YAAY;IA6BpB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,iBAAiB;IASzB;;;;;;;;OAQG;YACW,cAAc;IAoD5B,oGAAoG;IACpG,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,gBAAgB;IAUxB;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CA4EnC;YAEY,aAAa;CAyB5B"}
@@ -0,0 +1,391 @@
1
+ import { BaseHelper, getError, RequestIdGenerator } from '@venizia/ignis-helpers/core';
2
+ import { BffEnvelope } from '../envelope/encode.js';
3
+ import { WorkerBffTransport } from './worker.js';
4
+ const DEFAULT_CHANNEL_NAME = 'ignis.bff';
5
+ const DEFAULT_TIMEOUT_MS = 30_000;
6
+ /** Which part this tab plays. `ELECTING` is the brief window before the lock answers. */
7
+ export class BffRoles {
8
+ static { this.ELECTING = 'electing'; }
9
+ static { this.LEADER = 'leader'; }
10
+ static { this.FOLLOWER = 'follower'; }
11
+ }
12
+ class ChannelMessageKinds {
13
+ static { this.REQUEST = 'ignis.bff.request'; }
14
+ static { this.RESPONSE = 'ignis.bff.response'; }
15
+ static { this.ERROR = 'ignis.bff.error'; }
16
+ }
17
+ /**
18
+ * One BFF for every tab of an origin, instead of one per tab.
19
+ *
20
+ * The problem it solves is not a framework limitation, it is a storage one. PGlite in
21
+ * `opfs-ahp://` mode holds an exclusive access handle on the database file, and OPFS access handles
22
+ * are exclusive PER ORIGIN. Measured in Chromium with two tabs of the same page: the first tab
23
+ * works, and the second never boots its database -
24
+ * `Failed to execute 'createSyncAccessHandle' on 'FileSystemFileHandle': Access Handles cannot be
25
+ * created if there is another open Access Handle or Writable stream associated with the same file`.
26
+ * The first tab is unaffected, and closing it lets the second recover on reload - so the failure is
27
+ * contained and self-healing, but the second tab is simply dead until then.
28
+ *
29
+ * So exactly one tab may own the database. This transport elects that tab with the Web Locks API,
30
+ * gives it the Worker, and forwards every other tab's request to it over a `BroadcastChannel`,
31
+ * carried in the same envelope the Worker itself speaks. When the leader's tab closes, the lock is
32
+ * released by the browser and a follower is promoted automatically - no heartbeat, no timeout, no
33
+ * stale-leader window to reason about.
34
+ *
35
+ * A host with no `navigator.locks` runs single-tab, exactly as before. That is not a compromise:
36
+ * measured on a plain-http origin, `navigator.locks` and `navigator.storage.getDirectory` are BOTH
37
+ * undefined, because both are secure-context only. Wherever OPFS works the lock exists, and where it
38
+ * does not the database could not have started either.
39
+ */
40
+ export class SharedBffTransport extends BaseHelper {
41
+ constructor(opts) {
42
+ super({ scope: opts.scope ?? SharedBffTransport.name });
43
+ this.pendingRequests = new Map();
44
+ this.role = BffRoles.ELECTING;
45
+ this.isClosed = false;
46
+ /** Drops this tab out of the lock QUEUE on close, so a closed transport cannot be promoted. */
47
+ this.leadershipQueueAbort = new AbortController();
48
+ /** Callers parked in `fetch()` while the election is still running. */
49
+ this.roleWaiters = [];
50
+ /**
51
+ * A `BroadcastChannel` never delivers a message to the instance that sent it, so a leader does not
52
+ * hear its own answers and a follower does not hear its own requests. Every tab does hear every
53
+ * OTHER tab, which is why a response is matched by id and silently ignored when it belongs to
54
+ * someone else.
55
+ */
56
+ this.handleChannelMessage = (event) => {
57
+ const message = event.data;
58
+ switch (message?.kind) {
59
+ case ChannelMessageKinds.REQUEST: {
60
+ if (this.role !== BffRoles.LEADER) {
61
+ return;
62
+ }
63
+ // `serveAsLeader` answers its own failures on the channel; this catch is for the one case
64
+ // it cannot - the channel itself refusing the message, e.g. a body that will not clone.
65
+ this.serveAsLeader({ envelope: message.envelope }).catch((error) => {
66
+ this.logger
67
+ .for(this.serveAsLeader.name)
68
+ .error('Could not answer a follower at all | id: %s | error: %s', message.envelope.id, error);
69
+ });
70
+ return;
71
+ }
72
+ // Both decodes are guarded. The channel is origin-wide and its name is the application's, not
73
+ // a secret, and version skew is the realistic trigger: a long-lived tab on v1 and a new tab
74
+ // on v2 share the same channel and lock BY DESIGN. An envelope this build cannot read must
75
+ // fail its caller, never leave the promise unsettled for the full timeout.
76
+ case ChannelMessageKinds.RESPONSE: {
77
+ const pending = this.takePending({ id: message.envelope.id });
78
+ if (!pending) {
79
+ return;
80
+ }
81
+ try {
82
+ pending.resolve(BffEnvelope.decodeResponse({ envelope: message.envelope }));
83
+ }
84
+ catch (error) {
85
+ this.logger
86
+ .for(this.handleChannelMessage.name)
87
+ .error('Undecodable response envelope | id: %s | error: %s', message.envelope.id, error);
88
+ pending.reject(getError({
89
+ message: `[SharedBffTransport] The leader's response could not be decoded | id: ${message.envelope.id}`,
90
+ cause: error,
91
+ }));
92
+ }
93
+ return;
94
+ }
95
+ case ChannelMessageKinds.ERROR: {
96
+ const pending = this.takePending({ id: message.envelope.id });
97
+ if (!pending) {
98
+ return;
99
+ }
100
+ try {
101
+ pending.reject(BffEnvelope.decodeError({ envelope: message.envelope }));
102
+ }
103
+ catch (error) {
104
+ pending.reject(getError({
105
+ message: `[SharedBffTransport] The leader's error envelope could not be decoded | id: ${message.envelope.id}`,
106
+ cause: error,
107
+ }));
108
+ }
109
+ return;
110
+ }
111
+ default: {
112
+ return;
113
+ }
114
+ }
115
+ };
116
+ const channelName = opts.channelName ?? DEFAULT_CHANNEL_NAME;
117
+ this.createWorker = opts.createWorker;
118
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
119
+ this.lockName = `${channelName}.leader`;
120
+ this.requestIdGenerator = new RequestIdGenerator({ scope: SharedBffTransport.name });
121
+ // Attached BEFORE the election starts: a tab that becomes leader a moment later must not miss a
122
+ // request a follower posted while it was still deciding.
123
+ this.channel = new BroadcastChannel(channelName);
124
+ this.channel.addEventListener('message', this.handleChannelMessage);
125
+ // A failed election must not leave `fetch()` parked forever waiting for a role that never
126
+ // arrives: this tab falls back to serving itself, which is the pre-election behaviour.
127
+ this.elect().catch((error) => {
128
+ // `close()` aborts the queued lock request, and the rejection that produces is the expected
129
+ // end of the election - not a failure to report.
130
+ if (this.isClosed) {
131
+ return;
132
+ }
133
+ this.logger
134
+ .for(this.elect.name)
135
+ .error('Leader election failed, falling back to single-tab | error: %s', error);
136
+ if (this.role === BffRoles.ELECTING) {
137
+ this.becomeLeader();
138
+ }
139
+ });
140
+ }
141
+ getRole() {
142
+ return this.role;
143
+ }
144
+ async fetch(opts) {
145
+ if (this.isClosed) {
146
+ throw getError({
147
+ message: '[SharedBffTransport] The transport is closed | open a new one to make requests',
148
+ });
149
+ }
150
+ await this.whenRoleSettled();
151
+ // Re-checked AFTER the await: `close()` can land while the election is still running, and
152
+ // posting then reaches a channel that is already closed - the caller would get a raw
153
+ // `DOMException` instead of this error, and a pending entry plus a live timer would be left
154
+ // behind on a transport nothing can settle.
155
+ if (this.isClosed) {
156
+ throw getError({
157
+ message: '[SharedBffTransport] The transport is closed | open a new one to make requests',
158
+ });
159
+ }
160
+ if (this.leaderStartupError) {
161
+ throw getError({
162
+ message: '[SharedBffTransport] This tab leads but its BFF worker never started | reload the page',
163
+ cause: this.leaderStartupError,
164
+ });
165
+ }
166
+ if (this.role === BffRoles.LEADER && this.leaderTransport) {
167
+ return this.leaderTransport.fetch(opts);
168
+ }
169
+ return this.fetchViaLeader(opts);
170
+ }
171
+ close() {
172
+ this.isClosed = true;
173
+ // A follower is parked in the lock QUEUE rather than holding anything. Without this it stays
174
+ // queued after close, and the browser would eventually hand leadership to a dead transport -
175
+ // which returns straight away, but only after the tab that could have served was skipped.
176
+ this.leadershipQueueAbort.abort();
177
+ // ORDER IS THE POINT, and it is the opposite of the obvious one. The worker has to be GONE
178
+ // before the lock is released: releasing first promotes another tab, which opens the same OPFS
179
+ // database while this worker still holds the exclusive access handle - the exact failure this
180
+ // transport exists to prevent, reintroduced by its own teardown.
181
+ //
182
+ // `terminate()` is abrupt by design. It is no worse than the tab-close path the browser already
183
+ // takes on every leader, and leaving the worker alive is strictly worse than an abrupt stop.
184
+ this.leaderTransport?.close();
185
+ this.leaderTransport = undefined;
186
+ this.leaderWorker?.terminate();
187
+ this.leaderWorker = undefined;
188
+ // Only now: the next tab may open the database.
189
+ this.releaseLeadership?.();
190
+ this.releaseLeadership = undefined;
191
+ this.channel.removeEventListener('message', this.handleChannelMessage);
192
+ this.channel.close();
193
+ this.rejectAllPending({ reason: 'the transport was closed' });
194
+ this.settleRoleWaiters();
195
+ }
196
+ async elect() {
197
+ const locks = globalThis.navigator?.locks;
198
+ if (!locks) {
199
+ this.logger
200
+ .for(this.elect.name)
201
+ .warn('navigator.locks is unavailable - running single-tab | a second tab cannot open the same OPFS database');
202
+ this.becomeLeader();
203
+ return;
204
+ }
205
+ // `ifAvailable` answers NOW instead of queueing, which is what lets this tab find out it is a
206
+ // follower rather than hanging until the current leader goes away.
207
+ const hasWon = await locks.request(this.lockName, { ifAvailable: true }, async (lock) => {
208
+ if (!lock) {
209
+ return false;
210
+ }
211
+ // Symmetric with the queued branch below, and not defensive padding. A real `LockManager`
212
+ // grants from a queued task - it never runs this callback synchronously inside `request()` -
213
+ // so `close()` can land between the constructor and this line. Without the guard a CLOSED
214
+ // transport starts a worker, reports itself leader, and then holds the lock for the lifetime
215
+ // of the page: `close()` has already read and cleared `releaseLeadership`, so the assignment
216
+ // `heldUntilClosed()` makes below can never be resolved by anything. No other tab is ever
217
+ // promoted, and this one answers nobody - its channel listener is already detached.
218
+ //
219
+ // Returning true, not false: this tab DID hold the lock. Returning releases it immediately,
220
+ // which is exactly what a closed transport should do with it.
221
+ if (this.isClosed) {
222
+ return true;
223
+ }
224
+ this.becomeLeader();
225
+ await this.heldUntilClosed();
226
+ return true;
227
+ });
228
+ if (hasWon || this.isClosed) {
229
+ return;
230
+ }
231
+ this.becomeFollower();
232
+ // Queues behind the current leader. The browser releases its lock when that tab goes away - a
233
+ // crash included - so this resolves without anything having to detect the death.
234
+ await locks.request(this.lockName, { signal: this.leadershipQueueAbort.signal }, async () => {
235
+ if (this.isClosed) {
236
+ return;
237
+ }
238
+ this.logger
239
+ .for(this.elect.name)
240
+ .info('Promoted to leader | previous leader released the lock');
241
+ this.becomeLeader();
242
+ await this.heldUntilClosed();
243
+ });
244
+ }
245
+ becomeLeader() {
246
+ // Anything this tab had in flight was addressed to the leader that just went away. It cannot be
247
+ // replayed - a write may well have been applied - so it is failed explicitly rather than left
248
+ // to run out its timeout under a leader that will never answer it.
249
+ this.rejectAllPending({
250
+ reason: 'the leader serving it went away before this tab was promoted',
251
+ });
252
+ try {
253
+ this.leaderWorker = this.createWorker();
254
+ this.leaderTransport = new WorkerBffTransport({
255
+ worker: this.leaderWorker,
256
+ timeoutMs: this.timeoutMs,
257
+ });
258
+ }
259
+ catch (error) {
260
+ // A CSP refusal or a bad worker URL must still SETTLE the role. `whenRoleSettled()` has no
261
+ // timeout of its own and the per-request timer is armed only inside `fetchViaLeader()`, which
262
+ // this tab never reaches - so leaving the role unsettled hangs every caller forever, with no
263
+ // error and no log per request.
264
+ this.leaderStartupError = error;
265
+ this.logger
266
+ .for(this.becomeLeader.name)
267
+ .error('Could not start the BFF worker | error: %s', error);
268
+ }
269
+ this.role = BffRoles.LEADER;
270
+ this.settleRoleWaiters();
271
+ }
272
+ becomeFollower() {
273
+ this.role = BffRoles.FOLLOWER;
274
+ this.settleRoleWaiters();
275
+ }
276
+ heldUntilClosed() {
277
+ return new Promise(resolve => {
278
+ this.releaseLeadership = resolve;
279
+ });
280
+ }
281
+ whenRoleSettled() {
282
+ if (this.role !== BffRoles.ELECTING) {
283
+ return Promise.resolve();
284
+ }
285
+ return new Promise(resolve => {
286
+ this.roleWaiters.push(resolve);
287
+ });
288
+ }
289
+ settleRoleWaiters() {
290
+ const waiters = this.roleWaiters;
291
+ this.roleWaiters = [];
292
+ for (const waiter of waiters) {
293
+ waiter();
294
+ }
295
+ }
296
+ /**
297
+ * The caller's `AbortSignal` is honoured HERE as well as in `WorkerBffTransport`. Without it the
298
+ * same page code behaves differently depending on which tab won the lock: abort works in the
299
+ * leader and silently does nothing in every follower, so a data provider that aborts on unmount
300
+ * leaves promises pending for the full timeout in every tab but one.
301
+ *
302
+ * Aborting settles the CALLER. It does not cancel the leader's work - there is no cancel message,
303
+ * and a write already dispatched cannot be taken back.
304
+ */
305
+ async fetchViaLeader(opts) {
306
+ const { signal } = opts.request;
307
+ if (signal?.aborted) {
308
+ throw this.toAbortError({ signal });
309
+ }
310
+ const id = this.requestIdGenerator.nextId();
311
+ const envelope = await BffEnvelope.encodeRequest({
312
+ request: opts.request,
313
+ id,
314
+ url: BffEnvelope.toSyntheticUrl({ url: opts.request.url }),
315
+ });
316
+ return new Promise((resolve, reject) => {
317
+ const timer = setTimeout(() => {
318
+ this.takePending({ id })?.reject(getError({
319
+ message: `[SharedBffTransport] Timed out waiting for the leader tab | id: ${id} | timeoutMs: ${this.timeoutMs}`,
320
+ }));
321
+ }, this.timeoutMs);
322
+ const handleAbort = () => {
323
+ this.takePending({ id })?.reject(this.toAbortError({ signal }));
324
+ };
325
+ signal?.addEventListener('abort', handleAbort);
326
+ this.pendingRequests.set(id, {
327
+ resolve,
328
+ reject,
329
+ release: () => {
330
+ clearTimeout(timer);
331
+ signal?.removeEventListener('abort', handleAbort);
332
+ },
333
+ });
334
+ // Buffering the body took an await, and `addEventListener('abort')` never fires on a signal
335
+ // that aborted before it was attached - without this re-read that window loses the abort.
336
+ if (signal?.aborted) {
337
+ handleAbort();
338
+ return;
339
+ }
340
+ this.channel.postMessage({
341
+ kind: ChannelMessageKinds.REQUEST,
342
+ envelope,
343
+ });
344
+ });
345
+ }
346
+ /** `signal.reason` first, the way `globalThis.fetch` rejects, so a custom abort reason survives. */
347
+ toAbortError(opts) {
348
+ return (opts.signal?.reason ??
349
+ getError({ message: '[SharedBffTransport] The request was aborted by its caller' }));
350
+ }
351
+ takePending(opts) {
352
+ const pending = this.pendingRequests.get(opts.id);
353
+ if (!pending) {
354
+ return undefined;
355
+ }
356
+ this.pendingRequests.delete(opts.id);
357
+ pending.release();
358
+ return pending;
359
+ }
360
+ rejectAllPending(opts) {
361
+ for (const id of [...this.pendingRequests.keys()]) {
362
+ this.takePending({ id })?.reject(getError({
363
+ message: `[SharedBffTransport] Request abandoned | id: ${id} | reason: ${opts.reason}`,
364
+ }));
365
+ }
366
+ }
367
+ async serveAsLeader(opts) {
368
+ const { envelope } = opts;
369
+ try {
370
+ const response = await this.leaderTransport.fetch({
371
+ request: BffEnvelope.decodeRequest({ envelope }),
372
+ });
373
+ this.channel.postMessage({
374
+ kind: ChannelMessageKinds.RESPONSE,
375
+ envelope: await BffEnvelope.encodeResponse({ response, id: envelope.id }),
376
+ });
377
+ }
378
+ catch (error) {
379
+ // The follower gets the failure as a failure, rather than waiting out a timeout that would
380
+ // tell it nothing about what went wrong.
381
+ this.logger
382
+ .for(this.serveAsLeader.name)
383
+ .error('Failed to serve a follower request | id: %s | error: %s', envelope.id, error);
384
+ this.channel.postMessage({
385
+ kind: ChannelMessageKinds.ERROR,
386
+ envelope: BffEnvelope.encodeError({ id: envelope.id, error }),
387
+ });
388
+ }
389
+ }
390
+ }
391
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../src/transport/shared.ts"],"names":[],"mappings":"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"}