@marimo-team/marimo-export 0.0.0 → 0.0.1

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.
@@ -0,0 +1,703 @@
1
+ import { _ as portableJsonValue, c as isNotebookExportError, d as isJsonNumber, f as isJsonString, g as portableJsonObject, h as isStringValue, m as isRecordValue, p as isPropertyOwner } from "../loader-CDfnJ5Yb.mjs";
2
+ import { i as parsePortableJson } from "../schema-SJ_aIgcO.mjs";
3
+ import { n as normalizeBase, t as openExport } from "../export-6118xT3K.mjs";
4
+ //#region src/prepared/cancellation.ts
5
+ const preparedAbortReason = (reason, message = "Prepared export operation aborted") => {
6
+ if (reason instanceof Error || reason instanceof DOMException) return reason;
7
+ return new DOMException(message, "AbortError");
8
+ };
9
+ const isPreparedAbort = (value) => value instanceof DOMException && value.name === "AbortError" || value instanceof Error && value.name === "AbortError" || isNotebookExportError(value) && value.code === "abort";
10
+ const throwIfPreparedAborted = (signal, message) => {
11
+ if (signal?.aborted) throw preparedAbortReason(signal.reason, message);
12
+ };
13
+ const linkPreparedAbort = (controller, signal) => {
14
+ const abort = () => controller.abort(preparedAbortReason(signal.reason));
15
+ signal.addEventListener("abort", abort, { once: true });
16
+ if (signal.aborted) abort();
17
+ return () => signal.removeEventListener("abort", abort);
18
+ };
19
+ const disposePreparedOnAbort = (signal, lifecycle, dispose) => {
20
+ const disposeAfterAbort = () => {
21
+ dispose().catch(() => {});
22
+ };
23
+ const abort = () => {
24
+ lifecycle.abort(preparedAbortReason(signal.reason));
25
+ disposeAfterAbort();
26
+ };
27
+ signal.addEventListener("abort", abort, { once: true });
28
+ if (signal.aborted) {
29
+ lifecycle.abort(preparedAbortReason(signal.reason));
30
+ queueMicrotask(disposeAfterAbort);
31
+ }
32
+ return () => signal.removeEventListener("abort", abort);
33
+ };
34
+ //#endregion
35
+ //#region src/prepared/errors.ts
36
+ const PREPARED_EXPORT_ERROR_BRAND = Symbol.for("@marimo-team/marimo-export.PreparedExportError.v1");
37
+ const PREPARED_EXPORT_ERROR_CODE_SET = /* @__PURE__ */ new Set([
38
+ "manifest_invalid",
39
+ "manifest_read_failed",
40
+ "query_ambiguous",
41
+ "query_miss"
42
+ ]);
43
+ var PreparedExportError = class extends Error {
44
+ code;
45
+ cause;
46
+ constructor(code, message, options = {}) {
47
+ if (!PREPARED_EXPORT_ERROR_CODE_SET.has(code)) throw new TypeError("PreparedExportError code must be a known code.");
48
+ if (!isStringValue(message)) throw new TypeError("PreparedExportError message must be a string.");
49
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
50
+ Object.defineProperty(this, PREPARED_EXPORT_ERROR_BRAND, { value: true });
51
+ this.name = "PreparedExportError";
52
+ this.code = code;
53
+ this.cause = options.cause;
54
+ Object.freeze(this);
55
+ }
56
+ };
57
+ const isPreparedExportError = (value) => {
58
+ if (!isPropertyOwner(value) || !Object.isFrozen(value)) return false;
59
+ try {
60
+ return Object.getOwnPropertyDescriptor(value, PREPARED_EXPORT_ERROR_BRAND)?.value === true && Object.getOwnPropertyDescriptor(value, "name")?.value === "PreparedExportError" && isStringValue(Object.getOwnPropertyDescriptor(value, "message")?.value) && PREPARED_EXPORT_ERROR_CODE_SET.has(String(Object.getOwnPropertyDescriptor(value, "code")?.value));
61
+ } catch {
62
+ return false;
63
+ }
64
+ };
65
+ //#endregion
66
+ //#region src/prepared/manifest.ts
67
+ const MANIFEST_MAX_URL_BYTES = 8192;
68
+ const SHA256 = /^[\da-f]{64}$/u;
69
+ const encoder = new TextEncoder();
70
+ const parsePreparedExportManifest = (input) => {
71
+ try {
72
+ const root = portableJsonObject(input, "prepared manifest");
73
+ requireFields(root, [
74
+ "export_url",
75
+ "inputs",
76
+ "instance",
77
+ "schema",
78
+ "state_fingerprint"
79
+ ], ["refresh_interval_ms"]);
80
+ if (root.schema !== "marimo-export.prepared.v1") invalid("prepared manifest.schema must be marimo-export.prepared.v1");
81
+ const instance = digest(root.instance, "prepared manifest.instance");
82
+ const exportUrl = boundedUrl(root.export_url);
83
+ const inputs = portableJsonObject(root.inputs, "prepared manifest.inputs");
84
+ if (Object.keys(inputs).some((name) => name.length === 0)) invalid("prepared manifest.inputs must use nonempty input names");
85
+ const stateFingerprint = digest(root.state_fingerprint, "prepared manifest.state_fingerprint");
86
+ const refreshIntervalMs = parseRefreshInterval(root.refresh_interval_ms);
87
+ const manifest = {
88
+ schema: "marimo-export.prepared.v1",
89
+ instance,
90
+ exportUrl,
91
+ inputs,
92
+ stateFingerprint
93
+ };
94
+ if (refreshIntervalMs === void 0) return Object.freeze(manifest);
95
+ return Object.freeze({
96
+ ...manifest,
97
+ refreshIntervalMs
98
+ });
99
+ } catch (error) {
100
+ if (error instanceof PreparedExportError) throw error;
101
+ throw new PreparedExportError("manifest_invalid", "The prepared export manifest is invalid.", { cause: error });
102
+ }
103
+ };
104
+ const resolvePreparedPublication = (manifest, manifestUrl, notebookExport) => {
105
+ if (manifest.instance !== notebookExport.identity) invalid("The prepared manifest does not match the immutable export identity.");
106
+ const expected = preparedExportBase(manifest, manifestUrl);
107
+ if (notebookExport.base.href !== expected.href) invalid("The prepared manifest export URL does not match the opened immutable export.");
108
+ let state;
109
+ try {
110
+ state = notebookExport.resolve(manifest.inputs);
111
+ } catch (error) {
112
+ throw new PreparedExportError("manifest_invalid", "The prepared manifest selects a state absent from the immutable export.", { cause: error });
113
+ }
114
+ if (state.fingerprint !== manifest.stateFingerprint) invalid("The prepared manifest state fingerprint does not match its selected inputs.");
115
+ return Object.freeze({
116
+ manifest,
117
+ notebookExport,
118
+ state
119
+ });
120
+ };
121
+ const preparedExportBase = (manifest, manifestUrl) => normalizeBase(new URL(manifest.exportUrl, manifestUrl));
122
+ const openPreparedPublication = async (manifest, manifestUrl, options = {}) => {
123
+ throwIfPreparedAborted(options.signal);
124
+ const base = preparedExportBase(manifest, manifestUrl);
125
+ const opener = options.openExport ?? openExport;
126
+ const openOptions = {};
127
+ if (options.fetch !== void 0) openOptions.fetch = options.fetch;
128
+ if (options.signal !== void 0) openOptions.signal = options.signal;
129
+ const notebookExport = await opener(base, openOptions);
130
+ throwIfPreparedAborted(options.signal);
131
+ return resolvePreparedPublication(manifest, manifestUrl, notebookExport);
132
+ };
133
+ const requireFields = (value, required, optional) => {
134
+ const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
135
+ if (required.some((key) => !Object.hasOwn(value, key))) invalid("The prepared export manifest is missing a required field.");
136
+ if (Object.keys(value).some((key) => !allowed.has(key))) invalid("The prepared export manifest contains an unknown field.");
137
+ };
138
+ const digest = (value, path) => {
139
+ if (!isJsonString(value)) invalid(`${path} must be a lowercase SHA-256 digest.`);
140
+ if (!SHA256.test(value)) invalid(`${path} must be a lowercase SHA-256 digest.`);
141
+ return value;
142
+ };
143
+ const boundedUrl = (value) => {
144
+ if (!isJsonString(value)) invalid(`prepared manifest.export_url must be at most ${MANIFEST_MAX_URL_BYTES} UTF-8 bytes.`);
145
+ if (value.length === 0 || encoder.encode(value).byteLength > MANIFEST_MAX_URL_BYTES) invalid(`prepared manifest.export_url must be at most ${MANIFEST_MAX_URL_BYTES} UTF-8 bytes.`);
146
+ return value;
147
+ };
148
+ const parseRefreshInterval = (value) => {
149
+ if (value === void 0) return;
150
+ if (!isJsonNumber(value)) invalid("prepared manifest.refresh_interval_ms must be 0 or between 250 and 60000.");
151
+ if (!Number.isSafeInteger(value) || value !== 0 && (value < 250 || value > 6e4)) invalid("prepared manifest.refresh_interval_ms must be 0 or between 250 and 60000.");
152
+ return value;
153
+ };
154
+ function invalid(message) {
155
+ throw new PreparedExportError("manifest_invalid", message);
156
+ }
157
+ //#endregion
158
+ //#region src/prepared/manifest-fetch.ts
159
+ const MANIFEST_MAX_BYTES = 262144;
160
+ const fetchPreparedExportManifest = async (url, options = {}) => {
161
+ throwIfPreparedAborted(options.signal);
162
+ let response;
163
+ try {
164
+ const request = {
165
+ cache: "no-store",
166
+ headers: { Accept: "application/json" }
167
+ };
168
+ if (options.signal !== void 0) request.signal = options.signal;
169
+ response = await (options.fetch ?? globalThis.fetch)(url, request);
170
+ } catch (error) {
171
+ if (options.signal?.aborted) throw preparedAbortReason(options.signal.reason);
172
+ throw new PreparedExportError("manifest_read_failed", "The prepared export manifest could not be read.", { cause: error });
173
+ }
174
+ if (options.signal?.aborted) {
175
+ await cancelBody(response);
176
+ throwIfPreparedAborted(options.signal);
177
+ }
178
+ if (!response.ok) {
179
+ await cancelBody(response);
180
+ throw new PreparedExportError("manifest_read_failed", `The prepared export manifest request failed with ${response.status}.`);
181
+ }
182
+ const bytes = await readManifestBytes(response, options.signal);
183
+ try {
184
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
185
+ return parsePreparedExportManifest(parsePortableJson(text));
186
+ } catch (error) {
187
+ if (error instanceof PreparedExportError) throw error;
188
+ throw new PreparedExportError("manifest_invalid", "The prepared export manifest is not valid portable JSON.", { cause: error });
189
+ }
190
+ };
191
+ const readManifestBytes = async (response, signal) => {
192
+ if (signal?.aborted) {
193
+ await cancelBody(response);
194
+ throwIfPreparedAborted(signal);
195
+ }
196
+ const length = response.headers.get("Content-Length");
197
+ if (length !== null && /^\d+$/u.test(length) && Number(length) > MANIFEST_MAX_BYTES) {
198
+ await cancelBody(response);
199
+ throw manifestTooLarge();
200
+ }
201
+ const reader = response.body?.getReader();
202
+ if (reader === void 0) return /* @__PURE__ */ new Uint8Array();
203
+ const chunks = [];
204
+ let size = 0;
205
+ const abort = () => {
206
+ reader.cancel(signal?.reason).catch(() => {});
207
+ };
208
+ signal?.addEventListener("abort", abort, { once: true });
209
+ try {
210
+ if (signal?.aborted) {
211
+ await reader.cancel(signal.reason);
212
+ throwIfPreparedAborted(signal);
213
+ }
214
+ while (true) {
215
+ throwIfPreparedAborted(signal);
216
+ const chunk = await reader.read();
217
+ throwIfPreparedAborted(signal);
218
+ if (chunk.done) break;
219
+ size += chunk.value.byteLength;
220
+ if (size > MANIFEST_MAX_BYTES) {
221
+ await reader.cancel();
222
+ throw manifestTooLarge();
223
+ }
224
+ chunks.push(chunk.value);
225
+ }
226
+ } catch (error) {
227
+ if (signal?.aborted) throw preparedAbortReason(signal.reason);
228
+ if (error instanceof PreparedExportError) throw error;
229
+ throw new PreparedExportError("manifest_read_failed", "The prepared export manifest body could not be read.", { cause: error });
230
+ } finally {
231
+ signal?.removeEventListener("abort", abort);
232
+ reader.releaseLock();
233
+ }
234
+ const bytes = new Uint8Array(size);
235
+ let offset = 0;
236
+ for (const chunk of chunks) {
237
+ bytes.set(chunk, offset);
238
+ offset += chunk.byteLength;
239
+ }
240
+ return bytes;
241
+ };
242
+ const cancelBody = async (response) => {
243
+ try {
244
+ await response.body?.cancel();
245
+ } catch {}
246
+ };
247
+ const manifestTooLarge = () => new PreparedExportError("manifest_read_failed", `The prepared export manifest exceeds ${MANIFEST_MAX_BYTES} bytes.`);
248
+ //#endregion
249
+ //#region src/prepared/control.ts
250
+ const sameJsonValue = (left, right) => {
251
+ if (Object.is(left, right)) return true;
252
+ if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => sameJsonValue(value, right[index]));
253
+ if (!isJsonObject(left) || !isJsonObject(right)) return false;
254
+ const leftKeys = Object.keys(left);
255
+ return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => Object.hasOwn(right, key) && sameJsonValue(left[key], right[key]));
256
+ };
257
+ const samePreparedInputs = (left, right) => sameJsonValue(left, right);
258
+ const invalidBinding = (binding) => new PreparedExportError("manifest_invalid", `Control binding for input ${JSON.stringify(binding.input)} does not match the current state.`);
259
+ const replaceControlValue = (current, binding, offset, next) => {
260
+ const step = binding.path[offset];
261
+ if (step === void 0) return next;
262
+ if (step.kind === "element") throw invalidBinding(binding);
263
+ if (step.kind === "index" && Array.isArray(current)) {
264
+ if (step.value >= current.length) throw invalidBinding(binding);
265
+ const replaced = [...current];
266
+ replaced[step.value] = replaceControlValue(current[step.value], binding, offset + 1, next);
267
+ return Object.freeze(replaced);
268
+ }
269
+ if (!isJsonObject(current)) throw invalidBinding(binding);
270
+ const key = step.kind === "index" ? String(step.value) : step.value;
271
+ if (!Object.hasOwn(current, key)) throw invalidBinding(binding);
272
+ return portableJsonObject({
273
+ ...current,
274
+ [key]: replaceControlValue(current[key], binding, offset + 1, next)
275
+ });
276
+ };
277
+ const isJsonObject = (value) => isRecordValue(value);
278
+ const preparedControlInputPatch = (inputs, binding, value) => {
279
+ if (binding.path.some((step) => step.kind === "element")) return;
280
+ if (!Object.hasOwn(inputs, binding.input)) throw invalidBinding(binding);
281
+ return portableJsonObject({ [binding.input]: replaceControlValue(inputs[binding.input], binding, 0, portableJsonValue(value, "control value")) });
282
+ };
283
+ //#endregion
284
+ //#region src/prepared/state-selection.ts
285
+ const mergePreparedInputs = (base, patch) => portableJsonObject({
286
+ ...base,
287
+ ...patch
288
+ }, "prepared state patch");
289
+ const withPreparedState = (publication, state) => Object.freeze({
290
+ ...publication,
291
+ state
292
+ });
293
+ const selectPendingPublication = (publication, pendingInputs) => {
294
+ if (pendingInputs === void 0) return publication;
295
+ try {
296
+ return withPreparedState(publication, publication.notebookExport.resolve(pendingInputs));
297
+ } catch (error) {
298
+ if (!isPreparedStateUnavailable(error)) throw error;
299
+ return publication;
300
+ }
301
+ };
302
+ const samePreparedInputContract = (left, right) => {
303
+ const leftNames = left.notebookExport.inputNames;
304
+ const rightNames = new Set(right.notebookExport.inputNames);
305
+ return leftNames.length === rightNames.size && leftNames.every((name) => rightNames.has(name));
306
+ };
307
+ const pendingInputsForPublication = (current, next, pending) => pending !== void 0 && current !== void 0 && !samePreparedInputContract(current, next) ? void 0 : pending;
308
+ const isPreparedStateUnavailable = (value) => isNotebookExportError(value) && value.code === "state_unavailable";
309
+ const isPreparedStatePendingFailure = (value) => {
310
+ if (!isNotebookExportError(value)) return false;
311
+ if (value.code === "state_unavailable") return true;
312
+ return value.code === "read_failed" && value.details !== void 0 && Object.hasOwn(value.details, "status") && value.details.status === 404;
313
+ };
314
+ const pendingInputsAfterFailure = (pending, requested, error, callerAborted) => {
315
+ if (pending === void 0 || !samePreparedInputs(pending, requested)) return pending;
316
+ if (isPreparedAbort(error)) return callerAborted ? void 0 : pending;
317
+ return isPreparedStatePendingFailure(error) ? pending : void 0;
318
+ };
319
+ const preservePreparedSelection = (current, next) => {
320
+ if (current === void 0 || current.state.fingerprint === current.manifest.stateFingerprint || !samePreparedInputContract(current, next)) return next;
321
+ try {
322
+ return withPreparedState(next, next.notebookExport.resolve(current.state.inputs));
323
+ } catch (error) {
324
+ if (isNotebookExportError(error) && (error.code === "state_input_invalid" || error.code === "state_unavailable")) return next;
325
+ throw error;
326
+ }
327
+ };
328
+ //#endregion
329
+ //#region src/prepared/state-port.ts
330
+ var PreparedStateTransitions = class {
331
+ port;
332
+ lifecycle;
333
+ #current;
334
+ #target;
335
+ #controller;
336
+ #generation = 0;
337
+ #operation = Promise.resolve();
338
+ #closed = false;
339
+ constructor(port, lifecycle) {
340
+ this.port = port;
341
+ this.lifecycle = lifecycle;
342
+ }
343
+ get current() {
344
+ return this.#current;
345
+ }
346
+ get target() {
347
+ return this.#target;
348
+ }
349
+ snapshot(pendingInputs, disposed) {
350
+ return Object.freeze({
351
+ current: this.#current,
352
+ pendingInputs,
353
+ transition: Object.freeze({
354
+ generation: this.#generation,
355
+ target: this.#target,
356
+ active: this.#controller !== void 0
357
+ }),
358
+ disposed
359
+ });
360
+ }
361
+ setMetadata(publication) {
362
+ if (this.#target !== void 0) throw new Error("Prepared state transition is active.");
363
+ this.#current = publication;
364
+ }
365
+ apply(next, reason, parentSignal) {
366
+ if (this.#closed) throw new Error("Prepared state transitions are closed.");
367
+ throwIfPreparedAborted(parentSignal);
368
+ this.cancel(new DOMException("Prepared state transition superseded", "AbortError"));
369
+ const generation = this.#generation;
370
+ const controller = new AbortController();
371
+ const unlinkLifecycle = linkPreparedAbort(controller, this.lifecycle);
372
+ const unlinkParent = linkPreparedAbort(controller, parentSignal);
373
+ this.#controller = controller;
374
+ this.#target = next;
375
+ const operation = this.#operation.catch(() => {}).then(async () => {
376
+ throwIfPreparedAborted(controller.signal);
377
+ const previous = this.#current;
378
+ try {
379
+ await this.port.apply({
380
+ previous,
381
+ next,
382
+ reason
383
+ }, controller.signal);
384
+ throwIfPreparedAborted(controller.signal);
385
+ } catch (error) {
386
+ if (generation === this.#generation && !this.#closed && !isPreparedAbort(error)) await this.restoreAfter(error);
387
+ throw error;
388
+ }
389
+ if (generation === this.#generation && !this.#closed) this.#current = next;
390
+ });
391
+ this.#operation = operation.finally(() => {
392
+ unlinkLifecycle();
393
+ unlinkParent();
394
+ if (this.#controller === controller) this.#controller = void 0;
395
+ if (generation === this.#generation) this.#target = void 0;
396
+ });
397
+ return this.#operation;
398
+ }
399
+ async restoreAfter(primary) {
400
+ try {
401
+ await this.restore();
402
+ } catch (cleanup) {
403
+ throw new AggregateError([primary, cleanup], "Prepared state transition and restoration failed.");
404
+ }
405
+ }
406
+ async restore() {
407
+ if (this.#current !== void 0) await this.port.restore?.(this.#current);
408
+ }
409
+ cancel(reason) {
410
+ this.#generation += 1;
411
+ this.#target = void 0;
412
+ this.#controller?.abort(reason);
413
+ this.#controller = void 0;
414
+ }
415
+ settle() {
416
+ return Promise.allSettled([this.#operation]);
417
+ }
418
+ wait() {
419
+ return this.#operation;
420
+ }
421
+ async close(reason) {
422
+ this.#closed = true;
423
+ this.cancel(reason);
424
+ await this.#operation.catch(() => {});
425
+ this.#current = void 0;
426
+ this.#target = void 0;
427
+ this.#controller = void 0;
428
+ this.#operation = Promise.resolve();
429
+ }
430
+ };
431
+ //#endregion
432
+ //#region src/prepared/query.ts
433
+ const jsonKey = (value) => JSON.stringify(value);
434
+ const queryText = (value) => isStringValue(value) ? value : JSON.stringify(value);
435
+ const inputDomain = (notebookExport, name) => {
436
+ const values = /* @__PURE__ */ new Map();
437
+ for (const state of notebookExport.states()) if (Object.hasOwn(state.inputs, name)) {
438
+ const value = state.inputs[name];
439
+ values.set(jsonKey(value), value);
440
+ }
441
+ return Array.from(values.values());
442
+ };
443
+ const queryInput = (notebookExport, name, values) => {
444
+ if (values.length !== 1) throw new PreparedExportError("query_miss", `Query input ${JSON.stringify(name)} must have exactly one value.`);
445
+ const matches = inputDomain(notebookExport, name).filter((candidate) => queryText(candidate) === values[0]);
446
+ if (matches.length === 0) throw new PreparedExportError("query_miss", `Query input ${JSON.stringify(name)} has no exported value matching ${JSON.stringify(values[0])}.`);
447
+ if (matches.length > 1) throw new PreparedExportError("query_ambiguous", `Query input ${JSON.stringify(name)} matches more than one exported value.`);
448
+ return matches[0];
449
+ };
450
+ const resolvePreparedQuerySelection = (notebookExport, current, query) => {
451
+ const search = new URLSearchParams(query.startsWith("?") ? query.slice(1) : query);
452
+ const patch = [];
453
+ for (const name of notebookExport.inputNames) if (search.has(name)) patch.push([name, queryInput(notebookExport, name, search.getAll(name))]);
454
+ return patch.length === 0 ? void 0 : current.resolve(Object.fromEntries(patch));
455
+ };
456
+ const resolvePreparedQueryState = (notebookExport, current, query) => resolvePreparedQuerySelection(notebookExport, current, query) ?? current;
457
+ //#endregion
458
+ //#region src/prepared/state-update.ts
459
+ const preparedControlPatch = (publication, inputs, objectId, value) => {
460
+ const bindings = publication.notebookExport.controlBindings;
461
+ if (!Object.hasOwn(bindings, objectId)) return;
462
+ return preparedControlInputPatch(inputs, bindings[objectId], value);
463
+ };
464
+ const preparedQueryPatch = (publication, query) => resolvePreparedQuerySelection(publication.notebookExport, publication.state, query)?.inputs;
465
+ //#endregion
466
+ //#region src/prepared/controller.ts
467
+ var PreparedStateController = class {
468
+ #lifecycle = new AbortController();
469
+ #port;
470
+ #transitions;
471
+ #pendingInputs;
472
+ #disposed = false;
473
+ #disposal;
474
+ #unlinkLifecycle = () => {};
475
+ constructor(port, signal) {
476
+ this.#port = port;
477
+ this.#transitions = new PreparedStateTransitions(port, this.#lifecycle.signal);
478
+ if (signal !== void 0) this.#unlinkLifecycle = disposePreparedOnAbort(signal, this.#lifecycle, () => this.dispose());
479
+ }
480
+ snapshot() {
481
+ return this.#transitions.snapshot(this.#pendingInputs, this.#disposed);
482
+ }
483
+ start(publication, signal = this.#lifecycle.signal) {
484
+ this.#requireActive();
485
+ throwIfPreparedAborted(signal);
486
+ if (this.#transitions.current !== void 0 || this.#transitions.target !== void 0) throw new Error("Prepared state controller has already started.");
487
+ return this.#apply(publication, "start", signal);
488
+ }
489
+ async updateInputs(patch, signal = this.#lifecycle.signal) {
490
+ this.#requireActive();
491
+ throwIfPreparedAborted(signal);
492
+ const source = this.#transitions.target ?? this.#requireCurrent();
493
+ const base = this.#pendingInputs ?? source.state.inputs;
494
+ const requested = mergePreparedInputs(base, patch);
495
+ this.#pendingInputs = requested;
496
+ if (samePreparedInputs(requested, source.state.inputs)) {
497
+ if (this.#transitions.target !== void 0) await this.#transitions.wait();
498
+ else {
499
+ this.#pendingInputs = void 0;
500
+ await this.#transitions.restore();
501
+ }
502
+ return;
503
+ }
504
+ let next;
505
+ try {
506
+ next = withPreparedState(source, source.notebookExport.resolve(requested));
507
+ } catch (error) {
508
+ if (!isPreparedStateUnavailable(error)) this.#pendingInputs = void 0;
509
+ await this.#transitions.restoreAfter(error);
510
+ throw error;
511
+ }
512
+ try {
513
+ await this.#apply(next, "state", signal);
514
+ } catch (error) {
515
+ this.#pendingInputs = pendingInputsAfterFailure(this.#pendingInputs, requested, error, signal.aborted);
516
+ throw error;
517
+ }
518
+ }
519
+ async updateControl(objectId, value, signal = this.#lifecycle.signal) {
520
+ this.#requireActive();
521
+ throwIfPreparedAborted(signal);
522
+ const source = this.#transitions.target ?? this.#requireCurrent();
523
+ const patch = preparedControlPatch(source, this.#pendingInputs ?? source.state.inputs, objectId, value);
524
+ if (patch === void 0) return false;
525
+ await this.updateInputs(patch, signal);
526
+ return true;
527
+ }
528
+ async updateQuery(query, signal = this.#lifecycle.signal) {
529
+ this.#requireActive();
530
+ throwIfPreparedAborted(signal);
531
+ const source = this.#transitions.target ?? this.#requireCurrent();
532
+ const patch = preparedQueryPatch(source, query);
533
+ if (patch === void 0) return false;
534
+ await this.updateInputs(patch, signal);
535
+ return true;
536
+ }
537
+ async replacePublication(publication, signal = this.#lifecycle.signal) {
538
+ this.#requireActive();
539
+ throwIfPreparedAborted(signal);
540
+ const current = this.#transitions.current;
541
+ this.#pendingInputs = pendingInputsForPublication(current, publication, this.#pendingInputs);
542
+ const selected = selectPendingPublication(publication, this.#pendingInputs);
543
+ if (this.#transitions.target === void 0 && current !== void 0 && current.notebookExport.identity === selected.notebookExport.identity && current.state.fingerprint === selected.state.fingerprint) {
544
+ this.#transitions.setMetadata(selected);
545
+ this.#clearSatisfiedPending(selected);
546
+ return;
547
+ }
548
+ await this.#apply(selected, "publication", signal);
549
+ }
550
+ cancel(reason) {
551
+ this.#transitions.cancel(preparedAbortReason(reason, "Prepared state transition superseded"));
552
+ }
553
+ settle() {
554
+ return this.#transitions.settle();
555
+ }
556
+ dispose() {
557
+ this.#disposal ??= this.#dispose();
558
+ return this.#disposal;
559
+ }
560
+ async #apply(next, reason, signal) {
561
+ await this.#transitions.apply(next, reason, signal);
562
+ this.#clearSatisfiedPending(next);
563
+ }
564
+ #clearSatisfiedPending(publication) {
565
+ if (this.#pendingInputs !== void 0 && samePreparedInputs(this.#pendingInputs, publication.state.inputs)) this.#pendingInputs = void 0;
566
+ }
567
+ #requireCurrent() {
568
+ const current = this.#transitions.current;
569
+ if (current === void 0) throw new Error("Prepared state controller has not started.");
570
+ return current;
571
+ }
572
+ #requireActive() {
573
+ if (this.#disposed) throw new Error("Prepared state controller is disposed.");
574
+ }
575
+ async #dispose() {
576
+ this.#disposed = true;
577
+ this.#lifecycle.abort(new DOMException("Prepared state controller disposed", "AbortError"));
578
+ this.#unlinkLifecycle();
579
+ this.#unlinkLifecycle = () => {};
580
+ try {
581
+ await this.#transitions.close(this.#lifecycle.signal.reason);
582
+ await this.#port.dispose?.();
583
+ } finally {
584
+ this.#pendingInputs = void 0;
585
+ }
586
+ }
587
+ };
588
+ //#endregion
589
+ //#region src/prepared/refresh.ts
590
+ var PreparedPublicationRefresh = class {
591
+ #lifecycle = new AbortController();
592
+ #manifestUrl;
593
+ #state;
594
+ #dependencies;
595
+ #openOptions;
596
+ #onError;
597
+ #operation;
598
+ #controller;
599
+ #timer;
600
+ #disposed = false;
601
+ #unlinkLifecycle = () => {};
602
+ constructor(manifestUrl, state, options = {}) {
603
+ this.#manifestUrl = new URL(manifestUrl.href);
604
+ this.#state = state;
605
+ this.#dependencies = {
606
+ fetchManifest: options.dependencies?.fetchManifest ?? fetchPreparedExportManifest,
607
+ openPublication: options.dependencies?.openPublication ?? openPreparedPublication
608
+ };
609
+ const openOptions = {};
610
+ if (options.fetch !== void 0) openOptions.fetch = options.fetch;
611
+ if (options.openExport !== void 0) openOptions.openExport = options.openExport;
612
+ this.#openOptions = openOptions;
613
+ this.#onError = options.onError ?? (() => {});
614
+ if (options.signal !== void 0) this.#unlinkLifecycle = disposePreparedOnAbort(options.signal, this.#lifecycle, () => this.dispose());
615
+ }
616
+ start(signal = this.#lifecycle.signal) {
617
+ throwIfPreparedAborted(signal);
618
+ const snapshot = this.#state.snapshot();
619
+ if (snapshot.current !== void 0 || snapshot.transition.target !== void 0) throw new Error("Prepared publication refresh has already started.");
620
+ return this.#operation ?? this.#begin("start", signal);
621
+ }
622
+ refresh(signal = this.#lifecycle.signal) {
623
+ if (this.#disposed) return Promise.resolve();
624
+ throwIfPreparedAborted(signal);
625
+ return this.#operation ?? this.#begin("refresh", signal);
626
+ }
627
+ settle() {
628
+ return Promise.allSettled(this.#operation === void 0 ? [] : [this.#operation]);
629
+ }
630
+ syncPolling() {
631
+ this.#clearPolling();
632
+ if (this.#disposed) return;
633
+ const interval = this.#state.snapshot().current?.manifest.refreshIntervalMs ?? 0;
634
+ if (interval === 0) return;
635
+ this.#timer = setInterval(() => {
636
+ if (this.#disposed) return;
637
+ this.refresh().catch((error) => {
638
+ if (!this.#lifecycle.signal.aborted) this.#onError(error);
639
+ });
640
+ }, interval);
641
+ }
642
+ async dispose() {
643
+ if (this.#disposed) {
644
+ await this.settle();
645
+ return;
646
+ }
647
+ this.#disposed = true;
648
+ this.#clearPolling();
649
+ this.#lifecycle.abort(new DOMException("Prepared publication refresh disposed", "AbortError"));
650
+ this.#controller?.abort(this.#lifecycle.signal.reason);
651
+ this.#unlinkLifecycle();
652
+ this.#unlinkLifecycle = () => {};
653
+ await this.settle();
654
+ }
655
+ #begin(kind, parentSignal) {
656
+ throwIfPreparedAborted(parentSignal);
657
+ const controller = new AbortController();
658
+ const unlinkLifecycle = linkPreparedAbort(controller, this.#lifecycle.signal);
659
+ const unlinkParent = linkPreparedAbort(controller, parentSignal);
660
+ this.#controller = controller;
661
+ let tracked;
662
+ tracked = this.#perform(kind, controller.signal).finally(() => {
663
+ unlinkLifecycle();
664
+ unlinkParent();
665
+ if (this.#controller === controller) this.#controller = void 0;
666
+ if (this.#operation === tracked) this.#operation = void 0;
667
+ });
668
+ this.#operation = tracked;
669
+ return tracked;
670
+ }
671
+ async #perform(kind, signal) {
672
+ throwIfPreparedAborted(signal);
673
+ const manifestOptions = { signal };
674
+ if (this.#openOptions.fetch !== void 0) manifestOptions.fetch = this.#openOptions.fetch;
675
+ const manifest = await this.#dependencies.fetchManifest(this.#manifestUrl, manifestOptions);
676
+ throwIfPreparedAborted(signal);
677
+ if (this.#state.snapshot().transition.active) {
678
+ await this.#state.settle();
679
+ throwIfPreparedAborted(signal);
680
+ }
681
+ const current = this.#state.snapshot().current;
682
+ let publication;
683
+ if (current !== void 0 && current.notebookExport.identity === manifest.instance && current.notebookExport.base.href === preparedExportBase(manifest, this.#manifestUrl).href) publication = resolvePreparedPublication(manifest, this.#manifestUrl, current.notebookExport);
684
+ else publication = await this.#dependencies.openPublication(manifest, this.#manifestUrl, {
685
+ ...this.#openOptions,
686
+ signal
687
+ });
688
+ publication = preservePreparedSelection(current, publication);
689
+ throwIfPreparedAborted(signal);
690
+ if (kind === "start" && current === void 0) await this.#state.start(publication, signal);
691
+ else await this.#state.replacePublication(publication, signal);
692
+ throwIfPreparedAborted(signal);
693
+ this.syncPolling();
694
+ }
695
+ #clearPolling() {
696
+ if (this.#timer !== void 0) {
697
+ clearInterval(this.#timer);
698
+ this.#timer = void 0;
699
+ }
700
+ }
701
+ };
702
+ //#endregion
703
+ export { PreparedExportError, PreparedPublicationRefresh, PreparedStateController, fetchPreparedExportManifest, isPreparedAbort, isPreparedExportError, openPreparedPublication, parsePreparedExportManifest, preparedControlInputPatch, resolvePreparedPublication, resolvePreparedQuerySelection, resolvePreparedQueryState, samePreparedInputs };