@shenora/react 0.11.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.
@@ -4,24 +4,19 @@ export interface SubscribeOptions {
4
4
  /**
5
5
  * Only receive events carrying this app-defined scope. Omit to receive EVERY scope.
6
6
  *
7
- * The semantics mirror the host's `EventBus` exactly, and both halves matter: an unscoped
8
- * subscription sees every scope, AND a scope-less (global) event still reaches scoped subscribers —
9
- * so an app-wide announcement is not swallowed by a per-scope listener.
7
+ * ⚠ Both halves of the host's `EventBus` rule apply: an unscoped subscription sees every scope, AND
8
+ * a scope-less (global) event still reaches scoped subscribers.
10
9
  */
11
10
  scope?: string;
12
11
  }
13
12
  /**
14
- * The client-side event hub, ported from the primary desktop sibling: host notifications are
15
- * unbundled into it by the bridge, and app code (or the dev interceptor) can emit locally.
16
- * Event enums/maps are app schema, so apps layer their own typed wrappers on top (headless per D13).
17
- * A throwing handler is isolated: it never breaks the other subscribers or the emitter.
13
+ * The client-side event hub: the bridge unbundles each host notification BATCH into it, and app code
14
+ * (or the dev interceptor) can emit locally. Event enums and maps are app schema, so apps layer their
15
+ * own typed wrappers on top. A throwing handler is isolated — it never breaks the other subscribers
16
+ * or the emitter.
18
17
  *
19
- * Three subscription breadths, mirroring the host's `Shenora.IEventBus`: an exact
20
- * `(module, type)` pair, a whole {@link subscribeToModule | module}, or
21
- * {@link subscribeToAll | everything}. The broad two were added in P6.4 — the host had shipped them
22
- * from the start (`WebViewIpcBridge` itself consumes `SubscribeToAll`), so the client was the
23
- * asymmetric half of one concept and an observer that cannot enumerate the event vocabulary up front
24
- * had no supported expression at all.
18
+ * Three subscription breadths, mirroring the host's `Shenora.IEventBus`: an exact `(module, type)`
19
+ * pair, a whole {@link subscribeToModule | module}, or {@link subscribeToAll | everything}.
25
20
  */
