@motujs/runtime 0.0.0-stage → 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Scorbutics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ import type { Transport } from './index.js';
2
+ /**
3
+ * A Transport that dispatches to functions in this process instead of over the wire.
4
+ *
5
+ * HttpTransport exists because the Jakarta ocean keeps its data access on the server, where the
6
+ * browser cannot reach it without a request. That is not universal. An app built on row-level
7
+ * security does its reads from the browser already, holding the user's session — there is no server
8
+ * tier to call, and routing an island's query through one would add a hop, a second client, and a
9
+ * place for authorization to be reimplemented differently.
10
+ *
11
+ * What motu actually needs is not a network boundary but a SINGLE SEAM: one choke point every island
12
+ * call passes through, so the lagoon can put MockTransport in its place and replay fixtures with no
13
+ * backend. This gives that seam without inventing a server tier — the same island, calling the same
14
+ * contract, is offline in the lagoon and live in the host.
15
+ *
16
+ * Authorization is untouched and stays exactly where the app already put it: these functions are the
17
+ * app's own, and whatever they use (a session-bound client, row-level security) decides what the
18
+ * caller may see. motu adds no credential and can widen nothing.
19
+ *
20
+ * ```ts
21
+ * configure(new DirectTransport(services)) // host composition root
22
+ * configure(new MockTransport(fixtures)) // lagoon — same components, no backend
23
+ * ```
24
+ */
25
+ export declare class DirectTransport implements Transport {
26
+ private readonly services;
27
+ constructor(services: Record<string, Record<string, (...args: never[]) => Promise<unknown>>>);
28
+ call<T>(service: string, method: string, args: unknown[]): Promise<T>;
29
+ }
30
+ //# sourceMappingURL=direct-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"direct-transport.d.ts","sourceRoot":"","sources":["../src/direct-transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGzC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,eAAgB,YAAW,SAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAEvG,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;CAgB5E"}
@@ -0,0 +1,46 @@
1
+ import { MotuError } from './index.js';
2
+ /**
3
+ * A Transport that dispatches to functions in this process instead of over the wire.
4
+ *
5
+ * HttpTransport exists because the Jakarta ocean keeps its data access on the server, where the
6
+ * browser cannot reach it without a request. That is not universal. An app built on row-level
7
+ * security does its reads from the browser already, holding the user's session — there is no server
8
+ * tier to call, and routing an island's query through one would add a hop, a second client, and a
9
+ * place for authorization to be reimplemented differently.
10
+ *
11
+ * What motu actually needs is not a network boundary but a SINGLE SEAM: one choke point every island
12
+ * call passes through, so the lagoon can put MockTransport in its place and replay fixtures with no
13
+ * backend. This gives that seam without inventing a server tier — the same island, calling the same
14
+ * contract, is offline in the lagoon and live in the host.
15
+ *
16
+ * Authorization is untouched and stays exactly where the app already put it: these functions are the
17
+ * app's own, and whatever they use (a session-bound client, row-level security) decides what the
18
+ * caller may see. motu adds no credential and can widen nothing.
19
+ *
20
+ * ```ts
21
+ * configure(new DirectTransport(services)) // host composition root
22
+ * configure(new MockTransport(fixtures)) // lagoon — same components, no backend
23
+ * ```
24
+ */
25
+ export class DirectTransport {
26
+ constructor(services) {
27
+ this.services = services;
28
+ }
29
+ async call(service, method, args) {
30
+ // Own-property lookups only: a plain index would resolve 'constructor' or '__proto__' and hand
31
+ // back something that was never an exposed method. Deny-by-default has to mean the map, exactly.
32
+ if (!Object.prototype.hasOwnProperty.call(this.services, service)) {
33
+ throw new MotuError(404, `motu: no such service '${service}'`);
34
+ }
35
+ const impl = this.services[service];
36
+ if (!impl || !Object.prototype.hasOwnProperty.call(impl, method)) {
37
+ throw new MotuError(404, `motu: no such method '${service}.${method}'`);
38
+ }
39
+ const fn = impl[method];
40
+ if (typeof fn !== 'function') {
41
+ throw new MotuError(404, `motu: no such method '${service}.${method}'`);
42
+ }
43
+ return (await fn(...args));
44
+ }
45
+ }
46
+ //# sourceMappingURL=direct-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"direct-transport.js","sourceRoot":"","sources":["../src/direct-transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,eAAe;IAC1B,YAA6B,QAAgF;QAAhF,aAAQ,GAAR,QAAQ,CAAwE;IAAG,CAAC;IAEjH,KAAK,CAAC,IAAI,CAAI,OAAe,EAAE,MAAc,EAAE,IAAe;QAC5D,+FAA+F;QAC/F,iGAAiG;QACjG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC;YAClE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,0BAA0B,OAAO,GAAG,CAAC,CAAC;QACjE,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;YACjE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,yBAAyB,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC;QAC1E,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QACxB,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;YAC7B,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,yBAAyB,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,CAAC,MAAO,EAA4C,CAAC,GAAG,IAAI,CAAC,CAAM,CAAC;IAC7E,CAAC;CACF"}
@@ -0,0 +1,30 @@
1
+ import { type Transport } from './index.js';
2
+ /**
3
+ * Talks to the motu dispatcher mounted inside the legacy app's existing JAX-RS `/rest` stack.
4
+ *
5
+ * It inherits that stack's XSRF protection: AngularJS promotes an XSRF cookie into a matching
6
+ * header, and the app's `XsrfFilter` validates it. The names are module-specific (web-console uses
7
+ * cookie `M-XSRF-TOKEN` -> header `X-M-XSRF-TOKEN`), so they are configurable here.
8
+ *
9
+ * Auth failures come back as HTTP 401 (the app deliberately avoids 403 to sidestep an IP-ban
10
+ * heuristic), and an expired session may surface as a legacy 302 -> HTML redirect that `fetch`
11
+ * follows transparently; both are handled below.
12
+ */
13
+ export interface HttpTransportOptions {
14
+ /** Name of the XSRF cookie the host module sets (e.g. 'M-XSRF-TOKEN' for web-console). */
15
+ xsrfCookieName?: string;
16
+ /** Name of the header the host's XsrfFilter validates (e.g. 'X-M-XSRF-TOKEN'). */
17
+ xsrfHeaderName?: string;
18
+ onSessionLost?: () => void;
19
+ }
20
+ export declare class HttpTransport implements Transport {
21
+ private readonly base;
22
+ private readonly xsrfCookieName;
23
+ private readonly xsrfHeaderName;
24
+ private readonly onSessionLost;
25
+ constructor(base?: string, opts?: HttpTransportOptions);
26
+ /** The dispatcher path this call goes to — what the dock shows when one lands. */
27
+ endpoint(service: string, method: string): string;
28
+ call<T>(service: string, method: string, args: unknown[]): Promise<T>;
29
+ }
30
+ //# sourceMappingURL=http-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-transport.d.ts","sourceRoot":"","sources":["../src/http-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAQzE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,oBAAoB;IACnC,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,qBAAa,aAAc,YAAW,SAAS;IAM3C,OAAO,CAAC,QAAQ,CAAC,IAAI;IALvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAa;gBAGxB,IAAI,GAAE,MAAqB,EAC5C,IAAI,GAAE,oBAAyB;IAYjC,kFAAkF;IAClF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAI3C,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;CAgC5E"}
@@ -0,0 +1,51 @@
1
+ import { MotuError, SessionExpiredError } from './index.js';
2
+ function readCookie(name) {
3
+ const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
4
+ // Group 1 exists whenever the match does — the pattern has exactly one capture.
5
+ return match ? decodeURIComponent(match[1]) : undefined;
6
+ }
7
+ export class HttpTransport {
8
+ constructor(base = '/rest/motu', opts = {}) {
9
+ this.base = base;
10
+ this.xsrfCookieName = opts.xsrfCookieName ?? 'XSRF-TOKEN';
11
+ this.xsrfHeaderName = opts.xsrfHeaderName ?? 'X-XSRF-TOKEN';
12
+ this.onSessionLost =
13
+ opts.onSessionLost ??
14
+ (() => {
15
+ const returnUrl = encodeURIComponent(location.pathname + location.search);
16
+ location.href = '/login?returnUrl=' + returnUrl;
17
+ });
18
+ }
19
+ /** The dispatcher path this call goes to — what the dock shows when one lands. */
20
+ endpoint(service, method) {
21
+ return `${this.base}/${service}/${method}`;
22
+ }
23
+ async call(service, method, args) {
24
+ const xsrf = readCookie(this.xsrfCookieName);
25
+ const res = await fetch(this.endpoint(service, method), {
26
+ method: 'POST',
27
+ credentials: 'same-origin',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ 'X-Requested-With': 'XMLHttpRequest',
31
+ ...(xsrf ? { [this.xsrfHeaderName]: xsrf } : {}),
32
+ },
33
+ body: JSON.stringify(args),
34
+ });
35
+ if (res.status === 401) {
36
+ this.onSessionLost();
37
+ throw new SessionExpiredError();
38
+ }
39
+ // Backstop for a legacy 302 -> login HTML that fetch followed and returned as 200 text/html.
40
+ const contentType = res.headers.get('content-type') ?? '';
41
+ if (!contentType.includes('application/json')) {
42
+ this.onSessionLost();
43
+ throw new SessionExpiredError();
44
+ }
45
+ if (!res.ok) {
46
+ throw new MotuError(res.status, await res.text());
47
+ }
48
+ return (await res.json());
49
+ }
50
+ }
51
+ //# sourceMappingURL=http-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-transport.js","sourceRoot":"","sources":["../src/http-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAkB,MAAM,SAAS,CAAC;AAEzE,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,UAAU,GAAG,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC;IAChF,gFAAgF;IAChF,OAAO,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3D,CAAC;AAqBD,MAAM,OAAO,aAAa;IAKxB,YACmB,OAAe,YAAY,EAC5C,OAA6B,EAAE;QADd,SAAI,GAAJ,IAAI,CAAuB;QAG5C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,YAAY,CAAC;QAC1D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,cAAc,CAAC;QAC5D,IAAI,CAAC,aAAa;YAChB,IAAI,CAAC,aAAa;gBAClB,CAAC,GAAG,EAAE;oBACJ,MAAM,SAAS,GAAG,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;oBAC1E,QAAQ,CAAC,IAAI,GAAG,mBAAmB,GAAG,SAAS,CAAC;gBAClD,CAAC,CAAC,CAAC;IACP,CAAC;IAED,kFAAkF;IAClF,QAAQ,CAAC,OAAe,EAAE,MAAc;QACtC,OAAO,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,IAAI,CAAI,OAAe,EAAE,MAAc,EAAE,IAAe;QAC5D,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAE7C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE;YACtD,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,aAAa;YAC1B,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,kBAAkB,EAAE,gBAAgB;gBACpC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACjD;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;QAEH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,MAAM,IAAI,mBAAmB,EAAE,CAAC;QAClC,CAAC;QAED,6FAA6F;QAC7F,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAC1D,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,MAAM,IAAI,mBAAmB,EAAE,CAAC;QAClC,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QAED,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAC;IACjC,CAAC;CACF"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The injection seam. A component calls `call(...)` and never knows which Transport backs it —
3
+ * that decision lives only in a composition root (bridge / standalone / sandbox).
4
+ */
5
+ export interface Transport {
6
+ call<T>(service: string, method: string, args: unknown[]): Promise<T>;
7
+ /**
8
+ * WHERE a call to `service.method` goes, for chrome that shows a person what just left.
9
+ *
10
+ * Optional, and it has to be: a transport is an injection seam, and the two that ship here are a
11
+ * fetch to a dispatcher (which has a URL) and a direct in-process dispatch (which has none). A
12
+ * readout that invented an address for the second would be a lie in the surface built to end
13
+ * guessing about where data came from, so the absence is reported as an absence.
14
+ */
15
+ endpoint?(service: string, method: string): string;
16
+ }
17
+ /** Called once, at a composition root, to choose the transport for this runtime. */
18
+ export declare function configure(t: Transport): void;
19
+ /** A single lifecycle event for one contract call, emitted at start and again at completion. */
20
+ export interface CallEvent {
21
+ id: number;
22
+ service: string;
23
+ method: string;
24
+ args: unknown[];
25
+ /** The island the call was attributed to (its custom-element tag), or null if outside any window. */
26
+ island: string | null;
27
+ /** Where it went, when the transport can say — see `Transport.endpoint`. */
28
+ url?: string;
29
+ phase: 'start' | 'success' | 'error';
30
+ status?: number;
31
+ durationMs?: number;
32
+ error?: string;
33
+ }
34
+ type CallObserver = (e: CallEvent) => void;
35
+ /** Subscribe to every contract call's lifecycle (dev overlay). Returns an unsubscribe. */
36
+ export declare function observeCalls(observer: CallObserver): () => void;
37
+ /**
38
+ * Runs `fn` with `id` as the ambient island, so any `call()` that fires synchronously inside is
39
+ * attributed to it. The framework opens this window around an island's render/update; islands never
40
+ * touch it (zero per-island cost).
41
+ */
42
+ export declare function runWithIsland<T>(id: string | null, fn: () => T): T;
43
+ /** One captured call: its request (service/method/args) and the backend's response (or error status). */
44
+ export interface RecordedCall {
45
+ service: string;
46
+ method: string;
47
+ args: unknown[];
48
+ response?: unknown;
49
+ status?: number;
50
+ }
51
+ /** Begin capturing contract calls (dev tooling). Replaces any in-progress recording. */
52
+ export declare function startRecording(): void;
53
+ /** Stop capturing and return everything recorded since startRecording(). */
54
+ export declare function stopRecording(): RecordedCall[];
55
+ /** The single entry point the generated contract uses. Fails loudly if not configured. */
56
+ export declare function call<T>(service: string, method: string, args: unknown[]): Promise<T>;
57
+ export declare class MotuError extends Error {
58
+ readonly status: number;
59
+ readonly declared: boolean;
60
+ /**
61
+ * `declared` is true when a fixture ASKED for this failure. The application never reads it -- it
62
+ * takes the same path either way, which is the point of throwing the real error -- but the lagoon's
63
+ * call feed does, so a failure the state declares is not shown as one nobody expected.
64
+ */
65
+ constructor(status: number, message: string, declared?: boolean);
66
+ }
67
+ export declare class SessionExpiredError extends MotuError {
68
+ constructor();
69
+ }
70
+ export { HttpTransport } from './http-transport.js';
71
+ export { DirectTransport } from './direct-transport.js';
72
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtE;;;;;;;OAOG;IACH,QAAQ,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACpD;AAID,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,CAAC,EAAE,SAAS,GAAG,IAAI,CAE5C;AAQD,gGAAgG;AAChG,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,qGAAqG;IACrG,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,YAAY,GAAG,CAAC,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC;AAQ3C,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI,CAG/D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAElE;AAWD,yGAAyG;AACzG,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID,wFAAwF;AACxF,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,IAAI,YAAY,EAAE,CAI9C;AAED,0FAA0F;AAC1F,wBAAgB,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAsDpF;AAWD,qBAAa,SAAU,SAAQ,KAAK;aAMN,MAAM,EAAE,MAAM;aAAmC,QAAQ;IALrF;;;;OAIG;gBACyB,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAkB,QAAQ,UAAQ;CAI9F;AAED,qBAAa,mBAAoB,SAAQ,SAAS;;CAKjD;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,128 @@
1
+ // Stripped in production: the debug overlay's instrumentation is gated on this build-time constant,
2
+ // so the whole block below dead-code-eliminates when it is false (and is safely `undefined` under
3
+ // bare Node/tsc, where the typeof guard evaluates to false rather than throwing).
4
+ import { runWithIsland as coreRunWithIsland, ambientIsland, recordOutbound, beginCall } from '@motujs/core';
5
+ const DEBUG = typeof __MOTU_DEBUG__ !== 'undefined' && __MOTU_DEBUG__;
6
+ let current = null;
7
+ /** Called once, at a composition root, to choose the transport for this runtime. */
8
+ export function configure(t) {
9
+ current = t;
10
+ }
11
+ const callObservers = new Set();
12
+ let callSeq = 0;
13
+ // The window itself lives in @motujs/core, so a traced host-module call and a contract call are
14
+ // attributed by the SAME open window. Two copies meant `runWithIsland` from here left `traced` blind:
15
+ // the react adapter opens one window, and only the transport's calls carried a tag.
16
+ const currentIsland = () => ambientIsland();
17
+ /** Subscribe to every contract call's lifecycle (dev overlay). Returns an unsubscribe. */
18
+ export function observeCalls(observer) {
19
+ callObservers.add(observer);
20
+ return () => callObservers.delete(observer);
21
+ }
22
+ /**
23
+ * Runs `fn` with `id` as the ambient island, so any `call()` that fires synchronously inside is
24
+ * attributed to it. The framework opens this window around an island's render/update; islands never
25
+ * touch it (zero per-island cost).
26
+ */
27
+ export function runWithIsland(id, fn) {
28
+ return coreRunWithIsland(id, fn);
29
+ }
30
+ function emitCall(e) {
31
+ callObservers.forEach((o) => o(e));
32
+ }
33
+ let recordingSink = null;
34
+ /** Begin capturing contract calls (dev tooling). Replaces any in-progress recording. */
35
+ export function startRecording() {
36
+ recordingSink = [];
37
+ }
38
+ /** Stop capturing and return everything recorded since startRecording(). */
39
+ export function stopRecording() {
40
+ const out = recordingSink ?? [];
41
+ recordingSink = null;
42
+ return out;
43
+ }
44
+ /** The single entry point the generated contract uses. Fails loudly if not configured. */
45
+ export function call(service, method, args) {
46
+ if (!current) {
47
+ throw new Error('motu: configure() was not called before a service call');
48
+ }
49
+ if (!DEBUG)
50
+ return current.call(service, method, args);
51
+ const id = ++callSeq;
52
+ const island = currentIsland();
53
+ const t0 = performance.now();
54
+ // WHERE it goes, asked of the transport rather than assumed: only the transport knows, and one of
55
+ // the two that ship here has no answer. `''` reads as "no address", which the dock shows as the
56
+ // method name — never as a URL somebody could go looking for.
57
+ const url = current.endpoint?.(service, method) ?? '';
58
+ // THE LIVE FEED, which is the other half of the ledger below: `recordOutbound` says an ask LEFT
59
+ // (that is when the owner window is open), this says how it came back.
60
+ const landed = beginCall({ via: 'contract', url, method: 'POST', label: `${service}.${method}` });
61
+ // THE THIRD DOOR, in the same ledger as the other two. A contract call was observable only through
62
+ // `observeCalls` — the debug overlay — so no check ever printed it, and a region whose islands talk
63
+ // exclusively through the contract read as one that fetched nothing.
64
+ recordOutbound('contract', `${service}.${method}`, args.map((a) => (typeof a === 'object' && a !== null ? '…' : String(a))).join(', '));
65
+ emitCall({ id, service, method, args, island, url, phase: 'start' });
66
+ return current.call(service, method, args).then((result) => {
67
+ landed({ status: 200, ok: true });
68
+ emitCall({ id, service, method, args, island, url, phase: 'success', durationMs: performance.now() - t0 });
69
+ if (recordingSink)
70
+ recordingSink.push({ service, method, args, response: result });
71
+ return result;
72
+ }, (err) => {
73
+ const status = err instanceof MotuError ? err.status : undefined;
74
+ // A THROW THAT IS NOT A STATUS still landed, and 0 is how the feed spells "never got one" —
75
+ // dropping it would make a transport that rejected look like a call that is still in flight.
76
+ landed({
77
+ status: status ?? 0,
78
+ ok: false,
79
+ error: err instanceof Error ? err.message : String(err),
80
+ ...(err instanceof MotuError && err.declared ? { declared: true } : {}),
81
+ });
82
+ emitCall({
83
+ id,
84
+ service,
85
+ method,
86
+ args,
87
+ island,
88
+ url,
89
+ phase: 'error',
90
+ status,
91
+ durationMs: performance.now() - t0,
92
+ error: err instanceof Error ? err.message : String(err),
93
+ });
94
+ if (recordingSink)
95
+ recordingSink.push({ service, method, args, status });
96
+ throw err;
97
+ });
98
+ }
99
+ // In debug builds (the lagoon), expose the recorder to the page so `motu fixtures record` can drive
100
+ // it via the browser. Stripped in production along with the rest of the DEBUG block.
101
+ if (DEBUG && typeof globalThis !== 'undefined') {
102
+ globalThis.__motuRecorder = {
103
+ start: startRecording,
104
+ stop: stopRecording,
105
+ };
106
+ }
107
+ export class MotuError extends Error {
108
+ /**
109
+ * `declared` is true when a fixture ASKED for this failure. The application never reads it -- it
110
+ * takes the same path either way, which is the point of throwing the real error -- but the lagoon's
111
+ * call feed does, so a failure the state declares is not shown as one nobody expected.
112
+ */
113
+ constructor(status, message, declared = false) {
114
+ super(message);
115
+ this.status = status;
116
+ this.declared = declared;
117
+ this.name = 'MotuError';
118
+ }
119
+ }
120
+ export class SessionExpiredError extends MotuError {
121
+ constructor() {
122
+ super(401, 'motu: session expired');
123
+ this.name = 'SessionExpiredError';
124
+ }
125
+ }
126
+ export { HttpTransport } from './http-transport.js';
127
+ export { DirectTransport } from './direct-transport.js';
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}