@syncular/client 0.30.4 → 0.30.6
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/errors.d.ts +2 -1
- package/dist/errors.js +3 -1
- package/dist/http.js +37 -20
- package/dist/multi-tab.d.ts +1 -0
- package/dist/multi-tab.js +9 -1
- package/dist/reactive-store.js +10 -3
- package/dist/worker-entry.js +1 -0
- package/dist/worker-host.js +1 -1
- package/dist/worker-protocol.d.ts +1 -0
- package/package.json +3 -3
- package/src/errors.ts +8 -1
- package/src/http.ts +53 -22
- package/src/multi-tab.ts +13 -1
- package/src/reactive-store.ts +13 -3
- package/src/worker-entry.ts +1 -0
- package/src/worker-host.ts +1 -0
- package/src/worker-protocol.ts +1 -0
package/dist/errors.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ export declare class ClientSyncError extends Error {
|
|
|
20
20
|
readonly name: string;
|
|
21
21
|
readonly code: string;
|
|
22
22
|
readonly retryable: boolean;
|
|
23
|
-
|
|
23
|
+
readonly details: Readonly<Record<string, unknown>> | undefined;
|
|
24
|
+
constructor(code: string, message: string, retryable?: boolean, details?: Readonly<Record<string, unknown>>);
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* SPEC §7.5: the latest sync attempt failed while a live query still waited
|
package/dist/errors.js
CHANGED
|
@@ -19,10 +19,12 @@ export class ClientSyncError extends Error {
|
|
|
19
19
|
name = 'ClientSyncError';
|
|
20
20
|
code;
|
|
21
21
|
retryable;
|
|
22
|
-
|
|
22
|
+
details;
|
|
23
|
+
constructor(code, message, retryable = false, details) {
|
|
23
24
|
super(message);
|
|
24
25
|
this.code = code;
|
|
25
26
|
this.retryable = retryable;
|
|
27
|
+
this.details = details;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
/**
|
package/dist/http.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SSP2_CONTENT_TYPE } from './content-type.js';
|
|
2
2
|
import { ClientSyncError } from './errors.js';
|
|
3
|
-
async function throwHttpError(response) {
|
|
3
|
+
async function throwHttpError(response, details) {
|
|
4
4
|
let code = 'sync.transport_failed';
|
|
5
|
-
let message =
|
|
5
|
+
let message = 'HTTP request failed';
|
|
6
6
|
let retryable = response.status >= 500 || response.status === 429;
|
|
7
7
|
try {
|
|
8
8
|
const body = (await response.json());
|
|
@@ -16,7 +16,10 @@ async function throwHttpError(response) {
|
|
|
16
16
|
catch {
|
|
17
17
|
// non-JSON error body — keep the HTTP-status defaults
|
|
18
18
|
}
|
|
19
|
-
throw new ClientSyncError(code, message, retryable
|
|
19
|
+
throw new ClientSyncError(code, message, retryable, {
|
|
20
|
+
...details,
|
|
21
|
+
httpStatus: response.status,
|
|
22
|
+
});
|
|
20
23
|
}
|
|
21
24
|
function hostHeaders(options) {
|
|
22
25
|
const headers = options?.headers;
|
|
@@ -136,25 +139,39 @@ async function readSegmentBody(response, onProgress) {
|
|
|
136
139
|
}
|
|
137
140
|
export function httpSegmentDownloader(segmentsBaseUrl, options) {
|
|
138
141
|
const doFetch = options?.fetch ?? fetch;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
142
|
+
const download = async (url, init, onProgress, signed = false) => {
|
|
143
|
+
// A capability's query/fragment and URL credentials never enter details.
|
|
144
|
+
const path = URL.canParse(url)
|
|
145
|
+
? new URL(url).pathname
|
|
146
|
+
: url.split(/[?#]/)[0];
|
|
147
|
+
let response;
|
|
148
|
+
try {
|
|
149
|
+
response = await doFetch(url, init);
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
if (!signed)
|
|
152
|
+
await throwHttpError(response, { path });
|
|
153
|
+
throw new ClientSyncError('sync.transport_failed', 'signed segment request failed; invalidate the descriptor and re-pull', true, { path, httpStatus: response.status });
|
|
154
|
+
}
|
|
155
|
+
return await readSegmentBody(response, onProgress);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error instanceof ClientSyncError)
|
|
159
|
+
throw error;
|
|
160
|
+
throw new ClientSyncError('sync.transport_failed', 'segment transfer failed', true, {
|
|
161
|
+
path,
|
|
162
|
+
...(response !== undefined ? { httpStatus: response.status } : {}),
|
|
163
|
+
causeMessage: error instanceof Error ? error.message : String(error),
|
|
164
|
+
});
|
|
155
165
|
}
|
|
156
|
-
return readSegmentBody(response, onProgress);
|
|
157
166
|
};
|
|
167
|
+
const direct = (request) => download(`${segmentsBaseUrl}/${encodeURIComponent(request.segmentId)}`, {
|
|
168
|
+
headers: {
|
|
169
|
+
'X-Syncular-Scopes': request.requestedScopesJson,
|
|
170
|
+
...hostHeaders(options),
|
|
171
|
+
},
|
|
172
|
+
}, request.onProgress);
|
|
173
|
+
// Deliberately headerless: the URL is the bearer grant (§5.4).
|
|
174
|
+
const fetchUrl = (url, onProgress) => download(url, undefined, onProgress, true);
|
|
158
175
|
return Object.assign(direct, { fetchUrl });
|
|
159
176
|
}
|
|
160
177
|
/**
|
package/dist/multi-tab.d.ts
CHANGED
package/dist/multi-tab.js
CHANGED
|
@@ -187,7 +187,10 @@ export class LeaderBridge {
|
|
|
187
187
|
});
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
|
+
// A call can outlive close(); a closed BroadcastChannel throws on post.
|
|
190
191
|
this.#invoke(method, args).then((value) => {
|
|
192
|
+
if (this.#closed)
|
|
193
|
+
return;
|
|
191
194
|
this.#channel.postMessage({
|
|
192
195
|
t: 'res',
|
|
193
196
|
epoch: this.#epoch,
|
|
@@ -196,11 +199,16 @@ export class LeaderBridge {
|
|
|
196
199
|
value,
|
|
197
200
|
});
|
|
198
201
|
}, (error) => {
|
|
202
|
+
if (this.#closed)
|
|
203
|
+
return;
|
|
199
204
|
const shape = error instanceof ClientSyncError
|
|
200
205
|
? {
|
|
201
206
|
code: error.code,
|
|
202
207
|
message: error.message,
|
|
203
208
|
retryable: error.retryable,
|
|
209
|
+
...(error.details !== undefined
|
|
210
|
+
? { details: error.details }
|
|
211
|
+
: {}),
|
|
204
212
|
}
|
|
205
213
|
: {
|
|
206
214
|
code: WORKER_FAILED_CODE,
|
|
@@ -465,7 +473,7 @@ export class FollowerLink {
|
|
|
465
473
|
}
|
|
466
474
|
else {
|
|
467
475
|
const err = message.error;
|
|
468
|
-
inflight.reject(new ClientSyncError(err?.code ?? WORKER_FAILED_CODE, err?.message ?? 'follower request failed', err?.retryable ?? false));
|
|
476
|
+
inflight.reject(new ClientSyncError(err?.code ?? WORKER_FAILED_CODE, err?.message ?? 'follower request failed', err?.retryable ?? false, err?.details));
|
|
469
477
|
}
|
|
470
478
|
return;
|
|
471
479
|
}
|
package/dist/reactive-store.js
CHANGED
|
@@ -246,6 +246,13 @@ class ObservationCache {
|
|
|
246
246
|
}
|
|
247
247
|
const EMPTY_ROWS = Object.freeze([]);
|
|
248
248
|
const unreadQuerySnapshots = new Map();
|
|
249
|
+
/** Schema floors stop network transfer; the retained local replica remains readable.
|
|
250
|
+
* Leadership stops still refuse local access through the unavailable owner. */
|
|
251
|
+
function localReadsBlocked(availability) {
|
|
252
|
+
return (availability.state === 'blocked' &&
|
|
253
|
+
(availability.reason === 'leader-unreachable' ||
|
|
254
|
+
availability.reason === 'leader-incompatible'));
|
|
255
|
+
}
|
|
249
256
|
/** One frozen snapshot per phase and availability for entries that have no
|
|
250
257
|
* successful read, so switching between unread entries keeps the snapshot and
|
|
251
258
|
* `rows` identity. */
|
|
@@ -458,7 +465,7 @@ class QueryEntry {
|
|
|
458
465
|
}
|
|
459
466
|
#onAvailabilityChange() {
|
|
460
467
|
const availability = this.store.availabilitySnapshot();
|
|
461
|
-
if (availability
|
|
468
|
+
if (localReadsBlocked(availability)) {
|
|
462
469
|
this.#publish({
|
|
463
470
|
...this.#state,
|
|
464
471
|
phase: 'blocked',
|
|
@@ -512,7 +519,7 @@ class QueryEntry {
|
|
|
512
519
|
try {
|
|
513
520
|
do {
|
|
514
521
|
this.#requested = false;
|
|
515
|
-
if (this.store.availabilitySnapshot()
|
|
522
|
+
if (localReadsBlocked(this.store.availabilitySnapshot()))
|
|
516
523
|
break;
|
|
517
524
|
if (generation !== this.#generation || this.#listeners.size === 0)
|
|
518
525
|
return;
|
|
@@ -537,7 +544,7 @@ class QueryEntry {
|
|
|
537
544
|
if (this.#claimError !== undefined)
|
|
538
545
|
throw this.#claimError;
|
|
539
546
|
const availability = this.store.availabilitySnapshot();
|
|
540
|
-
if (availability
|
|
547
|
+
if (localReadsBlocked(availability)) {
|
|
541
548
|
this.#publish({
|
|
542
549
|
...this.#state,
|
|
543
550
|
phase: 'blocked',
|
package/dist/worker-entry.js
CHANGED
package/dist/worker-host.js
CHANGED
|
@@ -466,7 +466,7 @@ async function startWorkerCore(options) {
|
|
|
466
466
|
case 'error': {
|
|
467
467
|
const entry = pending.get(message.id);
|
|
468
468
|
pending.delete(message.id);
|
|
469
|
-
entry?.reject(new ClientSyncError(message.error.code, message.error.message, message.error.retryable));
|
|
469
|
+
entry?.reject(new ClientSyncError(message.error.code, message.error.message, message.error.retryable, message.error.details));
|
|
470
470
|
break;
|
|
471
471
|
}
|
|
472
472
|
case 'event':
|
|
@@ -175,6 +175,7 @@ export type MainToWorkerMessage = {
|
|
|
175
175
|
readonly config: WorkerInitConfig;
|
|
176
176
|
} | WorkerCallMessage;
|
|
177
177
|
export interface WorkerErrorShape {
|
|
178
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
178
179
|
readonly code: string;
|
|
179
180
|
readonly message: string;
|
|
180
181
|
readonly retryable: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.6",
|
|
4
4
|
"description": "Syncular TypeScript client core — offline-first sync over SQLite (WASM/OPFS, Bun, Node)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -89,10 +89,10 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
92
|
-
"@syncular/core": "0.30.
|
|
92
|
+
"@syncular/core": "0.30.6"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@syncular/server": "0.30.
|
|
95
|
+
"@syncular/server": "0.30.6",
|
|
96
96
|
"playwright": "1.63.0"
|
|
97
97
|
}
|
|
98
98
|
}
|
package/src/errors.ts
CHANGED
|
@@ -27,11 +27,18 @@ export class ClientSyncError extends Error {
|
|
|
27
27
|
override readonly name: string = 'ClientSyncError';
|
|
28
28
|
readonly code: string;
|
|
29
29
|
readonly retryable: boolean;
|
|
30
|
+
readonly details: Readonly<Record<string, unknown>> | undefined;
|
|
30
31
|
|
|
31
|
-
constructor(
|
|
32
|
+
constructor(
|
|
33
|
+
code: string,
|
|
34
|
+
message: string,
|
|
35
|
+
retryable = false,
|
|
36
|
+
details?: Readonly<Record<string, unknown>>,
|
|
37
|
+
) {
|
|
32
38
|
super(message);
|
|
33
39
|
this.code = code;
|
|
34
40
|
this.retryable = retryable;
|
|
41
|
+
this.details = details;
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
|
package/src/http.ts
CHANGED
|
@@ -15,9 +15,12 @@ import type {
|
|
|
15
15
|
SyncTransport,
|
|
16
16
|
} from './transport';
|
|
17
17
|
|
|
18
|
-
async function throwHttpError(
|
|
18
|
+
async function throwHttpError(
|
|
19
|
+
response: Response,
|
|
20
|
+
details?: Readonly<Record<string, unknown>>,
|
|
21
|
+
): Promise<never> {
|
|
19
22
|
let code = 'sync.transport_failed';
|
|
20
|
-
let message =
|
|
23
|
+
let message = 'HTTP request failed';
|
|
21
24
|
let retryable = response.status >= 500 || response.status === 429;
|
|
22
25
|
try {
|
|
23
26
|
const body = (await response.json()) as {
|
|
@@ -31,7 +34,10 @@ async function throwHttpError(response: Response): Promise<never> {
|
|
|
31
34
|
} catch {
|
|
32
35
|
// non-JSON error body — keep the HTTP-status defaults
|
|
33
36
|
}
|
|
34
|
-
throw new ClientSyncError(code, message, retryable
|
|
37
|
+
throw new ClientSyncError(code, message, retryable, {
|
|
38
|
+
...details,
|
|
39
|
+
httpStatus: response.status,
|
|
40
|
+
});
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export interface HttpTransportOptions {
|
|
@@ -192,12 +198,49 @@ export function httpSegmentDownloader(
|
|
|
192
198
|
options?: HttpTransportOptions,
|
|
193
199
|
): SegmentDownloader {
|
|
194
200
|
const doFetch = options?.fetch ?? fetch;
|
|
195
|
-
const
|
|
201
|
+
const download = async (
|
|
202
|
+
url: string,
|
|
203
|
+
init?: RequestInit,
|
|
204
|
+
onProgress?: (bytesReceived: number) => void,
|
|
205
|
+
signed = false,
|
|
206
|
+
) => {
|
|
207
|
+
// A capability's query/fragment and URL credentials never enter details.
|
|
208
|
+
const path = URL.canParse(url)
|
|
209
|
+
? new URL(url).pathname
|
|
210
|
+
: url.split(/[?#]/)[0];
|
|
211
|
+
let response: Response | undefined;
|
|
212
|
+
try {
|
|
213
|
+
response = await doFetch(url, init);
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
if (!signed) await throwHttpError(response, { path });
|
|
216
|
+
throw new ClientSyncError(
|
|
217
|
+
'sync.transport_failed',
|
|
218
|
+
'signed segment request failed; invalidate the descriptor and re-pull',
|
|
219
|
+
true,
|
|
220
|
+
{ path, httpStatus: response.status },
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return await readSegmentBody(response, onProgress);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof ClientSyncError) throw error;
|
|
226
|
+
throw new ClientSyncError(
|
|
227
|
+
'sync.transport_failed',
|
|
228
|
+
'segment transfer failed',
|
|
229
|
+
true,
|
|
230
|
+
{
|
|
231
|
+
path,
|
|
232
|
+
...(response !== undefined ? { httpStatus: response.status } : {}),
|
|
233
|
+
causeMessage: error instanceof Error ? error.message : String(error),
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const direct = (request: {
|
|
196
239
|
readonly segmentId: string;
|
|
197
240
|
readonly requestedScopesJson: string;
|
|
198
241
|
readonly onProgress?: (bytesReceived: number) => void;
|
|
199
|
-
}) =>
|
|
200
|
-
|
|
242
|
+
}) =>
|
|
243
|
+
download(
|
|
201
244
|
`${segmentsBaseUrl}/${encodeURIComponent(request.segmentId)}`,
|
|
202
245
|
{
|
|
203
246
|
headers: {
|
|
@@ -205,25 +248,13 @@ export function httpSegmentDownloader(
|
|
|
205
248
|
...hostHeaders(options),
|
|
206
249
|
},
|
|
207
250
|
},
|
|
251
|
+
request.onProgress,
|
|
208
252
|
);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
};
|
|
212
|
-
const fetchUrl = async (
|
|
253
|
+
// Deliberately headerless: the URL is the bearer grant (§5.4).
|
|
254
|
+
const fetchUrl = (
|
|
213
255
|
url: string,
|
|
214
256
|
onProgress?: (bytesReceived: number) => void,
|
|
215
|
-
) =>
|
|
216
|
-
// Deliberately headerless: the URL is the bearer grant (§5.4).
|
|
217
|
-
const response = await doFetch(url);
|
|
218
|
-
if (!response.ok) {
|
|
219
|
-
throw new ClientSyncError(
|
|
220
|
-
'sync.transport_failed',
|
|
221
|
-
`signed-URL fetch failed with HTTP ${response.status} (§5.4: descriptor invalidated, re-pull to recover)`,
|
|
222
|
-
true,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
return readSegmentBody(response, onProgress);
|
|
226
|
-
};
|
|
257
|
+
) => download(url, undefined, onProgress, true);
|
|
227
258
|
return Object.assign(direct, { fetchUrl });
|
|
228
259
|
}
|
|
229
260
|
|
package/src/multi-tab.ts
CHANGED
|
@@ -171,7 +171,12 @@ interface ResMessage {
|
|
|
171
171
|
readonly reqId: number;
|
|
172
172
|
readonly ok: boolean;
|
|
173
173
|
readonly value?: unknown;
|
|
174
|
-
readonly error?: {
|
|
174
|
+
readonly error?: {
|
|
175
|
+
code: string;
|
|
176
|
+
message: string;
|
|
177
|
+
retryable: boolean;
|
|
178
|
+
details?: Readonly<Record<string, unknown>>;
|
|
179
|
+
};
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
interface EventMessage {
|
|
@@ -316,8 +321,10 @@ export class LeaderBridge {
|
|
|
316
321
|
});
|
|
317
322
|
return;
|
|
318
323
|
}
|
|
324
|
+
// A call can outlive close(); a closed BroadcastChannel throws on post.
|
|
319
325
|
this.#invoke(method, args).then(
|
|
320
326
|
(value) => {
|
|
327
|
+
if (this.#closed) return;
|
|
321
328
|
this.#channel.postMessage({
|
|
322
329
|
t: 'res',
|
|
323
330
|
epoch: this.#epoch,
|
|
@@ -327,12 +334,16 @@ export class LeaderBridge {
|
|
|
327
334
|
});
|
|
328
335
|
},
|
|
329
336
|
(error: unknown) => {
|
|
337
|
+
if (this.#closed) return;
|
|
330
338
|
const shape =
|
|
331
339
|
error instanceof ClientSyncError
|
|
332
340
|
? {
|
|
333
341
|
code: error.code,
|
|
334
342
|
message: error.message,
|
|
335
343
|
retryable: error.retryable,
|
|
344
|
+
...(error.details !== undefined
|
|
345
|
+
? { details: error.details }
|
|
346
|
+
: {}),
|
|
336
347
|
}
|
|
337
348
|
: {
|
|
338
349
|
code: WORKER_FAILED_CODE,
|
|
@@ -679,6 +690,7 @@ export class FollowerLink {
|
|
|
679
690
|
err?.code ?? WORKER_FAILED_CODE,
|
|
680
691
|
err?.message ?? 'follower request failed',
|
|
681
692
|
err?.retryable ?? false,
|
|
693
|
+
err?.details,
|
|
682
694
|
),
|
|
683
695
|
);
|
|
684
696
|
}
|
package/src/reactive-store.ts
CHANGED
|
@@ -371,6 +371,16 @@ class ObservationCache {
|
|
|
371
371
|
const EMPTY_ROWS: readonly never[] = Object.freeze([]);
|
|
372
372
|
const unreadQuerySnapshots = new Map<string, LiveQueryResult<never>>();
|
|
373
373
|
|
|
374
|
+
/** Schema floors stop network transfer; the retained local replica remains readable.
|
|
375
|
+
* Leadership stops still refuse local access through the unavailable owner. */
|
|
376
|
+
function localReadsBlocked(availability: SyncAvailability): boolean {
|
|
377
|
+
return (
|
|
378
|
+
availability.state === 'blocked' &&
|
|
379
|
+
(availability.reason === 'leader-unreachable' ||
|
|
380
|
+
availability.reason === 'leader-incompatible')
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
374
384
|
/** One frozen snapshot per phase and availability for entries that have no
|
|
375
385
|
* successful read, so switching between unread entries keeps the snapshot and
|
|
376
386
|
* `rows` identity. */
|
|
@@ -598,7 +608,7 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
598
608
|
|
|
599
609
|
#onAvailabilityChange(): void {
|
|
600
610
|
const availability = this.store.availabilitySnapshot();
|
|
601
|
-
if (availability
|
|
611
|
+
if (localReadsBlocked(availability)) {
|
|
602
612
|
this.#publish({
|
|
603
613
|
...this.#state,
|
|
604
614
|
phase: 'blocked',
|
|
@@ -654,7 +664,7 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
654
664
|
try {
|
|
655
665
|
do {
|
|
656
666
|
this.#requested = false;
|
|
657
|
-
if (this.store.availabilitySnapshot()
|
|
667
|
+
if (localReadsBlocked(this.store.availabilitySnapshot())) break;
|
|
658
668
|
|
|
659
669
|
if (generation !== this.#generation || this.#listeners.size === 0)
|
|
660
670
|
return;
|
|
@@ -681,7 +691,7 @@ class QueryEntry<Row> implements ExternalStoreEntry<LiveQueryResult<Row>> {
|
|
|
681
691
|
return;
|
|
682
692
|
if (this.#claimError !== undefined) throw this.#claimError;
|
|
683
693
|
const availability = this.store.availabilitySnapshot();
|
|
684
|
-
if (availability
|
|
694
|
+
if (localReadsBlocked(availability)) {
|
|
685
695
|
this.#publish({
|
|
686
696
|
...this.#state,
|
|
687
697
|
phase: 'blocked',
|
package/src/worker-entry.ts
CHANGED
package/src/worker-host.ts
CHANGED
package/src/worker-protocol.ts
CHANGED