26
21
  export declare class ShenoraEventBus {
27
22
  /** Exact `(module, type)` subscriptions, keyed by {@link eventKey}. */
@@ -39,25 +34,21 @@ export declare class ShenoraEventBus {
39
34
  * Subscribe to EVERY event from one module, whatever its type; returns the cleanup function.
40
35
  *
41
36
  * For a feature whose event vocabulary is open — plug-in-contributed types, a module that grows
42
- * types over time — where enumerating pairs would mean editing the subscriber every time the host
43
- * gains an event.
37
+ * types over time — where enumerating pairs means editing the subscriber for every new event.
44
38
  */
45
39
  subscribeToModule<TPayload = unknown>(module: string, handler: (event: EventMessage<TPayload>) => void, options?: SubscribeOptions): () => void;
46
40
  /**
47
41
  * Subscribe to EVERY event on the bus; returns the cleanup function.
48
42
  *
49
- * The breadth is the point, so use it for cross-cutting observers — a diagnostics overlay, a
50
- * telemetry tap, a bridge that folds the whole stream into another state library, or an adoption
51
- * shim keeping a legacy "every host message" handler alive while individual features migrate onto
52
- * exact pairs. It is NOT the way to consume one feature's events: prefer {@link subscribe}, which
53
- * says what it listens for and does not wake for unrelated traffic.
43
+ * For cross-cutting observers — a diagnostics overlay, a telemetry tap, a bridge that folds the
44
+ * whole stream into another state library. ⚠ NOT the way to consume one feature's events: prefer
45
+ * {@link subscribe}, which says what it listens for and does not wake for unrelated traffic.
54
46
  */
55
47
  subscribeToAll<TPayload = unknown>(handler: (event: EventMessage<TPayload>) => void, options?: SubscribeOptions): () => void;
56
48
  /**
57
49
  * Emit to every matching subscriber (see {@link SubscribeOptions.scope} for the scope rule).
58
50
  *
59
- * Delivery order is stable: exact pair, then whole-module, then catch-all — narrowest first, so a
60
- * broad observer never runs ahead of the feature code it is observing.
51
+ * Delivery order is stable and narrowest-first: exact pair, then whole-module, then catch-all.
61
52
  */
62
53
  emit(event: EventMessage): void;
63
54
  /** Remove every subscription (tests). */
@@ -71,12 +62,6 @@ export declare class ShenoraEventBus {
71
62
  * - `(module)` — everything that would receive ANY event of that module: its exact-type
72
63
  * subscriptions, its whole-module ones, and the catch-alls.
73
64
  * - `(module, type)` — everything that would receive that one pair.
74
- *
75
- * 🔴 **The one-argument form used to silently answer the GLOBAL total.** The guard read
76
- * `if (module && type)`, so passing a module alone fell through to the count-everything branch — a
77
- * plausible number, for a different question, with nothing to indicate the substitution. The fix is
78
- * at RUNTIME rather than in an overload signature because this package ships JavaScript: a
79
- * type-level restriction would leave a JS consumer receiving exactly the same wrong number.
80
65
  */
81
66
  getSubscriptionCount(module?: string, type?: string): number;
82
67
  }
package/dist/eventBus.js CHANGED
@@ -1,15 +1,11 @@
1
1
  /**
2
- * The client-side event hub, ported from the primary desktop sibling: host notifications are
3
- * unbundled into it by the bridge, and app code (or the dev interceptor) can emit locally.
4
- * Event enums/maps are app schema, so apps layer their own typed wrappers on top (headless per D13).
5
- * A throwing handler is isolated: it never breaks the other subscribers or the emitter.
2
+ * The client-side event hub: the bridge unbundles each host notification BATCH into it, and app code
3
+ * (or the dev interceptor) can emit locally. Event enums and maps are app schema, so apps layer their
4
+ * own typed wrappers on top. A throwing handler is isolated — it never breaks the other subscribers
5
+ * or the emitter.
6
6
  *
7
- * Three subscription breadths, mirroring the host's `Shenora.IEventBus`: an exact
8
- * `(module, type)` pair, a whole {@link subscribeToModule | module}, or
9
- * {@link subscribeToAll | everything}. The broad two were added in P6.4 — the host had shipped them
10
- * from the start (`WebViewIpcBridge` itself consumes `SubscribeToAll`), so the client was the
11
- * asymmetric half of one concept and an observer that cannot enumerate the event vocabulary up front
12
- * had no supported expression at all.
7
+ * Three subscription breadths, mirroring the host's `Shenora.IEventBus`: an exact `(module, type)`
8
+ * pair, a whole {@link subscribeToModule | module}, or {@link subscribeToAll | everything}.
13
9
  */
14
10
  export class ShenoraEventBus {
15
11
  constructor() {
@@ -31,8 +27,7 @@ export class ShenoraEventBus {
31
27
  * Subscribe to EVERY event from one module, whatever its type; returns the cleanup function.
32
28
  *
33
29
  * For a feature whose event vocabulary is open — plug-in-contributed types, a module that grows
34
- * types over time — where enumerating pairs would mean editing the subscriber every time the host
35
- * gains an event.
30
+ * types over time — where enumerating pairs means editing the subscriber for every new event.
36
31
  */
37
32
  subscribeToModule(module, handler, options = {}) {
38
33
  return addTo(this.byModule, module, newSubscription(handler, options));
@@ -40,11 +35,9 @@ export class ShenoraEventBus {
40
35
  /**
41
36
  * Subscribe to EVERY event on the bus; returns the cleanup function.
42
37
  *
43
- * The breadth is the point, so use it for cross-cutting observers — a diagnostics overlay, a
44
- * telemetry tap, a bridge that folds the whole stream into another state library, or an adoption
45
- * shim keeping a legacy "every host message" handler alive while individual features migrate onto
46
- * exact pairs. It is NOT the way to consume one feature's events: prefer {@link subscribe}, which
47
- * says what it listens for and does not wake for unrelated traffic.
38
+ * For cross-cutting observers — a diagnostics overlay, a telemetry tap, a bridge that folds the
39
+ * whole stream into another state library. ⚠ NOT the way to consume one feature's events: prefer
40
+ * {@link subscribe}, which says what it listens for and does not wake for unrelated traffic.
48
41
  */
49
42
  subscribeToAll(handler, options = {}) {
50
43
  const subscription = newSubscription(handler, options);
@@ -54,19 +47,17 @@ export class ShenoraEventBus {
54
47
  /**
55
48
  * Emit to every matching subscriber (see {@link SubscribeOptions.scope} for the scope rule).
56
49
  *
57
- * Delivery order is stable: exact pair, then whole-module, then catch-all — narrowest first, so a
58
- * broad observer never runs ahead of the feature code it is observing.
50
+ * Delivery order is stable and narrowest-first: exact pair, then whole-module, then catch-all.
59
51
  */
60
52
  emit(event) {
61
53
  const exact = this.exact.get(eventKey(event.module, event.type));
62
54
  const byModule = this.byModule.get(event.module);
63
55
  if (!exact?.size && !byModule?.size && this.all.size === 0)
64
56
  return;
65
- // Snapshot ALL THREE breadths before invoking any handler, not one at a time. A handler may
66
- // subscribe or unsubscribe during delivery, and one event must reach exactly the subscribers
67
- // that existed when it was emitted. Reading the broad collections lazily — after the exact
68
- // handlers had already run — would let a handler that subscribes broadly while handling receive
69
- // the very event it is handling. Copying per-set was enough while there was only one set.
57
+ // Snapshot ALL THREE breadths before invoking any handler, never one at a time: a handler may
58
+ // subscribe or unsubscribe during delivery, and one event must reach exactly the subscribers that
59
+ // existed when it was emitted. Reading the broad collections lazily would let a handler that
60
+ // subscribes broadly while handling receive the very event it is handling.
70
61
  for (const subscription of [...(exact ?? []), ...(byModule ?? []), ...this.all]) {
71
62
  if (!scopeMatches(subscription.scope, event.scope))
72
63
  continue;
@@ -94,12 +85,6 @@ export class ShenoraEventBus {
94
85
  * - `(module)` — everything that would receive ANY event of that module: its exact-type
95
86
  * subscriptions, its whole-module ones, and the catch-alls.
96
87
  * - `(module, type)` — everything that would receive that one pair.
97
- *
98
- * 🔴 **The one-argument form used to silently answer the GLOBAL total.** The guard read
99
- * `if (module && type)`, so passing a module alone fell through to the count-everything branch — a
100
- * plausible number, for a different question, with nothing to indicate the substitution. The fix is
101
- * at RUNTIME rather than in an overload signature because this package ships JavaScript: a
102
- * type-level restriction would leave a JS consumer receiving exactly the same wrong number.
103
88
  */
104
89
  getSubscriptionCount(module, type) {
105
90
  if (module !== undefined && type !== undefined) {
@@ -129,7 +114,7 @@ export class ShenoraEventBus {
129
114
  function newSubscription(handler, options) {
130
115
  return { handler: handler, scope: options.scope };
131
116
  }
132
- /** Shared add-and-prune for the two keyed collections (an empty key is removed, as it always was). */
117
+ /** Shared add-and-prune for the two keyed collections; an emptied key is removed. */
133
118
  function addTo(collection, key, subscription) {
134
119
  let set = collection.get(key);
135
120
  if (!set) {
@@ -147,17 +132,11 @@ function addTo(collection, key, subscription) {
147
132
  };
148
133
  }
149
134
  /**
150
- * `'\0'`-joined, NOT `.`-joined (P5.5 H6).
135
+ * `'\0'`-joined, NOT `.`-joined, matching the host's `EventBus`.
151
136
  *
152
- * Module and type are both arbitrary app-defined strings, so a `.` separator makes
137
+ * 🔴 Module and type are both arbitrary app-defined strings, so a `.` separator makes
153
138
  * `("APP", "TASK.DONE")` and `("APP.TASK", "DONE")` the same key — one app's events silently delivered
154
- * to another's subscribers. The host's `EventBus` fixed exactly this and documented it; the client kept
155
- * the colliding form, so the two halves of one contract disagreed. `'\0'` cannot appear in a JS string
156
- * literal a developer types by accident, which is what makes it safe as a separator.
157
- *
158
- * The broad subscriptions use separate collections rather than a wildcard key. That is an
159
- * implementation choice here, not a correction of the host: the host's pattern matcher takes `"*"` and
160
- * that is fine, because a module or type named `*` is not a name any app has or wants.
139
+ * to another's subscribers. `'\0'` cannot appear in a string literal a developer types by accident.
161
140
  */
162
141
  function eventKey(module, type) {
163
142
  return `${module}\0${type}`;
@@ -7,8 +7,8 @@ export interface FileDialogFilter {
7
7
  extensions: string[];
8
8
  }
9
9
  /**
10
- * What EVERY dialog call takes. The per-dialog shapes below add what only that dialog can honour —
11
- * which is the point: a save-only field on a folder pick will not compile.
10
+ * What EVERY dialog call takes. The per-dialog shapes below add what only that dialog can honour, so a
11
+ * save-only field on a folder pick does not compile.
12
12
  */
13
13
  export interface FileDialogOptions {
14
14
  /** Dialog title. Omit for a neutral per-dialog default. */
@@ -95,10 +95,9 @@ export declare class FileDialogs extends BaseModuleService<FileDialogRequests> {
95
95
  /** Pick an existing file. Available on every shell. */
96
96
  openFile(options?: OpenFileOptions): Promise<FileDialogResult>;
97
97
  /**
98
- * Pick a folder. ⚠ DESKTOP only — gate on {@link FileDialogsHandle.canPickFolder}.
99
- *
100
- * On mobile "open folder" means the camera roll, the app's own space, or a scoped grant: the same
101
- * word with a different guarantee, which is why there is no portable version of it.
98
+ * Pick a folder. ⚠ DESKTOP only — gate on {@link FileDialogsHandle.canPickFolder}. There is no
99
+ * portable version: on mobile "open folder" means the camera roll, the app's own space, or a scoped
100
+ * grant — the same word with a different guarantee.
102
101
  */
103
102
  openFolder(options?: OpenFolderOptions): Promise<FileDialogResult>;
104
103
  /**
@@ -111,9 +110,8 @@ export declare class FileDialogs extends BaseModuleService<FileDialogRequests> {
111
110
  * Pick a destination AND write `text` to it, in one call — the PORTABLE save, working everywhere
112
111
  * because the HOST does the writing.
113
112
  *
114
- * ⚠ For text a page legitimately holds: an export, a report, a config. The content crosses the IPC
115
- * envelope as JSON, so anything large or binary should be produced host-side and saved through the
116
- * host's own `IFileDialogs.SaveAsync`, where it never enters a message.
113
+ * ⚠ The content crosses the IPC envelope as JSON, so anything large or binary should be produced
114
+ * host-side and saved through the host's own `IFileDialogs.SaveAsync` instead.
117
115
  */
118
116
  saveText(text: string, options?: SaveFileOptions): Promise<FileDialogResult>;
119
117
  }
@@ -143,8 +141,7 @@ export interface FileDialogsHandle {
143
141
  * ```
144
142
  *
145
143
  * ⚠ **Use these to decide what to RENDER, not what to catch.** A refused call rejects with
146
- * `IpcErrorCodes.capabilityNotSupported`, which is the honest answer to a question that should not
147
- * have been asked — the button should not have been there.
144
+ * `IpcErrorCodes.capabilityNotSupported` — the button should not have been there.
148
145
  *
149
146
  * ⚠ Every flag is `false` until the handshake has landed, so await `bridge.notifyReady()` before
150
147
  * rendering this tree — see {@link useShellInfo} for why the read is synchronous.
@@ -2,8 +2,8 @@
2
2
  * The page's half of the host's `SHENORA.DIALOGS` module — native file/folder/save dialogs, and the
3
3
  * capability gating that lets ONE bundle ship to every shell.
4
4
  *
5
- * Mirrors `Shenora.Core.Ipc.FileDialogModule`. The wire names here are pinned against the host's own
6
- * constants by `WireMirrorTests`, not by care.
5
+ * Mirrors `Shenora.Core.Ipc.FileDialogModule`, pinned against the host's own constants by
6
+ * `WireMirrorTests`.
7
7
  */
8
8
  import { useMemo } from 'react';
9
9
  import { useShellInfo } from './hooks.js';
@@ -26,10 +26,9 @@ export class FileDialogs extends BaseModuleService {
26
26
  return this.send('OPEN_FILE', { payload: { options } });
27
27
  }
28
28
  /**
29
- * Pick a folder. ⚠ DESKTOP only — gate on {@link FileDialogsHandle.canPickFolder}.
30
- *
31
- * On mobile "open folder" means the camera roll, the app's own space, or a scoped grant: the same
32
- * word with a different guarantee, which is why there is no portable version of it.
29
+ * Pick a folder. ⚠ DESKTOP only — gate on {@link FileDialogsHandle.canPickFolder}. There is no
30
+ * portable version: on mobile "open folder" means the camera roll, the app's own space, or a scoped
31
+ * grant — the same word with a different guarantee.
33
32
  */
34
33
  openFolder(options) {
35
34
  return this.send('OPEN_FOLDER', { payload: { options } });
@@ -46,9 +45,8 @@ export class FileDialogs extends BaseModuleService {
46
45
  * Pick a destination AND write `text` to it, in one call — the PORTABLE save, working everywhere
47
46
  * because the HOST does the writing.
48
47
  *
49
- * ⚠ For text a page legitimately holds: an export, a report, a config. The content crosses the IPC
50
- * envelope as JSON, so anything large or binary should be produced host-side and saved through the
51
- * host's own `IFileDialogs.SaveAsync`, where it never enters a message.
48
+ * ⚠ The content crosses the IPC envelope as JSON, so anything large or binary should be produced
49
+ * host-side and saved through the host's own `IFileDialogs.SaveAsync` instead.
52
50
  */
53
51
  saveText(text, options) {
54
52
  return this.send('SAVE_TEXT', { payload: { text, options } });
@@ -69,16 +67,14 @@ export class FileDialogs extends BaseModuleService {
69
67
  * ```
70
68
  *
71
69
  * ⚠ **Use these to decide what to RENDER, not what to catch.** A refused call rejects with
72
- * `IpcErrorCodes.capabilityNotSupported`, which is the honest answer to a question that should not
73
- * have been asked — the button should not have been there.
70
+ * `IpcErrorCodes.capabilityNotSupported` — the button should not have been there.
74
71
  *
75
72
  * ⚠ Every flag is `false` until the handshake has landed, so await `bridge.notifyReady()` before
76
73
  * rendering this tree — see {@link useShellInfo} for why the read is synchronous.
77
74
  */
78
75
  export function useFileDialogs(dialogs) {
79
76
  const shell = useShellInfo();
80
- // The service is stateless and holds only its module name, but a fresh instance per render would
81
- // still make it useless as an effect dependency — the same reason useWindowMaximized caches one.
77
+ // Cached so it is usable as an effect dependency, not because the service is expensive.
82
78
  const client = useMemo(() => dialogs ?? new FileDialogs(), [dialogs]);
83
79
  const capabilities = shell?.capabilities;
84
80
  return useMemo(() => ({
package/dist/hooks.d.ts CHANGED
@@ -4,11 +4,9 @@ import type { EventMessage, ShellInfo } from './types.js';
4
4
  /**
5
5
  * Host access for components: the (default) bridge and whether a host transport exists.
6
6
  *
7
- * ⚠ The result is MEMOIZED, and that is not a micro-optimisation. A fresh object every render is a
8
- * fresh dependency for every `useEffect`/`useMemo`/`useCallback` that lists it, so the natural
9
- * `const shenora = useShenora(); useEffect(…, [shenora])` re-runs on EVERY render — a subscribe/
10
- * unsubscribe cycle per frame in the worst case. The identity changes only when the bridge itself does,
11
- * or when `isAvailable` flips as a host attaches, which are the two moments a consumer means to react to.
7
+ * ⚠ The result is MEMOIZED, and not as a micro-optimisation. A fresh object every render is a fresh
8
+ * dependency for every hook that lists it, so `useEffect(…, [shenora])` would re-run on EVERY render.
9
+ * The identity changes only when the bridge does, or when `isAvailable` flips as a host attaches.
12
10
  */
13
11
  export declare function useShenora(): {
14
12
  isAvailable: boolean;
@@ -27,25 +25,18 @@ export declare function useShenora(): {
27
25
  * handshake that has not finished. **Treat absent as "assume nothing", never as "assume desktop".**
28
26
  *
29
27
  * ⚠ **Await `bridge.notifyReady()` before rendering the tree that depends on this.** The value is read
30
- * SYNCHRONOUSLY from the bridge's cache, deliberately — a capability learned after layout is a visible
31
- * flash — which means this hook does not re-render when a handshake lands later. A component mounted
32
- * mid-handshake sees `undefined` and keeps seeing it, which is the documented "assume nothing" tree
33
- * rather than a wrong one, but it is not the tree you wanted.
34
- *
35
- * ⚠ This hook was referenced by two doc examples in this package for several releases before it
36
- * existed — `bridge.shell` was the only way to read it. If you wrote that workaround, this replaces it.
28
+ * SYNCHRONOUSLY from the bridge's cache — a capability learned after layout is a visible flash — so
29
+ * this hook does NOT re-render when a handshake lands later. A component mounted mid-handshake sees
30
+ * `undefined` and keeps seeing it: the "assume nothing" tree, not the one you wanted.
37
31
  */
38
32
  export declare function useShellInfo(bridge?: ShenoraBridge): ShellInfo | undefined;
39
33
  /**
40
- * Subscribe to one (module, type) event for the component's lifetime, ported from the primary
41
- * desktop sibling. The handler receives the unwrapped payload (plus the full event). DEVIATION
42
- * from the source: instead of a deps array re-subscribing on change, the latest handler is kept
43
- * in a ref — no re-subscribe churn, no stale-closure trap.
34
+ * Subscribe to one (module, type) event for the component's lifetime. The handler receives the
35
+ * unwrapped payload plus the full event, and is kept in a ref rather than in the effect key — no
36
+ * re-subscribe churn, no stale-closure trap.
44
37
  *
45
- * Pass `scope` for a scoped app: the wire carries a scope and the host keys on it, but this hook had
46
- * no way to express one, so a component in profile A also woke for profile B's events with no filter
47
- * available (P5.5 H6). Omitting it still means "every scope", and a global (scope-less) event still
48
- * reaches a scoped subscriber — the host's rule, mirrored.
38
+ * Pass `scope` for a scoped app, or a component in profile A also wakes for profile B's events.
39
+ * Omitting it means "every scope", and a global (scope-less) event still reaches a scoped subscriber.
49
40
  */
50
41
  export declare function useShenoraEvent<TPayload = unknown>(module: string, type: string, handler: (payload: TPayload, event: EventMessage<TPayload>) => void, options?: {
51
42
  bus?: ShenoraEventBus;
@@ -61,10 +52,10 @@ export interface ShenoraQueryResult<TData> {
61
52
  refetch: () => void;
62
53
  }
63
54
  /**
64
- * Fetch-on-mount over the bridge: `invoke` + `{data, error, loading, refetch}`. Deliberately
65
- * minimal — no caching, no dedup, no background refresh (headless, D13): apps with data-layer
66
- * needs bring their own query library and call `bridge.invoke` from it. The payload participates
67
- * in the effect key BY VALUE (JSON), so inline object literals don't refetch every render.
55
+ * Fetch-on-mount over the bridge: `invoke` + `{data, error, loading, refetch}`. Minimal — no caching,
56
+ * no dedup, no background refresh; an app with data-layer needs brings its own query library and calls
57
+ * `bridge.invoke` from it. The payload participates in the effect key BY VALUE (JSON), so an inline
58
+ * object literal does not refetch every render.
68
59
  */
69
60
  export declare function useShenoraQuery<TData = unknown, TPayload = unknown>(module: string, type: string, options?: {
70
61
  payload?: TPayload;
package/dist/hooks.js CHANGED
@@ -4,11 +4,9 @@ import { eventBus as defaultEventBus } from './eventBus.js';
4
4
  /**
5
5
  * Host access for components: the (default) bridge and whether a host transport exists.
6
6
  *
7
- * ⚠ The result is MEMOIZED, and that is not a micro-optimisation. A fresh object every render is a
8
- * fresh dependency for every `useEffect`/`useMemo`/`useCallback` that lists it, so the natural
9
- * `const shenora = useShenora(); useEffect(…, [shenora])` re-runs on EVERY render — a subscribe/
10
- * unsubscribe cycle per frame in the worst case. The identity changes only when the bridge itself does,
11
- * or when `isAvailable` flips as a host attaches, which are the two moments a consumer means to react to.
7
+ * ⚠ The result is MEMOIZED, and not as a micro-optimisation. A fresh object every render is a fresh
8
+ * dependency for every hook that lists it, so `useEffect(…, [shenora])` would re-run on EVERY render.
9
+ * The identity changes only when the bridge does, or when `isAvailable` flips as a host attaches.
12
10
  */
13
11
  export function useShenora() {
14
12
  const bridge = getBridge();
@@ -28,27 +26,20 @@ export function useShenora() {
28
26
  * handshake that has not finished. **Treat absent as "assume nothing", never as "assume desktop".**
29
27
  *
30
28
  * ⚠ **Await `bridge.notifyReady()` before rendering the tree that depends on this.** The value is read
31
- * SYNCHRONOUSLY from the bridge's cache, deliberately — a capability learned after layout is a visible
32
- * flash — which means this hook does not re-render when a handshake lands later. A component mounted
33
- * mid-handshake sees `undefined` and keeps seeing it, which is the documented "assume nothing" tree
34
- * rather than a wrong one, but it is not the tree you wanted.
35
- *
36
- * ⚠ This hook was referenced by two doc examples in this package for several releases before it
37
- * existed — `bridge.shell` was the only way to read it. If you wrote that workaround, this replaces it.
29
+ * SYNCHRONOUSLY from the bridge's cache — a capability learned after layout is a visible flash — so
30
+ * this hook does NOT re-render when a handshake lands later. A component mounted mid-handshake sees
31
+ * `undefined` and keeps seeing it: the "assume nothing" tree, not the one you wanted.
38
32
  */
39
33
  export function useShellInfo(bridge) {
40
34
  return (bridge ?? getBridge()).shell;
41
35
  }
42
36
  /**
43
- * Subscribe to one (module, type) event for the component's lifetime, ported from the primary
44
- * desktop sibling. The handler receives the unwrapped payload (plus the full event). DEVIATION
45
- * from the source: instead of a deps array re-subscribing on change, the latest handler is kept
46
- * in a ref — no re-subscribe churn, no stale-closure trap.
37
+ * Subscribe to one (module, type) event for the component's lifetime. The handler receives the
38
+ * unwrapped payload plus the full event, and is kept in a ref rather than in the effect key — no
39
+ * re-subscribe churn, no stale-closure trap.
47
40
  *
48
- * Pass `scope` for a scoped app: the wire carries a scope and the host keys on it, but this hook had
49
- * no way to express one, so a component in profile A also woke for profile B's events with no filter
50
- * available (P5.5 H6). Omitting it still means "every scope", and a global (scope-less) event still
51
- * reaches a scoped subscriber — the host's rule, mirrored.
41
+ * Pass `scope` for a scoped app, or a component in profile A also wakes for profile B's events.
42
+ * Omitting it means "every scope", and a global (scope-less) event still reaches a scoped subscriber.
52
43
  */
53
44
  export function useShenoraEvent(module, type, handler, options = {}) {
54
45
  const handlerRef = useRef(handler);
@@ -58,10 +49,10 @@ export function useShenoraEvent(module, type, handler, options = {}) {
58
49
  useEffect(() => bus.subscribe(module, type, (event) => handlerRef.current(event.payload, event), { scope }), [module, type, bus, scope]);
59
50
  }
60
51
  /**
61
- * Fetch-on-mount over the bridge: `invoke` + `{data, error, loading, refetch}`. Deliberately
62
- * minimal — no caching, no dedup, no background refresh (headless, D13): apps with data-layer
63
- * needs bring their own query library and call `bridge.invoke` from it. The payload participates
64
- * in the effect key BY VALUE (JSON), so inline object literals don't refetch every render.
52
+ * Fetch-on-mount over the bridge: `invoke` + `{data, error, loading, refetch}`. Minimal — no caching,
53
+ * no dedup, no background refresh; an app with data-layer needs brings its own query library and calls
54
+ * `bridge.invoke` from it. The payload participates in the effect key BY VALUE (JSON), so an inline
55
+ * object literal does not refetch every render.
65
56
  */
66
57
  export function useShenoraQuery(module, type, options = {}) {
67
58
  const { payload, scope, enabled = true } = options;
@@ -77,8 +68,8 @@ export function useShenoraQuery(module, type, options = {}) {
77
68
  payloadRef.current = payload;
78
69
  useEffect(() => {
79
70
  if (!enabled) {
80
- // A fetch in flight when enabled flipped false was marked stale by the cleanup — without
81
- // this, `loading` would stay true forever (a spinner that never stops).
71
+ // The cleanup marked any in-flight fetch stale, so without this `loading` would stay true
72
+ // forever: a spinner that never stops.
82
73
  setState((previous) => (previous.loading ? { ...previous, loading: false } : previous));
83
74
  return;
84
75
  }
@@ -88,11 +79,8 @@ export function useShenoraQuery(module, type, options = {}) {
88
79
  .invoke(module, type, { payload: payloadRef.current, scope })
89
80
  .then((data) => { if (!stale)
90
81
  setState({ data, error: undefined, loading: false }); },
91
- // KEEP the previous data alongside the error (P5.5 H2). This used to set `data: undefined`, so
92
- // a failed REFETCH — a transient host hiccup, one timed-out call — blanked data the UI was
93
- // already showing correctly, turning a recoverable error into an empty screen. The caller has
94
- // both fields and can decide: render stale data with an error banner, or hide it. Blanking it
95
- // for them removes that choice. (A first fetch has no previous data, so it is unaffected.)
82
+ // KEEP the previous data alongside the error, so a failed REFETCH does not blank a screen the
83
+ // UI was showing correctly. The caller has both fields and decides which to render.
96
84
  (error) => { if (!stale)
97
85
  setState((previous) => ({ data: previous.data, error, loading: false })); });
98
86
  return () => { stale = true; };
package/dist/index.d.ts CHANGED
@@ -13,8 +13,8 @@ export { FileDialogs, useFileDialogs, type FileDialogFilter, type FileDialogOpti
13
13
  export { ClipboardAccess, useClipboard, PNG_IMAGE, HTML, type ClipboardContent, type ClipboardHandle, } from './clipboard.js';
14
14
  export { installDevInterceptor, type DevInterceptorOptions, type DevIpcEntry, type DevEventEntry, } from './devInterceptor.js';
15
15
  export { mediaUrl, encodeMediaPayload, decodeMediaPayload } from './media.js';
16
- export { parseManifest, pickMediaSource, nextSegment, segmentMimeType, codecsFromInitSegment, } from './segmentStream.js';
16
+ export { parseManifest, pickMediaSource, nextSegment, segmentMimeType, codecsFromInitSegment, remoteSegmentUrl, SEGMENT_REMOTE_PREFIX, } from './segmentStream.js';
17
17
  export { bindSegmentStream, SegmentBinderError } from './segmentBinder.js';
18
18
  export type { SegmentBinderOptions, SegmentBinding } from './segmentBinder.js';
19
19
  export type { SegmentEntry, SegmentManifest, MediaSourceKind, MediaSourceGlobals, FetchState, FetchPolicy, } from './segmentStream.js';
20
- export { useMediaPlayer, MEDIA_PLAYER_MODULE, MEDIA_PLAYER_REPORT, MediaPlayerCommands, type MediaPlayerReport, type MediaPlayerReportState, type UseMediaPlayerOptions, } from './mediaPlayer.js';
20
+ export { useMediaPlayer, MEDIA_PLAYER_MODULE, MEDIA_PLAYER_REPORT, MEDIA_PLAYER_STATUS, MediaPlayerCommands, MediaConversionEvents, MediaConversionErrorCodes, type MediaPlayerReport, type MediaPlayerReportState, type UseMediaPlayerOptions, } from './mediaPlayer.js';
package/dist/index.js CHANGED
@@ -10,15 +10,9 @@ export { ShenoraBridge, getBridge, configureBridge, } from './bridge.js';
10
10
  export { createShenoraStore, } from './store.js';
11
11
  export { BaseModuleService } from './moduleService.js';
12
12
  export { IpcRequestStates,
13
- // The event vocabulary + the default module name. `createRequestsStore` deliberately does NOT
14
- // subscribe to RESUME_REQUESTED / WAIT_REQUESTED — those target the OWNING module's own service,
15
- // not the generic store — so the app writing that handler needs both symbols, and until now had
16
- // neither: it had to hard-code the literals the wire-mirror tests exist to keep it from doing.
17
- IpcRequestEventTypes,
18
- // The ROUTE half of that same wire, and it was the one left behind: an app cancelling a request
19
- // without `useShenoraRequests` had the module name and the event names but had to hard-code
20
- // 'CANCEL'. Pinned by `WireMirrorTests.Request_route_names_match_the_hosts_module`.
21
- IpcRequestRoutes, IpcRequestsModuleName, createRequestsStore, useShenoraRequests, } from './requests.js';
13
+ // The event and route vocabulary + the default module name, so an app handling or cancelling a
14
+ // request without `useShenoraRequests` never types the wire literals by hand.
15
+ IpcRequestEventTypes, IpcRequestRoutes, IpcRequestsModuleName, createRequestsStore, useShenoraRequests, } from './requests.js';
22
16
  export { WindowCommands, useWindowMaximized, } from './windowCommands.js';
23
17
  export { useDropZone, DROP_ZONE_MODULE, } from './useDropZone.js';
24
18
  export { useShenora, useShenoraEvent, useShenoraQuery, useShellInfo, } from './hooks.js';
@@ -28,11 +22,9 @@ export { FileDialogs, useFileDialogs, } from './fileDialogs.js';
28
22
  // user gesture. The client half of the host's SHENORA.CLIPBOARD module.
29
23
  export { ClipboardAccess, useClipboard, PNG_IMAGE, HTML, } from './clipboard.js';
30
24
  export { installDevInterceptor, } from './devInterceptor.js';
31
- // Addressing local content the page cannot reach itself. A pure function, not a hook — building the URL
32
- // needs no React, and a `useMediaSource` can follow if an adopter wants load/error state.
25
+ // Addressing local content the page cannot reach itself. Pure functions, not hooks.
33
26
  export { mediaUrl, encodeMediaPayload, decodeMediaPayload } from './media.js';
34
- export { parseManifest, pickMediaSource, nextSegment, segmentMimeType, codecsFromInitSegment, } from './segmentStream.js';
27
+ export { parseManifest, pickMediaSource, nextSegment, segmentMimeType, codecsFromInitSegment, remoteSegmentUrl, SEGMENT_REMOTE_PREFIX, } from './segmentStream.js';
35
28
  export { bindSegmentStream, SegmentBinderError } from './segmentBinder.js';
36
29
  // The HOST-owned player (D58): .NET holds the lifecycle, the page's element is the display and the sound.
37
- // One hook, and the page stops deciding anything about formats.
38
- export { useMediaPlayer, MEDIA_PLAYER_MODULE, MEDIA_PLAYER_REPORT, MediaPlayerCommands, } from './mediaPlayer.js';
30
+ export { useMediaPlayer, MEDIA_PLAYER_MODULE, MEDIA_PLAYER_REPORT, MEDIA_PLAYER_STATUS, MediaPlayerCommands, MediaConversionEvents, MediaConversionErrorCodes, } from './mediaPlayer.js';
@@ -1,12 +1,6 @@
1
1
  /**
2
2
  * Shared internals for `@shenora/react`. NOT exported from the barrel — nothing here is public
3
3
  * surface, and it must not become so by accident.
4
- *
5
- * These lived as per-file copies until a second consumer appeared for each (P5.5 H2): the debounce
6
- * helper was private to `useDropZone` and `useWindowMaximized` needed the same thing, and the
7
- * `randomUUID`-with-fallback pair had drifted into two spellings. H4.5 deliberately left both alone
8
- * at the time, on the grounds that the package had no shared-internals home and inventing one for a
9
- * single consumer is speculation — this file exists now because the need is real.
10
4
  */
11
5
  /** A debounced void callback with a `cancel` for effect teardown. */
12
6
  export interface Debounced {
@@ -19,7 +13,7 @@ export interface Debounced {
19
13
  */
20
14
  export declare function debounce(fn: () => void, ms: number): Debounced;
21
15
  /**
22
- * A unique id, optionally prefixed. Correlation ids and zone ids only need uniqueness, not entropy,
23
- * so the non-`crypto` fallback (ancient or non-secure-context environments) is fine.
16
+ * A unique id, optionally prefixed. Correlation ids and zone ids need uniqueness, not entropy, so the
17
+ * non-`crypto` fallback for a non-secure context is fine.
24
18
  */
25
19
  export declare function randomId(prefix?: string): string;
package/dist/internal.js CHANGED
@@ -1,12 +1,6 @@
1
1
  /**
2
2
  * Shared internals for `@shenora/react`. NOT exported from the barrel — nothing here is public
3
3
  * surface, and it must not become so by accident.
4
- *
5
- * These lived as per-file copies until a second consumer appeared for each (P5.5 H2): the debounce
6
- * helper was private to `useDropZone` and `useWindowMaximized` needed the same thing, and the
7
- * `randomUUID`-with-fallback pair had drifted into two spellings. H4.5 deliberately left both alone
8
- * at the time, on the grounds that the package had no shared-internals home and inventing one for a
9
- * single consumer is speculation — this file exists now because the need is real.
10
4
  */
11
5
  /**
12
6
  * Trailing-edge debounce: the callback runs `ms` after the LAST call. `cancel` must be called from a
@@ -22,8 +16,8 @@ export function debounce(fn, ms) {
22
16
  return wrapped;
23
17
  }
24
18
  /**
25
- * A unique id, optionally prefixed. Correlation ids and zone ids only need uniqueness, not entropy,
26
- * so the non-`crypto` fallback (ancient or non-secure-context environments) is fine.
19
+ * A unique id, optionally prefixed. Correlation ids and zone ids need uniqueness, not entropy, so the
20
+ * non-`crypto` fallback for a non-secure context is fine.
27
21
  */
28
22
  export function randomId(prefix) {
29
23
  const id = typeof crypto !== 'undefined' && 'randomUUID' in crypto