@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.
- package/README.md +152 -165
- package/dist/bridge.d.ts +29 -46
- package/dist/bridge.js +47 -71
- package/dist/clipboard.d.ts +89 -0
- package/dist/clipboard.js +119 -0
- package/dist/devInterceptor.d.ts +11 -8
- package/dist/devInterceptor.js +16 -10
- package/dist/errors.d.ts +5 -6
- package/dist/errors.js +6 -7
- package/dist/eventBus.d.ts +21 -26
- package/dist/eventBus.js +38 -40
- package/dist/fileDialogs.d.ts +9 -12
- package/dist/fileDialogs.js +12 -16
- package/dist/hooks.d.ts +19 -20
- package/dist/hooks.js +26 -29
- package/dist/index.d.ts +8 -2
- package/dist/index.js +14 -10
- 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 +103 -0
- package/dist/mediaPlayer.js +202 -0
- package/dist/moduleService.d.ts +17 -21
- package/dist/moduleService.js +17 -21
- package/dist/requests.d.ts +145 -0
- package/dist/requests.js +113 -0
- package/dist/segmentBinder.d.ts +74 -0
- package/dist/segmentBinder.js +239 -0
- package/dist/segmentStream.d.ts +125 -0
- package/dist/segmentStream.js +239 -0
- package/dist/store.d.ts +18 -26
- package/dist/store.js +69 -36
- package/dist/transport.d.ts +9 -18
- package/dist/transport.js +9 -18
- package/dist/types.d.ts +33 -42
- package/dist/types.js +29 -25
- package/dist/useDropZone.d.ts +23 -22
- package/dist/useDropZone.js +41 -37
- package/dist/windowCommands.d.ts +15 -19
- package/dist/windowCommands.js +18 -25
- package/package.json +10 -3
- package/dist/operations.d.ts +0 -256
- package/dist/operations.js +0 -191
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.
|
|
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,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
|
|
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). */
|
|
64
55
|
clear(): void;
|
|
65
56
|
/**
|
|
66
|
-
* Subscription count (diagnostics).
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
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.
|
|
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;
|
|
@@ -86,17 +77,31 @@ export class ShenoraEventBus {
|
|
|
86
77
|
this.all.clear();
|
|
87
78
|
}
|
|
88
79
|
/**
|
|
89
|
-
* Subscription count (diagnostics).
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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}`;
|
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. */
|
|
@@ -83,7 +83,7 @@ interface FileDialogRequests {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Typed client for the host's `
|
|
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
|
-
*
|
|
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
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The page's half of the host's `
|
|
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.
|
|
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';
|
|
10
10
|
import { BaseModuleService } from './moduleService.js';
|
|
11
11
|
import { ShellCapabilities } from './types.js';
|
|
12
12
|
/**
|
|
13
|
-
* Typed client for the host's `
|
|
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('
|
|
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
|
-
*
|
|
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
|
@@ -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
|
-
/**
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
38
|
-
*
|
|
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}`.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
40
|
-
*
|
|
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}`.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
//
|
|
72
|
-
//
|
|
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
|
|
83
|
-
//
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
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
|
|
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.
|
|
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';
|
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;
|