@shenora/react 0.11.0 → 0.13.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 +65 -42
- package/dist/segmentStream.d.ts +43 -54
- package/dist/segmentStream.js +102 -70
- package/dist/store.d.ts +15 -26
- package/dist/store.js +63 -59
- 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/bridge.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface ShenoraBridgeOptions {
|
|
|
6
6
|
/**
|
|
7
7
|
* The channel to the host. Default: whichever Shenora host this page is in — WebView2 postMessage
|
|
8
8
|
* on the desktop shell, `HybridWebView` on the MAUI shell — else null (plain browser). Supply your
|
|
9
|
-
* own for another shell
|
|
9
|
+
* own for another shell, or a scripted fake for tests and preview harnesses.
|
|
10
10
|
*/
|
|
11
11
|
transport?: ShenoraTransport | null;
|
|
12
12
|
/** The event bus host notifications are unbundled into. Default: the shared bus. */
|
|
@@ -14,25 +14,23 @@ export interface ShenoraBridgeOptions {
|
|
|
14
14
|
/** Per-request timeout in ms when the call doesn't set one. Family default: 30 000. */
|
|
15
15
|
defaultTimeoutMs?: number;
|
|
16
16
|
/**
|
|
17
|
-
* Pure-UI development seam: answers requests when NO transport exists (plain browser tab
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* production stays hard-failing.
|
|
17
|
+
* Pure-UI development seam: answers requests when NO transport exists (a plain browser tab).
|
|
18
|
+
* Return the response data, or a promise; throw to reject.
|
|
19
|
+
*
|
|
20
|
+
* ⚠ Gate the call site with `import.meta.env.DEV` so production stays hard-failing.
|
|
22
21
|
*/
|
|
23
22
|
fallback?: (request: IpcRequest) => unknown;
|
|
24
23
|
/**
|
|
25
24
|
* Where a FAILED {@link ShenoraBridge.post} is reported. Default: `console.error`.
|
|
26
25
|
*
|
|
27
|
-
* A one-way send has no promise to reject, so
|
|
28
|
-
*
|
|
29
|
-
* "just stops working" with nothing to grep for. Route it into the app's logger/toast instead.
|
|
26
|
+
* ⚠ Route it into the app's logger or toast. A one-way send has no promise to reject, so its
|
|
27
|
+
* failures are otherwise invisible.
|
|
30
28
|
*/
|
|
31
29
|
onPostError?: (error: PostFailure) => void;
|
|
32
30
|
/**
|
|
33
31
|
* How many unawaited {@link ShenoraBridge.post} ids to remember for error reporting. Default 256.
|
|
34
|
-
* Capped
|
|
35
|
-
*
|
|
32
|
+
* Capped drop-oldest, so a host that never answers cannot grow the set without bound; evicting an
|
|
33
|
+
* id only loses its error report.
|
|
36
34
|
*/
|
|
37
35
|
maxTrackedPosts?: number;
|
|
38
36
|
}
|
|
@@ -59,10 +57,10 @@ export interface PostOptions<TPayload = unknown> {
|
|
|
59
57
|
scope?: string;
|
|
60
58
|
}
|
|
61
59
|
/**
|
|
62
|
-
* The client side of the Shenora IPC contract
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
60
|
+
* The client side of the Shenora IPC contract: correlated request/response over a pluggable
|
|
61
|
+
* transport, category routing of host messages (`ipc` → resolve the pending call, `notification` →
|
|
62
|
+
* unbundle the batch into the event bus), per-request timeout, the ready handshake, and a browser
|
|
63
|
+
* fallback seam for pure-UI development.
|
|
66
64
|
*
|
|
67
65
|
* Most apps use the lazy default instance via {@link getBridge}/{@link configureBridge}; create
|
|
68
66
|
* instances directly for tests or multi-transport setups.
|
|
@@ -82,9 +80,7 @@ export declare class ShenoraBridge {
|
|
|
82
80
|
constructor(options?: ShenoraBridgeOptions);
|
|
83
81
|
/**
|
|
84
82
|
* True when this bridge can actually send: a transport to a host exists AND the bridge has not been
|
|
85
|
-
* disposed.
|
|
86
|
-
* replaced still reported itself available while every `invoke` on it rejected with `NO_TRANSPORT` —
|
|
87
|
-
* the exact case the disposed check in `invoke` exists for (P5.5 H2).
|
|
83
|
+
* disposed. The check to make on a reference that may have outlived a {@link configureBridge} swap.
|
|
88
84
|
*/
|
|
89
85
|
get isAvailable(): boolean;
|
|
90
86
|
/**
|
|
@@ -96,28 +92,17 @@ export declare class ShenoraBridge {
|
|
|
96
92
|
/**
|
|
97
93
|
* Send WITHOUT awaiting a reply, and return the request id.
|
|
98
94
|
*
|
|
99
|
-
* This is the default shape for a desktop shell
|
|
100
|
-
* `
|
|
101
|
-
*
|
|
102
|
-
* design, because the dispatch pipeline preserves the caller's synchronization context so facades
|
|
103
|
-
* can touch the window. Reserve `invoke` for calls that are quick AND safe on the UI thread — the
|
|
104
|
-
* window commands are the model — and post everything else, streaming results back as notifications.
|
|
105
|
-
*
|
|
106
|
-
* ⚠ Posting is only HALF of freeing the UI thread. The host still dispatches on the UI thread
|
|
107
|
-
* whether or not the client awaits, so a handler that does heavy work synchronously stalls the
|
|
108
|
-
* window either way. The other half is the host's: return from the route immediately and stream.
|
|
95
|
+
* This is the default shape for a desktop shell and {@link invoke} is the special case (D23):
|
|
96
|
+
* reserve `invoke` for calls that are quick AND safe on the host's UI thread, and post everything
|
|
97
|
+
* else, streaming results back as notifications.
|
|
109
98
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* which caps the set drop-oldest so a host that never answers cannot grow it without bound. No TIMER is
|
|
114
|
-
* set, though, so there is no deadline and nothing to fire later.
|
|
99
|
+
* A failed response is reported through `onPostError` (default `console.error`) rather than
|
|
100
|
+
* dropped. The id is remembered for that — see {@link ShenoraBridgeOptions.maxTrackedPosts}. No
|
|
101
|
+
* timer is set, so there is no deadline.
|
|
115
102
|
*
|
|
116
|
-
* No transport (a plain browser tab) is a silent no-op,
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* while `post` just returns the id, so a stale reference kept across a `configureBridge` swap looks
|
|
120
|
-
* like it is still sending. `isAvailable` is the check.
|
|
103
|
+
* ⚠ No transport (a plain browser tab) is a silent no-op, and **so is a DISPOSED bridge** — where
|
|
104
|
+
* `invoke` would reject with `NO_TRANSPORT`, this just returns the id, so a stale reference kept
|
|
105
|
+
* across a {@link configureBridge} swap looks like it is still sending. `isAvailable` is the check.
|
|
121
106
|
*/
|
|
122
107
|
post<TPayload = unknown>(module: string, type: string, options?: PostOptions<TPayload>): string;
|
|
123
108
|
/**
|
|
@@ -127,22 +112,18 @@ export declare class ShenoraBridge {
|
|
|
127
112
|
* also the host's cue to reset per-page state. No-transport is a silent no-op so browser dev
|
|
128
113
|
* doesn't error.
|
|
129
114
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* was acked. `DropZoneManager` now clears on DOCUMENT CHANGE instead, which cannot race the client.
|
|
133
|
-
* The general point still stands for any per-page state YOUR host resets here — a reset keyed on
|
|
134
|
-
* the handshake races anything the page sends before it, and in React that is structural rather
|
|
135
|
-
* than a mistake, because CHILD effects run before PARENT effects.
|
|
115
|
+
* ⚠ Any per-page state YOUR host resets on this handshake races whatever the page sent before it,
|
|
116
|
+
* and in React that is structural rather than bad luck: CHILD effects run before PARENT effects.
|
|
136
117
|
*
|
|
137
|
-
* The returned promise REJECTS on a failed handshake (disposed bridge, timeout). Handle it —
|
|
118
|
+
* ⚠ The returned promise REJECTS on a failed handshake (disposed bridge, timeout). Handle it —
|
|
138
119
|
* `void bridge.notifyReady()` turns that into an unhandled rejection, which in a WebView2 page is
|
|
139
120
|
* a silent console error.
|
|
140
121
|
*/
|
|
141
122
|
notifyReady<TPayload = unknown>(payload?: TPayload): Promise<ShellInfo | undefined>;
|
|
142
123
|
/**
|
|
143
124
|
* What the host said it was during {@link notifyReady} — undefined before the handshake, or when
|
|
144
|
-
* the host advertised nothing. Cached so components can read it synchronously while rendering
|
|
145
|
-
*
|
|
125
|
+
* the host advertised nothing. Cached so components can read it synchronously while rendering; a
|
|
126
|
+
* capability learned after layout is a visible flash.
|
|
146
127
|
*/
|
|
147
128
|
get shell(): ShellInfo | undefined;
|
|
148
129
|
/** Reject everything in flight and detach from the transport. */
|
package/dist/bridge.js
CHANGED
|
@@ -8,10 +8,10 @@ const newId = () => randomId();
|
|
|
8
8
|
const isThenable = (value) => typeof value === 'object' && value !== null
|
|
9
9
|
&& typeof value.then === 'function';
|
|
10
10
|
/**
|
|
11
|
-
* The client side of the Shenora IPC contract
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* The client side of the Shenora IPC contract: correlated request/response over a pluggable
|
|
12
|
+
* transport, category routing of host messages (`ipc` → resolve the pending call, `notification` →
|
|
13
|
+
* unbundle the batch into the event bus), per-request timeout, the ready handshake, and a browser
|
|
14
|
+
* fallback seam for pure-UI development.
|
|
15
15
|
*
|
|
16
16
|
* Most apps use the lazy default instance via {@link getBridge}/{@link configureBridge}; create
|
|
17
17
|
* instances directly for tests or multi-transport setups.
|
|
@@ -20,7 +20,7 @@ export class ShenoraBridge {
|
|
|
20
20
|
constructor(options = {}) {
|
|
21
21
|
this.pending = new Map();
|
|
22
22
|
// Ids of one-way sends, kept ONLY so a failed response can be reported instead of vanishing.
|
|
23
|
-
//
|
|
23
|
+
// A Map for its insertion-ordered keys, so the cap can evict the oldest.
|
|
24
24
|
this.unawaited = new Map();
|
|
25
25
|
this.disposed = false;
|
|
26
26
|
this.transport = options.transport !== undefined ? options.transport : createHostTransport();
|
|
@@ -34,9 +34,7 @@ export class ShenoraBridge {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* True when this bridge can actually send: a transport to a host exists AND the bridge has not been
|
|
37
|
-
* disposed.
|
|
38
|
-
* replaced still reported itself available while every `invoke` on it rejected with `NO_TRANSPORT` —
|
|
39
|
-
* the exact case the disposed check in `invoke` exists for (P5.5 H2).
|
|
37
|
+
* disposed. The check to make on a reference that may have outlived a {@link configureBridge} swap.
|
|
40
38
|
*/
|
|
41
39
|
get isAvailable() {
|
|
42
40
|
return !this.disposed && this.transport !== null;
|
|
@@ -48,9 +46,8 @@ export class ShenoraBridge {
|
|
|
48
46
|
*/
|
|
49
47
|
invoke(module, type, options = {}) {
|
|
50
48
|
if (this.disposed) {
|
|
51
|
-
// Fail fast: the transport subscription is gone, so a response could never correlate
|
|
52
|
-
//
|
|
53
|
-
// configureBridge replaced the default are the typical way here).
|
|
49
|
+
// Fail fast: the transport subscription is gone, so a response could never correlate and the
|
|
50
|
+
// call would otherwise burn the full timeout.
|
|
54
51
|
return Promise.reject(new ShenoraError({
|
|
55
52
|
code: IpcErrorCodes.noTransport,
|
|
56
53
|
message: `Bridge disposed — ${module}.${type} cannot be sent.`,
|
|
@@ -74,18 +71,12 @@ export class ShenoraBridge {
|
|
|
74
71
|
catch (error) {
|
|
75
72
|
return Promise.reject(error);
|
|
76
73
|
}
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// none of the diagnostics the real path gives (P5.5 H2). Only a thenable needs racing; a plain
|
|
80
|
-
// value is already settled.
|
|
74
|
+
// An async fallback is raced against the timeout too, or one that never settles hangs the
|
|
75
|
+
// caller forever. Only a thenable needs racing; a plain value has already settled.
|
|
81
76
|
if (!isThenable(result))
|
|
82
77
|
return Promise.resolve(result);
|
|
83
|
-
// ⚠ The loser's timer
|
|
84
|
-
//
|
|
85
|
-
// live timer per call for the full timeout
|
|
86
|
-
// (30 s by default), each holding its closure. Harmless to a caller, because `race` has already
|
|
87
|
-
// settled and has a rejection handler attached either way, but it is the same "one site out of
|
|
88
|
-
// N" shape the rest of this file is careful about, and it keeps timers pending in a test run.
|
|
78
|
+
// ⚠ The loser's timer is cleared in `finally` — otherwise every call holds a live timer, and
|
|
79
|
+
// its closure, for the full timeout.
|
|
89
80
|
let timer;
|
|
90
81
|
return Promise.race([
|
|
91
82
|
Promise.resolve(result),
|
|
@@ -130,28 +121,17 @@ export class ShenoraBridge {
|
|
|
130
121
|
/**
|
|
131
122
|
* Send WITHOUT awaiting a reply, and return the request id.
|
|
132
123
|
*
|
|
133
|
-
* This is the default shape for a desktop shell
|
|
134
|
-
* `
|
|
135
|
-
*
|
|
136
|
-
* design, because the dispatch pipeline preserves the caller's synchronization context so facades
|
|
137
|
-
* can touch the window. Reserve `invoke` for calls that are quick AND safe on the UI thread — the
|
|
138
|
-
* window commands are the model — and post everything else, streaming results back as notifications.
|
|
124
|
+
* This is the default shape for a desktop shell and {@link invoke} is the special case (D23):
|
|
125
|
+
* reserve `invoke` for calls that are quick AND safe on the host's UI thread, and post everything
|
|
126
|
+
* else, streaming results back as notifications.
|
|
139
127
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
128
|
+
* A failed response is reported through `onPostError` (default `console.error`) rather than
|
|
129
|
+
* dropped. The id is remembered for that — see {@link ShenoraBridgeOptions.maxTrackedPosts}. No
|
|
130
|
+
* timer is set, so there is no deadline.
|
|
143
131
|
*
|
|
144
|
-
*
|
|
145
|
-
* `
|
|
146
|
-
*
|
|
147
|
-
* which caps the set drop-oldest so a host that never answers cannot grow it without bound. No TIMER is
|
|
148
|
-
* set, though, so there is no deadline and nothing to fire later.
|
|
149
|
-
*
|
|
150
|
-
* No transport (a plain browser tab) is a silent no-op, matching the fire-and-forget contract —
|
|
151
|
-
* unlike `invoke`, there is no caller waiting to be told. **A DISPOSED bridge is the same silent
|
|
152
|
-
* no-op**, and that one can surprise: `invoke` on a disposed bridge rejects with `NO_TRANSPORT`
|
|
153
|
-
* while `post` just returns the id, so a stale reference kept across a `configureBridge` swap looks
|
|
154
|
-
* like it is still sending. `isAvailable` is the check.
|
|
132
|
+
* ⚠ No transport (a plain browser tab) is a silent no-op, and **so is a DISPOSED bridge** — where
|
|
133
|
+
* `invoke` would reject with `NO_TRANSPORT`, this just returns the id, so a stale reference kept
|
|
134
|
+
* across a {@link configureBridge} swap looks like it is still sending. `isAvailable` is the check.
|
|
155
135
|
*/
|
|
156
136
|
post(module, type, options = {}) {
|
|
157
137
|
const request = {
|
|
@@ -164,8 +144,7 @@ export class ShenoraBridge {
|
|
|
164
144
|
};
|
|
165
145
|
if (this.disposed || !this.transport)
|
|
166
146
|
return request.id;
|
|
167
|
-
//
|
|
168
|
-
// cannot grow this without bound; an evicted id simply loses its error report.
|
|
147
|
+
// Remembered ONLY to report a failure, drop-oldest at the cap.
|
|
169
148
|
if (this.unawaited.size >= this.maxTrackedPosts) {
|
|
170
149
|
const oldest = this.unawaited.keys().next();
|
|
171
150
|
if (!oldest.done)
|
|
@@ -196,14 +175,10 @@ export class ShenoraBridge {
|
|
|
196
175
|
* also the host's cue to reset per-page state. No-transport is a silent no-op so browser dev
|
|
197
176
|
* doesn't error.
|
|
198
177
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* was acked. `DropZoneManager` now clears on DOCUMENT CHANGE instead, which cannot race the client.
|
|
202
|
-
* The general point still stands for any per-page state YOUR host resets here — a reset keyed on
|
|
203
|
-
* the handshake races anything the page sends before it, and in React that is structural rather
|
|
204
|
-
* than a mistake, because CHILD effects run before PARENT effects.
|
|
178
|
+
* ⚠ Any per-page state YOUR host resets on this handshake races whatever the page sent before it,
|
|
179
|
+
* and in React that is structural rather than bad luck: CHILD effects run before PARENT effects.
|
|
205
180
|
*
|
|
206
|
-
* The returned promise REJECTS on a failed handshake (disposed bridge, timeout). Handle it —
|
|
181
|
+
* ⚠ The returned promise REJECTS on a failed handshake (disposed bridge, timeout). Handle it —
|
|
207
182
|
* `void bridge.notifyReady()` turns that into an unhandled rejection, which in a WebView2 page is
|
|
208
183
|
* a silent console error.
|
|
209
184
|
*/
|
|
@@ -211,14 +186,9 @@ export class ShenoraBridge {
|
|
|
211
186
|
if (!this.transport)
|
|
212
187
|
return undefined;
|
|
213
188
|
// The host answers with what it IS and what it can do, so a page can render one tree on every
|
|
214
|
-
// shell instead of sniffing the platform. A host that says nothing
|
|
215
|
-
//
|
|
216
|
-
// desktop".
|
|
217
|
-
//
|
|
218
|
-
// undefined rather than null on purpose: JSON null means absent on this wire and the client
|
|
219
|
-
// convention is undefined (see .claude/knowledge/ipc-contracts.md). Returning null broke two
|
|
220
|
-
// existing tests that assert this resolves to undefined, which is the convention catching a
|
|
221
|
-
// deviation exactly where it should.
|
|
189
|
+
// shell instead of sniffing the platform. A host that says nothing leaves it UNDEFINED (never
|
|
190
|
+
// null — JSON null means absent on this wire) — and absent means "assume nothing", never
|
|
191
|
+
// "assume desktop".
|
|
222
192
|
const shell = await this.invoke(HANDSHAKE_MODULE, HANDSHAKE_TYPE, { payload });
|
|
223
193
|
this.shellInfo = shell && typeof shell === 'object' && typeof shell.name === 'string'
|
|
224
194
|
? { name: shell.name, capabilities: Array.isArray(shell.capabilities) ? shell.capabilities : [] }
|
|
@@ -227,8 +197,8 @@ export class ShenoraBridge {
|
|
|
227
197
|
}
|
|
228
198
|
/**
|
|
229
199
|
* What the host said it was during {@link notifyReady} — undefined before the handshake, or when
|
|
230
|
-
* the host advertised nothing. Cached so components can read it synchronously while rendering
|
|
231
|
-
*
|
|
200
|
+
* the host advertised nothing. Cached so components can read it synchronously while rendering; a
|
|
201
|
+
* capability learned after layout is a visible flash.
|
|
232
202
|
*/
|
|
233
203
|
get shell() {
|
|
234
204
|
return this.shellInfo;
|
|
@@ -244,8 +214,7 @@ export class ShenoraBridge {
|
|
|
244
214
|
entry.reject(new ShenoraError({ code: IpcErrorCodes.noTransport, message: 'Bridge disposed.' }));
|
|
245
215
|
this.pending.delete(id);
|
|
246
216
|
}
|
|
247
|
-
//
|
|
248
|
-
// holds no references (this is the instance `configureBridge` replaces).
|
|
217
|
+
// Pure bookkeeping with nothing to settle — dropped so a disposed bridge holds no references.
|
|
249
218
|
this.unawaited.clear();
|
|
250
219
|
}
|
|
251
220
|
onHostMessage(message) {
|
|
@@ -257,10 +226,9 @@ export class ShenoraBridge {
|
|
|
257
226
|
console.error('[shenora] ignored unparseable host message:', error);
|
|
258
227
|
return;
|
|
259
228
|
}
|
|
260
|
-
// A literal `null` is VALID JSON
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// yields undefined; null and only null did. Anything that isn't an object simply isn't ours.
|
|
229
|
+
// ⚠ A literal `null` is VALID JSON and survives the parse, and property access on it throws —
|
|
230
|
+
// out of a transport listener, i.e. an uncaught page error with no caller to catch it. Anything
|
|
231
|
+
// that is not an object simply is not ours.
|
|
264
232
|
if (parsed === null || typeof parsed !== 'object')
|
|
265
233
|
return;
|
|
266
234
|
const envelope = parsed;
|
|
@@ -270,10 +238,9 @@ export class ShenoraBridge {
|
|
|
270
238
|
return;
|
|
271
239
|
const entry = this.pending.get(response.id);
|
|
272
240
|
if (!entry) {
|
|
273
|
-
// No pending call. Either this answers a one-way `post
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// which stays ignored.
|
|
241
|
+
// No pending call. Either this answers a one-way `post`, whose FAILURE must be surfaced
|
|
242
|
+
// because there is no promise to reject, or it is a late/foreign response, which stays
|
|
243
|
+
// ignored.
|
|
277
244
|
const posted = this.unawaited.get(response.id);
|
|
278
245
|
if (posted) {
|
|
279
246
|
this.unawaited.delete(response.id);
|
package/dist/clipboard.d.ts
CHANGED
|
@@ -5,11 +5,9 @@ export declare const PNG_IMAGE = "image/png";
|
|
|
5
5
|
/** Media type for UTF-8 HTML, for a paste that keeps its formatting. */
|
|
6
6
|
export declare const HTML = "text/html";
|
|
7
7
|
/**
|
|
8
|
-
* One clipboard item and every representation it offers.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* lives in `formats`, keyed by media type — {@link PNG_IMAGE}, {@link HTML}, or your own
|
|
12
|
-
* `application/…` type, which the host carries verbatim so a paste can round-trip it losslessly.
|
|
8
|
+
* One clipboard item and every representation it offers. `text` and `files` are named because every
|
|
9
|
+
* platform has a first-class API for them; everything else lives in `formats`, keyed by media type —
|
|
10
|
+
* {@link PNG_IMAGE}, {@link HTML}, or your own `application/…` type, which the host carries verbatim.
|
|
13
11
|
*/
|
|
14
12
|
export interface ClipboardContent {
|
|
15
13
|
/** The plain-text representation. */
|
|
@@ -45,7 +43,7 @@ export declare class ClipboardAccess extends BaseModuleService<ClipboardRequests
|
|
|
45
43
|
constructor(bridge?: ShenoraBridge);
|
|
46
44
|
/**
|
|
47
45
|
* Everything the clipboard is offering — **no user gesture, no permission prompt, no focus
|
|
48
|
-
* requirement**,
|
|
46
|
+
* requirement**, unlike `navigator.clipboard.read()`.
|
|
49
47
|
*/
|
|
50
48
|
read(): Promise<ClipboardContent>;
|
|
51
49
|
/**
|
|
@@ -62,10 +60,7 @@ export declare class ClipboardAccess extends BaseModuleService<ClipboardRequests
|
|
|
62
60
|
export interface ClipboardHandle {
|
|
63
61
|
/** The typed client. Stable across renders. */
|
|
64
62
|
clipboard: ClipboardAccess;
|
|
65
|
-
/**
|
|
66
|
-
* This shell can put a FILE LIST on the clipboard — desktop only. Decide what to RENDER with it; a
|
|
67
|
-
* refused call rejects, which is the honest answer to a question that should not have been asked.
|
|
68
|
-
*/
|
|
63
|
+
/** This shell can put a FILE LIST on the clipboard — desktop only. Use it to decide what to RENDER. */
|
|
69
64
|
canCopyFiles: boolean;
|
|
70
65
|
}
|
|
71
66
|
/**
|
package/dist/clipboard.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The page's half of the host's `SHENORA.CLIPBOARD` module — the parts of the native clipboard the web
|
|
3
|
-
* platform withholds.
|
|
3
|
+
* platform withholds. Mirrors `Shenora.Modules.Clipboard.ClipboardModule`, pinned by `WireMirrorTests`.
|
|
4
4
|
*
|
|
5
|
-
* 🔴 **Reach for `navigator.clipboard` first.**
|
|
6
|
-
*
|
|
7
|
-
* expects. Two things it cannot do are why this module exists —
|
|
5
|
+
* 🔴 **Reach for `navigator.clipboard` first.** A gesture-driven "copy this text" or "copy this image"
|
|
6
|
+
* already works and needs no host round trip. Two things it cannot do are why this module exists —
|
|
8
7
|
*
|
|
9
8
|
* 1. **Files.** No web API puts a file list on the clipboard, so the user cannot paste into Explorer,
|
|
10
9
|
* Finder or a file manager. There is no polyfill.
|
|
11
10
|
* 2. **Access with no user gesture or focus.** `navigator.clipboard.read()` needs transient activation,
|
|
12
11
|
* document focus and a permission. A host has none of those constraints.
|
|
13
12
|
*
|
|
14
|
-
* ⚠ **
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* — writing its text half with `navigator.clipboard` and the files here leaves only the files, silently.
|
|
18
|
-
*
|
|
19
|
-
* Mirrors `Shenora.Modules.Clipboard.ClipboardModule`. The wire names are pinned against the host's own
|
|
20
|
-
* constants by `WireMirrorTests`, not by care.
|
|
13
|
+
* ⚠ **The choice is per-COPY, not per-format**: a clipboard set is atomic, one item, last writer wins.
|
|
14
|
+
* An item that includes files must be written entirely through {@link ClipboardAccess.write} — writing
|
|
15
|
+
* its text half with `navigator.clipboard` and the files here leaves only the files, silently.
|
|
21
16
|
*/
|
|
22
17
|
import { useMemo } from 'react';
|
|
23
18
|
import { useShellInfo } from './hooks.js';
|
|
@@ -28,11 +23,10 @@ export const PNG_IMAGE = 'image/png';
|
|
|
28
23
|
/** Media type for UTF-8 HTML, for a paste that keeps its formatting. */
|
|
29
24
|
export const HTML = 'text/html';
|
|
30
25
|
/**
|
|
31
|
-
* Base64 without a `Buffer
|
|
26
|
+
* Base64 without a `Buffer`.
|
|
32
27
|
*
|
|
33
|
-
* ⚠
|
|
34
|
-
*
|
|
35
|
-
* chunked loop has no such ceiling.
|
|
28
|
+
* ⚠ Never `btoa(String.fromCharCode(...bytes))`: the spread overflows the argument list and throws
|
|
29
|
+
* `RangeError` on anything past roughly a small screenshot. A chunked loop has no such ceiling.
|
|
36
30
|
*/
|
|
37
31
|
function toBase64(bytes) {
|
|
38
32
|
let binary = '';
|
|
@@ -61,7 +55,7 @@ export class ClipboardAccess extends BaseModuleService {
|
|
|
61
55
|
}
|
|
62
56
|
/**
|
|
63
57
|
* Everything the clipboard is offering — **no user gesture, no permission prompt, no focus
|
|
64
|
-
* requirement**,
|
|
58
|
+
* requirement**, unlike `navigator.clipboard.read()`.
|
|
65
59
|
*/
|
|
66
60
|
async read() {
|
|
67
61
|
const wire = await this.send('READ');
|
|
@@ -115,8 +109,7 @@ export class ClipboardAccess extends BaseModuleService {
|
|
|
115
109
|
*/
|
|
116
110
|
export function useClipboard(clipboard) {
|
|
117
111
|
const shell = useShellInfo();
|
|
118
|
-
//
|
|
119
|
-
// still make it useless as an effect dependency — the same reason useFileDialogs caches one.
|
|
112
|
+
// Cached so it is usable as an effect dependency, not because the service is expensive.
|
|
120
113
|
const client = useMemo(() => clipboard ?? new ClipboardAccess(), [clipboard]);
|
|
121
114
|
const capabilities = shell?.capabilities;
|
|
122
115
|
return useMemo(() => ({
|
package/dist/devInterceptor.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev-only IPC + event-hub interceptor
|
|
3
|
-
*
|
|
2
|
+
* Dev-only IPC + event-hub interceptor. ⚠ NEVER ship it in prod — gate the single call site with
|
|
3
|
+
* `import.meta.env.DEV`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* request/response/event into ring buffers, and (2) expose a window global so a CDP eval can
|
|
5
|
+
* For driving a page over CDP, where native dialogs and event-driven flows cannot be exercised by
|
|
6
|
+
* clicking. It wraps the bridge's `invoke` and the event bus's `emit` to (1) record + console.debug
|
|
7
|
+
* every request/response/event into ring buffers, and (2) expose a window global so a CDP eval can
|
|
9
8
|
* invoke ANY IPC directly and await events:
|
|
10
9
|
*
|
|
11
10
|
* window.__shenora.call('NOTES', 'ADD', { title: 'x' }) // drive an IPC, bypass the UI
|
|
@@ -47,11 +46,8 @@ export interface DevInterceptorOptions {
|
|
|
47
46
|
* Install the interceptor. Idempotent across HMR and StrictMode's double-invoke.
|
|
48
47
|
*
|
|
49
48
|
* 🔴 **Idempotency is keyed on WHICH bridge and bus were wrapped, not on "something is installed".**
|
|
50
|
-
* The wrapping mutates a specific `ShenoraBridge` INSTANCE,
|
|
51
|
-
* window global
|
|
52
|
-
*
|
|
53
|
-
* wrapping the live one. The tool then looked installed and recorded NOTHING, while
|
|
54
|
-
* `window.__shenora.call` drove a disposed bridge. A dev tool that fails silently is worse than one that
|
|
55
|
-
* is absent, because its silence reads as "no traffic".
|
|
49
|
+
* The wrapping mutates a specific `ShenoraBridge` INSTANCE, so a guard that only asks whether the
|
|
50
|
+
* window global exists leaves the interceptor pointing at the bridge `configureBridge()` disposed:
|
|
51
|
+
* installed-looking, recording NOTHING, and its silence reads as "no traffic".
|
|
56
52
|
*/
|
|
57
53
|
export declare function installDevInterceptor(options?: DevInterceptorOptions): void;
|
package/dist/devInterceptor.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dev-only IPC + event-hub interceptor
|
|
3
|
-
*
|
|
2
|
+
* Dev-only IPC + event-hub interceptor. ⚠ NEVER ship it in prod — gate the single call site with
|
|
3
|
+
* `import.meta.env.DEV`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* request/response/event into ring buffers, and (2) expose a window global so a CDP eval can
|
|
5
|
+
* For driving a page over CDP, where native dialogs and event-driven flows cannot be exercised by
|
|
6
|
+
* clicking. It wraps the bridge's `invoke` and the event bus's `emit` to (1) record + console.debug
|
|
7
|
+
* every request/response/event into ring buffers, and (2) expose a window global so a CDP eval can
|
|
9
8
|
* invoke ANY IPC directly and await events:
|
|
10
9
|
*
|
|
11
10
|
* window.__shenora.call('NOTES', 'ADD', { title: 'x' }) // drive an IPC, bypass the UI
|
|
@@ -18,12 +17,9 @@ import { eventBus as defaultEventBus } from './eventBus.js';
|
|
|
18
17
|
* Install the interceptor. Idempotent across HMR and StrictMode's double-invoke.
|
|
19
18
|
*
|
|
20
19
|
* 🔴 **Idempotency is keyed on WHICH bridge and bus were wrapped, not on "something is installed".**
|
|
21
|
-
* The wrapping mutates a specific `ShenoraBridge` INSTANCE,
|
|
22
|
-
* window global
|
|
23
|
-
*
|
|
24
|
-
* wrapping the live one. The tool then looked installed and recorded NOTHING, while
|
|
25
|
-
* `window.__shenora.call` drove a disposed bridge. A dev tool that fails silently is worse than one that
|
|
26
|
-
* is absent, because its silence reads as "no traffic".
|
|
20
|
+
* The wrapping mutates a specific `ShenoraBridge` INSTANCE, so a guard that only asks whether the
|
|
21
|
+
* window global exists leaves the interceptor pointing at the bridge `configureBridge()` disposed:
|
|
22
|
+
* installed-looking, recording NOTHING, and its silence reads as "no traffic".
|
|
27
23
|
*/
|
|
28
24
|
export function installDevInterceptor(options = {}) {
|
|
29
25
|
if (typeof window === 'undefined')
|
|
@@ -31,11 +27,10 @@ export function installDevInterceptor(options = {}) {
|
|
|
31
27
|
const globalName = options.globalName ?? '__shenora';
|
|
32
28
|
const w = window;
|
|
33
29
|
const ringSize = options.ringSize ?? 300;
|
|
34
|
-
// Resolved BEFORE the guard, because the guard's question is
|
|
30
|
+
// Resolved BEFORE the guard, because the guard's question is about these two objects.
|
|
35
31
|
const bridge = options.bridge ?? getBridge();
|
|
36
32
|
const bus = options.bus ?? defaultEventBus;
|
|
37
|
-
// Same pair = the HMR/StrictMode case
|
|
38
|
-
// stack a second recorder on the first.
|
|
33
|
+
// Same pair = the HMR/StrictMode case; wrapping twice doubles every log line.
|
|
39
34
|
const installed = w[globalName];
|
|
40
35
|
if (installed && installed.bridge === bridge && installed.eventBus === bus)
|
|
41
36
|
return;
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { IpcError } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* The rejection type for every failed bridge call — the client mirror of the host's
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* transport) reject through this same shape with the client-reserved codes.
|
|
3
|
+
* The rejection type for every failed bridge call — the client mirror of the host's `ShenoraException`.
|
|
4
|
+
* Carries the structured code + parameters so callers translate `errors.{code}` instead of matching
|
|
5
|
+
* message strings. Client-side failures (timeout, missing transport) reject through this same shape
|
|
6
|
+
* with the client-reserved codes.
|
|
8
7
|
*/
|
|
9
8
|
export declare class ShenoraError extends Error {
|
|
10
9
|
/** Error code / i18n key (e.g. `"IMPORT_FAILED"`, `"TIMEOUT"`). */
|
package/dist/errors.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The rejection type for every failed bridge call — the client mirror of the host's
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* transport) reject through this same shape with the client-reserved codes.
|
|
2
|
+
* The rejection type for every failed bridge call — the client mirror of the host's `ShenoraException`.
|
|
3
|
+
* Carries the structured code + parameters so callers translate `errors.{code}` instead of matching
|
|
4
|
+
* message strings. Client-side failures (timeout, missing transport) reject through this same shape
|
|
5
|
+
* with the client-reserved codes.
|
|
7
6
|
*/
|
|
8
7
|
export class ShenoraError extends Error {
|
|
9
8
|
constructor(error) {
|