@venizia/ignis-worker 0.4.7-0 → 0.4.7-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md CHANGED
@@ -38,7 +38,7 @@ This monorepo contains the following packages, all licensed under MIT:
38
38
  | `@venizia/ignis-helpers` | Utility helpers - logging, cron, Redis, queues, storage |
39
39
  | `@venizia/ignis-inversion` | Dependency Injection & IoC container |
40
40
  | `@venizia/dev-configs` | Shared oxlint, ESLint, TypeScript configurations |
41
- | `@venizia/ignis-docs` | Documentation and MCP Server |
41
+ | `@venizia/ignis-docs` | Documentation site content |
42
42
 
43
43
  ---
44
44
 
@@ -74,15 +74,12 @@ Ignis uses the following open-source libraries. We are grateful to the authors a
74
74
  | Library | License | Description |
75
75
  |---------|---------|-------------|
76
76
  | [reflect-metadata](https://github.com/rbuckton/reflect-metadata) | Apache-2.0 | Metadata reflection API |
77
- | [glob](https://github.com/isaacs/node-glob) | ISC | File pattern matching (used by boot) |
78
77
 
79
78
  ### Documentation Dependencies
80
79
 
81
80
  | Library | License | Description |
82
81
  |---------|---------|-------------|
83
82
  | [VitePress](https://github.com/vuejs/vitepress) | MIT | Static site generator |
84
- | [Fuse.js](https://github.com/krisk/fuse) | Apache-2.0 | Fuzzy search library |
85
- | [gray-matter](https://github.com/jonschlinkert/gray-matter) | MIT | Front-matter parser |
86
83
 
87
84
  ---
88
85
 
package/README.md CHANGED
@@ -23,7 +23,7 @@ Inside the Worker, extend `WorkerApplication` and call `listen()` instead of `st
23
23
  ```typescript
24
24
  // worker.ts
25
25
  import { z } from '@hono/zod-openapi';
26
- import { BaseRestController, controller, get, jsonResponse } from '@venizia/ignis-kernel';
26
+ import { BaseRestController, controller, get, OpenApiResponses } from '@venizia/ignis-kernel';
27
27
  import { WorkerApplication } from '@venizia/ignis-worker';
28
28
  import type { Context } from 'hono';
29
29
 
@@ -38,7 +38,7 @@ class NoteController extends BaseRestController {
38
38
  @get({
39
39
  configs: {
40
40
  path: '/',
41
- responses: jsonResponse({ schema: z.array(z.object({ title: z.string() })) }),
41
+ responses: OpenApiResponses.jsonResponse({ schema: z.array(z.object({ title: z.string() })) }),
42
42
  },
43
43
  })
44
44
  list(context: Context) {
@@ -87,13 +87,13 @@ request that arrives while the application is still booting.
87
87
 
88
88
  ## Make an existing client use it
89
89
 
90
- Most HTTP clients do not accept a custom fetcher. `installBffFetch` answers the page's global `fetch`
90
+ Most HTTP clients do not accept a custom fetcher. `BffFetchBridge.install` answers the page's global `fetch`
91
91
  for every URL under `basePath` and leaves every other call on the network:
92
92
 
93
93
  ```typescript
94
- import { installBffFetch } from '@venizia/ignis-worker';
94
+ import { BffFetchBridge } from '@venizia/ignis-worker';
95
95
 
96
- installBffFetch({ transport: bff, basePath: '/api' });
96
+ BffFetchBridge.install({ transport: bff, basePath: '/api' });
97
97
 
98
98
  await fetch('/api/notes'); // answered by the Worker
99
99
  ```
@@ -108,7 +108,7 @@ Install it before the code that calls `fetch` starts. It returns a function that
108
108
  | `SharedBffTransport` | One Worker per origin. It elects one tab with the Web Locks API and forwards the other tabs' requests over a `BroadcastChannel`. Use it by default |
109
109
  | `WorkerBffTransport` | One Worker per transport, for a BFF that holds no origin-exclusive resource |
110
110
  | `InProcessBffTransport` | No Worker - the same envelope round trip in one thread, for tests |
111
- | `installBffFetch` | Routes the global `fetch` into a transport |
111
+ | `BffFetchBridge` | `BffFetchBridge.install` routes the global `fetch` into a transport |
112
112
  | `BffEnvelope`, `BFF_SYNTHETIC_ORIGIN` | The request, response and error envelopes that cross `postMessage` |
113
113
 
114
114
  `SharedBffTransport` exists because a database like PGlite in OPFS holds an exclusive per-origin
@@ -21,21 +21,49 @@ export interface IInstallBffFetchOptions {
21
21
  /** Defaults to `globalThis`. */
22
22
  carrier?: IFetchCarrier;
23
23
  }
24
- /**
25
- * Routes the page's own `fetch` into a BFF transport for anything under `basePath`, and leaves every
26
- * other call on the network untouched.
27
- *
28
- * This is the seam that lets an existing HTTP client talk to an in-browser IGNIS application without
29
- * knowing one exists. A data provider, an SDK or a generated client reaches the network through the
30
- * global `fetch` and usually accepts no custom fetcher, so answering that `fetch` is the only place
31
- * an in-browser backend becomes a drop-in swap rather than a fork of the client.
32
- *
33
- * Install it BEFORE the application that will use it boots: a request issued while the original
34
- * `fetch` is still in place leaves the page and 404s against whatever is serving it.
35
- *
36
- * @returns The uninstall function. It restores the previous `fetch` only if this bridge is still the
37
- * installed one - if something patched `fetch` afterwards, restoring would silently discard that.
38
- */
39
- export declare const installBffFetch: (opts: IInstallBffFetchOptions) => (() => void);
24
+ /** Routes the page's own `fetch` into a BFF transport. */
25
+ export declare class BffFetchBridge {
26
+ /**
27
+ * Marks the patched `fetch` so a second install is refused rather than silently stacking. `Symbol.for`
28
+ * and not a private symbol: two copies of this package in one page must still see each other's mark.
29
+ */
30
+ private static readonly BRIDGE_MARKER;
31
+ /**
32
+ * Resolves a request's absolute URL WITHOUT constructing a `Request`.
33
+ *
34
+ * This is the whole reason this helper exists rather than one obvious line. `new Request(input)`,
35
+ * where `input` is a `Request` carrying a body, marks that body disturbed - so building one just to
36
+ * read `.url` breaks the very pass-through it is deciding about: the next line hands the same, now
37
+ * unreadable, request to the network.
38
+ *
39
+ * Measured, because the runtimes disagree. Chromium:
40
+ *
41
+ * original.bodyUsed // false
42
+ * new Request(original)
43
+ * original.bodyUsed // true
44
+ * await original.text() // TypeError: body stream already read
45
+ *
46
+ * Bun does NOT disturb it, so the unit tests pass either way and cannot guard this. The browser is
47
+ * where this code runs, so the browser is the behaviour to write for.
48
+ */
49
+ private static resolveRequestUrl;
50
+ private static toPrefixList;
51
+ /**
52
+ * Routes the page's own `fetch` into a BFF transport for anything under `basePath`, and leaves every
53
+ * other call on the network untouched.
54
+ *
55
+ * This is the seam that lets an existing HTTP client talk to an in-browser IGNIS application without
56
+ * knowing one exists. A data provider, an SDK or a generated client reaches the network through the
57
+ * global `fetch` and usually accepts no custom fetcher, so answering that `fetch` is the only place
58
+ * an in-browser backend becomes a drop-in swap rather than a fork of the client.
59
+ *
60
+ * Install it BEFORE the application that will use it boots: a request issued while the original
61
+ * `fetch` is still in place leaves the page and 404s against whatever is serving it.
62
+ *
63
+ * @returns The uninstall function. It restores the previous `fetch` only if this bridge is still the
64
+ * installed one - if something patched `fetch` afterwards, restoring would silently discard that.
65
+ */
66
+ static install(opts: IInstallBffFetchOptions): () => void;
67
+ }
40
68
  export {};
41
69
  //# sourceMappingURL=fetch-bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-bridge.d.ts","sourceRoot":"","sources":["../../../src/transport/fetch-bridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAQpD,KAAK,MAAM,GAAG,OAAO,UAAU,CAAC,KAAK,CAAC;AAEtC,2GAA2G;AAC3G,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,gGAAgG;IAChG,SAAS,EAAE,aAAa,CAAC;IACzB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,gCAAgC;IAChC,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAgDD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,uBAAuB,KAAG,CAAC,MAAM,IAAI,CAwD1E,CAAC"}
1
+ {"version":3,"file":"fetch-bridge.d.ts","sourceRoot":"","sources":["../../../src/transport/fetch-bridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,KAAK,MAAM,GAAG,OAAO,UAAU,CAAC,KAAK,CAAC;AAEtC,2GAA2G;AAC3G,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,gGAAgG;IAChG,SAAS,EAAE,aAAa,CAAC;IACzB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAChC,gCAAgC;IAChC,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,0DAA0D;AAC1D,qBAAa,cAAc;IACzB;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAiD;IAEtF;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAYhC,OAAO,CAAC,MAAM,CAAC,YAAY;IAgB3B;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,uBAAuB,GAAG,MAAM,IAAI;CAyD1D"}
@@ -1,100 +1,103 @@
1
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
- // Trailing slashes trimmed once, so the boundary test below is one comparison and one prefix check.
36
- const prefixes = (Array.isArray(basePath) ? basePath : [basePath])
37
- .map((prefix) => prefix.replace(/\/+$/, ''))
38
- .filter((prefix) => prefix.length > 0);
39
- if (prefixes.length === 0) {
40
- throw getError({
41
- message: '[installBffFetch] Invalid basePath | At least one non-empty path prefix is required',
42
- });
43
- }
44
- return prefixes;
45
- };
46
- /**
47
- * Routes the page's own `fetch` into a BFF transport for anything under `basePath`, and leaves every
48
- * other call on the network untouched.
49
- *
50
- * This is the seam that lets an existing HTTP client talk to an in-browser IGNIS application without
51
- * knowing one exists. A data provider, an SDK or a generated client reaches the network through the
52
- * global `fetch` and usually accepts no custom fetcher, so answering that `fetch` is the only place
53
- * an in-browser backend becomes a drop-in swap rather than a fork of the client.
54
- *
55
- * Install it BEFORE the application that will use it boots: a request issued while the original
56
- * `fetch` is still in place leaves the page and 404s against whatever is serving it.
57
- *
58
- * @returns The uninstall function. It restores the previous `fetch` only if this bridge is still the
59
- * installed one - if something patched `fetch` afterwards, restoring would silently discard that.
60
- */
61
- export const installBffFetch = (opts) => {
62
- const { transport, basePath } = opts;
63
- const origins = opts.origin === undefined
64
- ? undefined
65
- : new Set((Array.isArray(opts.origin) ? opts.origin : [opts.origin]).map((value) => new URL(value).origin));
66
- const carrier = opts.carrier ?? globalThis;
67
- const prefixes = toPrefixList(basePath);
68
- const previousFetch = carrier.fetch;
69
- if (Object.getOwnPropertyDescriptor(previousFetch, BRIDGE_MARKER)) {
70
- throw getError({
71
- message: '[installBffFetch] A BFF fetch bridge is already installed | Uninstall it first, or pass every prefix to a single install',
72
- });
2
+ /** Routes the page's own `fetch` into a BFF transport. */
3
+ export class BffFetchBridge {
4
+ /**
5
+ * Marks the patched `fetch` so a second install is refused rather than silently stacking. `Symbol.for`
6
+ * and not a private symbol: two copies of this package in one page must still see each other's mark.
7
+ */
8
+ static { this.BRIDGE_MARKER = Symbol.for('@venizia/ignis-worker/bff-fetch'); }
9
+ /**
10
+ * Resolves a request's absolute URL WITHOUT constructing a `Request`.
11
+ *
12
+ * This is the whole reason this helper exists rather than one obvious line. `new Request(input)`,
13
+ * where `input` is a `Request` carrying a body, marks that body disturbed - so building one just to
14
+ * read `.url` breaks the very pass-through it is deciding about: the next line hands the same, now
15
+ * unreadable, request to the network.
16
+ *
17
+ * Measured, because the runtimes disagree. Chromium:
18
+ *
19
+ * original.bodyUsed // false
20
+ * new Request(original)
21
+ * original.bodyUsed // true
22
+ * await original.text() // TypeError: body stream already read
23
+ *
24
+ * Bun does NOT disturb it, so the unit tests pass either way and cannot guard this. The browser is
25
+ * where this code runs, so the browser is the behaviour to write for.
26
+ */
27
+ static resolveRequestUrl(input) {
28
+ if (typeof input === 'string') {
29
+ return new URL(input, globalThis.location?.href).href;
30
+ }
31
+ if (input instanceof URL) {
32
+ return input.href;
33
+ }
34
+ return input.url;
73
35
  }
74
- const networkFetch = previousFetch.bind(carrier);
75
- const bridged = async (input, init) => {
76
- const url = new URL(resolveRequestUrl(input));
77
- const { pathname } = url;
78
- const isOwnOrigin = origins === undefined || origins.has(url.origin);
79
- const isOwnPath = prefixes.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
80
- if (!isOwnOrigin || !isOwnPath) {
81
- return networkFetch(input, init);
36
+ static toPrefixList(basePath) {
37
+ // Trailing slashes trimmed once, so the boundary test below is one comparison and one prefix check.
38
+ const prefixes = (Array.isArray(basePath) ? basePath : [basePath])
39
+ .map((prefix) => prefix.replace(/\/+$/, ''))
40
+ .filter((prefix) => prefix.length > 0);
41
+ if (prefixes.length === 0) {
42
+ throw getError({
43
+ message: '[installBffFetch] Invalid basePath | At least one non-empty path prefix is required',
44
+ });
82
45
  }
83
- // Constructed only now, on the branch that owns the request - so the pass-through above never
84
- // disturbs a body it is about to hand to the network.
85
- return transport.fetch({ request: new Request(input, init) });
86
- };
87
- // Whatever the runtime hung on `fetch` comes along - Bun puts `preconnect` there, and a
88
- // replacement that dropped it would break any caller reaching for it. Copying the descriptors is
89
- // also what makes this satisfy `typeof fetch` rather than merely resembling it.
90
- Object.defineProperties(bridged, Object.getOwnPropertyDescriptors(previousFetch));
91
- Object.defineProperty(bridged, BRIDGE_MARKER, { value: true });
92
- carrier.fetch = bridged;
93
- return () => {
94
- if (carrier.fetch !== bridged) {
95
- return;
46
+ return prefixes;
47
+ }
48
+ /**
49
+ * Routes the page's own `fetch` into a BFF transport for anything under `basePath`, and leaves every
50
+ * other call on the network untouched.
51
+ *
52
+ * This is the seam that lets an existing HTTP client talk to an in-browser IGNIS application without
53
+ * knowing one exists. A data provider, an SDK or a generated client reaches the network through the
54
+ * global `fetch` and usually accepts no custom fetcher, so answering that `fetch` is the only place
55
+ * an in-browser backend becomes a drop-in swap rather than a fork of the client.
56
+ *
57
+ * Install it BEFORE the application that will use it boots: a request issued while the original
58
+ * `fetch` is still in place leaves the page and 404s against whatever is serving it.
59
+ *
60
+ * @returns The uninstall function. It restores the previous `fetch` only if this bridge is still the
61
+ * installed one - if something patched `fetch` afterwards, restoring would silently discard that.
62
+ */
63
+ static install(opts) {
64
+ const { transport, basePath } = opts;
65
+ const origins = opts.origin === undefined
66
+ ? undefined
67
+ : new Set((Array.isArray(opts.origin) ? opts.origin : [opts.origin]).map((value) => new URL(value).origin));
68
+ const carrier = opts.carrier ?? globalThis;
69
+ const prefixes = BffFetchBridge.toPrefixList(basePath);
70
+ const previousFetch = carrier.fetch;
71
+ if (Object.getOwnPropertyDescriptor(previousFetch, BffFetchBridge.BRIDGE_MARKER)) {
72
+ throw getError({
73
+ message: '[installBffFetch] A BFF fetch bridge is already installed | Uninstall it first, or pass every prefix to a single install',
74
+ });
96
75
  }
97
- carrier.fetch = previousFetch;
98
- };
99
- };
76
+ const networkFetch = previousFetch.bind(carrier);
77
+ const bridged = async (input, init) => {
78
+ const url = new URL(BffFetchBridge.resolveRequestUrl(input));
79
+ const { pathname } = url;
80
+ const isOwnOrigin = origins === undefined || origins.has(url.origin);
81
+ const isOwnPath = prefixes.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
82
+ if (!isOwnOrigin || !isOwnPath) {
83
+ return networkFetch(input, init);
84
+ }
85
+ // Constructed only now, on the branch that owns the request - so the pass-through above never
86
+ // disturbs a body it is about to hand to the network.
87
+ return transport.fetch({ request: new Request(input, init) });
88
+ };
89
+ // Whatever the runtime hung on `fetch` comes along - Bun puts `preconnect` there, and a
90
+ // replacement that dropped it would break any caller reaching for it. Copying the descriptors is
91
+ // also what makes this satisfy `typeof fetch` rather than merely resembling it.
92
+ Object.defineProperties(bridged, Object.getOwnPropertyDescriptors(previousFetch));
93
+ Object.defineProperty(bridged, BffFetchBridge.BRIDGE_MARKER, { value: true });
94
+ carrier.fetch = bridged;
95
+ return () => {
96
+ if (carrier.fetch !== bridged) {
97
+ return;
98
+ }
99
+ carrier.fetch = previousFetch;
100
+ };
101
+ }
102
+ }
100
103
  //# sourceMappingURL=fetch-bridge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-bridge.js","sourceRoot":"","sources":["../../../src/transport/fetch-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["import { getError } from '@venizia/ignis-helpers/core';\nimport type { IBffTransport } from './common/types';\n\n/**\n * Marks the patched `fetch` so a second install is refused rather than silently stacking. `Symbol.for`\n * and not a private symbol: two copies of this package in one page must still see each other's mark.\n */\nconst BRIDGE_MARKER = Symbol.for('@venizia/ignis-worker/bff-fetch');\n\ntype TFetch = typeof globalThis.fetch;\n\n/** The carrier whose `fetch` is replaced - `globalThis` in a page, an object a test controls otherwise. */\ninterface IFetchCarrier {\n fetch: TFetch;\n}\n\nexport interface IInstallBffFetchOptions {\n /** Anything implementing the transport contract, so an in-process transport works in a test. */\n transport: IBffTransport;\n /**\n * Path prefixes the BFF owns, matched on a segment boundary: `/api` claims `/api` and `/api/...`,\n * never `/apix`. Everything else is passed to the original `fetch` untouched.\n */\n basePath: string | Array<string>;\n /**\n * The origin, or origins, the BFF answers for - the page's own, or an upstream gateway whose\n * calls the BFF stands in for. A request to any other origin goes to the network whatever its path.\n * Unset, only the path decides, so a call to ANY origin under `basePath` is claimed.\n */\n origin?: string | Array<string>;\n /** Defaults to `globalThis`. */\n carrier?: IFetchCarrier;\n}\n\n/**\n * Resolves a request's absolute URL WITHOUT constructing a `Request`.\n *\n * This is the whole reason this helper exists rather than one obvious line. `new Request(input)`,\n * where `input` is a `Request` carrying a body, marks that body disturbed - so building one just to\n * read `.url` breaks the very pass-through it is deciding about: the next line hands the same, now\n * unreadable, request to the network.\n *\n * Measured, because the runtimes disagree. Chromium:\n *\n * original.bodyUsed // false\n * new Request(original)\n * original.bodyUsed // true\n * await original.text() // TypeError: body stream already read\n *\n * Bun does NOT disturb it, so the unit tests pass either way and cannot guard this. The browser is\n * where this code runs, so the browser is the behaviour to write for.\n */\nconst resolveRequestUrl = (input: RequestInfo | URL): string => {\n if (typeof input === 'string') {\n return new URL(input, globalThis.location?.href).href;\n }\n\n if (input instanceof URL) {\n return input.href;\n }\n\n return input.url;\n};\n\nconst toPrefixList = (basePath: string | Array<string>): Array<string> => {\n // Trailing slashes trimmed once, so the boundary test below is one comparison and one prefix check.\n const prefixes = (Array.isArray(basePath) ? basePath : [basePath])\n .map((prefix) => prefix.replace(/\\/+$/, ''))\n .filter((prefix) => prefix.length > 0);\n\n if (prefixes.length === 0) {\n throw getError({\n message:\n '[installBffFetch] Invalid basePath | At least one non-empty path prefix is required',\n });\n }\n\n return prefixes;\n};\n\n/**\n * Routes the page's own `fetch` into a BFF transport for anything under `basePath`, and leaves every\n * other call on the network untouched.\n *\n * This is the seam that lets an existing HTTP client talk to an in-browser IGNIS application without\n * knowing one exists. A data provider, an SDK or a generated client reaches the network through the\n * global `fetch` and usually accepts no custom fetcher, so answering that `fetch` is the only place\n * an in-browser backend becomes a drop-in swap rather than a fork of the client.\n *\n * Install it BEFORE the application that will use it boots: a request issued while the original\n * `fetch` is still in place leaves the page and 404s against whatever is serving it.\n *\n * @returns The uninstall function. It restores the previous `fetch` only if this bridge is still the\n * installed one - if something patched `fetch` afterwards, restoring would silently discard that.\n */\nexport const installBffFetch = (opts: IInstallBffFetchOptions): (() => void) => {\n const { transport, basePath } = opts;\n const origins =\n opts.origin === undefined\n ? undefined\n : new Set(\n (Array.isArray(opts.origin) ? opts.origin : [opts.origin]).map(\n (value) => new URL(value).origin,\n ),\n );\n const carrier = opts.carrier ?? (globalThis as unknown as IFetchCarrier);\n\n const prefixes = toPrefixList(basePath);\n const previousFetch = carrier.fetch;\n\n if (Object.getOwnPropertyDescriptor(previousFetch, BRIDGE_MARKER)) {\n throw getError({\n message:\n '[installBffFetch] A BFF fetch bridge is already installed | Uninstall it first, or pass every prefix to a single install',\n });\n }\n\n const networkFetch = previousFetch.bind(carrier) as TFetch;\n\n const bridged = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const url = new URL(resolveRequestUrl(input));\n const { pathname } = url;\n const isOwnOrigin = origins === undefined || origins.has(url.origin);\n const isOwnPath = prefixes.some(\n (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`),\n );\n\n if (!isOwnOrigin || !isOwnPath) {\n return networkFetch(input, init);\n }\n\n // Constructed only now, on the branch that owns the request - so the pass-through above never\n // disturbs a body it is about to hand to the network.\n return transport.fetch({ request: new Request(input, init) });\n };\n\n // Whatever the runtime hung on `fetch` comes along - Bun puts `preconnect` there, and a\n // replacement that dropped it would break any caller reaching for it. Copying the descriptors is\n // also what makes this satisfy `typeof fetch` rather than merely resembling it.\n Object.defineProperties(bridged, Object.getOwnPropertyDescriptors(previousFetch));\n Object.defineProperty(bridged, BRIDGE_MARKER, { value: true });\n\n carrier.fetch = bridged as TFetch;\n\n return () => {\n if (carrier.fetch !== bridged) {\n return;\n }\n\n carrier.fetch = previousFetch;\n };\n};\n"]}
1
+ {"version":3,"file":"fetch-bridge.js","sourceRoot":"","sources":["../../../src/transport/fetch-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AA4BvD,0DAA0D;AAC1D,MAAM,OAAO,cAAc;IACzB;;;OAGG;aACqB,kBAAa,GAAG,MAAM,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IAEtF;;;;;;;;;;;;;;;;;OAiBG;IACK,MAAM,CAAC,iBAAiB,CAAC,KAAwB;QACvD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,IAAI,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;QACxD,CAAC;QAED,IAAI,KAAK,YAAY,GAAG,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,IAAI,CAAC;QACpB,CAAC;QAED,OAAO,KAAK,CAAC,GAAG,CAAC;IACnB,CAAC;IAEO,MAAM,CAAC,YAAY,CAAC,QAAgC;QAC1D,oGAAoG;QACpG,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;aAC/D,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;aAC3C,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAEzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,MAAM,QAAQ,CAAC;gBACb,OAAO,EACL,qFAAqF;aACxF,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,IAA6B;QAC1C,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;QACrC,MAAM,OAAO,GACX,IAAI,CAAC,MAAM,KAAK,SAAS;YACvB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,IAAI,GAAG,CACL,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAC5D,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,CACjC,CACF,CAAC;QACR,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAK,UAAuC,CAAC;QAEzE,MAAM,QAAQ,GAAG,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACvD,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC;QAEpC,IAAI,MAAM,CAAC,wBAAwB,CAAC,aAAa,EAAE,cAAc,CAAC,aAAa,CAAC,EAAE,CAAC;YACjF,MAAM,QAAQ,CAAC;gBACb,OAAO,EACL,0HAA0H;aAC7H,CAAC,CAAC;QACL,CAAC;QAED,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAW,CAAC;QAE3D,MAAM,OAAO,GAAG,KAAK,EAAE,KAAwB,EAAE,IAAkB,EAAqB,EAAE;YACxF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC;YAC7D,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC;YACzB,MAAM,WAAW,GAAG,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACrE,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAC7B,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,KAAK,MAAM,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CACrE,CAAC;YAEF,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,EAAE,CAAC;gBAC/B,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACnC,CAAC;YAED,8FAA8F;YAC9F,sDAAsD;YACtD,OAAO,SAAS,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC,CAAC;QAEF,wFAAwF;QACxF,iGAAiG;QACjG,gFAAgF;QAChF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,yBAAyB,CAAC,aAAa,CAAC,CAAC,CAAC;QAClF,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAE9E,OAAO,CAAC,KAAK,GAAG,OAAiB,CAAC;QAElC,OAAO,GAAG,EAAE;YACV,IAAI,OAAO,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;gBAC9B,OAAO;YACT,CAAC;YAED,OAAO,CAAC,KAAK,GAAG,aAAa,CAAC;QAChC,CAAC,CAAC;IACJ,CAAC","sourcesContent":["import { getError } from '@venizia/ignis-helpers/core';\nimport type { IBffTransport } from './common/types';\n\ntype TFetch = typeof globalThis.fetch;\n\n/** The carrier whose `fetch` is replaced - `globalThis` in a page, an object a test controls otherwise. */\ninterface IFetchCarrier {\n fetch: TFetch;\n}\n\nexport interface IInstallBffFetchOptions {\n /** Anything implementing the transport contract, so an in-process transport works in a test. */\n transport: IBffTransport;\n /**\n * Path prefixes the BFF owns, matched on a segment boundary: `/api` claims `/api` and `/api/...`,\n * never `/apix`. Everything else is passed to the original `fetch` untouched.\n */\n basePath: string | Array<string>;\n /**\n * The origin, or origins, the BFF answers for - the page's own, or an upstream gateway whose\n * calls the BFF stands in for. A request to any other origin goes to the network whatever its path.\n * Unset, only the path decides, so a call to ANY origin under `basePath` is claimed.\n */\n origin?: string | Array<string>;\n /** Defaults to `globalThis`. */\n carrier?: IFetchCarrier;\n}\n\n/** Routes the page's own `fetch` into a BFF transport. */\nexport class BffFetchBridge {\n /**\n * Marks the patched `fetch` so a second install is refused rather than silently stacking. `Symbol.for`\n * and not a private symbol: two copies of this package in one page must still see each other's mark.\n */\n private static readonly BRIDGE_MARKER = Symbol.for('@venizia/ignis-worker/bff-fetch');\n\n /**\n * Resolves a request's absolute URL WITHOUT constructing a `Request`.\n *\n * This is the whole reason this helper exists rather than one obvious line. `new Request(input)`,\n * where `input` is a `Request` carrying a body, marks that body disturbed - so building one just to\n * read `.url` breaks the very pass-through it is deciding about: the next line hands the same, now\n * unreadable, request to the network.\n *\n * Measured, because the runtimes disagree. Chromium:\n *\n * original.bodyUsed // false\n * new Request(original)\n * original.bodyUsed // true\n * await original.text() // TypeError: body stream already read\n *\n * Bun does NOT disturb it, so the unit tests pass either way and cannot guard this. The browser is\n * where this code runs, so the browser is the behaviour to write for.\n */\n private static resolveRequestUrl(input: RequestInfo | URL): string {\n if (typeof input === 'string') {\n return new URL(input, globalThis.location?.href).href;\n }\n\n if (input instanceof URL) {\n return input.href;\n }\n\n return input.url;\n }\n\n private static toPrefixList(basePath: string | Array<string>): Array<string> {\n // Trailing slashes trimmed once, so the boundary test below is one comparison and one prefix check.\n const prefixes = (Array.isArray(basePath) ? basePath : [basePath])\n .map((prefix) => prefix.replace(/\\/+$/, ''))\n .filter((prefix) => prefix.length > 0);\n\n if (prefixes.length === 0) {\n throw getError({\n message:\n '[installBffFetch] Invalid basePath | At least one non-empty path prefix is required',\n });\n }\n\n return prefixes;\n }\n\n /**\n * Routes the page's own `fetch` into a BFF transport for anything under `basePath`, and leaves every\n * other call on the network untouched.\n *\n * This is the seam that lets an existing HTTP client talk to an in-browser IGNIS application without\n * knowing one exists. A data provider, an SDK or a generated client reaches the network through the\n * global `fetch` and usually accepts no custom fetcher, so answering that `fetch` is the only place\n * an in-browser backend becomes a drop-in swap rather than a fork of the client.\n *\n * Install it BEFORE the application that will use it boots: a request issued while the original\n * `fetch` is still in place leaves the page and 404s against whatever is serving it.\n *\n * @returns The uninstall function. It restores the previous `fetch` only if this bridge is still the\n * installed one - if something patched `fetch` afterwards, restoring would silently discard that.\n */\n static install(opts: IInstallBffFetchOptions): () => void {\n const { transport, basePath } = opts;\n const origins =\n opts.origin === undefined\n ? undefined\n : new Set(\n (Array.isArray(opts.origin) ? opts.origin : [opts.origin]).map(\n (value) => new URL(value).origin,\n ),\n );\n const carrier = opts.carrier ?? (globalThis as unknown as IFetchCarrier);\n\n const prefixes = BffFetchBridge.toPrefixList(basePath);\n const previousFetch = carrier.fetch;\n\n if (Object.getOwnPropertyDescriptor(previousFetch, BffFetchBridge.BRIDGE_MARKER)) {\n throw getError({\n message:\n '[installBffFetch] A BFF fetch bridge is already installed | Uninstall it first, or pass every prefix to a single install',\n });\n }\n\n const networkFetch = previousFetch.bind(carrier) as TFetch;\n\n const bridged = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const url = new URL(BffFetchBridge.resolveRequestUrl(input));\n const { pathname } = url;\n const isOwnOrigin = origins === undefined || origins.has(url.origin);\n const isOwnPath = prefixes.some(\n (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`),\n );\n\n if (!isOwnOrigin || !isOwnPath) {\n return networkFetch(input, init);\n }\n\n // Constructed only now, on the branch that owns the request - so the pass-through above never\n // disturbs a body it is about to hand to the network.\n return transport.fetch({ request: new Request(input, init) });\n };\n\n // Whatever the runtime hung on `fetch` comes along - Bun puts `preconnect` there, and a\n // replacement that dropped it would break any caller reaching for it. Copying the descriptors is\n // also what makes this satisfy `typeof fetch` rather than merely resembling it.\n Object.defineProperties(bridged, Object.getOwnPropertyDescriptors(previousFetch));\n Object.defineProperty(bridged, BffFetchBridge.BRIDGE_MARKER, { value: true });\n\n carrier.fetch = bridged as TFetch;\n\n return () => {\n if (carrier.fetch !== bridged) {\n return;\n }\n\n carrier.fetch = previousFetch;\n };\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venizia/ignis-worker",
3
- "version": "0.4.7-0",
3
+ "version": "0.4.7-1",
4
4
  "description": "Browser Web Worker host for the IGNIS framework: the request/response envelope, the transport contract, and WorkerApplication - the layer that listens on `onmessage` instead of a socket, so a RestApplication can serve its controllers with no server anywhere.",
5
5
  "keywords": [
6
6
  "browser",
@@ -52,18 +52,17 @@
52
52
  "access": "public"
53
53
  },
54
54
  "scripts": {
55
- "force-update": "sh ../../scripts/build/force-update.sh",
56
- "prepublishOnly": "make rebuild"
55
+ "force-update": "sh ../../scripts/build/force-update.sh"
57
56
  },
58
57
  "dependencies": {
59
- "@venizia/ignis-helpers": "0.4.7-0",
60
- "@venizia/ignis-inversion": "0.4.6-0",
61
- "@venizia/ignis-kernel": "0.4.7-0",
58
+ "@venizia/ignis-helpers": "0.4.7-1",
59
+ "@venizia/ignis-inversion": "0.4.6-1",
60
+ "@venizia/ignis-kernel": "0.4.7-1",
62
61
  "reflect-metadata": "^0.2.2"
63
62
  },
64
63
  "devDependencies": {
65
64
  "@types/bun": "^1.4.3",
66
- "@venizia/dev-configs": "0.4.7-0",
65
+ "@venizia/dev-configs": "0.4.7-1",
67
66
  "eslint": "^10.13.0",
68
67
  "oxlint": "1.89.0",
69
68
  "oxlint-tsgolint": "7.0.2004",