@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
@@ -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.Core.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,34 +34,34 @@ 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). */
64
55
  clear(): void;
65
56
  /**
66
- * Subscription count (diagnostics). With no arguments: every subscription of any breadth. With a
67
- * `(module, type)`: every subscription that WOULD receive that pair — exact, whole-module and
68
- * catch-all — because "how many listeners does this event have?" is the question a diagnostic is
69
- * actually asking. Scope is not applied; a count is not a delivery.
57
+ * Subscription count (diagnostics). Every form answers "how many subscriptions WOULD receive this?",
58
+ * which is the question a diagnostic is actually asking — so each includes the broader breadths that
59
+ * would also fire. Scope is not applied; a count is not a delivery.
60
+ *
61
+ * - no arguments — every subscription of any breadth.
62
+ * - `(module)` — everything that would receive ANY event of that module: its exact-type
63
+ * subscriptions, its whole-module ones, and the catch-alls.
64
+ * - `(module, type)` — everything that would receive that one pair.
70
65
  */
71
66
  getSubscriptionCount(module?: string, type?: string): number;
72
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.Core.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;
@@ -86,17 +77,31 @@ export class ShenoraEventBus {
86
77
  this.all.clear();
87
78
  }
88
79
  /**
89
- * Subscription count (diagnostics). With no arguments: every subscription of any breadth. With a
90
- * `(module, type)`: every subscription that WOULD receive that pair — exact, whole-module and
91
- * catch-all — because "how many listeners does this event have?" is the question a diagnostic is
92
- * actually asking. Scope is not applied; a count is not a delivery.
80
+ * Subscription count (diagnostics). Every form answers "how many subscriptions WOULD receive this?",
81
+ * which is the question a diagnostic is actually asking — so each includes the broader breadths that
82
+ * would also fire. Scope is not applied; a count is not a delivery.
83
+ *
84
+ * - no arguments — every subscription of any breadth.
85
+ * - `(module)` — everything that would receive ANY event of that module: its exact-type
86
+ * subscriptions, its whole-module ones, and the catch-alls.
87
+ * - `(module, type)` — everything that would receive that one pair.
93
88
  */
