@shenora/react 0.10.0 → 0.12.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.
Files changed (44) hide show
  1. package/README.md +152 -165
  2. package/dist/bridge.d.ts +29 -46
  3. package/dist/bridge.js +47 -71
  4. package/dist/clipboard.d.ts +89 -0
  5. package/dist/clipboard.js +119 -0
  6. package/dist/devInterceptor.d.ts +11 -8
  7. package/dist/devInterceptor.js +16 -10
  8. package/dist/errors.d.ts +5 -6
  9. package/dist/errors.js +6 -7
  10. package/dist/eventBus.d.ts +21 -26
  11. package/dist/eventBus.js +38 -40
  12. package/dist/fileDialogs.d.ts +9 -12
  13. package/dist/fileDialogs.js +12 -16
  14. package/dist/hooks.d.ts +19 -20
  15. package/dist/hooks.js +26 -29
  16. package/dist/index.d.ts +8 -2
  17. package/dist/index.js +14 -10
  18. package/dist/internal.d.ts +2 -8
  19. package/dist/internal.js +2 -8
  20. package/dist/media.d.ts +14 -22
  21. package/dist/media.js +14 -22
  22. package/dist/mediaPlayer.d.ts +103 -0
  23. package/dist/mediaPlayer.js +202 -0
  24. package/dist/moduleService.d.ts +17 -21
  25. package/dist/moduleService.js +17 -21
  26. package/dist/requests.d.ts +145 -0
  27. package/dist/requests.js +113 -0
  28. package/dist/segmentBinder.d.ts +74 -0
  29. package/dist/segmentBinder.js +239 -0
  30. package/dist/segmentStream.d.ts +125 -0
  31. package/dist/segmentStream.js +239 -0
  32. package/dist/store.d.ts +18 -26
  33. package/dist/store.js +69 -36
  34. package/dist/transport.d.ts +9 -18
  35. package/dist/transport.js +9 -18
  36. package/dist/types.d.ts +33 -42
  37. package/dist/types.js +29 -25
  38. package/dist/useDropZone.d.ts +23 -22
  39. package/dist/useDropZone.js +41 -37
  40. package/dist/windowCommands.d.ts +15 -19
  41. package/dist/windowCommands.js +18 -25
  42. package/package.json +10 -3
  43. package/dist/operations.d.ts +0 -256
  44. package/dist/operations.js +0 -191
