@mengine/medeo-client 2.1.1-dsl.22 → 2.1.1-dsl.24

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 CHANGED
@@ -16,16 +16,14 @@ const [clipId] = session.editor.addClips({
16
16
  session.editor.setClipVolume(clipId, -3, { actor: { user_id: userId, role: 'user' } });
17
17
  await session.waitForServerAck(); // Local completion is separate from durable confirmation.
18
18
  const { content, diagnostics } = session.projectVideoDraft();
19
- session.destroy();
19
+ await session.close();
20
20
  ```
21
21
 
22
22
  `start()`, `snapshot()` and subscription events expose `MedeoDslSnapshot`. `session.dsl` exposes controlled `transact`/`applyMutations`; ordinary business callers use `session.editor`. Rich text uses the native text adapter. `readonlyMode` blocks editing and Undo/Redo. Closing a session also invalidates captured editor/document references.
23
23
 
24
- For explicit synchronization, use `ManualSyncDoc.open({client, peerId})`, then `pull()`, `editor`, `push()` and `dispose()`. Both lifecycles use the same binary snapshot/update and version-coverage contracts. Neither silently upgrades old documents or overwrites an incompatible cache.
25
-
26
24
  ## Local persistence
27
25
 
28
- Browser hosts can inject `IndexedDBDocStorage` through `localStorage`. An optional `initialSnapshot` merges a freshly fetched server snapshot into that storage before DocManager loads it; it never replaces locally saved, unconfirmed edits. The host can require a successful online fetch before constructing the session.
26
+ Browser hosts can inject `IndexedDBDocStorage` through `localStorage`. An optional `initialSnapshot` merges a freshly fetched server snapshot into that storage before DocManager loads it; it never replaces locally saved, unconfirmed edits. The incoming snapshot and combined local history are format-checked before the merge is persisted. The host can require a successful online fetch before constructing the session.
29
27
 
30
28
  `getLocalSaveState()` / `subscribeLocalSaveState()` report `{ status: 'saving' | 'saved' | 'failed', error }` for this session's local commits. This is separate from `getSyncState()` and server confirmation. The storage adapter determines durability: IndexedDB resolves only after its transaction commits; MemoryDocStorage remains in memory. Failed saves retain their causal order and retry without blocking other documents.
31
29
 
@@ -37,6 +35,8 @@ Edits received from another local-storage peer also remain unconfirmed until cov
37
35
 
38
36
  Reopening uses the stored Loro history and the server's version vector to derive missing updates, without replaying semantic operations or their external side effects. Undo history remains limited to the current session.
39
37
 
38
+ For explicit synchronization, use `ManualSyncDoc.open({client, peerId})`, then `pull()`, `editor`, `push()` and `dispose()`. Both lifecycles use the same binary snapshot/update and version-coverage contracts. Neither silently upgrades old documents or overwrites an incompatible cache.
39
+
40
40
  ## Editing and conversion
41
41
 
42
42
  - Track: create, reorder, hide and delete.
@@ -126,7 +126,7 @@ Each handle owns one long-lived Mirror. Ordinary fields and Tree read from its i
126
126
 
127
127
  Mirror 2.3.3 cannot safely move an existing node under a parent created in the same draft. The document transaction rejects this combination before writing. When a business Action actually needs it, implement its scoped native path with one audited commit as specified by [K03](../../docs/projects/medeo-dsl/known-unfixed.md#k03-mirror-同批新建父节点并移入已有节点); that fallback is not implemented yet.
128
128
 
129
- Loro 1.16.1 and loro-mirror 2.3.3 are the lockfile baseline. Mirror is pinned with a [workspace dependency patch](../../patches/README.md): its optional `setState(..., {applyNative})` hook runs after the ordinary draft reaches Loro and before one commit. The full native event updates Mirror before DSL subscribers are notified. The hook is internal to the DSL adapter; Actions never receive a native-write callback. It supports non-ephemeral Mirror instances, and must not commit/export/import or await. A registry install of loro-mirror never carries the patch, so the published package bundles the patched Mirror, runtime and declarations; consumers install only the `loro-crdt` peer and configure no patch. Transactions probe the Mirror once and refuse to write when the hook is missing rather than drop native edits. Native writes preserve marks and actual container identities; a plain Mirror string snapshot still does not contain rich-text marks. Tests cover snapshot/update round trips, concurrent moves and replacements, field merging, optional reset, malformed codecs, deletion/recreation, rich text, Cursor ownership, subscriptions and Action failure preflight. The conversion library is implemented; production server/editor/player entry points have not switched.
129
+ Loro 1.16.1 and loro-mirror 2.3.3 are the lockfile baseline. Mirror is pinned with a [workspace dependency patch](../../patches/README.md): its optional `setState(..., {applyNative})` hook runs after the ordinary draft reaches Loro and before one commit. The full native event updates Mirror before DSL subscribers are notified. The hook is internal to the DSL adapter; Actions never receive a native-write callback. It supports non-ephemeral Mirror instances, and must not commit/export/import or await. A registry install of loro-mirror never carries the patch, so the published package bundles the patched Mirror, runtime and declarations; consumers install only the `loro-crdt` peer and configure no patch. Transactions probe the Mirror once and refuse to write when the hook is missing rather than drop native edits. Native writes preserve marks and actual container identities; a plain Mirror string snapshot still does not contain rich-text marks. Tests cover snapshot/update round trips, concurrent moves and replacements, field merging, optional reset, malformed codecs, deletion/recreation, rich text, Cursor ownership, subscriptions and Action failure preflight. The server and current tool/sandbox source entrypoints use DSL documents. Deployment and each external consumer's acceptance are tracked separately in the status document.
130
130
 
131
131
  Run the package test/build tasks through `vp run @mengine/medeo-client#test` and `vp run @mengine/medeo-client#build`.
132
132
 
@@ -158,8 +158,12 @@ Caption text is matched forward in `speech.caption_ids` order. A failed exact ma
158
158
 
159
159
  ## Legacy consumers
160
160
 
161
- `SemanticEditor`, `VideoDocument`, their adapters and old timeline projection have been removed. `@mengine/medeo-client/legacy` retains only the old EntityTimelineEditor, entity transport and its direct VideoDraft projection. Old entity tool/sandbox imports use this entry until their business adaptation is implemented. The obsolete timeline-journal replay rejects nonempty plans before writing. They cannot open new DSL documents. `/schemas` remains the old tool's schema contract, not the new Editor input model.
161
+ `SemanticEditor`, `VideoDocument`, their adapters and old timeline projection have been removed. `@mengine/medeo-client/legacy` retains only the old EntityTimelineEditor, entity transport and its direct VideoDraft projection. Current medeo-tool/sandbox uses the DSL entrypoints; the legacy entry supports explicitly retained Entity consumers. The obsolete timeline-journal replay rejects nonempty plans before writing. They cannot open new DSL documents. `/schemas` retains the legacy Entity tool schema contract. The current Editor uses the DSL model.
162
162
 
163
163
  `/relay` and `/testing` remain host/test-only entrypoints. The private DSL package is bundled into the client distribution.
164
164
 
165
165
  See [DSL index](../../docs/projects/medeo-dsl/README.md), [capability mapping and cutover](../../docs/projects/medeo-dsl/rfc/03-editor-and-document-cutover.md) and [known limits](../../docs/projects/medeo-dsl/known-unfixed.md). K04 audit segmentation is unchanged; one native commit can contain multiple Changes.
166
+
167
+ ### PhoneticScript storage cutover
168
+
169
+ The row-owned atomic binding stores the selection, pronunciation and file facts together. Public Action methods and domain snapshots retain their shape; the native Loro field layout requires matching SDK/server versions. Old field-per-fact DSL documents require explicit migration and are refused by the new reader and relay. See [the cutover contract](../../docs/projects/medeo-dsl/rfc/06-phonetic-binding-cutover.md).
@@ -1,4 +1,4 @@
1
- import { $ as assertEntityPatch, A as getRootContainerByType, B as isValueOfContainerType, C as applyEncode, D as decodeNestedJsonValues, E as containerIdToContainerType, F as isArrayLike, G as tryUpdateToContainer, H as schemaToContainerType, I as isObject, J as getChildContainerSchema, K as valuesEqual, L as isObjectLike, M as hasTransform, N as inferContainerTypeFromValue, O as deepEqual, P as insertChildToMap, Q as assertEntity, R as isStateAndSchemaOfType, S as applyDecode, T as cidsEqual, U as stripUndefined, V as safeStringify, W as tryInferContainerType, X as getMapFieldSchema, Y as getChildSchema, Z as assertCaptionContent, _ as isLoroMovableListSchema, _t as resourceId, at as assertTimeAnchor, b as schemaContainsIgnoreField, c as isInvalidAtomic, ct as isRecord, d as schema, dt as relationTables, et as assertId, f as getDefaultValue, ft as validLanguage, g as isLoroMapSchema, gt as relationId, h as isLoroListSchema, ht as defaultIdFactory, it as assertScriptSegment, j as hardenCidDescriptors, k as defineCidProperty, l as phonemeCodec, lt as mergeFields, m as isListLikeSchema, mt as assertResourceIdentity, n as textStyles, nt as assertPhoneticScript, o as medeoDslLoroShape, ot as atomicFields, p as isContainerSchema, pt as assertRelationIdentity, q as CID_KEY, rt as assertRelation, s as captionContentCodec, st as entityTables, t as DEFAULT_UNIT_TIME_MS, tt as assertPhoneme, ut as patchRow, v as isLoroTreeSchema, w as applySchemaToInferOptions, x as validateSchema, y as isRootSchemaType, z as isTreeID } from "./video-draft-types-BfIXM8q3.js";
1
+ import { $ as assertCaptionContent, A as deepEqual, B as isStateAndSchemaOfType, C as validateSchema, D as cidsEqual, E as applySchemaToInferOptions, F as inferContainerTypeFromValue, G as stripUndefined, H as isValueOfContainerType, I as insertChildToMap, J as valuesEqual, K as tryInferContainerType, L as isArrayLike, M as getRootContainerByType, N as hardenCidDescriptors, O as containerIdToContainerType, P as hasTransform, Q as getMapFieldSchema, R as isObject, S as schemaContainsIgnoreField, T as applyEncode, U as safeStringify, V as isTreeID, W as schemaToContainerType, X as getChildContainerSchema, Y as CID_KEY, Z as getChildSchema, _ as isLoroListSchema, _t as assertRelationIdentity, a as assertPhoneticBindingFormat, at as assertPhoneticScript, b as isLoroTreeSchema, bt as relationId, c as captionContentCodec, ct as assertSeqMarker, d as phoneticBindingCodec, dt as entityTables, et as assertEntity, ft as isRecord, g as isListLikeSchema, gt as validLanguage, h as isContainerSchema, ht as relationTables, it as assertPhonemeScript, j as defineCidProperty, k as decodeNestedJsonValues, l as isInvalidAtomic, lt as assertTimeAnchor, m as getDefaultValue, mt as patchRow, n as textStyles, nt as assertId, ot as assertRelation, p as schema, pt as mergeFields, q as tryUpdateToContainer, rt as assertPhoneme, s as medeoDslLoroShape, st as assertScriptSegment, t as DEFAULT_UNIT_TIME_MS, tt as assertEntityPatch, u as phonemeCodec, ut as atomicFields, v as isLoroMapSchema, vt as assertResourceIdentity, w as applyDecode, x as isRootSchemaType, xt as resourceId, y as isLoroMovableListSchema, yt as defaultIdFactory, z as isObjectLike } from "./video-draft-types-DRGBN0we.js";
2
2
  import { Cursor, LoroDoc, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, isContainer, isContainerId } from "loro-crdt";
3
3
  import { Immer, setAutoFreeze } from "immer";
4
4
  //#region ../medeo-dsl/src/text.ts
@@ -311,6 +311,63 @@ function assertContainTree(rows, tree) {
311
311
  }
312
312
  }
313
313
  //#endregion
314
+ //#region ../medeo-dsl/src/clip-playback.ts
315
+ const playable = {
316
+ video_clip: ["video", "image"],
317
+ speech: ["audio", "voiceover-recording"],
318
+ caption: ["caption"],
319
+ bgm: ["audio"]
320
+ };
321
+ function supportsClipSpeed(source) {
322
+ return isRecord(source) && [
323
+ "video",
324
+ "audio",
325
+ "voiceover-recording"
326
+ ].includes(String(source.kind));
327
+ }
328
+ /** Content-kind admission is shared by Actions, script compilation and the final transaction. */
329
+ function assertClipSourceRole(role, kind) {
330
+ if (!playable[role]?.includes(kind)) throw new Error(`A ${role} Track does not play ${kind}`);
331
+ }
332
+ function assertClipWindow(window, duration) {
333
+ if (!Number.isSafeInteger(window.inMs) || !Number.isSafeInteger(window.outMs) || window.inMs < 0 || window.outMs <= window.inMs) throw new Error("Invalid Clip window");
334
+ if (duration !== void 0 && window.outMs > duration) throw new Error(`Invalid Clip window: ${window.outMs}ms is past its content's ${duration}ms`);
335
+ }
336
+ /** Check authored playback facts; a remote merge is still read by projection with diagnostics. */
337
+ function assertClipPlayback(clip, source, role) {
338
+ if (!isRecord(source)) throw new Error("Invalid Clip source");
339
+ const kind = source.kind ?? "caption";
340
+ const marker = clip.seqMarker;
341
+ assertSeqMarker(marker);
342
+ if (marker.outMs <= marker.inMs) throw new Error("Clip window must be nonempty");
343
+ if (role !== void 0) assertClipSourceRole(role, kind);
344
+ if (marker.rate !== void 0 && !supportsClipSpeed(source)) throw new Error("Only video and sound change speed");
345
+ const duration = typeof source.durationMs === "number" ? source.durationMs : void 0;
346
+ if (marker.fillTimeline) {
347
+ if (kind !== "audio") throw new Error("Only BGM audio fills the Timeline");
348
+ if (marker.inMs !== 0 || marker.rate !== void 0 && marker.rate !== 1 || marker.outMs !== duration) throw new Error(`A Clip that fills the Timeline plays its whole source at 1×: inMs 0, outMs ${duration ?? "its length"}, no rate; the Timeline's end cuts it`);
349
+ }
350
+ assertClipWindow(marker);
351
+ }
352
+ function clipParents(tree) {
353
+ const result = /* @__PURE__ */ new Map();
354
+ for (const track of tree) for (const clip of track.children ?? []) result.set(clip.entityId, track.entityId);
355
+ return result;
356
+ }
357
+ /** A new parent or Track role revalidates that binding, without blocking unrelated merged rows. */
358
+ function assertClipPlacements(before, after, previousTree, nextTree) {
359
+ const previous = clipParents(previousTree);
360
+ for (const [id, parent] of clipParents(nextTree)) {
361
+ const track = after["track-entities"][parent];
362
+ const previousTrack = before["track-entities"][parent];
363
+ if (previous.get(id) === parent && (track === void 0 || isRecord(track) && isRecord(previousTrack) && previousTrack.role === track.role)) continue;
364
+ const role = requireEntity(after, "track-entities", parent).role;
365
+ const clip = after["clip-entities"][id];
366
+ if (!clip) continue;
367
+ assertClipPlayback(clip, after["media-entities"][clip.source] ?? after["caption-entities"][clip.source], role);
368
+ }
369
+ }
370
+ //#endregion
314
371
  //#region ../medeo-dsl/src/plan.ts
315
372
  /** Include removed nested keys: a partial merge alone would conceal a deleted required field. */
316
373
  function replacementPatch(before, after) {
@@ -326,6 +383,7 @@ function replacementPatch(before, after) {
326
383
  */
327
384
  function assertChangeAdmitted(change) {
328
385
  const { before, after } = change;
386
+ const parents = change.containment ? clipParents(change.containment.after) : void 0;
329
387
  for (const [table, rows] of change.entities) for (const [id, fields] of rows) {
330
388
  const value = after[table][id];
331
389
  if (value === void 0) continue;
@@ -340,25 +398,17 @@ function assertChangeAdmitted(change) {
340
398
  }
341
399
  const row = value;
342
400
  for (const field of referenceFields[table] ?? []) if (!old || fields.has(field)) assertReferenceAdmitted(after, table, id, field, row[field]);
343
- const marker = row.seqMarker;
344
- if (table === "clip-entities" && (!old || fields.has("seqMarker")) && marker?.outMs <= marker?.inMs) throw new Error("Clip window must be nonempty");
345
- if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) assertFillsWithWholeSource(after, marker, row.source);
401
+ if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) {
402
+ const parent = parents?.get(id);
403
+ assertClipPlayback(row, after["media-entities"][row.source] ?? after["caption-entities"][row.source], parent === void 0 ? void 0 : requireEntity(after, "track-entities", parent).role);
404
+ }
346
405
  }
347
406
  if (change.entities.has("timeline-entities") && Object.keys(after["timeline-entities"]).length > 1) throw new Error("Document already has a Timeline");
348
407
  for (const [table, ids] of change.relations) for (const id of ids) {
349
408
  const value = after[table][id];
350
409
  if (value !== void 0) assertRelationAdmitted(after, table, id, value);
351
410
  }
352
- }
353
- /**
354
- * A Clip that fills the Timeline plays its whole source at 1×: the player loops a short source and
355
- * cuts a long one at the Timeline's end (RFC 02 §3.3), so a window or speed of its own has no meaning.
356
- */
357
- function assertFillsWithWholeSource(rows, marker, source) {
358
- if (marker?.fillTimeline !== true) return;
359
- const media = typeof source === "string" ? rows["media-entities"][source] : void 0;
360
- const durationMs = typeof media?.durationMs === "number" ? media.durationMs : void 0;
361
- if (marker.inMs !== 0 || marker.rate !== void 0 && marker.rate !== 1 || durationMs !== void 0 && marker.outMs !== durationMs) throw new Error(`A Clip that fills the Timeline plays its whole source at 1×: inMs 0, outMs ${durationMs ?? "its length"}, no rate; the Timeline's end cuts it`);
411
+ if (change.containment) assertClipPlacements(before, after, change.containment.before, change.containment.after);
362
412
  }
363
413
  //#endregion
364
414
  //#region src/dsl/projection/types.ts
@@ -594,15 +644,11 @@ function requireSource(s, id, role) {
594
644
  return media;
595
645
  }
596
646
  function assertMediaRole(media, role) {
597
- if (!media || !{
598
- video_clip: ["video", "image"],
599
- speech: ["audio", "voiceover-recording"],
600
- bgm: ["audio"],
601
- caption: []
602
- }[role].includes(media.kind)) throw new Error(`Source is not allowed on ${role} Track`);
647
+ if (!media) throw new Error(`Missing source for ${role} Track`);
648
+ assertClipSourceRole(role, media.kind);
603
649
  }
604
650
  function validateWindow(window, duration) {
605
- if (!Number.isSafeInteger(window.inMs) || !Number.isSafeInteger(window.outMs) || window.inMs < 0 || window.outMs <= window.inMs || duration !== void 0 && window.outMs > duration) throw new Error("Invalid Clip window");
651
+ assertClipWindow(window, duration);
606
652
  }
607
653
  function trackOf(s, id) {
608
654
  requireClip(s, id);
@@ -890,7 +936,8 @@ function replaceSourcePlan(s, clipId, input) {
890
936
  patch: {
891
937
  seqMarker: {
892
938
  inMs: input.inMs,
893
- outMs: input.outMs
939
+ outMs: input.outMs,
940
+ ...supportsClipSpeed(source.source) ? {} : { rate: void 0 }
894
941
  },
895
942
  source: source.id
896
943
  }
@@ -3905,6 +3952,54 @@ function mergeInitialIntoBaseWithSchema(base, init, rootSchema) {
3905
3952
  }
3906
3953
  }
3907
3954
  //#endregion
3955
+ //#region src/dsl/document/phonetic-binding.ts
3956
+ /**
3957
+ * Actions and the transaction guard keep their domain-shaped rows. The native schema stores
3958
+ * a PhoneticScript's facts under one binding; this lens applies draft edits to that single value.
3959
+ * It never reads the retired field-per-fact representation.
3960
+ */
3961
+ function logicalDslState(native) {
3962
+ const phonetics = new Proxy({}, {
3963
+ get: (_target, key) => typeof key === "string" ? native["phonetic-script-entities"]?.[key]?.binding : void 0,
3964
+ set: (_target, key, value) => {
3965
+ if (typeof key !== "string") throw new Error("Invalid PhoneticScript key");
3966
+ native["phonetic-script-entities"] ??= {};
3967
+ const existing = native["phonetic-script-entities"][key];
3968
+ if (existing) existing.binding = value;
3969
+ else native["phonetic-script-entities"][key] = { binding: value };
3970
+ return true;
3971
+ },
3972
+ deleteProperty: (_target, key) => {
3973
+ if (typeof key === "string") delete native["phonetic-script-entities"][key];
3974
+ return true;
3975
+ },
3976
+ ownKeys: () => Object.keys(native["phonetic-script-entities"] ?? {}),
3977
+ has: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key),
3978
+ getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key) ? {
3979
+ configurable: true,
3980
+ enumerable: true,
3981
+ writable: true,
3982
+ value: native["phonetic-script-entities"][key].binding
3983
+ } : void 0
3984
+ });
3985
+ const view = {};
3986
+ for (const key of new Set([
3987
+ ...Object.keys(native),
3988
+ ...entityTables,
3989
+ ...relationTables,
3990
+ "contain-relations"
3991
+ ])) Object.defineProperty(view, key, {
3992
+ configurable: true,
3993
+ enumerable: true,
3994
+ get: () => key === "phonetic-script-entities" ? phonetics : Reflect.get(native, key),
3995
+ set: (value) => {
3996
+ if (key === "phonetic-script-entities") throw new Error("Cannot replace the PhoneticScript table");
3997
+ Reflect.set(native, key, value);
3998
+ }
3999
+ });
4000
+ return view;
4001
+ }
4002
+ //#endregion
3908
4003
  //#region src/dsl/document/read.ts
