@mengine/medeo-client 1.3.1-alpha.9 → 1.4.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,9 @@
1
1
  import { u as volumeSchema } from "./shared-iWnU2osE.js";
2
2
  import { Mirror, schema } from "loro-mirror";
3
3
  import { z } from "zod";
4
- import { LoroDoc } from "loro-crdt";
4
+ import { LoroDoc, LoroMap, LoroMovableList, LoroText, UndoManager } from "loro-crdt";
5
+ import { sha256 } from "@noble/hashes/sha2.js";
6
+ import { bytesToHex } from "@noble/hashes/utils.js";
5
7
  import { produce } from "immer";
6
8
  //#region src/client/base64.ts
7
9
  function bytesToBase64(bytes) {
@@ -1181,7 +1183,7 @@ function toVideoDocument(draft) {
1181
1183
  owner_id: draft.owner_id,
1182
1184
  thumbnail_storage_key: draft.thumbnail_storage_key,
1183
1185
  chat_session_id: draft.chat_session_id,
1184
- video_creation_settings: clone(draft.video_creation_settings),
1186
+ video_creation_settings: clone$1(draft.video_creation_settings),
1185
1187
  version: draft.version
1186
1188
  },
1187
1189
  timeline: draft.timeline == null ? void 0 : { unit_time_ms: draft.timeline.unit_time_ms },
@@ -1327,7 +1329,7 @@ function fromVideoDocument(document) {
1327
1329
  duration_ms: view.durationMs,
1328
1330
  unit_time_ms: document.timeline?.unit_time_ms ?? 33.333
1329
1331
  },
1330
- video_creation_settings: clone(document.meta.video_creation_settings),
1332
+ video_creation_settings: clone$1(document.meta.video_creation_settings),
1331
1333
  chat_session_id: document.meta.chat_session_id,
1332
1334
  main_track: draftTrack(mainTrack, view.absByPartId),
1333
1335
  above_main_tracks: aboveTracks.map((t) => draftTrack(t, view.absByPartId)),
@@ -1359,25 +1361,25 @@ function toReadViewPartLibrary(partLibrary, durationMs, derivedFillerPartIds) {
1359
1361
  for (const [partId, part] of Object.entries(partLibrary)) {
1360
1362
  if (derivedFillerPartIds.has(partId)) continue;
1361
1363
  if (part.video_clip != null) {
1362
- const clip = clone(part.video_clip);
1364
+ const clip = clone$1(part.video_clip);
1363
1365
  out[partId] = { video_clip: {
1364
1366
  ...clip,
1365
1367
  duration_ms: effectiveVideoClipDurationMs(clip)
1366
1368
  } };
1367
1369
  } else if (part.speech != null) {
1368
- const speech = clone(part.speech);
1370
+ const speech = clone$1(part.speech);
1369
1371
  out[partId] = { speech: {
1370
1372
  ...speech,
1371
1373
  duration_ms: speech.media_duration_ms
1372
1374
  } };
1373
1375
  } else if (part.caption != null) {
1374
- const caption = clone(part.caption);
1376
+ const caption = clone$1(part.caption);
1375
1377
  out[partId] = { caption: {
1376
1378
  ...caption,
1377
1379
  duration_ms: caption.initial_duration_ms
1378
1380
  } };
1379
1381
  } else if (part.bgm != null) out[partId] = { bgm: {
1380
- ...clone(part.bgm),
1382
+ ...clone$1(part.bgm),
1381
1383
  duration_ms: durationMs
1382
1384
  } };
1383
1385
  }
@@ -1385,7 +1387,7 @@ function toReadViewPartLibrary(partLibrary, durationMs, derivedFillerPartIds) {
1385
1387
  }
1386
1388
  /** Deep-clone a part payload, dropping the derived read-view `duration_ms`. */
1387
1389
  function omitDurationMs(part) {
1388
- const { duration_ms: _drop, ...rest } = clone(part);
1390
+ const { duration_ms: _drop, ...rest } = clone$1(part);
1389
1391
  return rest;
1390
1392
  }
1391
1393
  /**
@@ -1410,7 +1412,7 @@ function withTrimWindowFromDuration(clip) {
1410
1412
  }
1411
1413
  /** Deep-clone a part payload, returning its `duration_ms` separately from the rest. */
1412
1414
  function splitDurationMs(part) {
1413
- const { duration_ms, ...rest } = clone(part);
1415
+ const { duration_ms, ...rest } = clone$1(part);
1414
1416
  return {
1415
1417
  duration_ms,
1416
1418
  rest
@@ -1428,11 +1430,11 @@ function draftTrack(track, absByPartId) {
1428
1430
  }))
1429
1431
  };
1430
1432
  }