package/README.md CHANGED
@@ -1,165 +1,152 @@
1
- # @shenora/react
2
-
3
- React client for [Shenora](https://github.com/JiarongGu/Shenora) desktop hosts (.NET + WinForms +
4
- WebView2). The typed bridge between a React frontend and the Shenora host: correlated `invoke`
5
- with timeouts and structured errors, the event hub host notifications stream into, typed module
6
- services, React hooks, and a pluggable transport with a browser fallback so the UI can be
7
- developed in a plain browser. Headless by design — no UI components, bring your own design
8
- system. Versioned in lockstep with the `Shenora.*` NuGet packages.
9
-
10
- ```ts
11
- import {
12
- getBridge, useShenoraEvent, useShenoraQuery, BaseModuleService, createShenoraStore,
13
- } from '@shenora/react';
14
-
15
- // Once at startup, after your listeners are attached: it starts notification delivery (anything the
16
- // host buffered arrives in the first batch). Drop zones need no particular ordering against it — the
17
- // host clears them when a new DOCUMENT loads, not on this handshake.
18
- await getBridge().notifyReady();
19
-
20
- // a typed service per backend module:
21
- interface NoteRequests { GET_ALL: void; ADD: { title: string } }
22
- class NoteService extends BaseModuleService<NoteRequests> {
23
- constructor() { super('NOTES'); }
24
- getAll() { return this.send<Note[]>('GET_ALL'); }
25
- add(title: string) { return this.send<Note>('ADD', { payload: { title } }); }
26
- }
27
-
28
- // in components:
29
- const { data, loading, refetch } = useShenoraQuery<Note[]>('NOTES', 'GET_ALL');
30
- useShenoraEvent<Note>('NOTES', 'ADDED', (note) => refetch());
31
- ```
32
-
33
- Failed calls reject with `OperationError` — a structured `code` (an i18n key: translate
34
- `errors.{code}`) plus interpolation `parameters`, never raw host exception text.
35
-
36
- ### Long-running work: post, then read a store
37
-
38
- `invoke` awaits a correlated reply and carries a timeout, and its handler's synchronous segment runs
39
- on the host's **UI thread** — so it is for calls that are quick and UI-thread-safe. Everything else
40
- posts and streams results back as events:
41
-
42
- ```ts
43
- const useDeploy = createShenoraStore('DEPLOY', {
44
- initial: { status: 'idle', lines: [] as string[] },
45
- // Loaded on the FIRST subscriber, so a component that mounts mid-run isn't empty:
46
- // events it missed cannot be replayed.
47
- snapshot: { type: 'GET_STATE', apply: (s, d) => ({ ...s, ...(d as object) }) },
48
- on: {
49
- PROGRESS: (s, p: { line: string }) => ({ ...s, lines: [...s.lines, p.line] }),
50
- ENDED: (s, p: { ok: boolean }) => ({ ...s, status: p.ok ? 'done' : 'failed' }),
51
- },
52
- actions: ({ post }) => ({ start: (cfg: unknown) => post('START', { payload: cfg }) }),
53
- });
54
-
55
- // any number of components share ONE subscription:
56
- const status = useDeploy((s) => s.status);
57
- useDeploy.actions.start({ env: 'prod' });
58
- ```
59
-
60
- Use the **store for shared or long-lived state** and **`useShenoraEvent` for a one-off reaction in a
61
- single component**. A failed `post` has no promise to reject, so it is reported through the bridge's
62
- `onPostError` rather than vanishing — it is bridge-wide, so wire it once at startup
63
- (`getBridge()` takes no options):
64
-
65
- ```ts
66
- configureBridge({ onPostError: (failure) => log.error(failure.module, failure.type, failure.error) });
67
- ```
68
-
69
- ### Tracked operations: a ready-made progress store
70
-
71
- For work the host tracks with `Shenora.Ipc`'s operation registry (an `IModuleContext.Run`/`Start`
72
- route on the C# side), `useShenoraOperations()` is a `createShenoraStore` instance built the same
73
- way as the example above — no per-feature event wiring needed:
74
-
75
- ```ts
76
- import { useShenoraOperations } from '@shenora/react';
77
-
78
- const running = useShenoraOperations((s) => s.running); // every in-flight operation
79
- const waiting = useShenoraOperations((s) => s.waiting); // stopped, awaiting a decision — "needs you", one bucket
80
- const importJob = useShenoraOperations((s) => s.byId[jobId]); // one, by id
81
-
82
- useShenoraOperations.actions.cancel(jobId); // only does anything if the op opted into Cancellable
83
- useShenoraOperations.actions.dismiss(jobId); // decline a waiting offer — refuses a running one
84
- useShenoraOperations.actions.wait(jobId); // ASK the host to wait running work — refuses anything not running
85
- useShenoraOperations.actions.clearFinished();
86
- ```
87
-
88
- `operation.progress` is an exported `OperationProgress` (`{ value: number; total?: number; unit?:
89
- string }`) — import the type rather than re-declaring the shape — the APP's own unit
90
- (bytes-of-a-known-total, items-of-a-known-total, an absolute count with no known total, or a genuine
91
- percent), never a kit-assumed percentage: `total` is the denominator when one is known and `undefined`
92
- when there isn't one, and `unit` is app-defined and uninterpreted, exactly like `kind`. The kit ships
93
- no percent helper — render a ratio only when you have a `total`:
94
-
95
- ```ts
96
- import type { OperationProgress } from '@shenora/react';
97
-
98
- function format(progress?: OperationProgress): string {
99
- if (!progress) return 'starting…';
100
- return progress.total
101
- ? `${Math.round((progress.value / progress.total) * 100)}%`
102
- : `${progress.value}${progress.unit ? ` ${progress.unit}` : ''}`; // no known total
103
- }
104
- ```
105
-
106
- That division is your own policy, not the kit's, which is why it lives here instead of in `src/`.
107
-
108
- For the two events the store deliberately does NOT subscribe to — `OPERATION_RESUME_REQUESTED` and
109
- `OPERATION_WAIT_REQUESTED`, which target the OWNING module's service rather than this generic store —
110
- the module name and the event vocabulary are exported too, so you match by symbol and stay inside the
111
- host↔client mirror the wire tests pin:
112
-
113
- ```ts
114
- import { OperationEventTypes, OperationModuleName, useShenoraEvent } from '@shenora/react';
115
-
116
- useShenoraEvent(OperationModuleName, OperationEventTypes.ResumeRequested, (payload) => …);
117
- ```
118
-
119
- It snapshots via `LIST` on first subscribe (so a progress strip that mounts mid-run isn't empty), then
120
- folds `OPERATION_UPDATED` by id — one subscription however many components read it. The client
121
- mirrors the host's three bands (design §5A.2): `running` (Active), `waiting` (Waiting), and `finished`
122
- (Terminal). All three are derived from `byId` on every read, never a second copy to keep in sync —
123
- `waiting` is a single-status filter, exactly like `running`. `OperationStatus` carries ONE waiting
124
- value reached ONE way — the host's `IOperation.Wait` on a live operation — so there is no sub-case to
125
- tell apart. Filtering by your own `module`/`kind` is a plain `Array.filter` over any of them. A
126
- `waiting` operation carries `waitReason` — an app-defined string, like `kind`, OPTIONAL on the host
127
- side — for your UI to branch on. The host never prunes a waiting entry on its own: it stays until
128
- someone resumes, dismisses, or finishes it. `resume`/`dismiss`/`wait` are all fire-and-forget client
129
- requests — the host's own `IOperation.Wait`/`Resume` (called by whoever owns the operation, hearing
130
- `OPERATION_WAIT_REQUESTED`/`OPERATION_RESUME_REQUESTED`) is what actually changes the state; asking
131
- is not acting. `clearFinished`/`resume`/`wait` do not touch local state themselves at all: the host's
132
- `OPERATION_REMOVED { operationIds }` is the ONE authoritative removal signal the store folds, deleting
133
- exactly the named ids — `MaxHistory` eviction and `clearFinished`
134
- publish it, so a long-lived store's mirror of bounded host history cannot drift from what the host
135
- actually did (this replaced two hand-written optimistic local prunes that a past release carried —
136
- one of which was this project's only Critical, a `resume` prune that dropped a still-waiting row).
137
- `dismiss` never needed one, since the host's `Dismiss` publishes an ordinary terminal snapshot over
138
- the wire, the same as a real cancel. Use `createOperationsStore({ module, scope })` instead of the
139
- default export if your host renamed `OperationRegistryOptions.ModuleName` or
140
- you need a scope-filtered instance (a secondary window, an auxiliary session) — `clearFinished`
141
- forwards that scope so clearing history in one window cannot wipe another's.
142
-
143
- ### Observing the whole stream
144
-
145
- `useShenoraEvent` and `createShenoraStore` listen for an exact `(module, type)`. When the vocabulary
146
- isn't knowable up front — plug-in-contributed events, a diagnostics tap, or an adoption shim keeping
147
- a legacy "every host message" handler alive while features migrate one at a time — subscribe broadly
148
- instead. Both mirror the host's `IEventBus` and return an unsubscribe:
149
-
150
- ```ts
151
- const off = eventBus.subscribeToAll((event) => log.debug(event.module, event.type, event.payload));
152
- eventBus.subscribeToModule('DEPLOY', (event) => audit(event)); // every type from one module
153
- ```
154
-
155
- Delivery is narrowest-first — exact pair, then module, then catch-all — so a broad observer never
156
- runs ahead of the feature code it is observing. Prefer `subscribe` when you know the pair: a
157
- catch-all wakes for every event on the bus.
158
-
159
- Pure-UI development in a plain browser: pass a `fallback` to `configureBridge` (gated behind
160
- `import.meta.env.DEV`) to answer requests with canned data. Other shells (WebSocket,
161
- mobile/Capacitor) implement the small `ShenoraTransport` seam and speak the same envelopes.
162
- For CDP-driven testing, `installDevInterceptor()` records IPC/event traffic into ring buffers
163
- and exposes `window.__shenora.call()/waitEvent()`.
164
-
165
- MIT © Jiarong Gu
1
+ # @shenora/react
2
+
3
+ React client for [Shenora](https://github.com/JiarongGu/Shenora) desktop hosts (.NET + WinForms +
4
+ WebView2). The typed bridge between a React frontend and the Shenora host: correlated `invoke`
5
+ with timeouts and structured errors, the event hub host notifications stream into, typed module
6
+ services, React hooks, and a pluggable transport with a browser fallback so the UI can be
7
+ developed in a plain browser. Headless by design — no UI components, bring your own design
8
+ system. Versioned in lockstep with the `Shenora.*` NuGet packages.
9
+
10
+ ```ts
11
+ import {
12
+ getBridge, useShenoraEvent, useShenoraQuery, BaseModuleService, createShenoraStore,
13
+ } from '@shenora/react';
14
+
15
+ // Once at startup, after your listeners are attached: it starts notification delivery (anything the
16
+ // host buffered arrives in the first batch). Drop zones need no particular ordering against it — the
17
+ // host clears them when a new DOCUMENT loads, not on this handshake.
18
+ await getBridge().notifyReady();
19
+
20
+ // a typed service per backend module:
21
+ interface NoteRequests { GET_ALL: void; ADD: { title: string } }
22
+ class NoteService extends BaseModuleService<NoteRequests> {
23
+ constructor() { super('NOTES'); }
24
+ getAll() { return this.send<Note[]>('GET_ALL'); }
25
+ add(title: string) { return this.send<Note>('ADD', { payload: { title } }); }
26
+ }
27
+
28
+ // in components:
29
+ const { data, loading, refetch } = useShenoraQuery<Note[]>('NOTES', 'GET_ALL');
30
+ useShenoraEvent<Note>('NOTES', 'ADDED', (note) => refetch());
31
+ ```
32
+
33
+ Failed calls reject with `ShenoraError` — a structured `code` (an i18n key: translate
34
+ `errors.{code}`) plus interpolation `parameters`, never raw host exception text.
35
+
36
+ ### Long-running work: post, then read a store
37
+
38
+ `invoke` awaits a correlated reply and carries a timeout, and its handler's synchronous segment runs
39
+ on the host's **UI thread** — so it is for calls that are quick and UI-thread-safe. Everything else
40
+ posts and streams results back as events:
41
+
42
+ ```ts
43
+ const useDeploy = createShenoraStore('DEPLOY', {
44
+ initial: { status: 'idle', lines: [] as string[] },
45
+ // Loaded on the FIRST subscriber, so a component that mounts mid-run isn't empty:
46
+ // events it missed cannot be replayed.
47
+ snapshot: { type: 'GET_STATE', apply: (s, d) => ({ ...s, ...(d as object) }) },
48
+ on: {
49
+ PROGRESS: (s, p: { line: string }) => ({ ...s, lines: [...s.lines, p.line] }),
50
+ ENDED: (s, p: { ok: boolean }) => ({ ...s, status: p.ok ? 'done' : 'failed' }),
51
+ },
52
+ actions: ({ post }) => ({ start: (cfg: unknown) => post('START', { payload: cfg }) }),
53
+ });
54
+
55
+ // any number of components share ONE subscription:
56
+ const status = useDeploy((s) => s.status);
57
+ useDeploy.actions.start({ env: 'prod' });
58
+ ```
59
+
60
+ Use the **store for shared or long-lived state** and **`useShenoraEvent` for a one-off reaction in a
61
+ single component**. A failed `post` has no promise to reject, so it is reported through the bridge's
62
+ `onPostError` rather than vanishing — it is bridge-wide, so wire it once at startup
63
+ (`getBridge()` takes no options):
64
+
65
+ ```ts
66
+ configureBridge({ onPostError: (failure) => log.error(failure.module, failure.type, failure.error) });
67
+ ```
68
+
69
+ ### Requests in flight: a ready-made progress store
70
+
71
+ Every request the host handles is tracked automatically — there is nothing to declare on either side.
72
+ `useShenoraRequests()` is a `createShenoraStore` instance built the same way as the example above:
73
+
74
+ ```ts
75
+ import { useShenoraRequests } from '@shenora/react';
76
+
77
+ const running = useShenoraRequests((s) => s.running); // every request still in flight
78
+ const finished = useShenoraRequests((s) => s.finished); // retained history, newest first
79
+ const importJob = useShenoraRequests((s) => s.byId[requestId]); // one, by id
80
+
81
+ useShenoraRequests.actions.cancel(requestId); // XMLHttpRequest.abort() — the id you sent with
82
+ useShenoraRequests.actions.clearFinished();
83
+ ```
84
+
85
+ 🔴 **The id is the one you already have.** `requestId` is the `id` of the request you sent — there is no
86
+ second identity to correlate. Cancelling it targets the token the route is running under.
87
+
88
+ ⚠ **Most requests never appear here, and that is the design.** The host stays SILENT for the first
89
+ 50 ms (`IpcRequestTrackerOptions.GracePeriod`): a request that finishes inside that window emits no
90
+ event at all — no running snapshot, no completion, nothing retained. So this store is a list of work
91
+ that is actually *taking a while*, not a log of every call your page made. Nobody wants a spinner for
92
+ five milliseconds of work, and the clock decides that at run time rather than a module author guessing
93
+ at authoring time.
94
+
95
+ `request.progress` is an exported `IpcProgress` (`{ value: number; total?: number; unit?: string }`) —
96
+ import the type rather than re-declaring the shape. It is the APP's own unit (bytes of a known total,
97
+ items of a known total, an absolute count with no known total, or a genuine percent), never a
98
+ kit-assumed percentage: `total` is the denominator when one is known and `undefined` when there is
99
+ none, and `unit` is app-defined and uninterpreted. The kit ships no percent helper — render a ratio
100
+ only when you have a `total`:
101
+
102
+ ```ts
103
+ import type { IpcProgress } from '@shenora/react';
104
+
105
+ function format(progress?: IpcProgress): string {
106
+ if (!progress) return 'starting…';
107
+ return progress.total
108
+ ? `${Math.round((progress.value / progress.total) * 100)}%`
109
+ : `${progress.value}${progress.unit ? ` ${progress.unit}` : ''}`; // no known total
110
+ }
111
+ ```
112
+
113
+ That division is your own policy, not the kit's, which is why it lives here instead of in `src/`.
114
+
115
+ The store snapshots via `LIST` on first subscribe (so a progress strip that mounts mid-run is not
116
+ empty), then folds `REQUEST_UPDATED` by id — one subscription however many components read it. Two
117
+ bands, both derived from `byId` on every read: `running` and `finished`. Filtering by your own
118
+ `module`/`type` is a plain `Array.filter` over either.
119
+
120
+ `clearFinished` does not touch local state itself: the host's `REQUEST_REMOVED { requestIds }` is the
121
+ one authoritative removal signal the store folds, deleting exactly the named ids. History eviction and
122
+ `clearFinished` both publish it, so a long-lived store's mirror of bounded host history cannot drift
123
+ from what the host actually did.
124
+
125
+ ⚠ **Work nobody requested does not belong here.** A scheduled job, a background sync, anything the host
126
+ starts on its own — those have no request behind them and no response to wait for, so they report on
127
+ their own event stream via `useShenoraEvent`. Squeezing them in here is what the previous design did,
128
+ and it is why it needed a "waiting" state nothing else could explain.
129
+
130
+ ### Observing the whole stream
131
+
132
+ `useShenoraEvent` and `createShenoraStore` listen for an exact `(module, type)`. When the vocabulary
133
+ isn't knowable up front — plug-in-contributed events, a diagnostics tap, or an adoption shim keeping
134
+ a legacy "every host message" handler alive while features migrate one at a time — subscribe broadly
135
+ instead. Both mirror the host's `IEventBus` and return an unsubscribe:
136
+
137
+ ```ts
138
+ const off = eventBus.subscribeToAll((event) => log.debug(event.module, event.type, event.payload));
139
+ eventBus.subscribeToModule('DEPLOY', (event) => audit(event)); // every type from one module
140
+ ```
141
+
142
+ Delivery is narrowest-first — exact pair, then module, then catch-all — so a broad observer never
143
+ runs ahead of the feature code it is observing. Prefer `subscribe` when you know the pair: a
144
+ catch-all wakes for every event on the bus.
145
+
146
+ Pure-UI development in a plain browser: pass a `fallback` to `configureBridge` (gated behind
147
+ `import.meta.env.DEV`) to answer requests with canned data. Other shells (WebSocket,
148
+ mobile/Capacitor) implement the small `ShenoraTransport` seam and speak the same envelopes.
149
+ For CDP-driven testing, `installDevInterceptor()` records IPC/event traffic into ring buffers
150
+ and exposes `window.__shenora.call()/waitEvent()`.
151
+
152
+ MIT © Jiarong Gu
package/dist/bridge.d.ts CHANGED
@@ -6,7 +6,7 @@ export interface ShenoraBridgeOptions {
6
6
  /**
7
7
  * The channel to the host. Default: whichever Shenora host this page is in — WebView2 postMessage
8
8
  * on the desktop shell, `HybridWebView` on the MAUI shell — else null (plain browser). Supply your
9
- * own for another shell (a WebSocket, D16) or a scripted fake for tests/preview harnesses.
9
+ * own for another shell, or a scripted fake for tests and preview harnesses.
10
10
  */
11
11
  transport?: ShenoraTransport | null;
12
12
  /** The event bus host notifications are unbundled into. Default: the shared bus. */
@@ -14,25 +14,23 @@ export interface ShenoraBridgeOptions {
14
14
  /** Per-request timeout in ms when the call doesn't set one. Family default: 30 000. */
15
15
  defaultTimeoutMs?: number;
16
16
  /**
17
- * Pure-UI development seam: answers requests when NO transport exists (plain browser tab —
18
- * component/layout work without the desktop host). Return the response data (or a promise;
19
- * throw to reject). Generalized from the source app's hardcoded dev mocks: the mocks are app
20
- * schema, so the app supplies them — gate with `import.meta.env.DEV` at the call site so
21
- * production stays hard-failing.
17
+ * Pure-UI development seam: answers requests when NO transport exists (a plain browser tab).
18
+ * Return the response data, or a promise; throw to reject.
19
+ *
20
+ * ⚠ Gate the call site with `import.meta.env.DEV` so production stays hard-failing.
22
21
  */
23
22
  fallback?: (request: IpcRequest) => unknown;
24
23
  /**
25
24
  * Where a FAILED {@link ShenoraBridge.post} is reported. Default: `console.error`.
26
25
  *
27
- * A one-way send has no promise to reject, so without this its failures would be invisible — and an
28
- * unmatched response is dropped silently by the inbound handler, which is exactly how a feature
29
- * "just stops working" with nothing to grep for. Route it into the app's logger/toast instead.
26
+ * ⚠ Route it into the app's logger or toast. A one-way send has no promise to reject, so its
27
+ * failures are otherwise invisible.
30
28
  */
31
29
  onPostError?: (error: PostFailure) => void;
32
30
  /**
33
31
  * How many unawaited {@link ShenoraBridge.post} ids to remember for error reporting. Default 256.
34
- * Capped (drop-oldest) so a host that never answers cannot grow the set without bound — the same
35
- * shape as the host's own bounded notification queue. Evicting an id only loses its error report.
32
+ * Capped drop-oldest, so a host that never answers cannot grow the set without bound; evicting an
33
+ * id only loses its error report.
36
34
  */
37
35
  maxTrackedPosts?: number;
38
36
  }
@@ -59,10 +57,10 @@ export interface PostOptions<TPayload = unknown> {
59
57
  scope?: string;
60
58
  }
61
59
  /**
62
- * The client side of the Shenora IPC contract, ported from the primary desktop sibling:
63
- * correlated request/response over a pluggable transport, category routing of host messages
64
- * (`ipc` → resolve the pending call, `notification` → unbundle the batch into the event bus),
65
- * per-request timeout, the ready handshake, and a browser fallback seam for pure-UI development.
60
+ * The client side of the Shenora IPC contract: correlated request/response over a pluggable
61
+ * transport, category routing of host messages (`ipc` → resolve the pending call, `notification` →
62
+ * unbundle the batch into the event bus), per-request timeout, the ready handshake, and a browser
63
+ * fallback seam for pure-UI development.
66
64
  *
67
65
  * Most apps use the lazy default instance via {@link getBridge}/{@link configureBridge}; create
68
66
  * instances directly for tests or multi-transport setups.
@@ -82,40 +80,29 @@ export declare class ShenoraBridge {
82
80
  constructor(options?: ShenoraBridgeOptions);
83
81
  /**
84
82
  * True when this bridge can actually send: a transport to a host exists AND the bridge has not been
85
- * disposed. It used to ignore `disposed`, so a stale reference to a bridge that `configureBridge`
86
- * replaced still reported itself available while every `invoke` on it rejected with `NO_TRANSPORT` —
87
- * the exact case the disposed check in `invoke` exists for (P5.5 H2).
83
+ * disposed. The check to make on a reference that may have outlived a {@link configureBridge} swap.
88
84
  */
89
85
  get isAvailable(): boolean;
90
86
  /**
91
87
  * Send a request and await its typed response data. A failed response rejects with the
92
- * structured {@link OperationError} (code + parameters); no response within the timeout
88
+ * structured {@link ShenoraError} (code + parameters); no response within the timeout
93
89
  * rejects with code `TIMEOUT`; no transport and no fallback rejects with `NO_TRANSPORT`.
94
90
  */
95
91
  invoke<TData = unknown, TPayload = unknown>(module: string, type: string, options?: InvokeOptions<TPayload>): Promise<TData>;
96
92
  /**
97
93
  * Send WITHOUT awaiting a reply, and return the request id.
98
94
  *
99
- * This is the default shape for a desktop shell, and {@link invoke} is the special case — see
100
- * `docs/DECISIONS.md` D23. Two reasons: a correlated call carries a deadline
101
- * (30 s by default) and real work does not; and request/response is UI-THREAD-COUPLED here by
102
- * design, because the dispatch pipeline preserves the caller's synchronization context so facades
103
- * can touch the window. Reserve `invoke` for calls that are quick AND safe on the UI thread — the
104
- * window commands are the model — and post everything else, streaming results back as notifications.
105
- *
106
- * ⚠ Posting is only HALF of freeing the UI thread. The host still dispatches on the UI thread
107
- * whether or not the client awaits, so a handler that does heavy work synchronously stalls the
108
- * window either way. The other half is the host's: return from the route immediately and stream.
95
+ * This is the default shape for a desktop shell and {@link invoke} is the special case (D23):
96
+ * reserve `invoke` for calls that are quick AND safe on the host's UI thread, and post everything
97
+ * else, streaming results back as notifications.
109
98
  *
110
- * Failures are not silent. There is no promise to reject, so a failed response is reported through
111
- * `onPostError` (default `console.error`) rather than being dropped the way an unmatched response
112
- * otherwise is. Nothing is queued and no timer is set, so there is nothing to leak and no deadline.
99
+ * A failed response is reported through `onPostError` (default `console.error`) rather than
100
+ * dropped. The id is remembered for that — see {@link ShenoraBridgeOptions.maxTrackedPosts}. No
101
+ * timer is set, so there is no deadline.
113
102
  *
114
- * No transport (a plain browser tab) is a silent no-op, matching the fire-and-forget contract —
115
- * unlike `invoke`, there is no caller waiting to be told. **A DISPOSED bridge is the same silent
116
- * no-op**, and that one can surprise: `invoke` on a disposed bridge rejects with `NO_TRANSPORT`
117
- * while `post` just returns the id, so a stale reference kept across a `configureBridge` swap looks
118
- * like it is still sending. `isAvailable` is the check.
103
+ * ⚠ No transport (a plain browser tab) is a silent no-op, and **so is a DISPOSED bridge** — where
104
+ * `invoke` would reject with `NO_TRANSPORT`, this just returns the id, so a stale reference kept
105
+ * across a {@link configureBridge} swap looks like it is still sending. `isAvailable` is the check.
119
106
  */
120
107
  post<TPayload = unknown>(module: string, type: string, options?: PostOptions<TPayload>): string;
121
108
  /**
@@ -125,22 +112,18 @@ export declare class ShenoraBridge {
125
112
  * also the host's cue to reset per-page state. No-transport is a silent no-op so browser dev
126
113
  * doesn't error.
127
114
  *
128
- * Ordering used to matter here and no longer does for drop zones: the host cleared the previous
129
- * page's overlays on this handshake, so a `REGISTER` sent before `READY` was wiped even though it
130
- * was acked. `DropZoneManager` now clears on DOCUMENT CHANGE instead, which cannot race the client.
131
- * The general point still stands for any per-page state YOUR host resets here — a reset keyed on
132
- * the handshake races anything the page sends before it, and in React that is structural rather
133
- * than a mistake, because CHILD effects run before PARENT effects.
115
+ * ⚠ Any per-page state YOUR host resets on this handshake races whatever the page sent before it,
116
+ * and in React that is structural rather than bad luck: CHILD effects run before PARENT effects.
134
117
  *
135
- * The returned promise REJECTS on a failed handshake (disposed bridge, timeout). Handle it —
118
+ * ⚠ The returned promise REJECTS on a failed handshake (disposed bridge, timeout). Handle it —
136
119
  * `void bridge.notifyReady()` turns that into an unhandled rejection, which in a WebView2 page is
137
120
  * a silent console error.
138
121
  */
139
122
  notifyReady<TPayload = unknown>(payload?: TPayload): Promise<ShellInfo | undefined>;
140
123
  /**
141
124
  * What the host said it was during {@link notifyReady} — undefined before the handshake, or when
142
- * the host advertised nothing. Cached so components can read it synchronously while rendering,
143
- * which is the whole point: a capability learned after layout is a flash.
125
+ * the host advertised nothing. Cached so components can read it synchronously while rendering; a
126
+ * capability learned after layout is a visible flash.
144
127
  */
145
128
  get shell(): ShellInfo | undefined;
146
129
  /** Reject everything in flight and detach from the transport. */