@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.
- package/dist/bridge.d.ts +28 -47
- package/dist/bridge.js +37 -70
- package/dist/clipboard.d.ts +5 -10
- package/dist/clipboard.js +11 -18
- package/dist/devInterceptor.d.ts +8 -12
- package/dist/devInterceptor.js +10 -15
- package/dist/errors.d.ts +4 -5
- package/dist/errors.js +4 -5
- package/dist/eventBus.d.ts +13 -28
- package/dist/eventBus.js +19 -40
- package/dist/fileDialogs.d.ts +8 -11
- package/dist/fileDialogs.js +9 -13
- package/dist/hooks.d.ts +15 -24
- package/dist/hooks.js +19 -31
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -14
- package/dist/internal.d.ts +2 -8
- package/dist/internal.js +2 -8
- package/dist/media.d.ts +14 -22
- package/dist/media.js +14 -22
- package/dist/mediaPlayer.d.ts +39 -22
- package/dist/mediaPlayer.js +54 -44
- package/dist/moduleService.d.ts +11 -22
- package/dist/moduleService.js +11 -22
- package/dist/requests.d.ts +34 -65
- package/dist/requests.js +21 -54
- package/dist/segmentBinder.d.ts +13 -26
- package/dist/segmentBinder.js +25 -42
- package/dist/segmentStream.d.ts +43 -54
- package/dist/segmentStream.js +52 -61
- package/dist/store.d.ts +15 -26
- package/dist/store.js +27 -55
- package/dist/transport.d.ts +9 -18
- package/dist/transport.js +9 -18
- package/dist/types.d.ts +23 -57
- package/dist/types.js +19 -40
- package/dist/useDropZone.d.ts +13 -25
- package/dist/useDropZone.js +24 -41
- package/dist/windowCommands.d.ts +14 -18
- package/dist/windowCommands.js +16 -23
- package/package.json +1 -1
package/dist/eventBus.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
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,
|
|
66
|
-
// subscribe or unsubscribe during delivery, and one event must reach exactly the subscribers
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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}`;
|
package/dist/fileDialogs.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* ⚠
|
|
115
|
-
*
|
|
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
|
|
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.
|
package/dist/fileDialogs.js
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* ⚠
|
|
50
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
|
46
|
-
*
|
|
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}`.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
49
|
-
*
|
|
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}`.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
-
//
|
|
81
|
-
//
|
|
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
|
|
92
|
-
//
|
|
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
|
|
14
|
-
//
|
|
15
|
-
|
|
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.
|
|
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
|
-
|
|
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';
|
package/dist/internal.d.ts
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
|
/** 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
|
|
23
|
-
*
|
|
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
|
|
26
|
-
*
|
|
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
|