1431
- function clone(value) {
1432
- if (Array.isArray(value)) return value.map((item) => clone(item));
1433
+ function clone$1(value) {
1434
+ if (Array.isArray(value)) return value.map((item) => clone$1(item));
1433
1435
  if (value != null && typeof value === "object") {
1434
1436
  const result = {};
1435
- for (const [key, child] of Object.entries(value)) result[key] = clone(child);
1437
+ for (const [key, child] of Object.entries(value)) result[key] = clone$1(child);
1436
1438
  return result;
1437
1439
  }
1438
1440
  return value;
@@ -1539,7 +1541,7 @@ function createId(value, label) {
1539
1541
  //#region ../medeo-dsl/src/json-values.ts
1540
1542
  const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
1541
1543
  function isJsonObject(value) {
1542
- return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
1544
+ return typeof value === "object" && value !== null && !Array.isArray(value) && isJsonValue(value, /* @__PURE__ */ new Set());
1543
1545
  }
1544
1546
  function isJsonValue(value, ancestors) {
1545
1547
  if (value === null || typeof value === "string" || typeof value === "boolean") return true;
@@ -1782,9 +1784,8 @@ function entityToRow(entity) {
1782
1784
  //#region ../medeo-dsl/src/invariants.ts
1783
1785
  /**
1784
1786
  * Media variants own their Asset identity directly: a `physical-asset`
1785
- * Relation may not bind them. Caption text is composed domain content, so
1786
- * Captions do not bind Physical Assets either; the Relation vocabulary stays
1787
- * open for future physical pipeline semantics.
1787
+ * Relation may not bind them. Caption may associate with a physical text Asset
1788
+ * independently of its AudioScript composition.
1788
1789
  */
1789
1790
  function validateMediaAssetIdentity(entity, index) {
1790
1791
  return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
@@ -1943,11 +1944,6 @@ function validatePhoneticComposition(phonetic, index, entities = []) {
1943
1944
  }
1944
1945
  function validateVariantComposition(variant, index, entities, kind) {
1945
1946
  const issues = [];
1946
- if (kind === "caption" && ofKind([...index.relationsOf(variant)], "physical-asset").length > 0) issues.push({
1947
- code: "caption_physical_asset_forbidden",
1948
- entityId: variant.entityId,
1949
- message: "Caption text is composed from its AudioScript; Caption must not bind a Physical Asset"
1950
- });
1951
1947
  const rows = {
1952
1948
  entities: collectRelatedEntities([...entities, variant], index).entities.map((ref) => entityToRow(ref.current())),
1953
1949
  relations: []
@@ -3307,6 +3303,670 @@ function videoSpeed(content, marker, placement) {
3307
3303
  };
3308
3304
  }
3309
3305
  //#endregion
3306
+ //#region src/entity-crdt/json-containers.ts
3307
+ const atomicFields = new Set([
3308
+ "sequence",
3309
+ "sourceRange",
3310
+ "targetRange",
3311
+ "duration",
3312
+ "extent",
3313
+ "timeRemapping",
3314
+ "textRange",
3315
+ "external"
3316
+ ]);
3317
+ const textFields = new Set(["text", "phonemeScript"]);
3318
+ const orderedFields = new Set([
3319
+ "segments",
3320
+ "selections",
3321
+ "baseEntityIds"
3322
+ ]);
3323
+ function equalJson(a, b) {
3324
+ return canonicalJson(a) === canonicalJson(b);
3325
+ }
3326
+ function canonicalJson(value) {
3327
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
3328
+ if (value !== null && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
3329
+ return JSON.stringify(value);
3330
+ }
3331
+ /** Compile only the changed fields; untouched containers retain their operation identity. */
3332
+ function writeObject(map, before, after) {
3333
+ for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) map.delete(key);
3334
+ for (const [key, value] of Object.entries(after)) {
3335
+ if (equalJson(before[key], value)) continue;
3336
+ if (atomicFields.has(key)) map.set(key, value);
3337
+ else if (typeof value === "string" && textFields.has(key)) map.ensureMergeableText(key).update(value);
3338
+ else if (Array.isArray(value) && orderedFields.has(key)) writeList(map.ensureMergeableMovableList(key), value);
3339
+ else if (isJsonObject(value)) writeObject(map.ensureMergeableMap(key), isJsonObject(before[key]) ? before[key] : {}, value);
3340
+ else map.set(key, value);
3341
+ }
3342
+ }
3343
+ function itemKey(value) {
3344
+ if (typeof value === "string") return value;
3345
+ if (isJsonObject(value) && typeof value.segmentId === "string") return value.segmentId;
3346
+ throw new Error("Collaborative lists require stable segmentId or unique string identities");
3347
+ }
3348
+ function readItem(list, index) {
3349
+ const value = list.get(index);
3350
+ return value instanceof LoroMap ? value.toJSON() : value;
3351
+ }
3352
+ function writeList(list, values) {
3353
+ const wanted = values.map(itemKey);
3354
+ if (new Set(wanted).size !== wanted.length) throw new Error("Duplicate collaborative list identity");
3355
+ for (let index = list.length - 1; index >= 0; index--) if (!wanted.includes(itemKey(readItem(list, index)))) list.delete(index, 1);
3356
+ for (let index = 0; index < values.length; index++) {
3357
+ const value = values[index];
3358
+ const key = wanted[index];
3359
+ let found = -1;
3360
+ for (let cursor = index; cursor < list.length; cursor++) if (itemKey(readItem(list, cursor)) === key) {
3361
+ found = cursor;
3362
+ break;
3363
+ }
3364
+ if (found === -1) if (isJsonObject(value)) writeObject(list.insertContainer(index, new LoroMap()), {}, value);
3365
+ else list.insert(index, value);
3366
+ else {
3367
+ if (found !== index) list.move(found, index);
3368
+ const current = list.get(index);
3369
+ if (isJsonObject(value)) {
3370
+ if (!(current instanceof LoroMap)) throw new Error("Collaborative list item changed type");
3371
+ writeObject(current, current.toJSON(), value);
3372
+ }
3373
+ }
3374
+ }
3375
+ }
3376
+ function requireText(map, path) {
3377
+ let value = map;
3378
+ for (const part of path) if (value instanceof LoroMap && typeof part === "string") value = value.get(part);
3379
+ else if (value instanceof LoroMovableList && typeof part === "number") value = value.get(part);
3380
+ else throw new Error("Invalid collaborative text path");
3381
+ if (!(value instanceof LoroText)) throw new Error("Field is not collaborative text");
3382
+ return value;
3383
+ }
3384
+ //#endregion
3385
+ //#region src/entity-crdt/ordering.ts
3386
+ /** Domain order is scoped by membership; relation storage has no global order. */
3387
+ function entityOrderGroups(rows) {
3388
+ const entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
3389
+ const groups = /* @__PURE__ */ new Map();
3390
+ for (const relation of rows.relations) {
3391
+ const childKind = relation.relationKind === "timeline-track" ? "track" : relation.relationKind === "track-clip" ? "clip" : null;
3392
+ if (!childKind) continue;
3393
+ const endpoints = [relation.endpoint0EntityId, relation.endpoint1EntityId];
3394
+ const childId = endpoints.find((id) => entities.get(id)?.entityKind === childKind);
3395
+ if (!childId) continue;
3396
+ const child = entities.get(childId);
3397
+ if (typeof child.payload.order !== "number") continue;
3398
+ const parent = endpoints.find((id) => id !== childId);
3399
+ const values = groups.get(parent) ?? [];
3400
+ values.push({
3401
+ id: childId,
3402
+ order: child.payload.order
3403
+ });
3404
+ groups.set(parent, values);
3405
+ }
3406
+ return new Map([...groups].map(([parent, values]) => [parent, values.sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).map((item) => item.id)]));
3407
+ }
3408
+ function ordering(doc) {
3409
+ const map = doc.getMap("medeo").get("ordering");
3410
+ if (!(map instanceof LoroMap)) throw new Error("Missing native entity ordering");
3411
+ return map;
3412
+ }
3413
+ /** Returns a derived ordinal while the stable element retains its native move identity. */
3414
+ function entityOrdinal(doc, root) {
3415
+ let found;
3416
+ for (const key of ordering(doc).keys()) {
3417
+ const list = ordering(doc).get(key);
3418
+ if (!(list instanceof LoroMovableList)) throw new Error("Invalid entity order container");
3419
+ const index = list.toArray().indexOf(root);
3420
+ if (index < 0) continue;
3421
+ if (found !== void 0) throw new Error("Entity belongs to multiple ordered groups");
3422
+ found = index;
3423
+ }
3424
+ return found;
3425
+ }
3426
+ /** Move existing elements, insert new identities and delete removed members on the causal list. */
3427
+ function writeEntityOrder(doc, group, roots, token) {
3428
+ if (new Set(roots).size !== roots.length) throw new Error("Duplicate ordered entity identity");
3429
+ const list = ordering(doc).ensureMergeableMovableList(group);
3430
+ const before = list.toArray();
3431
+ for (let index = list.length - 1; index >= 0; index--) if (!roots.includes(String(list.get(index)))) list.delete(index, 1);
3432
+ roots.forEach((root, index) => {
3433
+ const current = list.toArray().indexOf(root);
3434
+ if (current < 0) list.insert(index, root);
3435
+ else if (current !== index) list.move(current, index);
3436
+ });
3437
+ if (token) {
3438
+ const editing = doc.getMap("medeo").get("editing");
3439
+ for (const root of new Set([...before, ...roots])) {
3440
+ if (before.indexOf(root) < 0 || before.indexOf(root) === roots.indexOf(root)) continue;
3441
+ editing.get(root).get("changes").set(token, true);
3442
+ }
3443
+ }
3444
+ }
3445
+ //#endregion
3446
+ //#region src/entity-crdt/entity-document.ts
3447
+ const LORO_ENTITY_SCHEMA = "medeo.entities.loro.v1";
3448
+ const MATERIALIZE_ORIGIN = "medeo.materialize";
3449
+ function hash(value) {
3450
+ return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
3451
+ }
3452
+ function clone(value) {
3453
+ return structuredClone(value);
3454
+ }
3455
+ function root(doc) {
3456
+ return doc.getMap("medeo");
3457
+ }
3458
+ function requireMap(parent, key) {
3459
+ const value = parent.get(key);
3460
+ if (!(value instanceof LoroMap)) throw new Error(`Missing Loro map ${key}`);
3461
+ return value;
3462
+ }
3463
+ function section(doc, key) {
3464
+ return requireMap(root(doc), key);
3465
+ }
3466
+ function workspace(doc, id) {
3467
+ const value = section(doc, "editing").get(id);
3468
+ if (!(value instanceof LoroMap)) throw new Error(`Unknown entity editing identity ${id}`);
3469
+ return value;
3470
+ }
3471
+ function content(map) {
3472
+ return requireMap(map, "content").toJSON();
3473
+ }
3474
+ function entityContent(doc, key) {
3475
+ const payload = content(workspace(doc, key));
3476
+ const order = entityOrdinal(doc, key);
3477
+ return order === void 0 ? payload : {
3478
+ ...payload,
3479
+ order
3480
+ };
3481
+ }
3482
+ function bases(map) {
3483
+ const links = requireMap(map, "bases");
3484
+ return links.keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((key) => String(links.get(key)));
3485
+ }
3486
+ function owned(payload) {
3487
+ const { baseEntityIds: _bases, ...fields } = payload;
3488
+ return fields;
3489
+ }
3490
+ function baseIds(payload) {
3491
+ const ids = payload.baseEntityIds ?? [];
3492
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string") || new Set(ids).size !== ids.length) throw new Error("baseEntityIds must contain unique entity IDs");
3493
+ return ids;
3494
+ }
3495
+ function family(doc, id) {
3496
+ const value = section(doc, "origins").get(id);
3497
+ if (typeof value === "string") return value;
3498
+ for (const [key, value] of currentIds(doc)) if (value === id) return key;
3499
+ throw new Error(`Unknown entity ${id}`);
3500
+ }
3501
+ function currentIds(doc) {
3502
+ const result = /* @__PURE__ */ new Map();
3503
+ for (const key of section(doc, "editing").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
3504
+ const map = workspace(doc, key);
3505
+ if (map.get("deleted") === true) continue;
3506
+ const tokens = requireMap(map, "changes").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3507
+ const initial = section(doc, "entities").get(key);
3508
+ const { id: _id, kind: _kind, ...initialPayload } = isJsonObject(initial) ? initial : {};
3509
+ const payload = entityContent(doc, key);
3510
+ const changed = tokens.length > 0 || initial !== void 0 && !equalJson(owned(initialPayload), payload);
3511
+ result.set(key, changed ? `entity_${hash([
3512
+ key,
3513
+ tokens,
3514
+ payload
3515
+ ])}` : key);
3516
+ }
3517
+ return result;
3518
+ }
3519
+ function currentId(doc, id, ids = currentIds(doc)) {
3520
+ const resolved = ids.get(family(doc, id));
3521
+ if (resolved === void 0) throw new Error(`Referenced entity ${id} was deleted`);
3522
+ return resolved;
3523
+ }
3524
+ /** Plain domain objects assembled from native Loro state, never a second writable authority. */
3525
+ function readEntityDocument(doc, allowSelectionConflicts = false) {
3526
+ if (root(doc).get("schema") !== "medeo.entities.loro.v1") throw new Error("Document has no Loro entity state");
3527
+ const ids = currentIds(doc);
3528
+ const entities = [...ids].map(([key, entityId]) => {
3529
+ const map = workspace(doc, key);
3530
+ const baseEntityIds = bases(map).map((id) => currentId(doc, id, ids));
3531
+ return {
3532
+ entityId: createEntityId(entityId),
3533
+ entityKind: map.get("kind"),
3534
+ payload: {
3535
+ ...entityContent(doc, key),
3536
+ ...baseEntityIds.length ? { baseEntityIds } : {}
3537
+ }
3538
+ };
3539
+ });
3540
+ const relations = [];
3541
+ for (const key of section(doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
3542
+ const map = section(doc, "relations").get(key);
3543
+ if (!(map instanceof LoroMap) || map.get("deleted") === true) continue;
3544
+ const original = map.get("identity");
3545
+ if (original.endpoints.some((id) => !ids.has(family(doc, id)))) continue;
3546
+ const endpoints = original.endpoints.map((id) => currentId(doc, id, ids));
3547
+ const relationId = equalJson(endpoints, original.endpoints) ? key : `relation_${hash([key, endpoints])}`;
3548
+ relations.push({
3549
+ relationId: createRelationId(relationId),
3550
+ relationKind: original.kind,
3551
+ endpoint0EntityId: createEntityId(endpoints[0]),
3552
+ endpoint1EntityId: createEntityId(endpoints[1]),
3553
+ metadata: requireMap(map, "metadata").toJSON(),
3554
+ trace: requireMap(map, "trace").toJSON()
3555
+ });
3556
+ }
3557
+ const project = {};
3558
+ for (const [key, value] of Object.entries(section(doc, "attachments").toJSON())) {
3559
+ if (typeof value !== "string") throw new Error(`Invalid project attachment ${key}`);
3560
+ project[key] = currentId(doc, value, ids);
3561
+ }
3562
+ const state = {
3563
+ rows: {
3564
+ entities,
3565
+ relations
3566
+ },
3567
+ project
3568
+ };
3569
+ if (!allowSelectionConflicts) for (const [key, id] of ids) {
3570
+ const fingerprint = selectionFingerprint(state.rows, id);
3571
+ if (fingerprint !== void 0 && workspace(doc, key).get("selectionBaseline") !== fingerprint) throw new Error(`Caption text selection conflict at ${id}: source text changed; revise its text ranges explicitly`);
3572
+ }
3573
+ return state;
3574
+ }
3575
+ /** Native editing containers survive domain version changes and merge independently. */
3576
+ var LoroEntityDocument = class LoroEntityDocument {
3577
+ doc;
3578
+ validate;
3579
+ undoManager;
3580
+ constructor(doc, validate) {
3581
+ this.doc = doc;
3582
+ this.validate = validate;
3583
+ this.undoManager = new UndoManager(doc, {
3584
+ mergeInterval: 0,
3585
+ excludeOriginPrefixes: [MATERIALIZE_ORIGIN]
3586
+ });
3587
+ }
3588
+ /** Open a causal snapshot using the same Loro runtime as the compiler. */
3589
+ static fromSnapshot(snapshot, validate) {
3590
+ const doc = new LoroDoc();
3591
+ doc.import(snapshot);
3592
+ readEntityDocument(doc);
3593
+ return new LoroEntityDocument(doc, validate);
3594
+ }
3595
+ static create(rows, project = {}, peerId) {
3596
+ const doc = new LoroDoc();
3597
+ if (peerId !== void 0) doc.setPeerId(peerId);
3598
+ root(doc).set("schema", LORO_ENTITY_SCHEMA);
3599
+ for (const name of [
3600
+ "editing",
3601
+ "entities",
3602
+ "origins",
3603
+ "relations",
3604
+ "project",
3605
+ "attachments",
3606
+ "ordering"
3607
+ ]) root(doc).ensureMergeableMap(name);
3608
+ for (const row of rows.entities) apply(doc, {
3609
+ type: "create",
3610
+ row
3611
+ });
3612
+ for (const row of rows.relations) apply(doc, {
3613
+ type: "relation",
3614
+ row
3615
+ });
3616
+ for (const [slot, entityId] of Object.entries(project)) apply(doc, {
3617
+ type: "attach",
3618
+ slot,
3619
+ entityId
3620
+ });
3621
+ for (const [group, ids] of entityOrderGroups(rows)) writeEntityOrder(doc, group, ids);
3622
+ const initialState = readEntityDocument(doc, true);
3623
+ for (const row of initialState.rows.entities) {
3624
+ const fingerprint = selectionFingerprint(initialState.rows, row.entityId);
3625
+ if (fingerprint !== void 0) workspace(doc, row.entityId).set("selectionBaseline", fingerprint);
3626
+ }
3627
+ assertState(readEntityDocument(doc));
3628
+ materialize(doc);
3629
+ doc.commit({ origin: MATERIALIZE_ORIGIN });
3630
+ return new LoroEntityDocument(doc);
3631
+ }
3632
+ snapshot() {
3633
+ return readEntityDocument(this.doc);
3634
+ }
3635
+ relationsOf(entityId) {
3636
+ const id = currentIds(this.doc).get(family(this.doc, entityId));
3637
+ if (id === void 0) return [];
3638
+ return this.snapshot().rows.relations.filter((row) => row.endpoint0EntityId === id || row.endpoint1EntityId === id);
3639
+ }
3640
+ getVersion(id) {
3641
+ const entity = section(this.doc, "entities").get(id);
3642
+ if (!isJsonObject(entity)) return void 0;
3643
+ const { id: entityId, kind, ...payload } = entity;
3644
+ if (typeof entityId !== "string") throw new Error("Invalid stored entity ID");
3645
+ return {
3646
+ entityId: createEntityId(entityId),
3647
+ entityKind: kind,
3648
+ payload
3649
+ };
3650
+ }
3651
+ /** The callback and every domain validation execute on a causally complete fork first. */
3652
+ transact(edit) {
3653
+ const probe = this.doc.fork();
3654
+ const draft = new LoroEntityDraft(probe);
3655
+ edit(draft);
3656
+ draft.captureSelectionBaselines();
3657
+ const state = readEntityDocument(probe);
3658
+ assertState(state);
3659
+ this.validate?.(state);
3660
+ materialize(probe);
3661
+ const from = this.doc.oplogVersion();
3662
+ for (const command of draft.commands) apply(this.doc, command);
3663
+ this.doc.commit({ origin: "medeo.edit" });
3664
+ materialize(this.doc);
3665
+ this.doc.commit({ origin: MATERIALIZE_ORIGIN });
3666
+ return this.doc.export({
3667
+ mode: "update",
3668
+ from
3669
+ });
3670
+ }
3671
+ /** Import preserves native operation identities; projection errors never discard remote operations. */
3672
+ import(update) {
3673
+ this.doc.import(update);
3674
+ const state = this.snapshot();
3675
+ assertState(state);
3676
+ this.validate?.(state);
3677
+ materialize(this.doc);
3678
+ this.doc.commit({ origin: MATERIALIZE_ORIGIN });
3679
+ }
3680
+ canUndo() {
3681
+ return this.undoManager.canUndo();
3682
+ }
3683
+ canRedo() {
3684
+ return this.undoManager.canRedo();
3685
+ }
3686
+ undo() {
3687
+ return this.historyEdit(() => this.undoManager.undo());
3688
+ }
3689
+ redo() {
3690
+ return this.historyEdit(() => this.undoManager.redo());
3691
+ }
3692
+ historyEdit(run) {
3693
+ const before = this.snapshot();
3694
+ if (!run()) return false;
3695
+ const after = this.snapshot();
3696
+ for (const row of after.rows.entities) {
3697
+ const id = family(this.doc, row.entityId);
3698
+ const previous = before.rows.entities.find((item) => family(this.doc, item.entityId) === id);
3699
+ if (previous && !equalJson(owned(previous.payload), owned(row.payload))) workspace(this.doc, id).ensureMergeableMap("changes").set(globalThis.crypto.randomUUID(), true);
3700
+ }
3701
+ materialize(this.doc);
3702
+ this.doc.commit({ origin: MATERIALIZE_ORIGIN });
3703
+ return true;
3704
+ }
3705
+ };
3706
+ var LoroEntityDraft = class {
3707
+ doc;
3708
+ commands = [];
3709
+ selectedRoots = /* @__PURE__ */ new Set();
3710
+ constructor(doc) {
3711
+ this.doc = doc;
3712
+ }
3713
+ snapshot() {
3714
+ return readEntityDocument(this.doc, true);
3715
+ }
3716
+ order(groupId, entityIds) {
3717
+ this.record({
3718
+ type: "order",
3719
+ group: family(this.doc, groupId),
3720
+ roots: entityIds.map((id) => family(this.doc, id)),
3721
+ token: globalThis.crypto.randomUUID()
3722
+ });
3723
+ }
3724
+ create(row) {
3725
+ this.record({
3726
+ type: "create",
3727
+ row: clone(row)
3728
+ });
3729
+ }
3730
+ delete(entityId) {
3731
+ this.record({
3732
+ type: "delete",
3733
+ root: family(this.doc, entityId)
3734
+ });
3735
+ }
3736
+ attach(slot, entityId) {
3737
+ this.record({
3738
+ type: "attach",
3739
+ slot,
3740
+ entityId
3741
+ });
3742
+ }
3743
+ link(row) {
3744
+ this.record({
3745
+ type: "relation",
3746
+ row: clone(row)
3747
+ });
3748
+ }
3749
+ patchRelation(relationId, metadata, trace) {
3750
+ this.record({
3751
+ type: "relation-fields",
3752
+ root: this.relationRoot(relationId),
3753
+ metadata: clone(metadata),
3754
+ trace: clone(trace)
3755
+ });
3756
+ }
3757
+ unlink(relationId) {
3758
+ this.record({
3759
+ type: "unlink",
3760
+ root: this.relationRoot(relationId)
3761
+ });
3762
+ }
3763
+ relationRoot(relationId) {
3764
+ const ids = currentIds(this.doc);
3765
+ for (const key of section(this.doc, "relations").keys()) {
3766
+ const map = section(this.doc, "relations").get(key);
3767
+ if (!(map instanceof LoroMap) || map.get("deleted") === true) continue;
3768
+ const identity = map.get("identity");
3769
+ if (identity.endpoints.some((id) => !ids.has(family(this.doc, id)))) continue;
3770
+ const endpoints = identity.endpoints.map((id) => currentId(this.doc, id, ids));
3771
+ if ((equalJson(endpoints, identity.endpoints) ? key : `relation_${hash([key, endpoints])}`) === relationId) return key;
3772
+ }
3773
+ throw new Error(`Unknown relation ${relationId}`);
3774
+ }
3775
+ update(entityId, fields) {
3776
+ const state = this.snapshot();
3777
+ const id = createEntityId(currentId(this.doc, entityId));
3778
+ const updates = /* @__PURE__ */ new Map();
3779
+ for (const [field, value] of Object.entries(fields)) {
3780
+ const owner = resolveEntityFieldOwner(state.rows, id, field);
3781
+ const payload = updates.get(owner) ?? clone(state.rows.entities.find((row) => row.entityId === owner).payload);
3782
+ updates.set(owner, {
3783
+ ...payload,
3784
+ [field]: clone(value)
3785
+ });
3786
+ }
3787
+ for (const [owner, payload] of updates) this.replaceOwned(owner, payload);
3788
+ }
3789
+ declare(entityId, fields) {
3790
+ const row = this.snapshot().rows.entities.find((row) => row.entityId === currentId(this.doc, entityId));
3791
+ if (!row) throw new Error(`Unknown entity ${entityId}`);
3792
+ this.replaceOwned(entityId, {
3793
+ ...row.payload,
3794
+ ...fields
3795
+ });
3796
+ }
3797
+ replaceOwned(entityId, payload) {
3798
+ this.record({
3799
+ type: "write",
3800
+ root: family(this.doc, entityId),
3801
+ payload: clone(payload),
3802
+ token: globalThis.crypto.randomUUID()
3803
+ });
3804
+ }
3805
+ editText(entityId, path, start, deleteCount, text) {
3806
+ if (typeof path[0] !== "string") throw new Error("Text path must start with a field");
3807
+ const owner = resolveEntityFieldOwner(this.snapshot().rows, createEntityId(currentId(this.doc, entityId)), path[0]);
3808
+ this.record({
3809
+ type: "text",
3810
+ root: family(this.doc, owner),
3811
+ path: [...path],
3812
+ start,
3813
+ deleteCount,
3814
+ text,
3815
+ token: globalThis.crypto.randomUUID()
3816
+ });
3817
+ }
3818
+ captureSelectionBaselines() {
3819
+ const state = this.snapshot();
3820
+ for (const key of this.selectedRoots) {
3821
+ const id = currentIds(this.doc).get(key);
3822
+ if (id === void 0) continue;
3823
+ const fingerprint = selectionFingerprint(state.rows, id);
3824
+ if (fingerprint !== void 0) this.record({
3825
+ type: "selection-baseline",
3826
+ root: key,
3827
+ fingerprint
3828
+ });
3829
+ }
3830
+ }
3831
+ record(command) {
3832
+ if (command.type === "create") this.selectedRoots.add(command.row.entityId);
3833
+ if (command.type === "write" && !equalJson(content(workspace(this.doc, command.root)).selections, command.payload.selections)) this.selectedRoots.add(command.root);
3834
+ apply(this.doc, command);
3835
+ this.commands.push(command);
3836
+ }
3837
+ };
3838
+ function apply(doc, command) {
3839
+ switch (command.type) {
3840
+ case "selection-baseline":
3841
+ workspace(doc, command.root).set("selectionBaseline", command.fingerprint);
3842
+ break;
3843
+ case "order":
3844
+ writeEntityOrder(doc, command.group, command.roots, command.token);
3845
+ break;
3846
+ case "create": {
3847
+ const row = command.row;
3848
+ if (section(doc, "origins").get(row.entityId) !== void 0) throw new Error(`Duplicate entity ${row.entityId}`);
3849
+ const map = section(doc, "editing").ensureMergeableMap(row.entityId);
3850
+ map.set("kind", row.entityKind);
3851
+ map.ensureMergeableMap("changes");
3852
+ section(doc, "origins").set(row.entityId, row.entityId);
3853
+ writeObject(map.ensureMergeableMap("content"), {}, owned(row.payload));
3854
+ writeBases(doc, map, baseIds(row.payload));
3855
+ break;
3856
+ }
3857
+ case "write": {
3858
+ const map = workspace(doc, command.root);
3859
+ const before = content(map);
3860
+ const after = owned(command.payload);
3861
+ if (!equalJson(before, after)) {
3862
+ writeObject(map.ensureMergeableMap("content"), before, after);
3863
+ map.ensureMergeableMap("changes").set(command.token, true);
3864
+ }
3865
+ writeBases(doc, map, baseIds(command.payload));
3866
+ break;
3867
+ }
3868
+ case "text": {
3869
+ const map = workspace(doc, command.root);
3870
+ const text = requireText(map.ensureMergeableMap("content"), command.path);
3871
+ if (!Number.isSafeInteger(command.start) || !Number.isSafeInteger(command.deleteCount) || command.start < 0 || command.deleteCount < 0 || command.start + command.deleteCount > Array.from(text.toString()).length) throw new Error("Invalid text edit range");
3872
+ const start = text.convertPos(command.start, "unicode", "utf16");
3873
+ const end = text.convertPos(command.start + command.deleteCount, "unicode", "utf16");
3874
+ text.splice(start, end - start, command.text);
3875
+ map.ensureMergeableMap("changes").set(command.token, true);
3876
+ break;
3877
+ }
3878
+ case "delete":
3879
+ workspace(doc, command.root).set("deleted", true);
3880
+ break;
3881
+ case "relation": {
3882
+ const row = command.row;
3883
+ const map = section(doc, "relations").ensureMergeableMap(row.relationId);
3884
+ const identity = {
3885
+ kind: row.relationKind,
3886
+ endpoints: [row.endpoint0EntityId, row.endpoint1EntityId]
3887
+ };
3888
+ const previous = map.get("identity");
3889
+ if (previous !== void 0 && !equalJson(previous, identity)) throw new Error("Relation identity cannot change kind or endpoint positions");
3890
+ if (previous === void 0) map.set("identity", identity);
3891
+ if (map.get("deleted") === true) throw new Error("Deleted relation IDs cannot be reused");
3892
+ const metadata = map.ensureMergeableMap("metadata");
3893
+ const trace = map.ensureMergeableMap("trace");
3894
+ writeObject(metadata, metadata.toJSON(), row.metadata);
3895
+ writeObject(trace, trace.toJSON(), row.trace);
3896
+ break;
3897
+ }
3898
+ case "unlink": {
3899
+ const map = section(doc, "relations").get(command.root);
3900
+ if (!(map instanceof LoroMap)) throw new Error("Unknown relation");
3901
+ map.set("deleted", true);
3902
+ break;
3903
+ }
3904
+ case "relation-fields": {
3905
+ const map = section(doc, "relations").get(command.root);
3906
+ if (!(map instanceof LoroMap)) throw new Error("Unknown relation");
3907
+ const metadata = map.ensureMergeableMap("metadata");
3908
+ const trace = map.ensureMergeableMap("trace");
3909
+ writeObject(metadata, metadata.toJSON(), command.metadata);
3910
+ writeObject(trace, trace.toJSON(), command.trace);
3911
+ break;
3912
+ }
3913
+ case "attach":
3914
+ section(doc, "attachments").set(command.slot, command.entityId);
3915
+ break;
3916
+ }
3917
+ }
3918
+ function writeBases(doc, map, ids) {
3919
+ const links = map.ensureMergeableMap("bases");
3920
+ const wanted = new Map(ids.map((id) => {
3921
+ const origin = section(doc, "origins").get(id);
3922
+ return [typeof origin === "string" ? origin : [...currentIds(doc)].find(([, current]) => current === id)?.[0] ?? id, id];
3923
+ }));
3924
+ if (wanted.size !== ids.length) throw new Error("Duplicate base entity family");
3925
+ for (const key of links.keys()) if (!wanted.has(key)) links.delete(key);
3926
+ for (const [key, id] of wanted) if (links.get(key) === void 0) links.set(key, id);
3927
+ }
3928
+ /** Numeric sub-spans must not silently quote different text after a collaborative edit. */
3929
+ function selectionFingerprint(rows, id) {
3930
+ const row = rows.entities.find((row) => row.entityId === id);
3931
+ if (row?.entityKind !== "caption" || !Array.isArray(row.payload.selections)) return void 0;
3932
+ const ranged = row.payload.selections.filter((selection) => isJsonObject(selection) && selection.textRange !== void 0);
3933
+ if (ranged.length === 0) return void 0;
3934
+ const segments = assembleEntityContent(rows, row.entityId).payload.segments;
3935
+ if (!Array.isArray(segments)) throw new Error("Caption text selection has no source segments");
3936
+ return hash(ranged.map((selection) => {
3937
+ if (!isJsonObject(selection)) throw new Error("Invalid Caption text selection");
3938
+ const source = segments.find((segment) => isJsonObject(segment) && segment.segmentId === selection.segmentId);
3939
+ if (!isJsonObject(source) || typeof source.text !== "string") throw new Error("Caption text selection source is missing");
3940
+ return [selection, source.text];
3941
+ }));
3942
+ }
3943
+ function assertState(state) {
3944
+ for (const row of state.rows.entities) assembleEntityContent(state.rows, row.entityId);
3945
+ const ids = new Set(state.rows.entities.map((row) => String(row.entityId)));
3946
+ for (const row of state.rows.relations) if (!ids.has(row.endpoint0EntityId) || !ids.has(row.endpoint1EntityId)) throw new Error("Relation has a missing endpoint");
3947
+ }
3948
+ function materialize(doc) {
3949
+ const state = readEntityDocument(doc);
3950
+ const byId = section(doc, "entities");
3951
+ const ids = currentIds(doc);
3952
+ for (const [source, id] of ids) {
3953
+ const origins = section(doc, "origins");
3954
+ if (origins.get(id) !== source) origins.set(id, source);
3955
+ }
3956
+ for (const row of state.rows.entities) {
3957
+ const next = {
3958
+ id: row.entityId,
3959
+ kind: row.entityKind,
3960
+ ...row.payload
3961
+ };
3962
+ const previous = byId.get(row.entityId);
3963
+ if (isJsonObject(previous) && !equalJson(owned(previous), owned(next))) throw new Error("Immutable entity version collision");
3964
+ if (!equalJson(previous, next)) byId.set(row.entityId, next);
3965
+ }
3966
+ const project = section(doc, "project");
3967
+ for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
3968
+ }
3969
+ //#endregion
3310
3970
  //#region src/document/mirror-read.ts
3311
3971
  /**
3312
3972
  * Project the mirror state (`VideoDocumentDraft`) into the authoritative
@@ -3398,7 +4058,9 @@ var MirrorVideoDocumentAdapter = class {
3398
4058
  });
3399
4059
  }
3400
4060
  snapshot() {
3401
- return readVideoDocumentFromDraft(this.mirror.getState());
4061
+ const document = readVideoDocumentFromDraft(this.mirror.getState());
4062
+ if (this.doc.getMap("medeo").get("schema") === "medeo.entities.loro.v1") return projectEntityTimeline(readEntityDocument(this.doc).rows, document.meta);
4063
+ return document;
3402
4064
  }
3403
4065
  /**
3404
4066
  * True once the doc holds real document content. A fresh mirror over an empty
@@ -3620,4 +4282,4 @@ function createPlainMemoryAdapter(document, options) {
3620
4282
  return new PlainMemoryAdapter(document, options);
3621
4283
  }
3622
4284
  //#endregion
3623
- export { isEmptyVideoClip as $, buildSpeechHostMap as A, ensureLaneTrack as B, variantBaseEntityIds as C, hasSequence as D, createRelationId as E, assertValidVideoDocument as F, arrangeMainTrackSeamlessly as G, laneTrackId as H, validateVideoDocument as I, recalculateTimelineDuration as J, fillMainTrackTimeGaps as K, partUnionSchema as L, fromVideoDocument as M, toVideoDocument as N, isMediaAssetVariantKind as O, VideoDocumentValidationError as P, TIMELINE_SKELETON_DURATION_MS as Q, videoDocumentSchema as R, updateEntityFields as S, createEntityId as T, solveVideoDocument as U, findLaneTrack as V, cascadeAfterVideoClipChanges as W, resolveSpeechOverlapByShiftingVideos as X, resolveAllSpeechOverlaps as Y, syncAggregatedClipsTimePosition as Z, assembleEntityContent as _, applyEntityTimelineProjection as a, effectiveVideoClipDurationMs as at, findComposedAudioScript as b, writeVideoDocumentToDraft as c, recordEntries as ct, EntityProjectionGraph as d, bytesToBase64 as dt, partDurationMs as et, resolveEntityTimelineLayout as f, assembleCaptionContent as g, ScriptCompositionError as h, MirrorVideoDocumentAdapter as i, VIDEO_DOCUMENT_SCHEMA_VERSION as it, derivePositionFromAbs as j, buildInitialVideoDocument as k, readVideoDocumentFromDraft as l, videoDocumentMirrorSchema as lt, decodeEntityRelationRows as m, createPlainMemoryAdapter as n, DEFAULT_UNIT_TIME_MS as nt, createMirrorVideoDocument as o, speedOf as ot, EntityTimelineProjectionError as p, reassignSpeechesToVideoClipsByTime as q, generatePartId as r, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as rt, createMirrorVideoDocumentAdapter as s, partUnionToDraft as st, PlainMemoryAdapter as t, safeDurationMs as tt, projectEntityTimeline as u, base64ToBytes as ut, assemblePhoneticScriptContent as v, isJsonObject as w, selectAudioScriptSegments as x, assembleScriptText as y, LANE_KINDS_IN_STACK_ORDER as z };
4285
+ export { reassignSpeechesToVideoClipsByTime as $, isJsonObject as A, VideoDocumentValidationError as B, assembleEntityContent as C, selectAudioScriptSegments as D, findComposedAudioScript as E, buildInitialVideoDocument as F, LANE_KINDS_IN_STACK_ORDER as G, validateVideoDocument as H, buildSpeechHostMap as I, laneTrackId as J, ensureLaneTrack as K, derivePositionFromAbs as L, createRelationId as M, hasSequence as N, updateEntityFields as O, isMediaAssetVariantKind as P, fillMainTrackTimeGaps as Q, fromVideoDocument as R, assembleCaptionContent as S, assembleScriptText as T, partUnionSchema as U, assertValidVideoDocument as V, videoDocumentSchema as W, cascadeAfterVideoClipChanges as X, solveVideoDocument as Y, arrangeMainTrackSeamlessly as Z, EntityProjectionGraph as _, bytesToBase64 as _t, applyEntityTimelineProjection as a, isEmptyVideoClip as at, decodeEntityRelationRows as b, writeVideoDocumentToDraft as c, DEFAULT_UNIT_TIME_MS as ct, LoroEntityDocument as d, effectiveVideoClipDurationMs as dt, recalculateTimelineDuration as et, LoroEntityDraft as f, speedOf as ft, projectEntityTimeline as g, base64ToBytes as gt, equalJson as h, videoDocumentMirrorSchema as ht, MirrorVideoDocumentAdapter as i, TIMELINE_SKELETON_DURATION_MS as it, createEntityId as j, variantBaseEntityIds as k, readVideoDocumentFromDraft as l, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as lt, entityOrderGroups as m, recordEntries as mt, createPlainMemoryAdapter as n, resolveSpeechOverlapByShiftingVideos as nt, createMirrorVideoDocument as o, partDurationMs as ot, readEntityDocument as p, partUnionToDraft as pt, findLaneTrack as q, generatePartId as r, syncAggregatedClipsTimePosition as rt, createMirrorVideoDocumentAdapter as s, safeDurationMs as st, PlainMemoryAdapter as t, resolveAllSpeechOverlaps as tt, LORO_ENTITY_SCHEMA as u, VIDEO_DOCUMENT_SCHEMA_VERSION as ut, resolveEntityTimelineLayout as v, assemblePhoneticScriptContent as w, ScriptCompositionError as x, EntityTimelineProjectionError as y, toVideoDocument as z };