3909
4004
  /** Looking up a missing root must not create it. */
3910
4005
  function rootMap(doc, name) {
@@ -3988,6 +4083,7 @@ function referenceTargets(table, row) {
3988
4083
  }
3989
4084
  }
3990
4085
  function readMedeoDsl(doc, state) {
4086
+ const logical = logicalDslState(state);
3991
4087
  const diagnostics = [];
3992
4088
  const diagnostic = (code, path, error) => diagnostics.push({
3993
4089
  code,
@@ -3997,7 +4093,7 @@ function readMedeoDsl(doc, state) {
3997
4093
  const entities = Object.fromEntries(entityTables.map((t) => [t, {}]));
3998
4094
  const relations = Object.fromEntries(relationTables.map((t) => [t, {}]));
3999
4095
  const richText = {};
4000
- for (const table of entityTables) for (const [id, raw] of Object.entries(state[table] ?? {})) try {
4096
+ for (const table of entityTables) for (const [id, raw] of Object.entries(logical[table] ?? {})) try {
4001
4097
  assertId(id);
4002
4098
  let v = raw;
4003
4099
  for (const field of optionalReferences[table] ?? []) {
@@ -4503,7 +4599,11 @@ function validateDraft(tracked) {
4503
4599
  before,
4504
4600
  after: state,
4505
4601
  entities: tracked.entities,
4506
- relations: tracked.relations
4602
+ relations: tracked.relations,
4603
+ containment: {
4604
+ before: plainTree(before["contain-relations"] ?? []),
4605
+ after: plainTree(state["contain-relations"] ?? [])
4606
+ }
4507
4607
  });
4508
4608
  if (!tracked.treeChanged) return;
4509
4609
  const oldNodes = nodeIndex(before["contain-relations"] ?? []);
@@ -4580,8 +4680,11 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4580
4680
  const steps = [];
4581
4681
  const newCaptionTexts = /* @__PURE__ */ new Map();
4582
4682
  const bodies = /* @__PURE__ */ new Map();
4583
- const before = mirror.getState();
4584
- mirror.setState((raw) => {
4683
+ const nativeBefore = mirror.getState();
4684
+ const before = logicalDslState(nativeBefore);
4685
+ const phoneticRows = /* @__PURE__ */ new Map();
4686
+ mirror.setState((native) => {
4687
+ const raw = logicalDslState(native);
4585
4688
  const tracked = new TransactionDraft(raw, before);
4586
4689
  setReject((error) => tracked.fail(error));
4587
4690
  /** The current text of a Caption's own Text or a script segment, as this transaction leaves it so far. */
@@ -4773,6 +4876,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4773
4876
  for (const step of steps) if (step.type === "text") if ("captionId" in step.target) requireEntity(raw, "caption-entities", step.target.captionId);
4774
4877
  else requireSegment(raw, step.target.audioScriptId, step.target.segmentId);
4775
4878
  assertSelectionsAfter(raw, before, tracked, steps);
4879
+ const changedPhonetics = new Set([...tracked.entities.get("phonetic-script-entities")?.keys() ?? [], ...steps.flatMap((step) => step.type !== "text" && step.table === "phonetic-script-entities" ? [step.id] : [])]);
4880
+ for (const id of changedPhonetics) {
4881
+ const row = raw["phonetic-script-entities"][id];
4882
+ phoneticRows.set(id, row ? copyInput(row) : void 0);
4883
+ if (!row) continue;
4884
+ const old = nativeBefore["phonetic-script-entities"][id];
4885
+ if (old) native["phonetic-script-entities"][id] = old;
4886
+ else delete native["phonetic-script-entities"][id];
4887
+ }
4776
4888
  for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
4777
4889
  const row = raw["caption-entities"][id];
4778
4890
  if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
@@ -4781,6 +4893,7 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4781
4893
  }
4782
4894
  }
4783
4895
  for (const table of Object.keys(selectionField)) {
4896
+ if (table === "phonetic-script-entities") continue;
4784
4897
  const field = selectionField[table];
4785
4898
  for (const [id, fields] of tracked.entities.get(table) ?? []) {
4786
4899
  const row = raw[table][id];
@@ -4820,6 +4933,8 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4820
4933
  applyTextEdits(text, step.edits);
4821
4934
  continue;
4822
4935
  }
4936
+ const phonetic = step.table === "phonetic-script-entities" ? phoneticRows.get(step.id) : void 0;
4937
+ if (step.table === "phonetic-script-entities" && !phonetic) continue;
4823
4938
  const row = entityRow(doc, step.table, step.id);
4824
4939
  const field = selectionField[step.table];
4825
4940
  if (step.type === "range") {
@@ -4838,12 +4953,21 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4838
4953
  if (!value.start || !value.end) throw new Error("Unable to construct prepared range");
4839
4954
  }
4840
4955
  if (step.phonemeScript !== void 0) value.phonemeScript = step.phonemeScript;
4841
- if (!row) throw new Error("Unable to construct prepared range");
4842
- row.set(field, codecs[step.table].encode(value));
4843
- } else if (step.value !== void 0) {
4956
+ if (phonetic) phonetic.phoneme = value;
4957
+ else {
4958
+ if (!row) throw new Error("Unable to construct prepared range");
4959
+ row.set(field, codecs[step.table].encode(value));
4960
+ }
4961
+ } else if (step.value !== void 0) if (phonetic) phonetic.phoneme = step.value;
4962
+ else {
4844
4963
  if (!row) throw new Error("Prepared selection owner is missing");
4845
4964
  row.set(field, codecs[step.table].encode(step.value));
4846
- } else if (row?.get(field) !== void 0) row.delete(field);
4965
+ }
4966
+ else if (row?.get(field) !== void 0) row.delete(field);
4967
+ }
4968
+ for (const [id, binding] of phoneticRows) {
4969
+ if (!binding) continue;
4970
+ (entityRow(doc, "phonetic-script-entities", id) ?? doc.getMap("phonetic-script-entities").setContainer(id, new LoroMap())).set("binding", phoneticBindingCodec.encode(binding));
4847
4971
  }
4848
4972
  }
4849
4973
  });
@@ -4890,7 +5014,7 @@ function assertSelectionsAfter(raw, before, tracked, steps) {
4890
5014
  if (!row) continue;
4891
5015
  const phoneme = phonemes.has(id) ? phonemes.get(id) : row.phoneme;
4892
5016
  const old = before["phonetic-script-entities"][id];
4893
- assertPhoneticScript(old, {
5017
+ assertPhoneticScript(old ? { ...old } : void 0, {
4894
5018
  ...row,
4895
5019
  phoneme: isRecord(phoneme) ? phoneme : void 0
4896
5020
  });
@@ -4929,6 +5053,7 @@ var MedeoDsl = class MedeoDsl {
4929
5053
  "tracks",
4930
5054
  "part_library"
4931
5055
  ].some((key) => Object.hasOwn(roots, key))) throw new Error("Expected a new DSL document, not a legacy content model");
5056
+ assertPhoneticBindingFormat(doc);
4932
5057
  doc.configTextStyle(textStyles);
4933
5058
  this.mirror = new Mirror({
4934
5059
  doc,
@@ -4992,6 +5117,11 @@ var MedeoDsl = class MedeoDsl {
4992
5117
  editCaptionText(captionId, expectedBodyId, edits, audit) {
4993
5118
  this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
4994
5119
  }
5120
+ /** Check the session's write capability before an Action starts host-owned resource I/O. */
5121
+ assertWritable() {
5122
+ this.assertReadyToWrite();
5123
+ this.options.mutationGuard?.run(() => {});
5124
+ }
4995
5125
  /**
4996
5126
  * A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
4997
5127
  * include insertions at their end. K01 remains deferred.
@@ -5024,13 +5154,7 @@ var MedeoDsl = class MedeoDsl {
5024
5154
  if (this.ownsDoc) this.doc.free();
5025
5155
  }
5026
5156
  mutate(run) {
5027
- this.assertActive();
5028
- if (this.writing || this.notifying) {
5029
- const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
5030
- this.rejectTransaction?.(error);
5031
- throw error;
5032
- }
5033
- if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
5157
+ this.assertReadyToWrite();
5034
5158
  const apply = () => {
5035
5159
  this.writing = true;
5036
5160
  try {
@@ -5045,6 +5169,15 @@ var MedeoDsl = class MedeoDsl {
5045
5169
  if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
5046
5170
  else apply();
5047
5171
  }
5172
+ assertReadyToWrite() {
5173
+ this.assertActive();
5174
+ if (this.writing || this.notifying) {
5175
+ const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
5176
+ this.rejectTransaction?.(error);
5177
+ throw error;
5178
+ }
5179
+ if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
5180
+ }
5048
5181
  notify() {
5049
5182
  if (this.disposed || !this.dirty) return;
5050
5183
  this.dirty = false;
@@ -5769,8 +5902,12 @@ var DslEditor = class {
5769
5902
  * it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
5770
5903
  */
5771
5904
  async setPhoneticScript(input, options) {
5905
+ this.dsl.assertWritable();
5772
5906
  const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
5773
5907
  const range = segmentRange(input);
5908
+ assertId(id);
5909
+ assertSegmentRange(range, requireSegment(this.dsl.getSnapshot(), range.audioScriptId, range.segmentId).text);
5910
+ assertPhonemeScript(input.phonemeScript);
5774
5911
  const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
5775
5912
  const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
5776
5913
  const cleared = {
package/dist/dsl.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { $ as TextMutation, A as DeepReadonly, C as CaptionContent, D as Clip, E as CaptionText, F as EntityTables, G as ResourceSystem, H as RelationTable, I as EntityValues, K as RichTextSnapshot, L as FieldPatch, M as DslMutation, N as EntityMutation, O as ContainMutation, P as EntityTable, Q as TextMark, R as MedeoDslSnapshot, S as Caption, T as CaptionStyle, U as RelationTables, V as RelationMutation, W as RelationValues, X as TextDelta, Y as StoredFile, Z as TextEdit, _ as defaultIdFactory, a as VideoDraftCaptionStyle, b as tupleHash, c as VideoDraftInput, d as VideoDraftTrack, et as TextRun, f as VideoDraftTrackItem, g as EntityIdKind, h as DslIdFactory, i as VideoDraft, it as Voice, j as DslDiagnostic, k as ContainNode, l as VideoDraftPartUnion, m as OpActor, n as DEFAULT_UNIT_TIME_MS, nt as Track, o as VideoDraftContent, p as CommitOptions, r as VideoCreationSettings, rt as TrackRole, s as VideoDraftContentPartUnion, t as CaptionDisplayCue, tt as TimeAnchor, u as VideoDraftTimeline, v as relationId, w as CaptionSelection, x as AudioScript, y as resourceId, z as Media } from "./video-draft-types-0J2vfXBu.js";
2
- import { A as MedeoDslDraft, C as UnsupportedDocumentFormatError, D as captionContentCodec, E as InvalidAtomic, F as ClipSource, I as DslClipUpdate, L as MoveDslClipsInput, M as medeoDslLoroShape, N as AddDslClip, O as isInvalidAtomic, P as AddDslClipsInput, R as ReplaceDslClipsInput, S as DslTransaction, T as documentFormat, _ as VoiceoverCaptionInput, a as toAudioScriptViews, b as MedeoDsl, c as resolveDslLayout, d as DslProjectionError, f as DslTrackLayout, g as ApplyVoiceoverInput, h as UploadText, i as AudioScriptView, j as MedeoDslLoroState, k as timeAnchorCodec, l as DslClipLayout, m as DslEditor, n as ImportedVideoDraft, o as VideoDraftProjection, p as createInitialMedeoDsl, r as fromVideoDraft, s as toVideoDraft, t as FromVideoDraftOptions, u as DslLayout, v as VoiceoverResult, w as assertDslDocument, x as DslAudit, y as CreateMedeoDslOptions, z as ClipPlacement } from "./index-DVmvFhEn.js";
1
+ import { $ as TextMark, A as DeepReadonly, C as CaptionContent, D as Clip, E as CaptionText, F as EntityTables, G as RelationValues, H as RelationMutation, I as EntityValues, K as ResourceSystem, L as FieldPatch, M as DslMutation, N as EntityMutation, O as ContainMutation, P as EntityTable, Q as TextEdit, R as MedeoDslSnapshot, S as Caption, T as CaptionStyle, U as RelationTable, W as RelationTables, X as StoredFile, Z as TextDelta, _ as defaultIdFactory, a as VideoDraftCaptionStyle, at as Voice, b as tupleHash, c as VideoDraftInput, d as VideoDraftTrack, et as TextMutation, f as VideoDraftTrackItem, g as EntityIdKind, h as DslIdFactory, i as VideoDraft, it as TrackRole, j as DslDiagnostic, k as ContainNode, l as VideoDraftPartUnion, m as OpActor, n as DEFAULT_UNIT_TIME_MS, nt as TimeAnchor, o as VideoDraftContent, p as CommitOptions, q as RichTextSnapshot, r as VideoCreationSettings, rt as Track, s as VideoDraftContentPartUnion, t as CaptionDisplayCue, tt as TextRun, u as VideoDraftTimeline, v as relationId, w as CaptionSelection, x as AudioScript, y as resourceId, z as Media } from "./video-draft-types-slnHaNs9.js";
2
+ import { A as MedeoDslDraft, C as UnsupportedDocumentFormatError, D as captionContentCodec, E as InvalidAtomic, F as ClipSource, I as DslClipUpdate, L as MoveDslClipsInput, M as medeoDslLoroShape, N as AddDslClip, O as isInvalidAtomic, P as AddDslClipsInput, R as ReplaceDslClipsInput, S as DslTransaction, T as documentFormat, _ as VoiceoverCaptionInput, a as toAudioScriptViews, b as MedeoDsl, c as resolveDslLayout, d as DslProjectionError, f as DslTrackLayout, g as ApplyVoiceoverInput, h as UploadText, i as AudioScriptView, j as MedeoDslLoroState, k as timeAnchorCodec, l as DslClipLayout, m as DslEditor, n as ImportedVideoDraft, o as VideoDraftProjection, p as createInitialMedeoDsl, r as fromVideoDraft, s as toVideoDraft, t as FromVideoDraftOptions, u as DslLayout, v as VoiceoverResult, w as assertDslDocument, x as DslAudit, y as CreateMedeoDslOptions, z as ClipPlacement } from "./index-CmuDB_Gm.js";
3
3
  export { type AddDslClip, type AddDslClipsInput, type ApplyVoiceoverInput, type AudioScript, type AudioScriptView, type Caption, type CaptionContent, type CaptionDisplayCue, type CaptionSelection, type CaptionStyle, type CaptionText, type Clip, type ClipPlacement, type ClipSource, type CommitOptions, type ContainMutation, type ContainNode, type CreateMedeoDslOptions, DEFAULT_UNIT_TIME_MS, type DeepReadonly, type DslAudit, type DslClipLayout, type DslClipUpdate, type DslDiagnostic, DslEditor, type DslIdFactory, type DslLayout, type DslMutation, DslProjectionError, type DslTrackLayout, type DslTransaction, type EntityIdKind, type EntityMutation, type EntityTable, type EntityTables, type EntityValues, type FieldPatch, type FromVideoDraftOptions, type ImportedVideoDraft, type InvalidAtomic, MedeoDsl, type MedeoDslDraft, type MedeoDslLoroState, type MedeoDslSnapshot, type Media, type MoveDslClipsInput, type OpActor, type RelationMutation, type RelationTable, type RelationTables, type RelationValues, type ReplaceDslClipsInput, type ResourceSystem, type RichTextSnapshot, type StoredFile, type TextDelta, type TextEdit, type TextMark, type TextMutation, type TextRun, type TimeAnchor, type Track, type TrackRole, UnsupportedDocumentFormatError, type UploadText, type VideoCreationSettings, type VideoDraft, type VideoDraftCaptionStyle, type VideoDraftContent, type VideoDraftContentPartUnion, type VideoDraftInput, type VideoDraftPartUnion, type VideoDraftProjection, type VideoDraftTimeline, type VideoDraftTrack, type VideoDraftTrackItem, type Voice, type VoiceoverCaptionInput, type VoiceoverResult, assertDslDocument, captionContentCodec, createInitialMedeoDsl, defaultIdFactory, documentFormat, fromVideoDraft, isInvalidAtomic, medeoDslLoroShape, relationId, resolveDslLayout, resourceId, timeAnchorCodec, toAudioScriptViews, toVideoDraft, tupleHash };
package/dist/dsl.js CHANGED
@@ -1,3 +1,3 @@
1
- import { _t as resourceId, a as documentFormat, c as isInvalidAtomic, gt as relationId, ht as defaultIdFactory, i as assertDslDocument, o as medeoDslLoroShape, r as UnsupportedDocumentFormatError, s as captionContentCodec, t as DEFAULT_UNIT_TIME_MS, u as timeAnchorCodec, vt as tupleHash } from "./video-draft-types-BfIXM8q3.js";
2
- import { a as toAudioScriptViews, c as DslProjectionError, i as DslEditor, n as toVideoDraft, o as MedeoDsl, r as createInitialMedeoDsl, s as resolveDslLayout, t as fromVideoDraft } from "./dsl-B7-4dMjr.js";
1
+ import { St as tupleHash, bt as relationId, c as captionContentCodec, f as timeAnchorCodec, i as assertDslDocument, l as isInvalidAtomic, o as documentFormat, r as UnsupportedDocumentFormatError, s as medeoDslLoroShape, t as DEFAULT_UNIT_TIME_MS, xt as resourceId, yt as defaultIdFactory } from "./video-draft-types-DRGBN0we.js";
2
+ import { a as toAudioScriptViews, c as DslProjectionError, i as DslEditor, n as toVideoDraft, o as MedeoDsl, r as createInitialMedeoDsl, s as resolveDslLayout, t as fromVideoDraft } from "./dsl-Bvv_51Ns.js";
3
3
  export { DEFAULT_UNIT_TIME_MS, DslEditor, DslProjectionError, MedeoDsl, UnsupportedDocumentFormatError, assertDslDocument, captionContentCodec, createInitialMedeoDsl, defaultIdFactory, documentFormat, fromVideoDraft, isInvalidAtomic, medeoDslLoroShape, relationId, resolveDslLayout, resourceId, timeAnchorCodec, toAudioScriptViews, toVideoDraft, tupleHash };
@@ -1,6 +1,6 @@
1
- import { G as ResourceSystem, R as MedeoDslSnapshot, rt as TrackRole } from "./video-draft-types-0J2vfXBu.js";
2
- import { b as MedeoDsl, h as UploadText, i as AudioScriptView, m as DslEditor, o as VideoDraftProjection } from "./index-DVmvFhEn.js";
3
- import { _ as ManualSyncTransport, a as MengineDocSessionOptions, g as ManualSyncDocOptions, n as DocumentSession, r as DocumentSessionUpdateEvent } from "./index-D4Bgukns.js";
1
+ import { K as ResourceSystem, R as MedeoDslSnapshot, it as TrackRole } from "./video-draft-types-slnHaNs9.js";
2
+ import { b as MedeoDsl, h as UploadText, i as AudioScriptView, m as DslEditor, o as VideoDraftProjection } from "./index-CmuDB_Gm.js";
3
+ import { P as MengineDraftResponse, _ as ManualSyncTransport, a as MengineDocSessionOptions, g as ManualSyncDocOptions, n as DocumentSession, r as DocumentSessionUpdateEvent } from "./index-BOgr8p8C.js";
4
4
 
5
5
  //#region src/manual-sync/manual-sync-doc.d.ts
6
6
  /** Explicit synchronization over the same DSL/editor used by the live Session. */
@@ -180,6 +180,8 @@ declare class ManualSyncDoc extends ManualSyncTransport {
180
180
  }[];
181
181
  };
182
182
  projectVideoDraft(): VideoDraftProjection;
183
+ /** Read the acknowledged server revision; without a new push, confirm against the current server view. */
184
+ readPersistedDraft(updateSeq?: number): Promise<MengineDraftResponse>;
183
185
  dispose(): void;
184
186
  }
185
187
  //#endregion
@@ -1,4 +1,4 @@
1
- import { j as DslDiagnostic, o as VideoDraftContent, p as CommitOptions } from "./video-draft-types-0J2vfXBu.js";
1
+ import { j as DslDiagnostic, o as VideoDraftContent, p as CommitOptions } from "./video-draft-types-slnHaNs9.js";
2
2
  import { LoroDoc, PeerID, VersionVector } from "loro-crdt";
3
3
  import { DocState, LocalSaveState } from "@mengine/sync";
4
4
  import { BaseDocStorage, Connection, DocDiff, DocPushReceipt, DocSnapshotRecord, DocStorage, DocStorageOptions, DocUpdate, DocUpdateRecord } from "@mengine/storage";
@@ -255,7 +255,7 @@ type PushResultKind = 'ack' | 'duplicate' | 'rejected' | 'failed' | 'nothing_to_
255
255
  */
256
256
  interface PushResult {
257
257
  kind: PushResultKind;
258
- /** Server document sequence observed after a successful `ack` or `duplicate`. */
258
+ /** Server sequence from an `ack`/`duplicate`, retained on an idle retry so its revision can be verified. */
259
259
  mengineUpdateSeq?: number | undefined;
260
260
  /** @deprecated Ack-only sequence retained until consumers adopt `mengineUpdateSeq`. */
261
261
  updateSeq?: number | undefined;
@@ -301,10 +301,12 @@ interface ManualSyncDocOptions {
301
301
  }
302
302
  /** Shared explicit pull/push lifecycle; model-specific editors never implement transport. */
303
303
  declare class ManualSyncTransport {
304
- private readonly client;
304
+ protected readonly client: MengineHttpClient;
305
305
  protected readonly doc: LoroDoc;
306
306
  private watermark;
307
307
  private disposed;
308
+ /** A retry with no new bytes still verifies the revision from its last durable receipt. */
309
+ private confirmedSequence?;
308
310
  protected constructor(client: MengineHttpClient, doc: LoroDoc, watermark: VersionVector);
309
311
  editorPeerId(): PeerID;
310
312
  protected assertActive(): void;
@@ -581,6 +583,7 @@ type DocumentSessionModel<Snapshot, Editor, Document> = (doc: LoroDoc, guard: Do
581
583
  declare class DocumentSession<Snapshot, Editor, Document> {
582
584
  private readonly options;
583
585
  private readonly model;
586
+ private readonly validateDocument;
584
587
  private readonly docId;
585
588
  private readonly local;
586
589
  private readonly server;
@@ -607,7 +610,7 @@ declare class DocumentSession<Snapshot, Editor, Document> {
607
610
  * doc (e.g. nothing was edited since the last confirmed push).
608
611
  */
609
612
  private serverVVValue;
610
- constructor(options: MengineDocSessionOptions, model: DocumentSessionModel<Snapshot, Editor, Document>);
613
+ constructor(options: MengineDocSessionOptions, model: DocumentSessionModel<Snapshot, Editor, Document>, validateDocument: (doc: LoroDoc) => void);
611
614
  /**
612
615
  * Observe every push verdict, including `duplicate` and failures.
613
616
  *
@@ -1,4 +1,4 @@
1
- import { B as PhonemeSelection, J as SegmentSelection, L as FieldPatch, M as DslMutation, R as MedeoDslSnapshot, T as CaptionStyle, Y as StoredFile, Z as TextEdit, c as VideoDraftInput, h as DslIdFactory, it as Voice, j as DslDiagnostic, o as VideoDraftContent, p as CommitOptions, q as SegmentRange, rt as TrackRole, tt as TimeAnchor, w as CaptionSelection, z as Media } from "./video-draft-types-0J2vfXBu.js";
1
+ import { B as PhonemeSelection, J as SegmentRange, L as FieldPatch, M as DslMutation, Q as TextEdit, R as MedeoDslSnapshot, T as CaptionStyle, V as PhoneticScript, X as StoredFile, Y as SegmentSelection, at as Voice, c as VideoDraftInput, h as DslIdFactory, it as TrackRole, j as DslDiagnostic, nt as TimeAnchor, o as VideoDraftContent, p as CommitOptions, w as CaptionSelection, z as Media } from "./video-draft-types-slnHaNs9.js";
2
2
  import { ContainerType, Cursor, LoroDoc, PeerID, TreeID } from "loro-crdt";
3
3
 
4
4
  //#region src/dsl/action-context.d.ts
@@ -483,20 +483,12 @@ declare const medeoDslLoroShape: RootSchemaType<{
483
483
  options: {};
484
484
  };
485
485
  'phonetic-script-entities': LoroMapSchemaWithCatchall<{}, LoroMapSchema<{
486
- phoneme: /*elided*/any;
487
- system: /*elided*/any;
488
- key: /*elided*/any;
489
- storageKey: /*elided*/any;
490
- digest: /*elided*/any;
486
+ binding: /*elided*/any;
491
487
  }> & {
492
488
  options: {};
493
489
  } & {
494
490
  catchall: <C extends SchemaType>(catchallSchema: C) => LoroMapSchemaWithCatchall<{
495
- phoneme: /*elided*/any;
496
- system: /*elided*/any;
497
- key: /*elided*/any;
498
- storageKey: /*elided*/any;
499
- digest: /*elided*/any;
491
+ binding: /*elided*/any;
500
492
  }, C>;
501
493
  }> & {
502
494
  options: {};
@@ -589,8 +581,9 @@ type ContainDraftNode = Omit<Input['contain-relations'][number], 'id' | 'childre
589
581
  children: ContainDraftNode[];
590
582
  };
591
583
  type DraftFields<T> = T extends Cursor ? T : T extends (infer Item)[] ? DraftFields<Item>[] : T extends object ? { [K in keyof T as undefined extends T[K] ? never : K]: DraftFields<T[K]> } & { [K in keyof T as undefined extends T[K] ? K : never]?: DraftFields<T[K]> } : T;
592
- type MedeoDslDraft = DraftFields<Omit<Input, 'contain-relations'>> & {
584
+ type MedeoDslDraft = DraftFields<Omit<Input, 'contain-relations' | 'phonetic-script-entities'>> & {
593
585
  'contain-relations': ContainDraftNode[];
586
+ 'phonetic-script-entities': Record<string, PhoneticScript>;
594
587
  };
595
588
  //#endregion
596
589
  //#region ../medeo-dsl/src/schema/codec.d.ts
@@ -614,7 +607,7 @@ declare const captionContentCodec: AtomicCodec<SegmentSelection>;
614
607
  declare function documentFormat(doc: LoroDoc): 'dsl' | 'legacy' | 'mixed' | 'unknown';
615
608
  declare class UnsupportedDocumentFormatError extends Error {
616
609
  readonly code = "unsupported_document_format";
617
- constructor();
610
+ constructor(message?: string);
618
611
  }
619
612
  /** Run after initial import, before opening an editable handle. Empty new documents still have one Timeline. */
620
613
  declare function assertDslDocument(doc: LoroDoc): void;
@@ -691,6 +684,8 @@ declare class MedeoDsl {
691
684
  applyMutations(mutations: readonly DslMutation[], audit: DslAudit): void;
692
685
  editText(scriptId: string, segmentId: string, expectedBodyId: string, edits: readonly TextEdit[], audit: DslAudit): void;
693
686
  editCaptionText(captionId: string, expectedBodyId: string, edits: readonly TextEdit[], audit: DslAudit): void;
687
+ /** Check the session's write capability before an Action starts host-owned resource I/O. */
688
+ assertWritable(): void;
694
689
  /**
695
690
  * A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
696
691
  * include insertions at their end. K01 remains deferred.
@@ -698,6 +693,7 @@ declare class MedeoDsl {
698
693
  textRange(scriptId: string, segmentId: string, start: number, end: number): SegmentSelection;
699
694
  dispose(): void;
700
695
  private mutate;
696
+ private assertReadyToWrite;
701
697
  private notify;
702
698
  private assertActive;
703
699
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as TextMutation, A as DeepReadonly, C as CaptionContent, D as Clip, E as CaptionText, F as EntityTables, G as ResourceSystem, H as RelationTable, I as EntityValues, K as RichTextSnapshot, L as FieldPatch, M as DslMutation, N as EntityMutation, O as ContainMutation, P as EntityTable, Q as TextMark, R as MedeoDslSnapshot, S as Caption, T as CaptionStyle, U as RelationTables, V as RelationMutation, W as RelationValues, X as TextDelta, Y as StoredFile, Z as TextEdit, _ as defaultIdFactory, a as VideoDraftCaptionStyle, b as tupleHash, c as VideoDraftInput, d as VideoDraftTrack, et as TextRun, f as VideoDraftTrackItem, g as EntityIdKind, h as DslIdFactory, i as VideoDraft, it as Voice, j as DslDiagnostic, k as ContainNode, l as VideoDraftPartUnion, m as OpActor, n as DEFAULT_UNIT_TIME_MS, nt as Track, o as VideoDraftContent, p as CommitOptions, r as VideoCreationSettings, rt as TrackRole, s as VideoDraftContentPartUnion, t as CaptionDisplayCue, tt as TimeAnchor, u as VideoDraftTimeline, v as relationId, w as CaptionSelection, x as AudioScript, y as resourceId, z as Media } from "./video-draft-types-0J2vfXBu.js";
2
- import { A as MedeoDslDraft, C as UnsupportedDocumentFormatError, D as captionContentCodec, E as InvalidAtomic, F as ClipSource, I as DslClipUpdate, L as MoveDslClipsInput, M as medeoDslLoroShape, N as AddDslClip, O as isInvalidAtomic, P as AddDslClipsInput, R as ReplaceDslClipsInput, S as DslTransaction, T as documentFormat, _ as VoiceoverCaptionInput, a as toAudioScriptViews, b as MedeoDsl, c as resolveDslLayout, d as DslProjectionError, f as DslTrackLayout, g as ApplyVoiceoverInput, h as UploadText, i as AudioScriptView, j as MedeoDslLoroState, k as timeAnchorCodec, l as DslClipLayout, m as DslEditor, n as ImportedVideoDraft, o as VideoDraftProjection, p as createInitialMedeoDsl, r as fromVideoDraft, s as toVideoDraft, t as FromVideoDraftOptions, u as DslLayout, v as VoiceoverResult, w as assertDslDocument, x as DslAudit, y as CreateMedeoDslOptions, z as ClipPlacement } from "./index-DVmvFhEn.js";
3
- import { A as MenginePushRejectedError, B as MengineSyncResponse, C as PushResult, D as MengineHttpClient, E as readMengineEventStream, F as MenginePushResponse, H as base64ToBytes, I as MenginePushUpdateResponse, L as MengineRejectedResponse, M as MengineAuditResponse, N as MengineDocumentVersion, O as MengineHttpClientOptions, P as MengineDraftResponse, R as MengineSnapshotResponse, S as PullResult, T as MengineEventStreamOptions, U as bytesToBase64, V as MengineUpdateMeta, a as MengineDocSessionOptions, b as DocVersionMark, c as WaitForLocalSaveOptions, d as MengineUndoState, f as MedeoHttpDocStorage, g as ManualSyncDocOptions, h as PushOutcomeKind, i as MengineAckFailedError, j as MengineAuditEntry, k as MengineHttpRequestError, l as WaitForServerAckOptions, m as PushOutcome, o as MengineDocSyncState, p as MedeoHttpDocStorageOptions, s as MengineLocalSaveState, t as MemoryDocStorage, u as DocStorageLike, v as decodeDocVersionMark, w as PushResultKind, x as PullFailureReason, y as encodeDocVersionMark, z as MengineSseUpdateEvent } from "./index-D4Bgukns.js";
4
- import { n as MengineDocSessionUpdateEvent, r as ManualSyncDoc, t as MengineDocSession } from "./index-CcNkkQll.js";
1
+ import { $ as TextMark, A as DeepReadonly, C as CaptionContent, D as Clip, E as CaptionText, F as EntityTables, G as RelationValues, H as RelationMutation, I as EntityValues, K as ResourceSystem, L as FieldPatch, M as DslMutation, N as EntityMutation, O as ContainMutation, P as EntityTable, Q as TextEdit, R as MedeoDslSnapshot, S as Caption, T as CaptionStyle, U as RelationTable, W as RelationTables, X as StoredFile, Z as TextDelta, _ as defaultIdFactory, a as VideoDraftCaptionStyle, at as Voice, b as tupleHash, c as VideoDraftInput, d as VideoDraftTrack, et as TextMutation, f as VideoDraftTrackItem, g as EntityIdKind, h as DslIdFactory, i as VideoDraft, it as TrackRole, j as DslDiagnostic, k as ContainNode, l as VideoDraftPartUnion, m as OpActor, n as DEFAULT_UNIT_TIME_MS, nt as TimeAnchor, o as VideoDraftContent, p as CommitOptions, q as RichTextSnapshot, r as VideoCreationSettings, rt as Track, s as VideoDraftContentPartUnion, t as CaptionDisplayCue, tt as TextRun, u as VideoDraftTimeline, v as relationId, w as CaptionSelection, x as AudioScript, y as resourceId, z as Media } from "./video-draft-types-slnHaNs9.js";
2
+ import { A as MedeoDslDraft, C as UnsupportedDocumentFormatError, D as captionContentCodec, E as InvalidAtomic, F as ClipSource, I as DslClipUpdate, L as MoveDslClipsInput, M as medeoDslLoroShape, N as AddDslClip, O as isInvalidAtomic, P as AddDslClipsInput, R as ReplaceDslClipsInput, S as DslTransaction, T as documentFormat, _ as VoiceoverCaptionInput, a as toAudioScriptViews, b as MedeoDsl, c as resolveDslLayout, d as DslProjectionError, f as DslTrackLayout, g as ApplyVoiceoverInput, h as UploadText, i as AudioScriptView, j as MedeoDslLoroState, k as timeAnchorCodec, l as DslClipLayout, m as DslEditor, n as ImportedVideoDraft, o as VideoDraftProjection, p as createInitialMedeoDsl, r as fromVideoDraft, s as toVideoDraft, t as FromVideoDraftOptions, u as DslLayout, v as VoiceoverResult, w as assertDslDocument, x as DslAudit, y as CreateMedeoDslOptions, z as ClipPlacement } from "./index-CmuDB_Gm.js";
3
+ import { A as MenginePushRejectedError, B as MengineSyncResponse, C as PushResult, D as MengineHttpClient, E as readMengineEventStream, F as MenginePushResponse, H as base64ToBytes, I as MenginePushUpdateResponse, L as MengineRejectedResponse, M as MengineAuditResponse, N as MengineDocumentVersion, O as MengineHttpClientOptions, P as MengineDraftResponse, R as MengineSnapshotResponse, S as PullResult, T as MengineEventStreamOptions, U as bytesToBase64, V as MengineUpdateMeta, a as MengineDocSessionOptions, b as DocVersionMark, c as WaitForLocalSaveOptions, d as MengineUndoState, f as MedeoHttpDocStorage, g as ManualSyncDocOptions, h as PushOutcomeKind, i as MengineAckFailedError, j as MengineAuditEntry, k as MengineHttpRequestError, l as WaitForServerAckOptions, m as PushOutcome, o as MengineDocSyncState, p as MedeoHttpDocStorageOptions, s as MengineLocalSaveState, t as MemoryDocStorage, u as DocStorageLike, v as decodeDocVersionMark, w as PushResultKind, x as PullFailureReason, y as encodeDocVersionMark, z as MengineSseUpdateEvent } from "./index-BOgr8p8C.js";
4
+ import { n as MengineDocSessionUpdateEvent, r as ManualSyncDoc, t as MengineDocSession } from "./index-BGEcTwoD.js";
5
5
  export { type AddDslClip, type AddDslClipsInput, type ApplyVoiceoverInput, type AudioScript, type AudioScriptView, type Caption, type CaptionContent, type CaptionDisplayCue, type CaptionSelection, type CaptionStyle, type CaptionText, type Clip, type ClipPlacement, type ClipSource, type CommitOptions, type ContainMutation, type ContainNode, type CreateMedeoDslOptions, DEFAULT_UNIT_TIME_MS, type DeepReadonly, type DocStorageLike, type DocVersionMark, type DslAudit, type DslClipLayout, type DslClipUpdate, type DslDiagnostic, DslEditor, type DslIdFactory, type DslLayout, type DslMutation, DslProjectionError, type DslTrackLayout, type DslTransaction, type EntityIdKind, type EntityMutation, type EntityTable, type EntityTables, type EntityValues, type FieldPatch, type FromVideoDraftOptions, type ImportedVideoDraft, type InvalidAtomic, ManualSyncDoc, type ManualSyncDocOptions, MedeoDsl, type MedeoDslDraft, type MedeoDslLoroState, type MedeoDslSnapshot, MedeoHttpDocStorage, type MedeoHttpDocStorageOptions, type Media, MemoryDocStorage, MengineAckFailedError, type MengineAuditEntry, type MengineAuditResponse, MengineDocSession, type MengineDocSessionOptions, type MengineDocSessionUpdateEvent, type MengineDocSyncState, type MengineDocumentVersion, type MengineDraftResponse, type MengineEventStreamOptions, MengineHttpClient, type MengineHttpClientOptions, MengineHttpRequestError, type MengineLocalSaveState, MenginePushRejectedError, type MenginePushResponse, type MenginePushUpdateResponse, type MengineRejectedResponse, type MengineSnapshotResponse, type MengineSseUpdateEvent, type MengineSyncResponse, type MengineUndoState, type MengineUpdateMeta, type MoveDslClipsInput, type OpActor, type PullFailureReason, type PullResult, type PushOutcome, type PushOutcomeKind, type PushResult, type PushResultKind, type RelationMutation, type RelationTable, type RelationTables, type RelationValues, type ReplaceDslClipsInput, type ResourceSystem, type RichTextSnapshot, type StoredFile, type TextDelta, type TextEdit, type TextMark, type TextMutation, type TextRun, type TimeAnchor, type Track, type TrackRole, UnsupportedDocumentFormatError, type UploadText, type VideoCreationSettings, type VideoDraft, type VideoDraftCaptionStyle, type VideoDraftContent, type VideoDraftContentPartUnion, type VideoDraftInput, type VideoDraftPartUnion, type VideoDraftProjection, type VideoDraftTimeline, type VideoDraftTrack, type VideoDraftTrackItem, type Voice, type VoiceoverCaptionInput, type VoiceoverResult, type WaitForLocalSaveOptions, type WaitForServerAckOptions, assertDslDocument, base64ToBytes, bytesToBase64, captionContentCodec, createInitialMedeoDsl, decodeDocVersionMark, defaultIdFactory, documentFormat, encodeDocVersionMark, fromVideoDraft, isInvalidAtomic, medeoDslLoroShape, readMengineEventStream, relationId, resolveDslLayout, resourceId, timeAnchorCodec, toAudioScriptViews, toVideoDraft, tupleHash };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { n as bytesToBase64, t as base64ToBytes } from "./base64-D-jd-dr5.js";
2
- import { a as covers, c as readMengineEventStream, d as MenginePushRejectedError, i as openManualDocument, l as MengineHttpClient, n as MedeoHttpDocStorage, o as decodeDocVersionMark, r as ManualSyncTransport, s as encodeDocVersionMark, t as MemoryDocStorage, u as MengineHttpRequestError } from "./storage-BW3tI_ER.js";
3
- import { _t as resourceId, a as documentFormat, c as isInvalidAtomic, gt as relationId, ht as defaultIdFactory, i as assertDslDocument, o as medeoDslLoroShape, r as UnsupportedDocumentFormatError, s as captionContentCodec, t as DEFAULT_UNIT_TIME_MS, u as timeAnchorCodec, vt as tupleHash } from "./video-draft-types-BfIXM8q3.js";
4
- import { a as toAudioScriptViews, c as DslProjectionError, i as DslEditor, n as toVideoDraft, o as MedeoDsl, r as createInitialMedeoDsl, s as resolveDslLayout, t as fromVideoDraft } from "./dsl-B7-4dMjr.js";
5
- import { UndoManager, VersionVector, decodeImportBlobMeta } from "loro-crdt";
2
+ import { a as covers, c as readMengineEventStream, d as MenginePushRejectedError, i as openManualDocument, l as MengineHttpClient, n as MedeoHttpDocStorage, o as decodeDocVersionMark, r as ManualSyncTransport, s as encodeDocVersionMark, t as MemoryDocStorage, u as MengineHttpRequestError } from "./storage-evqvKS4B.js";
3
+ import { St as tupleHash, bt as relationId, c as captionContentCodec, f as timeAnchorCodec, i as assertDslDocument, l as isInvalidAtomic, o as documentFormat, r as UnsupportedDocumentFormatError, s as medeoDslLoroShape, t as DEFAULT_UNIT_TIME_MS, xt as resourceId, yt as defaultIdFactory } from "./video-draft-types-DRGBN0we.js";
4
+ import { a as toAudioScriptViews, c as DslProjectionError, i as DslEditor, n as toVideoDraft, o as MedeoDsl, r as createInitialMedeoDsl, s as resolveDslLayout, t as fromVideoDraft } from "./dsl-Bvv_51Ns.js";
5
+ import { LoroDoc, UndoManager, VersionVector, decodeImportBlobMeta } from "loro-crdt";
6
6
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
7
7
  import { DisposableSet, EventBus } from "@mengine/utils";
8
8
  //#region src/manual-sync/manual-sync-doc.ts
@@ -27,6 +27,15 @@ var ManualSyncDoc = class ManualSyncDoc extends ManualSyncTransport {
27
27
  projectVideoDraft() {
28
28
  return toVideoDraft(this.snapshot());
29
29
  }
30
+ /** Read the acknowledged server revision; without a new push, confirm against the current server view. */
31
+ async readPersistedDraft(updateSeq) {
32
+ this.assertActive();
33
+ const result = updateSeq === void 0 ? await this.client.fetchDraft() : await this.client.fetchDraftAt(updateSeq);
34
+ this.assertActive();
35
+ if (updateSeq !== void 0 && result.version.update_seq !== updateSeq) throw new Error("The projected revision does not match the push receipt");
36
+ if (!covers(VersionVector.decode(base64ToBytes(result.version.server_vv)), this.doc.oplogVersion())) throw new Error("The projected server revision does not hold the local update");
37
+ return result;
38
+ }
30
39
  dispose() {
31
40
  this.dsl.dispose();
32
41
  super.dispose();
@@ -209,6 +218,7 @@ var MengineAckFailedError = class extends Error {
209
218
  var DocumentSession = class {
210
219
  options;
211
220
  model;
221
+ validateDocument;
212
222
  docId;
213
223
  local;
214
224
  server;
@@ -235,9 +245,10 @@ var DocumentSession = class {
235
245
  * doc (e.g. nothing was edited since the last confirmed push).
236
246
  */
237
247
  serverVVValue = null;
238
- constructor(options, model) {
248
+ constructor(options, model, validateDocument) {
239
249
  this.options = options;
240
250
  this.model = model;
251
+ this.validateDocument = validateDocument;
241
252
  this.mutationGuard = new DocumentMutationGuard(options.readonlyMode);
242
253
  this.docId = options.docId;
243
254
  this.local = options.localStorage ?? new MemoryDocStorage();
@@ -441,6 +452,17 @@ var DocumentSession = class {
441
452
  await this.local.connection.waitForConnected().abortOn(this.lifetime.signal);
442
453
  if (this.destroyed) throw new Error("mengine doc session destroyed");
443
454
  if (this.options.initialSnapshot) {
455
+ const preview = new LoroDoc();
456
+ try {
457
+ if (preview.import(this.options.initialSnapshot).pending) throw new Error("Snapshot has missing dependencies");
458
+ this.validateDocument(preview);
459
+ const cached = await this.local.getDoc(this.docId);
460
+ if (cached && preview.import(cached.data).pending) throw new Error("Cached document has missing dependencies");
461
+ this.validateDocument(preview);
462
+ } finally {
463
+ preview.free();
464
+ }
465
+ if (this.destroyed) throw new Error("mengine doc session destroyed");
444
466
  await this.local.pushDocUpdate({
445
467
  docId: this.docId,
446
468
  data: this.options.initialSnapshot
@@ -637,7 +659,7 @@ var MengineDocSession = class extends DocumentSession {
637
659
  subscribe: (listener) => dsl.subscribe(listener),
638
660
  dispose: () => dsl.dispose()
639
661
  };
640
- });
662
+ }, assertDslDocument);
641
663
  }
642
664
  get dsl() {
643
665
  return this.document;
package/dist/legacy.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { i as VideoDraft, l as VideoDraftPartUnion, o as VideoDraftContent, p as CommitOptions, t as CaptionDisplayCue } from "./video-draft-types-0J2vfXBu.js";
2
- import { A as MenginePushRejectedError, B as MengineSyncResponse, C as PushResult, D as MengineHttpClient, E as readMengineEventStream, F as MenginePushResponse, H as base64ToBytes, I as MenginePushUpdateResponse, L as MengineRejectedResponse, M as MengineAuditResponse, N as MengineDocumentVersion, O as MengineHttpClientOptions, P as MengineDraftResponse, R as MengineSnapshotResponse, S as PullResult, T as MengineEventStreamOptions, U as bytesToBase64, V as MengineUpdateMeta, _ as ManualSyncTransport, a as MengineDocSessionOptions, b as DocVersionMark, d as MengineUndoState, f as MedeoHttpDocStorage, g as ManualSyncDocOptions, h as PushOutcomeKind, j as MengineAuditEntry, k as MengineHttpRequestError, l as WaitForServerAckOptions, m as PushOutcome, o as MengineDocSyncState, p as MedeoHttpDocStorageOptions, t as MemoryDocStorage, u as DocStorageLike, v as decodeDocVersionMark, w as PushResultKind, x as PullFailureReason, y as encodeDocVersionMark, z as MengineSseUpdateEvent } from "./index-D4Bgukns.js";
1
+ import { i as VideoDraft, l as VideoDraftPartUnion, o as VideoDraftContent, p as CommitOptions, t as CaptionDisplayCue } from "./video-draft-types-slnHaNs9.js";
2
+ import { A as MenginePushRejectedError, B as MengineSyncResponse, C as PushResult, D as MengineHttpClient, E as readMengineEventStream, F as MenginePushResponse, H as base64ToBytes, I as MenginePushUpdateResponse, L as MengineRejectedResponse, M as MengineAuditResponse, N as MengineDocumentVersion, O as MengineHttpClientOptions, P as MengineDraftResponse, R as MengineSnapshotResponse, S as PullResult, T as MengineEventStreamOptions, U as bytesToBase64, V as MengineUpdateMeta, _ as ManualSyncTransport, a as MengineDocSessionOptions, b as DocVersionMark, d as MengineUndoState, f as MedeoHttpDocStorage, g as ManualSyncDocOptions, h as PushOutcomeKind, j as MengineAuditEntry, k as MengineHttpRequestError, l as WaitForServerAckOptions, m as PushOutcome, o as MengineDocSyncState, p as MedeoHttpDocStorageOptions, t as MemoryDocStorage, u as DocStorageLike, v as decodeDocVersionMark, w as PushResultKind, x as PullFailureReason, y as encodeDocVersionMark, z as MengineSseUpdateEvent } from "./index-BOgr8p8C.js";
3
3
  import { LoroDoc, PeerID } from "loro-crdt";
4
4
 
5
5
  //#region ../medeo-dsl/depreacted/sandbox.d.ts
package/dist/legacy.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as bytesToBase64, t as base64ToBytes } from "./base64-D-jd-dr5.js";
2
- import { c as readMengineEventStream, d as MenginePushRejectedError, i as openManualDocument, l as MengineHttpClient, n as MedeoHttpDocStorage, o as decodeDocVersionMark, r as ManualSyncTransport, s as encodeDocVersionMark, t as MemoryDocStorage, u as MengineHttpRequestError } from "./storage-BW3tI_ER.js";
3
- import { a as documentFormat, r as UnsupportedDocumentFormatError, t as DEFAULT_UNIT_TIME_MS } from "./video-draft-types-BfIXM8q3.js";
2
+ import { c as readMengineEventStream, d as MenginePushRejectedError, i as openManualDocument, l as MengineHttpClient, n as MedeoHttpDocStorage, o as decodeDocVersionMark, r as ManualSyncTransport, s as encodeDocVersionMark, t as MemoryDocStorage, u as MengineHttpRequestError } from "./storage-evqvKS4B.js";
3
+ import { o as documentFormat, r as UnsupportedDocumentFormatError, t as DEFAULT_UNIT_TIME_MS } from "./video-draft-types-DRGBN0we.js";
4
4
  import { u as volumeSchema } from "./shared-iWnU2osE.js";
5
5
  import { sha256 } from "@noble/hashes/sha2.js";
6
6
  import { LoroDoc, LoroMap, LoroMovableList, LoroText, UndoManager } from "loro-crdt";
@@ -293,6 +293,8 @@ var ManualSyncTransport = class {
293
293
  doc;
294
294
  watermark;
295
295
  disposed = false;
296
+ /** A retry with no new bytes still verifies the revision from its last durable receipt. */
297
+ confirmedSequence;
296
298
  constructor(client, doc, watermark) {
297
299
  this.client = client;
298
300
  this.doc = doc;
@@ -395,7 +397,8 @@ var ManualSyncTransport = class {
395
397
  const localVersion = this.doc.oplogVersion();
396
398
  if (covers(this.watermark, localVersion)) return {
397
399
  kind: "nothing_to_push",
398
- collaborated: false
400
+ collaborated: false,
401
+ ...this.confirmedSequence === void 0 ? {} : { mengineUpdateSeq: this.confirmedSequence }
399
402
  };
400
403
  const update = this.doc.export({
401
404
  mode: "update",
@@ -405,6 +408,7 @@ var ManualSyncTransport = class {
405
408
  const response = await this.client.pushUpdate(update);
406
409
  const serverVV = this.serverVVFrom(response.version?.server_vv);
407
410
  if (serverVV != null) this.watermark = serverVV;
411
+ this.confirmedSequence = response.version?.update_seq ?? response.update_seq ?? void 0;
408
412
  return {
409
413
  kind: response.kind,
410
414
  mengineUpdateSeq: response.version?.update_seq ?? response.update_seq ?? void 0,
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { i as VideoDraft } from "./video-draft-types-0J2vfXBu.js";
2
- import { b as MedeoDsl } from "./index-DVmvFhEn.js";
1
+ import { i as VideoDraft } from "./video-draft-types-slnHaNs9.js";
2
+ import { b as MedeoDsl } from "./index-CmuDB_Gm.js";
3
3
  import { LoroDoc } from "loro-crdt";
4
4
 
5
5
  //#region src/testing/in-memory-mengine-server.d.ts
@@ -16,6 +16,7 @@ import { LoroDoc } from "loro-crdt";
16
16
  declare class InMemoryMengineServer {
17
17
  private readonly doc;
18
18
  private readonly updates;
19
+ private readonly revisions;
19
20
  private readonly subscribers;
20
21
  private seq;
21
22
  private hasSnapshot;
@@ -28,6 +29,7 @@ declare class InMemoryMengineServer {
28
29
  readonly fetchImpl: typeof fetch;
29
30
  private parsePath;
30
31
  private version;
32
+ private handleDraft;
31
33
  private handleSnapshot;
32
34
  private handleBootstrap;
33
35
  private handleSync;
package/dist/testing.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { n as bytesToBase64, t as base64ToBytes } from "./base64-D-jd-dr5.js";
2
- import { a as documentFormat, i as assertDslDocument } from "./video-draft-types-BfIXM8q3.js";
3
- import { i as DslEditor, n as toVideoDraft, o as MedeoDsl, r as createInitialMedeoDsl } from "./dsl-B7-4dMjr.js";
2
+ import { a as assertPhoneticBindingFormat, i as assertDslDocument, o as documentFormat } from "./video-draft-types-DRGBN0we.js";
3
+ import { i as DslEditor, n as toVideoDraft, o as MedeoDsl, r as createInitialMedeoDsl } from "./dsl-Bvv_51Ns.js";
4
4
  import { t as classifyUpdate } from "./loro-relay-doc-Bi6JKqtQ.js";
5
5
  import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
6
6
  //#region src/testing/in-memory-mengine-server.ts
@@ -17,6 +17,7 @@ import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
17
17
  var InMemoryMengineServer = class {
18
18
  doc = new LoroDoc();
19
19
  updates = [];
20
+ revisions = /* @__PURE__ */ new Map();
20
21
  subscribers = /* @__PURE__ */ new Set();
21
22
  seq = 0;
22
23
  hasSnapshot = false;
@@ -40,6 +41,7 @@ var InMemoryMengineServer = class {
40
41
  try {
41
42
  this.doc.import(dsl.doc.export({ mode: "snapshot" }));
42
43
  this.hasSnapshot = true;
44
+ this.revisions.set(0, this.doc.export({ mode: "snapshot" }));
43
45
  return new Response(await this.handleSnapshot().text(), {
44
46
  status: 201,
45
47
  headers: { "content-type": "application/json" }
@@ -48,24 +50,9 @@ var InMemoryMengineServer = class {
48
50
  dsl.dispose();
49
51
  }
50
52
  }
51
- if (path === "draft" && method === "GET") {
52
- if (!this.hasSnapshot) return this.handleSnapshot();
53
- assertDslDocument(this.doc);
54
- const dsl = new MedeoDsl(this.doc);
55
- try {
56
- return Response.json({
57
- ...toVideoDraft(dsl.getSnapshot()),
58
- version: this.version()
59
- });
60
- } catch (error) {
61
- return Response.json({ error: {
62
- code: "projection_unsupported",
63
- message: String(error)
64
- } }, { status: 422 });
65
- } finally {
66
- dsl.dispose();
67
- }
68
- }
53
+ if (path === "draft" && method === "GET") return this.handleDraft();
54
+ const revision = /^revisions\/(\d+)\/draft$/.exec(path);
55
+ if (revision && method === "GET") return this.handleDraft(Number(revision[1]));
69
56
  if (/^\/internal\/mengine\/v1\/docs\/[^/]+\/bootstrap$/.test(new URL(url).pathname) && method === "POST") return this.handleBootstrap(init);
70
57
  if (path.startsWith("sync") && method === "GET") return this.handleSync(url);
71
58
  if (path === "updates" && method === "POST") return this.handlePush(init);
@@ -82,13 +69,37 @@ var InMemoryMengineServer = class {
82
69
  const afterDocId = url.slice(start + 21);
83
70
  return afterDocId.slice(afterDocId.indexOf("/") + 1);
84
71
  }
85
- version() {
72
+ version(doc = this.doc, seq = this.seq) {
86
73
  return {
87
- update_seq: this.seq,
88
- server_vv: bytesToBase64(this.doc.oplogVersion().encode()),
89
- frontiers: bytesToBase64(encodeFrontiers(this.doc.oplogFrontiers()))
74
+ update_seq: seq,
75
+ server_vv: bytesToBase64(doc.oplogVersion().encode()),
76
+ frontiers: bytesToBase64(encodeFrontiers(doc.oplogFrontiers()))
90
77
  };
91
78
  }
79
+ handleDraft(updateSeq) {
80
+ if (!this.hasSnapshot) return this.handleSnapshot();
81
+ const seq = updateSeq ?? this.seq;
82
+ const snapshot = this.revisions.get(seq);
83
+ if (!snapshot) return Response.json({ code: "not_found" }, { status: 404 });
84
+ const doc = new LoroDoc();
85
+ doc.import(snapshot);
86
+ const dsl = new MedeoDsl(doc);
87
+ try {
88
+ assertDslDocument(doc);
89
+ return Response.json({
90
+ ...toVideoDraft(dsl.getSnapshot()),
91
+ version: this.version(doc, seq)
92
+ });
93
+ } catch (error) {
94
+ return Response.json({
95
+ code: updateSeq === void 0 ? "projection_unsupported" : "revision_unprojectable",
96
+ message: String(error)
97
+ }, { status: 422 });
98
+ } finally {
99
+ dsl.dispose();
100
+ doc.free();
101
+ }
102
+ }
92
103
  handleSnapshot() {
93
104
  if (!this.hasSnapshot) return Response.json({
94
105
  kind: "rejected",
@@ -105,6 +116,7 @@ var InMemoryMengineServer = class {
105
116
  if (body.snapshot != null && !this.hasSnapshot) {
106
117
  this.doc.import(base64ToBytes(body.snapshot));
107
118
  this.hasSnapshot = true;
119
+ this.revisions.set(0, this.doc.export({ mode: "snapshot" }));
108
120
  }
109
121
  return this.handleSnapshot();
110
122
  }
@@ -167,10 +179,16 @@ var InMemoryMengineServer = class {
167
179
  try {
168
180
  probe.import(this.doc.export({ mode: "snapshot" }));
169
181
  probe.import(data);
170
- if (documentFormat(probe) === "mixed") return Response.json({
182
+ let unsupported = documentFormat(probe) === "mixed";
183
+ if (documentFormat(probe) === "dsl") try {
184
+ assertPhoneticBindingFormat(probe);
185
+ } catch {
186
+ unsupported = true;
187
+ }
188
+ if (unsupported) return Response.json({
171
189
  kind: "rejected",
172
190
  code: "unsupported_document_format",
173
- message: "Cannot mix legacy and DSL content roots",
191
+ message: "Unsupported or mixed DSL storage format",
174
192
  server_version: serverVersion
175
193
  }, { status: 422 });
176
194
  } finally {
@@ -179,6 +197,7 @@ var InMemoryMengineServer = class {
179
197
  this.doc.import(data);
180
198
  this.hasSnapshot = true;
181
199
  this.seq += 1;
200
+ this.revisions.set(this.seq, this.doc.export({ mode: "snapshot" }));
182
201
  const meta = extractMeta(this.doc, baseVV);
183
202
  const stored = {
184
203
  updateSeq: this.seq,
@@ -1,5 +1,5 @@
1
1
  import { sha256 } from "@noble/hashes/sha2.js";
2
- import { Cursor } from "loro-crdt";
2
+ import { Cursor, isContainer } from "loro-crdt";
3
3
  //#region ../medeo-dsl/src/ids.ts
4
4
  function bytesToBase64url(bytes) {
5
5
  let binary = "";
@@ -145,8 +145,12 @@ function assertCaptionContent(value) {
145
145
  }
146
146
  const phonemeFields = {
147
147
  ...selectionFields,
148
- phonemeScript: optional$1(string)
148
+ phonemeScript: optional$1(nonempty)
149
149
  };
150
+ /** D190: omitted pronunciation uses the selected source words; an authored pronunciation is never empty. */
151
+ function assertPhonemeScript(value) {
152
+ if (value !== void 0 && !nonempty(value)) throw new Error("phonemeScript must be nonempty when provided");
153
+ }
150
154
  function assertPhoneme(value) {
151
155
  if (!segmentSelection(phonemeFields)(value)) throw new Error("Invalid phoneme");
152
156
  }
@@ -1375,6 +1379,30 @@ const phonemeCodec = codec((v) => {
1375
1379
  assertPhoneme(value);
1376
1380
  return value;
1377
1381
  });
1382
+ /** A row-owned binding couples the selection, pronunciation and its immutable file in one LWW value. */
1383
+ const phoneticBindingCodec = codec((value) => {
1384
+ assertEntity("phonetic-script-entities", value);
1385
+ assertPhoneticScript(void 0, { ...value });
1386
+ return JSON.stringify({
1387
+ phoneme: JSON.parse(phonemeCodec.encode(value.phoneme)),
1388
+ system: value.system,
1389
+ key: value.key,
1390
+ storageKey: value.storageKey,
1391
+ digest: value.digest
1392
+ });
1393
+ }, (raw) => {
1394
+ const value = JSON.parse(raw);
1395
+ if (!isRecord(value)) throw new Error("Expected a PhoneticScript binding");
1396
+ const phoneme = phonemeCodec.decode(JSON.stringify(value.phoneme));
1397
+ if (isInvalidAtomic(phoneme)) throw new Error(phoneme.reason);
1398
+ const binding = {
1399
+ ...value,
1400
+ phoneme
1401
+ };
1402
+ assertEntity("phonetic-script-entities", binding);
1403
+ assertPhoneticScript(void 0, binding);
1404
+ return binding;
1405
+ });
1378
1406
  /** A Clip's sequence marker is stored as one string, so it is taken or replaced whole. */
1379
1407
  const seqMarkerCodec = codec((v) => {
1380
1408
  assertSeqMarker(v);
@@ -1441,13 +1469,7 @@ const medeoDslLoroShape = schema({
1441
1469
  storageKey: schema.String(optional),
1442
1470
  digest: schema.String(optional)
1443
1471
  })),
1444
- "phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({
1445
- phoneme: atomicValue(phonemeCodec),
1446
- system: schema.String(optional),
1447
- key: schema.String(optional),
1448
- storageKey: schema.String(optional),
1449
- digest: schema.String(optional)
1450
- })),
1472
+ "phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({ binding: atomicValue(phoneticBindingCodec) })),
1451
1473
  "caption-entities": schema.LoroMapRecord(schema.LoroMap({
1452
1474
  selection: optionalAtomicValue(captionContentCodec),
1453
1475
  text: schema.LoroText(optional),
@@ -1499,14 +1521,24 @@ function documentFormat(doc) {
1499
1521
  }
1500
1522
  var UnsupportedDocumentFormatError = class extends Error {
1501
1523
  code = "unsupported_document_format";
1502
- constructor() {
1503
- super("Expected a new DSL document; legacy, mixed or uninitialized content is not supported");
1524
+ constructor(message = "Expected a new DSL document; legacy, mixed or uninitialized content is not supported") {
1525
+ super(message);
1504
1526
  this.name = "UnsupportedDocumentFormatError";
1505
1527
  }
1506
1528
  };
1507
1529
  /** Run after initial import, before opening an editable handle. Empty new documents still have one Timeline. */
1508
1530
  function assertDslDocument(doc) {
1509
1531
  if (documentFormat(doc) !== "dsl" || doc.getMap("timeline-entities").keys().length !== 1) throw new UnsupportedDocumentFormatError();
1532
+ assertPhoneticBindingFormat(doc);
1533
+ }
1534
+ /** This storage cutover has no field-per-fact reader or mixed-version write path. */
1535
+ function assertPhoneticBindingFormat(doc) {
1536
+ if (!Object.hasOwn(doc.getShallowValue(), "phonetic-script-entities")) return;
1537
+ const table = doc.getMap("phonetic-script-entities");
1538
+ for (const id of table.keys()) {
1539
+ const row = table.get(id);
1540
+ if (!isContainer(row) || row.kind() !== "Map" || row.keys().some((key) => key !== "binding") || typeof row.get("binding") !== "string") throw new UnsupportedDocumentFormatError("PhoneticScript storage requires one atomic binding; old DSL documents need explicit migration");
1541
+ }
1510
1542
  }
1511
1543
  //#endregion
1512
1544
  //#region ../medeo-dsl/src/schema/text-styles.ts
@@ -1516,4 +1548,4 @@ const textStyles = { language: { expand: "after" } };
1516
1548
  //#region src/dsl/projection/video-draft-types.ts
1517
1549
  const DEFAULT_UNIT_TIME_MS = 33.333;
1518
1550
  //#endregion
1519
- export { assertEntityPatch as $, getRootContainerByType as A, isValueOfContainerType as B, applyEncode as C, decodeNestedJsonValues as D, containerIdToContainerType as E, isArrayLike as F, tryUpdateToContainer as G, schemaToContainerType as H, isObject as I, getChildContainerSchema as J, valuesEqual as K, isObjectLike as L, hasTransform as M, inferContainerTypeFromValue as N, deepEqual as O, insertChildToMap as P, assertEntity as Q, isStateAndSchemaOfType as R, applyDecode as S, cidsEqual as T, stripUndefined as U, safeStringify as V, tryInferContainerType as W, getMapFieldSchema as X, getChildSchema as Y, assertCaptionContent as Z, isLoroMovableListSchema as _, resourceId as _t, documentFormat as a, assertTimeAnchor as at, schemaContainsIgnoreField as b, isInvalidAtomic as c, isRecord as ct, schema as d, relationTables as dt, assertId as et, getDefaultValue as f, validLanguage as ft, isLoroMapSchema as g, relationId as gt, isLoroListSchema as h, defaultIdFactory as ht, assertDslDocument as i, assertScriptSegment as it, hardenCidDescriptors as j, defineCidProperty as k, phonemeCodec as l, mergeFields as lt, isListLikeSchema as m, assertResourceIdentity as mt, textStyles as n, assertPhoneticScript as nt, medeoDslLoroShape as o, atomicFields as ot, isContainerSchema as p, assertRelationIdentity as pt, CID_KEY as q, UnsupportedDocumentFormatError as r, assertRelation as rt, captionContentCodec as s, entityTables as st, DEFAULT_UNIT_TIME_MS as t, assertPhoneme as tt, timeAnchorCodec as u, patchRow as ut, isLoroTreeSchema as v, tupleHash as vt, applySchemaToInferOptions as w, validateSchema as x, isRootSchemaType as y, isTreeID as z };
1551
+ export { assertCaptionContent as $, deepEqual as A, isStateAndSchemaOfType as B, validateSchema as C, cidsEqual as D, applySchemaToInferOptions as E, inferContainerTypeFromValue as F, stripUndefined as G, isValueOfContainerType as H, insertChildToMap as I, valuesEqual as J, tryInferContainerType as K, isArrayLike as L, getRootContainerByType as M, hardenCidDescriptors as N, containerIdToContainerType as O, hasTransform as P, getMapFieldSchema as Q, isObject as R, schemaContainsIgnoreField as S, tupleHash as St, applyEncode as T, safeStringify as U, isTreeID as V, schemaToContainerType as W, getChildContainerSchema as X, CID_KEY as Y, getChildSchema as Z, isLoroListSchema as _, assertRelationIdentity as _t, assertPhoneticBindingFormat as a, assertPhoneticScript as at, isLoroTreeSchema as b, relationId as bt, captionContentCodec as c, assertSeqMarker as ct, phoneticBindingCodec as d, entityTables as dt, assertEntity as et, timeAnchorCodec as f, isRecord as ft, isListLikeSchema as g, validLanguage as gt, isContainerSchema as h, relationTables as ht, assertDslDocument as i, assertPhonemeScript as it, defineCidProperty as j, decodeNestedJsonValues as k, isInvalidAtomic as l, assertTimeAnchor as lt, getDefaultValue as m, patchRow as mt, textStyles as n, assertId as nt, documentFormat as o, assertRelation as ot, schema as p, mergeFields as pt, tryUpdateToContainer as q, UnsupportedDocumentFormatError as r, assertPhoneme as rt, medeoDslLoroShape as s, assertScriptSegment as st, DEFAULT_UNIT_TIME_MS as t, assertEntityPatch as tt, phonemeCodec as u, atomicFields as ut, isLoroMapSchema as v, assertResourceIdentity as vt, applyDecode as w, isRootSchemaType as x, resourceId as xt, isLoroMovableListSchema as y, defaultIdFactory as yt, isObjectLike as z };
@@ -879,4 +879,4 @@ type VideoDraftContent = Pick<VideoDraft, 'timeline' | 'main_track' | 'above_mai
879
879
  type VideoDraftInput = Pick<VideoDraft, keyof VideoDraftContent>;
880
880
  declare const DEFAULT_UNIT_TIME_MS = 33.333;
881
881
  //#endregion
882
- export { TextMutation as $, DeepReadonly as A, PhonemeSelection as B, CaptionContent as C, Clip as D, CaptionText as E, EntityTables as F, ResourceSystem as G, RelationTable as H, EntityValues as I, SegmentSelection as J, RichTextSnapshot as K, FieldPatch as L, DslMutation as M, EntityMutation as N, ContainMutation as O, EntityTable as P, TextMark as Q, MedeoDslSnapshot as R, Caption as S, CaptionStyle$1 as T, RelationTables as U, RelationMutation as V, RelationValues as W, TextDelta as X, StoredFile as Y, TextEdit as Z, defaultIdFactory as _, VideoDraftCaptionStyle as a, tupleHash as b, VideoDraftInput as c, VideoDraftTrack as d, TextRun as et, VideoDraftTrackItem as f, EntityIdKind as g, DslIdFactory as h, VideoDraft as i, Voice as it, DslDiagnostic as j, ContainNode as k, VideoDraftPartUnion as l, OpActor as m, DEFAULT_UNIT_TIME_MS as n, Track$1 as nt, VideoDraftContent as o, CommitOptions as p, SegmentRange as q, VideoCreationSettings as r, TrackRole as rt, VideoDraftContentPartUnion as s, CaptionDisplayCue as t, TimeAnchor as tt, VideoDraftTimeline as u, relationId as v, CaptionSelection as w, AudioScript as x, resourceId as y, Media as z };
882
+ export { TextMark as $, DeepReadonly as A, PhonemeSelection as B, CaptionContent as C, Clip as D, CaptionText as E, EntityTables as F, RelationValues as G, RelationMutation as H, EntityValues as I, SegmentRange as J, ResourceSystem as K, FieldPatch as L, DslMutation as M, EntityMutation as N, ContainMutation as O, EntityTable as P, TextEdit as Q, MedeoDslSnapshot as R, Caption as S, CaptionStyle$1 as T, RelationTable as U, PhoneticScript as V, RelationTables as W, StoredFile as X, SegmentSelection as Y, TextDelta as Z, defaultIdFactory as _, VideoDraftCaptionStyle as a, Voice as at, tupleHash as b, VideoDraftInput as c, VideoDraftTrack as d, TextMutation as et, VideoDraftTrackItem as f, EntityIdKind as g, DslIdFactory as h, VideoDraft as i, TrackRole as it, DslDiagnostic as j, ContainNode as k, VideoDraftPartUnion as l, OpActor as m, DEFAULT_UNIT_TIME_MS as n, TimeAnchor as nt, VideoDraftContent as o, CommitOptions as p, RichTextSnapshot as q, VideoCreationSettings as r, Track$1 as rt, VideoDraftContentPartUnion as s, CaptionDisplayCue as t, TextRun as tt, VideoDraftTimeline as u, relationId as v, CaptionSelection as w, AudioScript as x, resourceId as y, Media as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mengine/medeo-client",
3
- "version": "2.1.1-dsl.22",
3
+ "version": "2.1.1-dsl.24",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,9 +29,9 @@
29
29
  "@noble/hashes": "^2.4.0",
30
30
  "immer": "^10.2.0",
31
31
  "zod": "^4.4.3",
32
- "@mengine/storage": "2.1.1-dsl.22",
33
- "@mengine/sync": "2.1.1-dsl.22",
34
- "@mengine/utils": "2.1.1-dsl.22"
32
+ "@mengine/storage": "2.1.1-dsl.24",
33
+ "@mengine/sync": "2.1.1-dsl.24",
34
+ "@mengine/utils": "2.1.1-dsl.24"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",
@@ -42,8 +42,8 @@
42
42
  "typescript": "^6.0.3",
43
43
  "vite-plugin-wasm": "^3.6.0",
44
44
  "vite-plus": "^0.1.23",
45
- "@mengine/idl-codegen": "0.0.0",
46
- "@mengine/medeo-dsl": "0.0.0"
45
+ "@mengine/medeo-dsl": "0.0.0",
46
+ "@mengine/idl-codegen": "0.0.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "loro-crdt": "^1.16.1"