94
89
  getSubscriptionCount(module, type) {
95
- if (module && type) {
90
+ if (module !== undefined && type !== undefined) {
96
91
  return (this.exact.get(eventKey(module, type))?.size ?? 0)
97
92
  + (this.byModule.get(module)?.size ?? 0)
98
93
  + this.all.size;
99
94
  }
95
+ if (module !== undefined) {
96
+ let total = this.all.size + (this.byModule.get(module)?.size ?? 0);
97
+ // The exact map is keyed `module\0type`, so a module's own entries are its prefix — compared
98
+ // against the separator so that a module named `APP` cannot pick up `APPLE`'s subscriptions.
99
+ const prefix = `${module}\0`;
100
+ for (const [key, set] of this.exact)
101
+ if (key.startsWith(prefix))
102
+ total += set.size;
103
+ return total;
104
+ }
100
105
  let total = this.all.size;
101
106
  for (const set of this.exact.values())
102
107
  total += set.size;
@@ -109,7 +114,7 @@ export class ShenoraEventBus {
109
114
  function newSubscription(handler, options) {
110
115
  return { handler: handler, scope: options.scope };
111
116
  }
112
- /** 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. */
113
118
  function addTo(collection, key, subscription) {
114
119
  let set = collection.get(key);
115
120
  if (!set) {
@@ -127,18 +132,11 @@ function addTo(collection, key, subscription) {
127
132
  };
128
133
  }
129
134
  /**
130
- * `'\0'`-joined, NOT `.`-joined (P5.5 H6).
135
+ * `'\0'`-joined, NOT `.`-joined, matching the host's `EventBus`.
131
136
  *
132
- * 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
133
138
  * `("APP", "TASK.DONE")` and `("APP.TASK", "DONE")` the same key — one app's events silently delivered
134
- * to another's subscribers. The host's `EventBus` fixed exactly this and documented it; the client kept
135
- * the colliding form, so the two halves of one contract disagreed. `'\0'` cannot appear in a JS string
136
- * literal a developer types by accident, which is what makes it safe as a separator.
137
- *
138
- * The broad subscriptions deliberately do NOT reuse this with a `"*"` sentinel the way the host's
139
- * pattern matcher does: a module or type legitimately named `*` would then silently become a
140
- * catch-all. Separate collections cannot collide with an app string at all — same lesson as above,
141
- * applied before it could be earned a second time.
139
+ * to another's subscribers. `'\0'` cannot appear in a string literal a developer types by accident.
142
140
  */
143
141
  function eventKey(module, type) {
144
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. */
@@ -83,7 +83,7 @@ interface FileDialogRequests {
83
83
  };
84
84
  }
85
85
  /**
86
- * Typed client for the host's `FILE_DIALOGS` module (`FileDialogFacade`).
86
+ * Typed client for the host's `SHENORA.DIALOGS` module (`FileDialogModule`).
87
87
  *
88
88
  * ⚠ **Two of these are DESKTOP capabilities.** `openFolder` and `saveFile` have no expression on a
89
89
  * phone and reject with `IpcErrorCodes.capabilityNotSupported` there. Do not catch that — ask first,
@@ -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.
@@ -1,16 +1,16 @@
1
1
  /**
2
- * The page's half of the host's `FILE_DIALOGS` module — native file/folder/save dialogs, and the
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.Ipc.FileDialogFacade`. 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';
10
10
  import { BaseModuleService } from './moduleService.js';
11
11
  import { ShellCapabilities } from './types.js';
12
12
  /**
13
- * Typed client for the host's `FILE_DIALOGS` module (`FileDialogFacade`).
13
+ * Typed client for the host's `SHENORA.DIALOGS` module (`FileDialogModule`).
14
14
  *
15
15
  * ⚠ **Two of these are DESKTOP capabilities.** `openFolder` and `saveFile` have no expression on a
16
16
  * phone and reject with `IpcErrorCodes.capabilityNotSupported` there. Do not catch that — ask first,
@@ -19,17 +19,16 @@ import { ShellCapabilities } from './types.js';
19
19
  */
20
20
  export class FileDialogs extends BaseModuleService {
21
21
  constructor(bridge) {
22
- super('FILE_DIALOGS', bridge);
22
+ super('SHENORA.DIALOGS', bridge);
23
23
  }
24
24
  /** Pick an existing file. Available on every shell. */
25
25
  openFile(options) {
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
@@ -1,7 +1,13 @@
1
1
  import { type ShenoraBridge } from './bridge.js';
2
2
  import { type ShenoraEventBus } from './eventBus.js';
3
3
  import type { EventMessage, ShellInfo } from './types.js';
4
- /** Host access for components: the (default) bridge and whether a host transport exists. */
4
+ /**
5
+ * Host access for components: the (default) bridge and whether a host transport exists.
6
+ *
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.
10
+ */
5
11
  export declare function useShenora(): {
6
12
  isAvailable: boolean;
7
13
  bridge: ShenoraBridge;
@@ -19,25 +25,18 @@ export declare function useShenora(): {
19
25
  * handshake that has not finished. **Treat absent as "assume nothing", never as "assume desktop".**
20
26
  *
21
27
  * ⚠ **Await `bridge.notifyReady()` before rendering the tree that depends on this.** The value is read
22
- * SYNCHRONOUSLY from the bridge's cache, deliberately — a capability learned after layout is a visible
23
- * flash — which means this hook does not re-render when a handshake lands later. A component mounted
24
- * mid-handshake sees `undefined` and keeps seeing it, which is the documented "assume nothing" tree
25
- * rather than a wrong one, but it is not the tree you wanted.
26
- *
27
- * ⚠ This hook was referenced by two doc examples in this package for several releases before it
28
- * 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.
29
31
  */
30
32
  export declare function useShellInfo(bridge?: ShenoraBridge): ShellInfo | undefined;
31
33
  /**
32
- * Subscribe to one (module, type) event for the component's lifetime, ported from the primary
33
- * desktop sibling. The handler receives the unwrapped payload (plus the full event). DEVIATION
34
- * from the source: instead of a deps array re-subscribing on change, the latest handler is kept
35
- * 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.
36
37
  *
37
- * Pass `scope` for a scoped app: the wire carries a scope and the host keys on it, but this hook had
38
- * no way to express one, so a component in profile A also woke for profile B's events with no filter
39
- * available (P5.5 H6). Omitting it still means "every scope", and a global (scope-less) event still
40
- * 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.
41
40
  */
42
41
  export declare function useShenoraEvent<TPayload = unknown>(module: string, type: string, handler: (payload: TPayload, event: EventMessage<TPayload>) => void, options?: {
43
42
  bus?: ShenoraEventBus;
@@ -53,10 +52,10 @@ export interface ShenoraQueryResult<TData> {
53
52
  refetch: () => void;
54
53
  }
55
54
  /**
56
- * Fetch-on-mount over the bridge: `invoke` + `{data, error, loading, refetch}`. Deliberately
57
- * minimal — no caching, no dedup, no background refresh (headless, D13): apps with data-layer
58
- * needs bring their own query library and call `bridge.invoke` from it. The payload participates
59
- * 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.
60
59
  */
61
60
  export declare function useShenoraQuery<TData = unknown, TPayload = unknown>(module: string, type: string, options?: {
62
61
  payload?: TPayload;
package/dist/hooks.js CHANGED
@@ -1,10 +1,17 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { getBridge } from './bridge.js';
3
3
  import { eventBus as defaultEventBus } from './eventBus.js';
4
- /** Host access for components: the (default) bridge and whether a host transport exists. */
4
+ /**
5
+ * Host access for components: the (default) bridge and whether a host transport exists.
6
+ *
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.
10
+ */
5
11
  export function useShenora() {
6
12
  const bridge = getBridge();
7
- return { isAvailable: bridge.isAvailable, bridge };
13
+ const isAvailable = bridge.isAvailable;
14
+ return useMemo(() => ({ isAvailable, bridge }), [isAvailable, bridge]);
8
15
  }
9
16
  /**
10
17
  * What the host IS and what it can do — the way one bundle renders correctly on every shell.
@@ -19,27 +26,20 @@ export function useShenora() {
19
26
  * handshake that has not finished. **Treat absent as "assume nothing", never as "assume desktop".**
20
27
  *
21
28
  * ⚠ **Await `bridge.notifyReady()` before rendering the tree that depends on this.** The value is read
22
- * SYNCHRONOUSLY from the bridge's cache, deliberately — a capability learned after layout is a visible
23
- * flash — which means this hook does not re-render when a handshake lands later. A component mounted
24
- * mid-handshake sees `undefined` and keeps seeing it, which is the documented "assume nothing" tree
25
- * rather than a wrong one, but it is not the tree you wanted.
26
- *
27
- * ⚠ This hook was referenced by two doc examples in this package for several releases before it
28
- * 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.
29
32
  */
30
33
  export function useShellInfo(bridge) {
31
34
  return (bridge ?? getBridge()).shell;
32
35
  }
33
36
  /**
34
- * Subscribe to one (module, type) event for the component's lifetime, ported from the primary
35
- * desktop sibling. The handler receives the unwrapped payload (plus the full event). DEVIATION
36
- * from the source: instead of a deps array re-subscribing on change, the latest handler is kept
37
- * 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.
38
40
  *
39
- * Pass `scope` for a scoped app: the wire carries a scope and the host keys on it, but this hook had
40
- * no way to express one, so a component in profile A also woke for profile B's events with no filter
41
- * available (P5.5 H6). Omitting it still means "every scope", and a global (scope-less) event still
42
- * 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.
43
43
  */
44
44
  export function useShenoraEvent(module, type, handler, options = {}) {
45
45
  const handlerRef = useRef(handler);
@@ -49,10 +49,10 @@ export function useShenoraEvent(module, type, handler, options = {}) {
49
49
  useEffect(() => bus.subscribe(module, type, (event) => handlerRef.current(event.payload, event), { scope }), [module, type, bus, scope]);
50
50
  }
51
51
  /**
52
- * Fetch-on-mount over the bridge: `invoke` + `{data, error, loading, refetch}`. Deliberately
53
- * minimal — no caching, no dedup, no background refresh (headless, D13): apps with data-layer
54
- * needs bring their own query library and call `bridge.invoke` from it. The payload participates
55
- * 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.
56
56
  */
57
57
  export function useShenoraQuery(module, type, options = {}) {
58
58
  const { payload, scope, enabled = true } = options;
@@ -68,8 +68,8 @@ export function useShenoraQuery(module, type, options = {}) {
68
68
  payloadRef.current = payload;
69
69
  useEffect(() => {
70
70
  if (!enabled) {
71
- // A fetch in flight when enabled flipped false was marked stale by the cleanup — without
72
- // 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.
73
73
  setState((previous) => (previous.loading ? { ...previous, loading: false } : previous));
74
74
  return;
75
75
  }
@@ -79,11 +79,8 @@ export function useShenoraQuery(module, type, options = {}) {
79
79
  .invoke(module, type, { payload: payloadRef.current, scope })
80
80
  .then((data) => { if (!stale)
81
81
  setState({ data, error: undefined, loading: false }); },
82
- // KEEP the previous data alongside the error (P5.5 H2). This used to set `data: undefined`, so
83
- // a failed REFETCH — a transient host hiccup, one timed-out call — blanked data the UI was
84
- // already showing correctly, turning a recoverable error into an empty screen. The caller has
85
- // both fields and can decide: render stale data with an error banner, or hide it. Blanking it
86
- // 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.
87
84
  (error) => { if (!stale)
88
85
  setState((previous) => ({ data: previous.data, error, loading: false })); });
89
86
  return () => { stale = true; };
package/dist/index.d.ts CHANGED
@@ -1,14 +1,20 @@
1
1
  export { IpcCategories, IpcErrorCodes, HANDSHAKE_MODULE, HANDSHAKE_TYPE, type IpcRequest, type IpcResponse, type IpcError, type IpcNotification, type IpcNotificationBatch, type EventMessage, ShellCapabilities, type ShellInfo, } from './types.js';
2
- export { OperationError } from './errors.js';
2
+ export { ShenoraError } from './errors.js';
3
3
  export { isShenoraAvailable, createHostTransport, createWebView2Transport, createHybridWebViewTransport, type ShenoraTransport, } from './transport.js';
4
4
  export { ShenoraEventBus, eventBus, type SubscribeOptions, } from './eventBus.js';
5
5
  export { ShenoraBridge, getBridge, configureBridge, type ShenoraBridgeOptions, type InvokeOptions, type PostOptions, type PostFailure, } from './bridge.js';
6
6
  export { createShenoraStore, type ShenoraStore, type ShenoraStoreOptions, type ShenoraStoreIo, type ShenoraStoreSnapshot, } from './store.js';
7
7
  export { BaseModuleService } from './moduleService.js';
8
- export { OperationStatuses, OperationEventTypes, OperationModuleName, createOperationsStore, useShenoraOperations, type OperationStatus, type OperationLabel, type OperationProgress, type OperationInfo, type OperationsState, type OperationsActions, type OperationsStoreOptions, } from './operations.js';
8
+ export { IpcRequestStates, IpcRequestEventTypes, IpcRequestRoutes, IpcRequestsModuleName, createRequestsStore, useShenoraRequests, type IpcRequestState, type IpcLabel, type IpcProgress, type IpcRequestStatus, type RequestsState, type RequestsActions, type RequestsStoreOptions, } from './requests.js';
9
9
  export { WindowCommands, useWindowMaximized, type WindowResizeEdge, type CaptionButtonKind, type CaptionButtonRect, } from './windowCommands.js';
10
10
  export { useDropZone, DROP_ZONE_MODULE, type DropZoneFileDrop, type UseDropZoneOptions, } from './useDropZone.js';
11
11
  export { useShenora, useShenoraEvent, useShenoraQuery, useShellInfo, type ShenoraQueryResult, } from './hooks.js';
12
12
  export { FileDialogs, useFileDialogs, type FileDialogFilter, type FileDialogOptions, type OpenFileOptions, type OpenFolderOptions, type SaveFileOptions, type FileDialogResult, type FileDialogsHandle, } from './fileDialogs.js';
13
+ export { ClipboardAccess, useClipboard, PNG_IMAGE, HTML, type ClipboardContent, type ClipboardHandle, } from './clipboard.js';
13
14
  export { installDevInterceptor, type DevInterceptorOptions, type DevIpcEntry, type DevEventEntry, } from './devInterceptor.js';
14
15
  export { mediaUrl, encodeMediaPayload, decodeMediaPayload } from './media.js';
16
+ export { parseManifest, pickMediaSource, nextSegment, segmentMimeType, codecsFromInitSegment, remoteSegmentUrl, SEGMENT_REMOTE_PREFIX, } from './segmentStream.js';
17
+ export { bindSegmentStream, SegmentBinderError } from './segmentBinder.js';
18
+ export type { SegmentBinderOptions, SegmentBinding } from './segmentBinder.js';
19
+ export type { SegmentEntry, SegmentManifest, MediaSourceKind, MediaSourceGlobals, FetchState, FetchPolicy, } from './segmentStream.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
@@ -3,24 +3,28 @@
3
3
  // React hooks, and the dev interceptor for CDP-driven testing. Headless by design (D13): no UI
4
4
  // components, no design-system dependency — apps bring their own.
5
5
  export { IpcCategories, IpcErrorCodes, HANDSHAKE_MODULE, HANDSHAKE_TYPE, ShellCapabilities, } from './types.js';
6
- export { OperationError } from './errors.js';
6
+ export { ShenoraError } from './errors.js';
7
7
  export { isShenoraAvailable, createHostTransport, createWebView2Transport, createHybridWebViewTransport, } from './transport.js';
8
8
  export { ShenoraEventBus, eventBus, } from './eventBus.js';
9
9
  export { ShenoraBridge, getBridge, configureBridge, } from './bridge.js';
10
10
  export { createShenoraStore, } from './store.js';
11
11
  export { BaseModuleService } from './moduleService.js';
12
- export { OperationStatuses,
13
- // The event vocabulary + the default module name. `createOperationsStore` 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
- OperationEventTypes, OperationModuleName, createOperationsStore, useShenoraOperations, } from './operations.js';
12
+ export { IpcRequestStates,
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';
18
16
  export { WindowCommands, useWindowMaximized, } from './windowCommands.js';
19
17
  export { useDropZone, DROP_ZONE_MODULE, } from './useDropZone.js';
20
18
  export { useShenora, useShenoraEvent, useShenoraQuery, useShellInfo, } from './hooks.js';
21
- // Native dialogs, capability-gated. The client half of the host's FILE_DIALOGS module.
19
+ // Native dialogs, capability-gated. The client half of the host's SHENORA.DIALOGS module.
22
20
  export { FileDialogs, useFileDialogs, } from './fileDialogs.js';
21
+ // The native clipboard, for the two things navigator.clipboard cannot do: FILES, and access with no
22
+ // user gesture. The client half of the host's SHENORA.CLIPBOARD module.
23
+ export { ClipboardAccess, useClipboard, PNG_IMAGE, HTML, } from './clipboard.js';
23
24
  export { installDevInterceptor, } from './devInterceptor.js';
24
- // Addressing local content the page cannot reach itself. A pure function, not a hook — building the URL
25
- // 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.
26
26
  export { mediaUrl, encodeMediaPayload, decodeMediaPayload } from './media.js';
27
+ export { parseManifest, pickMediaSource, nextSegment, segmentMimeType, codecsFromInitSegment, remoteSegmentUrl, SEGMENT_REMOTE_PREFIX, } from './segmentStream.js';
28
+ export { bindSegmentStream, SegmentBinderError } from './segmentBinder.js';
29
+ // The HOST-owned player (D58): .NET holds the lifecycle, the page's element is the display and the sound.
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;