@mengine/medeo-client 2.0.1-alpha.6 → 2.0.1-alpha.8
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 +31 -0
- package/dist/{document-N-8GwtOX.js → document-DdwzfZ1g.js} +259 -211
- package/dist/{index-B31KeDzA.d.ts → index-CDrPUhBH.d.ts} +276 -181
- package/dist/index.d.ts +113 -143
- package/dist/index.js +469 -164
- package/dist/{loro-relay-doc-ssdYpuef.js → loro-relay-doc-cJSY-uau.js} +7 -2
- package/dist/relay.js +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +4 -19
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as findLaneTrack, A as assemblePhoneticScriptContent, B as isMediaAssetVariantKind, C as EntityProjectionGraph, D as ScriptCompositionError, E as decodeEntityRelationRows, F as variantBaseEntityIds, G as toVideoDocument, H as buildSpeechHostMap, I as isJsonObject, J as validateVideoDocument, K as VideoDocumentValidationError, L as createEntityId, M as findComposedAudioScript, N as selectAudioScriptSegment, O as assembleCaptionContent, P as updateEntityFields, Q as ensureLaneTrack, R as createRelationId, S as projectEntityTimeline, St as bytesToBase64, T as EntityTimelineProjectionError, U as derivePositionFromAbs, V as buildInitialVideoDocument, W as fromVideoDocument, X as videoDocumentSchema, Y as partUnionSchema, Z as LANE_KINDS_IN_STACK_ORDER, _ as hasAudioScriptAsset, _t as speedOf, a as createMirrorVideoDocument, at as fillMainTrackTimeGaps, b as assertFieldChanges, bt as videoDocumentMirrorSchema, c as DocumentMutationGuard, ct as resolveAllSpeechOverlaps, d as LoroEntityDraft, dt as TIMELINE_SKELETON_DURATION_MS, et as hasResolvedPlacement, f as readEntityDocument, ft as isEmptyVideoClip, g as audioScriptAssetHash, gt as effectiveVideoClipDurationMs, h as audioScriptAssetFields, ht as DEFAULT_UNIT_TIME_MS, i as MirrorVideoDocumentAdapter, it as arrangeMainTrackSeamlessly, j as assembleScriptText, k as assembleEntityContent, l as LORO_ENTITY_SCHEMA, lt as resolveSpeechOverlapByShiftingVideos, m as audioScriptAssetContent, mt as safeDurationMs, n as createPlainMemoryAdapter, nt as solveVideoDocument, o as createMirrorVideoDocumentAdapter, ot as reassignSpeechesToVideoClipsByTime, p as entityOrderGroups, pt as partDurationMs, q as assertValidVideoDocument, r as generatePartId, rt as cascadeAfterVideoClipChanges, s as readVideoDocumentFromDraft, st as recalculateTimelineDuration, t as PlainMemoryAdapter, tt as laneTrackId, u as LoroEntityDocument, ut as syncAggregatedClipsTimePosition, v as equalJson, vt as partUnionToDraft, w as resolveEntityTimelineLayout, x as resolveFieldPath, xt as base64ToBytes, y as applyFieldChanges, yt as recordEntries, z as hasSequence } from "./document-DdwzfZ1g.js";
|
|
2
2
|
import { addSpeechesInputSchema, addVideoClipsInputSchema, adjustBgmVolumeInputSchema, adjustSpeechVolumeInputSchema, adjustVideoClipDurationInputSchema, adjustVideoClipVolumeInputSchema, changeSpeechScriptInputSchema, changeSpeechVoiceInputSchema, deleteBgmInputSchema, deleteSpeechesInputSchema, deleteVideoClipsInputSchema, moveSpeechesInputSchema, moveVideoClipsByAnchorInputSchema, moveVideoClipsInputSchema, replaceVideoClipContentInputSchema, replaceVideoClipSequenceInputSchema, setBgmInputSchema, setCaptionStyleInputSchema, setCaptionVisibilityInputSchema, setVideoClipSpeedShiftInputSchema, t as schemas_exports } from "./schemas.js";
|
|
3
|
-
import { LoroDoc, VersionVector } from "loro-crdt";
|
|
3
|
+
import { LoroDoc, UndoManager, VersionVector } from "loro-crdt";
|
|
4
4
|
import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
|
|
5
5
|
import { DisposableSet, EventBus, Task } from "@mengine/utils";
|
|
6
6
|
import { BaseDocStorage, DummyConnection } from "@mengine/storage";
|
|
@@ -60,12 +60,6 @@ var MengineHttpClient = class {
|
|
|
60
60
|
async fetchSnapshot() {
|
|
61
61
|
return await this.requestJson("snapshot");
|
|
62
62
|
}
|
|
63
|
-
async bootstrapSnapshot(snapshot) {
|
|
64
|
-
return await this.requestJson("bootstrap", {
|
|
65
|
-
method: "POST",
|
|
66
|
-
body: JSON.stringify({ snapshot: bytesToBase64(snapshot) })
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
63
|
/**
|
|
70
64
|
* Loro VV-diff pull: send the caller's oplog `VersionVector.encode` as `from`
|
|
71
65
|
* (omit for a full pull) and receive exactly the updates it is missing plus
|
|
@@ -168,9 +162,6 @@ async function safeReadJson$1(response) {
|
|
|
168
162
|
//#endregion
|
|
169
163
|
//#region src/client/sse.ts
|
|
170
164
|
async function readMengineEventStream(options) {
|
|
171
|
-
const onUpdate = (event) => {
|
|
172
|
-
options.onUpdate(event);
|
|
173
|
-
};
|
|
174
165
|
const response = await options.client.fetch(options.client.eventsUrl(), {
|
|
175
166
|
headers: options.client.headers(),
|
|
176
167
|
signal: options.signal
|
|
@@ -189,15 +180,19 @@ async function readMengineEventStream(options) {
|
|
|
189
180
|
while (splitAt >= 0) {
|
|
190
181
|
const frame = buffer.slice(0, splitAt);
|
|
191
182
|
buffer = buffer.slice(buffer[splitAt] === "\r" ? splitAt + 4 : splitAt + 2);
|
|
192
|
-
handleSseFrame(frame,
|
|
183
|
+
handleSseFrame(frame, options);
|
|
193
184
|
splitAt = findSseFrameBoundary(buffer);
|
|
194
185
|
}
|
|
195
186
|
}
|
|
196
187
|
}
|
|
197
|
-
function handleSseFrame(frame,
|
|
188
|
+
function handleSseFrame(frame, options) {
|
|
198
189
|
const event = parseSseFrame(frame);
|
|
190
|
+
if (event.event === "resync") {
|
|
191
|
+
options.onResync?.();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
199
194
|
if (event.event !== "update" || event.data.length === 0) return;
|
|
200
|
-
onUpdate(JSON.parse(event.data.join("\n")));
|
|
195
|
+
options.onUpdate(JSON.parse(event.data.join("\n")));
|
|
201
196
|
}
|
|
202
197
|
function parseSseFrame(frame) {
|
|
203
198
|
let event = null;
|
|
@@ -584,36 +579,55 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
584
579
|
"image",
|
|
585
580
|
"video",
|
|
586
581
|
"audio",
|
|
587
|
-
"
|
|
582
|
+
"voiceover"
|
|
588
583
|
].includes(fact.kind)) throw new Error("Unsupported media kind");
|
|
589
584
|
if (fact.kind !== "image" && (!Number.isFinite(fact.durationMs) || fact.durationMs <= 0)) throw new Error(`${fact.kind} ${fact.assetId} requires its factual positive duration`);
|
|
590
|
-
if ((fact.kind === "audio" || fact.kind === "
|
|
585
|
+
if ((fact.kind === "audio" || fact.kind === "voiceover") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
|
|
591
586
|
decodeEntityRelationRows(initial, numericCoordinates);
|
|
592
587
|
assertCanonicalEditorResources(initial);
|
|
593
588
|
const entities = structuredClone([...initial.entities]);
|
|
594
589
|
const relations = structuredClone([...initial.relations]);
|
|
595
|
-
const system = fact.kind === "
|
|
590
|
+
const system = fact.kind === "voiceover" ? "memota-speech" : "memota";
|
|
591
|
+
const entityKind = fact.kind === "voiceover" ? "audio" : fact.kind;
|
|
592
|
+
const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
|
|
596
593
|
const existing = entities.filter((row) => {
|
|
597
594
|
const external = row.payload.external;
|
|
598
595
|
return isMediaAssetVariantKind(row.entityKind) && isJsonObject(external) && external.system === system && external.key === fact.assetId;
|
|
599
596
|
});
|
|
597
|
+
const requireMatchingVariant = (row) => {
|
|
598
|
+
const extent = row.payload.extent;
|
|
599
|
+
if (row.entityKind !== entityKind || fact.kind !== "image" && (!isJsonObject(extent) || extent.start !== 0 || extent.end !== fact.durationMs)) throw new Error(`Media facts conflict with existing identity for ${fact.assetId}`);
|
|
600
|
+
if (fact.storageKey !== void 0 && row.payload.storageKey !== fact.storageKey) {
|
|
601
|
+
if (row.payload.storageKey !== void 0) throw new Error(`Media facts conflict with existing storage key for ${fact.assetId}`);
|
|
602
|
+
return {
|
|
603
|
+
...row,
|
|
604
|
+
payload: {
|
|
605
|
+
...row.payload,
|
|
606
|
+
storageKey: fact.storageKey
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return row;
|
|
611
|
+
};
|
|
600
612
|
if (existing.length > 1) throw new Error(`Duplicate media identities for ${fact.assetId}; reuse ${existing[0].entityId}`);
|
|
601
613
|
if (existing[0] !== void 0) {
|
|
602
|
-
const variant = requireMatchingVariant(existing[0]
|
|
614
|
+
const variant = requireMatchingVariant(existing[0]);
|
|
603
615
|
replaceEntity$1(entities, variant.entityId, variant);
|
|
616
|
+
const voiceEntityId = bindVoiceTimbre(entities, relations, variant.entityId, timbre, idFactory);
|
|
604
617
|
return {
|
|
605
618
|
rows: {
|
|
606
619
|
entities,
|
|
607
620
|
relations
|
|
608
621
|
},
|
|
609
|
-
contentEntityId: variant.entityId
|
|
622
|
+
contentEntityId: variant.entityId,
|
|
623
|
+
...voiceEntityId
|
|
610
624
|
};
|
|
611
625
|
}
|
|
612
626
|
const entityId = createEntityId(idFactory("entity"));
|
|
613
627
|
if (entities.some((row) => row.entityId === entityId)) throw new Error(`Duplicate entity id ${entityId}`);
|
|
614
628
|
const variant = {
|
|
615
629
|
entityId,
|
|
616
|
-
entityKind
|
|
630
|
+
entityKind,
|
|
617
631
|
payload: {
|
|
618
632
|
extent: fact.kind === "image" ? {
|
|
619
633
|
kind: "unbounded",
|
|
@@ -629,15 +643,11 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
629
643
|
system,
|
|
630
644
|
key: fact.assetId
|
|
631
645
|
},
|
|
632
|
-
...fact.storageKey === void 0 ? {} : { storageKey: fact.storageKey }
|
|
633
|
-
...fact.kind === "voice" && fact.voice !== void 0 ? { voice: {
|
|
634
|
-
system: fact.voice.system,
|
|
635
|
-
key: fact.voice.key,
|
|
636
|
-
...fact.voice.name === void 0 ? {} : { name: fact.voice.name }
|
|
637
|
-
} } : {}
|
|
646
|
+
...fact.storageKey === void 0 ? {} : { storageKey: fact.storageKey }
|
|
638
647
|
}
|
|
639
648
|
};
|
|
640
649
|
entities.push(variant);
|
|
650
|
+
const voiceEntityId = bindVoiceTimbre(entities, relations, entityId, timbre, idFactory);
|
|
641
651
|
const rows = {
|
|
642
652
|
entities,
|
|
643
653
|
relations
|
|
@@ -646,33 +656,58 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
646
656
|
assertCanonicalEditorResources(rows);
|
|
647
657
|
return {
|
|
648
658
|
rows,
|
|
649
|
-
contentEntityId: entityId
|
|
659
|
+
contentEntityId: entityId,
|
|
660
|
+
...voiceEntityId
|
|
650
661
|
};
|
|
651
662
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
};
|
|
663
|
+
/**
|
|
664
|
+
* Attach the timbre a synthesized voiceover was rendered with. One Voice
|
|
665
|
+
* identity is shared by every take naming the same library entry, so an
|
|
666
|
+
* existing row is reused rather than duplicated. Recorded media declares no
|
|
667
|
+
* timbre and keeps no link.
|
|
668
|
+
*/
|
|
669
|
+
function bindVoiceTimbre(entities, relations, audioEntityId, descriptor, idFactory) {
|
|
670
|
+
const linked = relations.filter((relation) => relation.relationKind === "voice-timbre" && (relation.endpoint0EntityId === audioEntityId || relation.endpoint1EntityId === audioEntityId));
|
|
671
|
+
if (linked.length > 1) throw new Error(`Voiceover ${audioEntityId} has multiple Voice timbre relations`);
|
|
672
|
+
if (descriptor === void 0) {
|
|
673
|
+
if (linked.length > 0) throw new Error(`Recorded media ${audioEntityId} cannot keep a synthesized Voice timbre`);
|
|
674
|
+
return {};
|
|
664
675
|
}
|
|
665
|
-
|
|
676
|
+
const voice = entities.find((row) => row.entityKind === "voice" && sameVoiceDescriptor(row.payload.voice, descriptor)) ?? appendVoiceIdentity(entities, descriptor, idFactory);
|
|
677
|
+
const existing = linked[0];
|
|
678
|
+
if (existing === void 0) relations.push({
|
|
679
|
+
relationId: createRelationId(idFactory("relation")),
|
|
680
|
+
relationKind: "voice-timbre",
|
|
681
|
+
endpoint0EntityId: createEntityId(audioEntityId),
|
|
682
|
+
endpoint1EntityId: createEntityId(voice.entityId),
|
|
683
|
+
metadata: {},
|
|
684
|
+
trace: {}
|
|
685
|
+
});
|
|
686
|
+
else if (existing.endpoint0EntityId !== audioEntityId || existing.endpoint1EntityId !== voice.entityId) throw new Error(`Voiceover ${audioEntityId} already carries a different Voice timbre`);
|
|
687
|
+
return { voiceEntityId: voice.entityId };
|
|
688
|
+
}
|
|
689
|
+
function appendVoiceIdentity(entities, descriptor, idFactory) {
|
|
690
|
+
const voice = {
|
|
691
|
+
entityId: createEntityId(idFactory("entity")),
|
|
692
|
+
entityKind: "voice",
|
|
693
|
+
payload: { voice: {
|
|
694
|
+
system: descriptor.system,
|
|
695
|
+
key: descriptor.key,
|
|
696
|
+
...descriptor.name === void 0 ? {} : { name: descriptor.name }
|
|
697
|
+
} }
|
|
698
|
+
};
|
|
699
|
+
entities.push(voice);
|
|
700
|
+
return voice;
|
|
701
|
+
}
|
|
702
|
+
/** Two takes share a Voice identity only when they name the same library entry. */
|
|
703
|
+
function sameVoiceDescriptor(stored, descriptor) {
|
|
704
|
+
return isJsonObject(stored) && stored.system === descriptor.system && stored.key === descriptor.key && stored.name === descriptor.name;
|
|
666
705
|
}
|
|
667
706
|
function replaceEntity$1(entities, entityId, replacement) {
|
|
668
707
|
const index = entities.findIndex((row) => row.entityId === entityId);
|
|
669
708
|
if (index < 0) throw new Error(`Entity id ${entityId} does not exist`);
|
|
670
709
|
entities[index] = replacement;
|
|
671
710
|
}
|
|
672
|
-
function sameVoice(left, right) {
|
|
673
|
-
if (right === void 0) return left === void 0;
|
|
674
|
-
return isJsonObject(left) && left.system === right.system && left.key === right.key && left.name === right.name && Object.keys(left).every((key) => key === "system" || key === "key" || key === "name");
|
|
675
|
-
}
|
|
676
711
|
//#endregion
|
|
677
712
|
//#region src/entity-editor/entity-timeline-editor.ts
|
|
678
713
|
/**
|
|
@@ -1208,20 +1243,20 @@ var EntityTimelineEditor = class {
|
|
|
1208
1243
|
hostClipEntityId: input.hostClipEntityId,
|
|
1209
1244
|
anchorOffset: input.anchorOffset
|
|
1210
1245
|
};
|
|
1211
|
-
if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.anchorOffset !== void 0)) throw new Error("Specify
|
|
1246
|
+
if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.anchorOffset !== void 0)) throw new Error("Specify voiceover placement or hostClipEntityId/anchorOffset, not both");
|
|
1212
1247
|
requireVolume(input.volume);
|
|
1213
1248
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1214
1249
|
replaceRows(draft, imported.rows);
|
|
1215
|
-
const
|
|
1250
|
+
const voiceover = requireEntityKind(draft, imported.contentEntityId, "audio");
|
|
1216
1251
|
const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
|
|
1217
|
-
this.ensurePhoneticRender(draft,
|
|
1252
|
+
this.ensurePhoneticRender(draft, voiceover.entityId, phonetic.entityId);
|
|
1218
1253
|
const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
|
|
1219
1254
|
const existing = draft.entities.find((row) => row.entityId === input.voiceoverClipEntityId);
|
|
1220
1255
|
let voiceoverClipEntityId;
|
|
1221
1256
|
if (existing === void 0) voiceoverClipEntityId = this.insertPlacedClipIntoDraft(draft, {
|
|
1222
1257
|
clipEntityId: input.voiceoverClipEntityId,
|
|
1223
1258
|
trackEntityId: speechTrack.entityId,
|
|
1224
|
-
contentEntityId:
|
|
1259
|
+
contentEntityId: voiceover.entityId,
|
|
1225
1260
|
sourceRange: {
|
|
1226
1261
|
start: 0,
|
|
1227
1262
|
end: input.media.durationMs
|
|
@@ -1245,7 +1280,7 @@ var EntityTimelineEditor = class {
|
|
|
1245
1280
|
});
|
|
1246
1281
|
this.relinkRelation(draft, topology.markerContentRelation, {
|
|
1247
1282
|
endpoint0EntityId: topology.marker.entityId,
|
|
1248
|
-
endpoint1EntityId:
|
|
1283
|
+
endpoint1EntityId: voiceover.entityId
|
|
1249
1284
|
});
|
|
1250
1285
|
this.applyClipPlacement(draft, topology.clip.entityId, placement);
|
|
1251
1286
|
const placed = requireClipTopology(draft, topology.clip.entityId);
|
|
@@ -1264,11 +1299,12 @@ var EntityTimelineEditor = class {
|
|
|
1264
1299
|
}
|
|
1265
1300
|
});
|
|
1266
1301
|
}
|
|
1267
|
-
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId,
|
|
1302
|
+
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceover.entityId, phonetic.entityId, input.captions);
|
|
1268
1303
|
this.resolveOneVoiceoverOverlap(draft);
|
|
1269
1304
|
return {
|
|
1270
1305
|
voiceoverClipEntityId,
|
|
1271
|
-
|
|
1306
|
+
voiceoverAudioEntityId: voiceover.entityId,
|
|
1307
|
+
...imported.voiceEntityId === void 0 ? {} : { voiceEntityId: imported.voiceEntityId },
|
|
1272
1308
|
phoneticScriptEntityId: phonetic.entityId,
|
|
1273
1309
|
audioScriptEntityId: requireComposedAudioScript(draft, phonetic.entityId),
|
|
1274
1310
|
captionClipEntityIds: input.captions.map((caption) => caption.captionClipEntityId)
|
|
@@ -1474,23 +1510,23 @@ var EntityTimelineEditor = class {
|
|
|
1474
1510
|
draft.relations.push(this.createRelation(draft, "timeline-track", timeline.entityId, track.entityId));
|
|
1475
1511
|
return track;
|
|
1476
1512
|
}
|
|
1477
|
-
/** The existing pronunciation variant a
|
|
1513
|
+
/** The existing pronunciation variant a voiceover consumes; composition is required. */
|
|
1478
1514
|
requireComposedPhoneticScript(draft, phoneticScriptEntityId) {
|
|
1479
1515
|
const phonetic = requireEntityKind(draft, phoneticScriptEntityId, "phonetic-script");
|
|
1480
1516
|
requireComposedAudioScript(draft, phonetic.entityId);
|
|
1481
1517
|
return phonetic;
|
|
1482
1518
|
}
|
|
1483
1519
|
/** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
|
|
1484
|
-
ensurePhoneticRender(draft,
|
|
1485
|
-
const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId ===
|
|
1486
|
-
if (renders.length > 1) throw new Error(`
|
|
1520
|
+
ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
|
|
1521
|
+
const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
|
|
1522
|
+
if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
|
|
1487
1523
|
if (renders[0] !== void 0) {
|
|
1488
|
-
if (otherEndpoint(renders[0],
|
|
1489
|
-
throw new Error(`
|
|
1524
|
+
if (otherEndpoint(renders[0], voiceoverAudioEntityId) === phoneticScriptEntityId) return;
|
|
1525
|
+
throw new Error(`Voiceover "${voiceoverAudioEntityId}" already has a different PhoneticScript synthesis input`);
|
|
1490
1526
|
}
|
|
1491
|
-
draft.relations.push(this.createRelation(draft, "phonetic-script-render",
|
|
1527
|
+
draft.relations.push(this.createRelation(draft, "phonetic-script-render", voiceoverAudioEntityId, phoneticScriptEntityId));
|
|
1492
1528
|
}
|
|
1493
|
-
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId,
|
|
1529
|
+
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceoverAudioEntityId, phoneticScriptEntityId, captions) {
|
|
1494
1530
|
const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
|
|
1495
1531
|
const requested = new Set(captions.map((caption) => caption.captionClipEntityId));
|
|
1496
1532
|
if (requested.size !== captions.length) throw new Error("Caption Clip ids must be unique within a voiceover take");
|
|
@@ -1521,7 +1557,7 @@ var EntityTimelineEditor = class {
|
|
|
1521
1557
|
};
|
|
1522
1558
|
draft.entities.push(content);
|
|
1523
1559
|
assertCaptionScript(draft, content.entityId, audioScriptEntityId);
|
|
1524
|
-
draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId,
|
|
1560
|
+
draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceoverAudioEntityId, { alignment: {
|
|
1525
1561
|
start: startMs,
|
|
1526
1562
|
end: startMs + durationMs
|
|
1527
1563
|
} }));
|
|
@@ -1578,7 +1614,7 @@ var EntityTimelineEditor = class {
|
|
|
1578
1614
|
}
|
|
1579
1615
|
}
|
|
1580
1616
|
});
|
|
1581
|
-
this.replaceCaptionAlignment(draft, content.entityId,
|
|
1617
|
+
this.replaceCaptionAlignment(draft, content.entityId, voiceoverAudioEntityId, startMs, durationMs);
|
|
1582
1618
|
}
|
|
1583
1619
|
}
|
|
1584
1620
|
ensureRoleTrackForClip(draft, clipEntityId, role) {
|
|
@@ -1587,10 +1623,10 @@ var EntityTimelineEditor = class {
|
|
|
1587
1623
|
const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
|
|
1588
1624
|
return this.ensureRoleTrack(draft, timeline.entityId, role);
|
|
1589
1625
|
}
|
|
1590
|
-
replaceCaptionAlignment(draft, captionEntityId,
|
|
1626
|
+
replaceCaptionAlignment(draft, captionEntityId, voiceoverAudioEntityId, startMs, durationMs) {
|
|
1591
1627
|
const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
|
|
1592
1628
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1593
|
-
draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId,
|
|
1629
|
+
draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceoverAudioEntityId, { alignment: {
|
|
1594
1630
|
start: startMs,
|
|
1595
1631
|
end: startMs + durationMs
|
|
1596
1632
|
} }));
|
|
@@ -1999,7 +2035,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
1999
2035
|
}, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`) {
|
|
2000
2036
|
assertSupportedLegacy(legacy);
|
|
2001
2037
|
if (initial.entities.some((row) => row.entityKind === "timeline")) fail("Timeline already exists; migration is not an editing operation");
|
|
2002
|
-
if (legacy
|
|
2038
|
+
if (hasResolvedPlacement(legacy)) fail("An entity projection cannot be migrated as legacy state");
|
|
2003
2039
|
const normalizedMainRanges = overlapNormalizedMainRanges(legacy);
|
|
2004
2040
|
let entities = structuredClone([...initial.entities]);
|
|
2005
2041
|
let relations = structuredClone([...initial.relations]);
|
|
@@ -2053,7 +2089,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2053
2089
|
relations
|
|
2054
2090
|
}, {
|
|
2055
2091
|
assetId: speech.origin_speech_id,
|
|
2056
|
-
kind: "
|
|
2092
|
+
kind: "voiceover",
|
|
2057
2093
|
durationMs: speech.media_duration_ms,
|
|
2058
2094
|
storageKey: speech.audio_storage_key,
|
|
2059
2095
|
voice: {
|
|
@@ -2141,8 +2177,8 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2141
2177
|
} else if (part.caption !== void 0) {
|
|
2142
2178
|
const caption = part.caption;
|
|
2143
2179
|
duration = caption.initial_duration_ms;
|
|
2144
|
-
const
|
|
2145
|
-
const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId ===
|
|
2180
|
+
const voiceover = contentByPartId.get(caption.speech_part_id);
|
|
2181
|
+
const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceover);
|
|
2146
2182
|
const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
|
|
2147
2183
|
const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
|
|
2148
2184
|
if (script === void 0) fail("Legacy Caption has no composed AudioScript");
|
|
@@ -2169,7 +2205,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2169
2205
|
selection,
|
|
2170
2206
|
...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
|
|
2171
2207
|
});
|
|
2172
|
-
link("caption-alignment", content,
|
|
2208
|
+
link("caption-alignment", content, voiceover, { alignment: {
|
|
2173
2209
|
start: caption.start_ms,
|
|
2174
2210
|
end: caption.start_ms + duration
|
|
2175
2211
|
} });
|
|
@@ -2225,27 +2261,23 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2225
2261
|
entities,
|
|
2226
2262
|
relations
|
|
2227
2263
|
};
|
|
2228
|
-
assertEntityProjectionPreservesLegacy(legacy, projectEntityTimeline(rows
|
|
2264
|
+
assertEntityProjectionPreservesLegacy(legacy, projectEntityTimeline(rows), rows);
|
|
2229
2265
|
return rows;
|
|
2230
2266
|
}
|
|
2231
2267
|
/** Server gate: a cutover may change representation, never the existing render facts. */
|
|
2232
2268
|
function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
|
|
2233
2269
|
assertSupportedLegacy(legacy);
|
|
2234
|
-
if (projection
|
|
2270
|
+
if (!hasResolvedPlacement(projection)) fail("Expected an entity projection for migration");
|
|
2235
2271
|
const normalizedMainRanges = overlapNormalizedMainRanges(legacy);
|
|
2236
|
-
const compatibilityFacts = (document) => {
|
|
2237
|
-
const
|
|
2272
|
+
const compatibilityFacts = (document, placementResolved) => {
|
|
2273
|
+
const view = fromVideoDocument(document, { placementResolved });
|
|
2238
2274
|
for (const part of Object.values(view.part_library ?? {})) {
|
|
2239
2275
|
const speech = part.speech;
|
|
2240
2276
|
if (speech?.phoneme_script === null) delete speech.phoneme_script;
|
|
2241
2277
|
}
|
|
2242
|
-
|
|
2243
|
-
return {
|
|
2244
|
-
meta,
|
|
2245
|
-
view
|
|
2246
|
-
};
|
|
2278
|
+
return { view };
|
|
2247
2279
|
};
|
|
2248
|
-
if (canonical(compatibilityFacts(legacy)) !== canonical(compatibilityFacts(projection))) fail("Entity migration must preserve all existing timeline facts; edit after cutover");
|
|
2280
|
+
if (canonical(compatibilityFacts(legacy, false)) !== canonical(compatibilityFacts(projection, true))) fail("Entity migration must preserve all existing timeline facts; edit after cutover");
|
|
2249
2281
|
const trackOrders = [];
|
|
2250
2282
|
for (const oldTrack of legacy.tracks ?? []) {
|
|
2251
2283
|
const candidates = rows.entities.filter((row) => row.entityKind === "track" && (oldTrack.id === void 0 ? row.payload.role === oldTrack.parts_kind : row.entityId === oldTrack.id));
|
|
@@ -2512,21 +2544,21 @@ function requirePart(document, partId) {
|
|
|
2512
2544
|
if (part === void 0) fail(`Placed Clip ${partId} has no part payload`);
|
|
2513
2545
|
return part;
|
|
2514
2546
|
}
|
|
2515
|
-
function ensureSpeechPhoneticChain(
|
|
2516
|
-
const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId ===
|
|
2517
|
-
if (renders.length > 1) fail(`
|
|
2547
|
+
function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
|
|
2548
|
+
const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceoverAudioEntityId);
|
|
2549
|
+
if (renders.length > 1) fail(`Voiceover ${voiceoverAudioEntityId} has ambiguous PhoneticScript provenance`);
|
|
2518
2550
|
if (renders.length === 1) {
|
|
2519
2551
|
const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
|
|
2520
|
-
if (phonetic === void 0) fail(`
|
|
2552
|
+
if (phonetic === void 0) fail(`Voiceover ${voiceoverAudioEntityId} has a dangling PhoneticScript render`);
|
|
2521
2553
|
const script = composedAudioScript(phonetic, entities);
|
|
2522
|
-
if (script === void 0 || scriptText(script) !== audioScriptText) fail(`
|
|
2554
|
+
if (script === void 0 || scriptText(script) !== audioScriptText) fail(`Voiceover ${voiceoverAudioEntityId} conflicts with the legacy Speech script`);
|
|
2523
2555
|
return phonetic.entityId;
|
|
2524
2556
|
}
|
|
2525
2557
|
const script = entities.find((entity) => entity.entityId === documentAudioScriptEntityId);
|
|
2526
2558
|
if (script === void 0 || script.entityKind !== "audio-script") fail("Migration must carry the fixed document AudioScript");
|
|
2527
2559
|
const segments = script.payload.segments;
|
|
2528
2560
|
if (Array.isArray(segments) && segments.length > 0) {
|
|
2529
|
-
if (scriptText(script) !== audioScriptText) fail(`
|
|
2561
|
+
if (scriptText(script) !== audioScriptText) fail(`Voiceover ${voiceoverAudioEntityId} conflicts with the project AudioScript text`);
|
|
2530
2562
|
} else {
|
|
2531
2563
|
const scriptIndex = entities.findIndex((row) => row.entityId === script.entityId);
|
|
2532
2564
|
entities[scriptIndex] = {
|
|
@@ -2541,7 +2573,7 @@ function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript
|
|
|
2541
2573
|
baseEntityIds: [script.entityId],
|
|
2542
2574
|
...phonemeScript === void 0 ? {} : { phonemeScript }
|
|
2543
2575
|
});
|
|
2544
|
-
link("phonetic-script-render",
|
|
2576
|
+
link("phonetic-script-render", voiceoverAudioEntityId, phonetic);
|
|
2545
2577
|
return phonetic;
|
|
2546
2578
|
}
|
|
2547
2579
|
function composedAudioScript(phonetic, entities) {
|
|
@@ -3648,7 +3680,7 @@ function writeSpeechAssets(draft, assets) {
|
|
|
3648
3680
|
if (caption == null) continue;
|
|
3649
3681
|
const priorCaption = draft.part_library?.[captionId]?.caption;
|
|
3650
3682
|
setPart(draft, captionId, { caption: {
|
|
3651
|
-
...priorCaption,
|
|
3683
|
+
...priorCaption ?? { style: {} },
|
|
3652
3684
|
id: captionId,
|
|
3653
3685
|
kind: "caption",
|
|
3654
3686
|
initial_duration_ms: caption.duration_ms,
|
|
@@ -4057,6 +4089,10 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4057
4089
|
client;
|
|
4058
4090
|
doc;
|
|
4059
4091
|
adapter;
|
|
4092
|
+
/** Whether this document's timeline is owned by the Entity graph. */
|
|
4093
|
+
isEntityDocument() {
|
|
4094
|
+
return this.adapter.isEntityDocument();
|
|
4095
|
+
}
|
|
4060
4096
|
editor;
|
|
4061
4097
|
/**
|
|
4062
4098
|
* The version the server is known to hold. Starts empty (nothing confirmed)
|
|
@@ -4075,17 +4111,24 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4075
4111
|
* Fetches the snapshot up front rather than starting empty and converging: the
|
|
4076
4112
|
* agent's first act is to read the document, so there is no useful state before
|
|
4077
4113
|
* the snapshot lands. This also fails fast and loudly on a document that does
|
|
4078
|
-
* not exist
|
|
4079
|
-
* waiting forever for content that will never arrive.
|
|
4114
|
+
* not exist or whose first snapshot is incomplete or invalid.
|
|
4080
4115
|
*/
|
|
4081
4116
|
static async open(options) {
|
|
4082
|
-
const doc = new LoroDoc();
|
|
4083
|
-
if (options.peerId != null) doc.setPeerId(options.peerId);
|
|
4084
4117
|
const response = await options.client.fetchSnapshot();
|
|
4085
|
-
doc
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4118
|
+
const doc = new LoroDoc();
|
|
4119
|
+
let manualSyncDoc;
|
|
4120
|
+
try {
|
|
4121
|
+
if (options.peerId != null) doc.setPeerId(options.peerId);
|
|
4122
|
+
if (doc.import(base64ToBytes(response.snapshot)).pending != null) throw new Error("Initial mengine snapshot has missing dependencies");
|
|
4123
|
+
manualSyncDoc = new ManualSyncDoc(options.client, doc);
|
|
4124
|
+
assertValidVideoDocument(manualSyncDoc.snapshot());
|
|
4125
|
+
manualSyncDoc.watermark = manualSyncDoc.serverVVFrom(response.version.server_vv) ?? doc.oplogVersion();
|
|
4126
|
+
return manualSyncDoc;
|
|
4127
|
+
} catch (error) {
|
|
4128
|
+
manualSyncDoc?.adapter.dispose();
|
|
4129
|
+
doc.free();
|
|
4130
|
+
throw error;
|
|
4131
|
+
}
|
|
4089
4132
|
}
|
|
4090
4133
|
/** Current document read model (authoritative shape). */
|
|
4091
4134
|
snapshot() {
|
|
@@ -4104,9 +4147,9 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4104
4147
|
editorPeerId() {
|
|
4105
4148
|
return this.doc.peerIdStr;
|
|
4106
4149
|
}
|
|
4107
|
-
/** Current
|
|
4108
|
-
|
|
4109
|
-
return fromVideoDocument(this.adapter.snapshot());
|
|
4150
|
+
/** Current content projected into the compatible track and part read shape. */
|
|
4151
|
+
content() {
|
|
4152
|
+
return fromVideoDocument(this.adapter.snapshot(), { placementResolved: this.isEntityDocument() });
|
|
4110
4153
|
}
|
|
4111
4154
|
/**
|
|
4112
4155
|
* Replay a deterministic sandbox journal into this document.
|
|
@@ -4126,9 +4169,8 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4126
4169
|
*
|
|
4127
4170
|
* This pair replaces the legacy integer-version comparison that
|
|
4128
4171
|
* `DraftVersionDetector` used to tell the LLM "the draft was modified
|
|
4129
|
-
* externally, reload before editing".
|
|
4130
|
-
*
|
|
4131
|
-
* vectors restores the same capability.
|
|
4172
|
+
* externally, reload before editing". Comparing Loro version vectors covers
|
|
4173
|
+
* all collaborative edits without a business revision field in the document.
|
|
4132
4174
|
*
|
|
4133
4175
|
* Same capability, not a stronger one: like the legacy detector, this only
|
|
4134
4176
|
* reports what changed between two moments the caller chose to sample.
|
|
@@ -4212,6 +4254,7 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4212
4254
|
if (serverVV != null) this.watermark = serverVV;
|
|
4213
4255
|
return {
|
|
4214
4256
|
kind: response.kind,
|
|
4257
|
+
mengineUpdateSeq: response.version?.update_seq ?? response.update_seq ?? void 0,
|
|
4215
4258
|
updateSeq: response.update_seq ?? void 0,
|
|
4216
4259
|
collaborated: serverVV != null && !covers(localVersion, serverVV)
|
|
4217
4260
|
};
|
|
@@ -4250,22 +4293,24 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4250
4293
|
* recovers whatever the stream missed while it was down. Recovery therefore
|
|
4251
4294
|
* lives in the synchronizer (keyed on the real doc version), not here.
|
|
4252
4295
|
*
|
|
4253
|
-
*
|
|
4296
|
+
* Update frames are forwarded via `onUpdate`, resync hints via `onResync`; this
|
|
4254
4297
|
* connection keeps no version cursor and does no catch-up of its own. Dedup is
|
|
4255
4298
|
* unnecessary because `LoroDoc.import` is idempotent by OpId/VV.
|
|
4256
4299
|
*/
|
|
4257
4300
|
var SseConnection = class {
|
|
4258
4301
|
client;
|
|
4259
4302
|
onUpdate;
|
|
4303
|
+
onResync;
|
|
4260
4304
|
reconnectDelayMs;
|
|
4261
4305
|
inner = void 0;
|
|
4262
4306
|
event = new EventBus();
|
|
4263
4307
|
streamTask = null;
|
|
4264
4308
|
_status = "idle";
|
|
4265
4309
|
_error;
|
|
4266
|
-
constructor(client, onUpdate, reconnectDelayMs) {
|
|
4310
|
+
constructor(client, onUpdate, onResync, reconnectDelayMs) {
|
|
4267
4311
|
this.client = client;
|
|
4268
4312
|
this.onUpdate = onUpdate;
|
|
4313
|
+
this.onResync = onResync;
|
|
4269
4314
|
this.reconnectDelayMs = reconnectDelayMs;
|
|
4270
4315
|
}
|
|
4271
4316
|
get status() {
|
|
@@ -4326,10 +4371,12 @@ var SseConnection = class {
|
|
|
4326
4371
|
client: this.client,
|
|
4327
4372
|
signal,
|
|
4328
4373
|
onOpen: () => this.setStatus("connected"),
|
|
4374
|
+
onResync: this.onResync,
|
|
4329
4375
|
onUpdate: (event) => {
|
|
4330
4376
|
for (const update of event.updates) this.onUpdate(update);
|
|
4331
4377
|
}
|
|
4332
4378
|
});
|
|
4379
|
+
if (!signal.aborted) this.setStatus("connecting", /* @__PURE__ */ new Error("SSE stream closed"));
|
|
4333
4380
|
} catch (error) {
|
|
4334
4381
|
if (signal.aborted) break;
|
|
4335
4382
|
this.setStatus("connecting", error instanceof Error ? error : new Error(String(error)));
|
|
@@ -4374,7 +4421,7 @@ var MedeoHttpDocStorage = class {
|
|
|
4374
4421
|
this.options = options;
|
|
4375
4422
|
this.client = options.client;
|
|
4376
4423
|
this.docId = options.docId;
|
|
4377
|
-
this.connection = new SseConnection(this.client, (update) => this.emitUpdate(update), options.sseReconnectDelayMs ?? 500);
|
|
4424
|
+
this.connection = new SseConnection(this.client, (update) => this.emitUpdate(update), () => this.events.emit("resync"), options.sseReconnectDelayMs ?? 500);
|
|
4378
4425
|
}
|
|
4379
4426
|
get isReadonly() {
|
|
4380
4427
|
return this.options.readonlyMode ?? false;
|
|
@@ -4382,6 +4429,7 @@ var MedeoHttpDocStorage = class {
|
|
|
4382
4429
|
async getDoc(docId) {
|
|
4383
4430
|
this.assertDocId(docId);
|
|
4384
4431
|
const snapshot = await this.client.fetchSnapshot();
|
|
4432
|
+
this.reportServerVersion(snapshot.version?.server_vv);
|
|
4385
4433
|
const now = /* @__PURE__ */ new Date();
|
|
4386
4434
|
return {
|
|
4387
4435
|
docId,
|
|
@@ -4393,6 +4441,7 @@ var MedeoHttpDocStorage = class {
|
|
|
4393
4441
|
async getDocDiff(docId, knownVersion) {
|
|
4394
4442
|
this.assertDocId(docId);
|
|
4395
4443
|
const response = await this.client.sync(knownVersion);
|
|
4444
|
+
this.reportServerVersion(response.server_vv);
|
|
4396
4445
|
return {
|
|
4397
4446
|
docId,
|
|
4398
4447
|
missing: base64ToBytes(response.update),
|
|
@@ -4423,7 +4472,7 @@ var MedeoHttpDocStorage = class {
|
|
|
4423
4472
|
if (update.data.byteLength === 0) return {};
|
|
4424
4473
|
try {
|
|
4425
4474
|
const response = await this.client.pushUpdate(update.data);
|
|
4426
|
-
const serverVV =
|
|
4475
|
+
const serverVV = this.reportServerVersion(response.version?.server_vv);
|
|
4427
4476
|
this.events.emit("pushOutcome", {
|
|
4428
4477
|
kind: response.kind,
|
|
4429
4478
|
updateSeq: response.update_seq ?? void 0,
|
|
@@ -4451,6 +4500,19 @@ var MedeoHttpDocStorage = class {
|
|
|
4451
4500
|
subscribePushOutcome(callback) {
|
|
4452
4501
|
return this.events.on("pushOutcome", callback);
|
|
4453
4502
|
}
|
|
4503
|
+
/** Observe live hints that require VV catch-up without replacing the SSE connection. */
|
|
4504
|
+
subscribeResync(callback) {
|
|
4505
|
+
return this.events.on("resync", callback);
|
|
4506
|
+
}
|
|
4507
|
+
/** Authoritative HTTP reads and accepted pushes, never inferred from SSE bytes. */
|
|
4508
|
+
subscribeServerVersion(callback) {
|
|
4509
|
+
return this.events.on("serverVersion", callback);
|
|
4510
|
+
}
|
|
4511
|
+
reportServerVersion(encoded) {
|
|
4512
|
+
const version = decodeServerVV(encoded);
|
|
4513
|
+
if (version != null) this.events.emit("serverVersion", version);
|
|
4514
|
+
return version;
|
|
4515
|
+
}
|
|
4454
4516
|
async deleteDoc(_docId) {
|
|
4455
4517
|
throw new Error("MedeoHttpDocStorage does not support deleteDoc");
|
|
4456
4518
|
}
|
|
@@ -4567,6 +4629,118 @@ var MemoryDocStorage = class extends BaseDocStorage {
|
|
|
4567
4629
|
}
|
|
4568
4630
|
};
|
|
4569
4631
|
//#endregion
|
|
4632
|
+
//#region src/session/document-undo-manager.ts
|
|
4633
|
+
/**
|
|
4634
|
+
* Tracks the shared LoroDoc independently of semantic editing. Undo/redo use
|
|
4635
|
+
* the same mutation guard as the adapter and publish settled availability.
|
|
4636
|
+
*/
|
|
4637
|
+
var DocumentUndoManager = class {
|
|
4638
|
+
doc;
|
|
4639
|
+
mutationGuard;
|
|
4640
|
+
onChange;
|
|
4641
|
+
manager;
|
|
4642
|
+
unsubscribe;
|
|
4643
|
+
disposed = false;
|
|
4644
|
+
notificationQueued = false;
|
|
4645
|
+
published = {
|
|
4646
|
+
canUndo: false,
|
|
4647
|
+
canRedo: false
|
|
4648
|
+
};
|
|
4649
|
+
activeSource = null;
|
|
4650
|
+
constructor(doc, mutationGuard, onChange) {
|
|
4651
|
+
this.doc = doc;
|
|
4652
|
+
this.mutationGuard = mutationGuard;
|
|
4653
|
+
this.onChange = onChange;
|
|
4654
|
+
const peer = doc.peerIdStr;
|
|
4655
|
+
this.manager = new UndoManager(doc, {
|
|
4656
|
+
mergeInterval: 0,
|
|
4657
|
+
maxUndoSteps: 100,
|
|
4658
|
+
excludeOriginPrefixes: ["mengine.bootstrap", "mengine.initialize"],
|
|
4659
|
+
onPush: (_isUndo, range) => {
|
|
4660
|
+
this.scheduleNotification();
|
|
4661
|
+
return {
|
|
4662
|
+
value: this.activeSource ?? {
|
|
4663
|
+
peer,
|
|
4664
|
+
counter: range.start
|
|
4665
|
+
},
|
|
4666
|
+
cursors: []
|
|
4667
|
+
};
|
|
4668
|
+
},
|
|
4669
|
+
onPop: () => this.scheduleNotification()
|
|
4670
|
+
});
|
|
4671
|
+
this.unsubscribe = doc.subscribe(() => this.scheduleNotification());
|
|
4672
|
+
}
|
|
4673
|
+
getState() {
|
|
4674
|
+
this.assertActive();
|
|
4675
|
+
return {
|
|
4676
|
+
canUndo: this.manager.canUndo(),
|
|
4677
|
+
canRedo: this.manager.canRedo()
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
4680
|
+
undo(options) {
|
|
4681
|
+
return this.applyUndoRedo("Undo", options);
|
|
4682
|
+
}
|
|
4683
|
+
redo(options) {
|
|
4684
|
+
return this.applyUndoRedo("Redo", options);
|
|
4685
|
+
}
|
|
4686
|
+
beginGroup() {
|
|
4687
|
+
this.assertActive();
|
|
4688
|
+
this.mutationGuard.run(() => this.manager.groupStart());
|
|
4689
|
+
}
|
|
4690
|
+
endGroup() {
|
|
4691
|
+
this.assertActive();
|
|
4692
|
+
this.mutationGuard.run(() => this.manager.groupEnd());
|
|
4693
|
+
this.scheduleNotification();
|
|
4694
|
+
}
|
|
4695
|
+
applyUndoRedo(kind, options) {
|
|
4696
|
+
this.assertActive();
|
|
4697
|
+
return this.mutationGuard.run(() => {
|
|
4698
|
+
if (kind === "Undo" ? !this.manager.canUndo() : !this.manager.canRedo()) return false;
|
|
4699
|
+
this.activeSource = kind === "Undo" ? this.manager.topUndoValue() : this.manager.topRedoValue();
|
|
4700
|
+
try {
|
|
4701
|
+
this.doc.setNextCommitOptions({ message: JSON.stringify({
|
|
4702
|
+
semantic_op: kind,
|
|
4703
|
+
payload: { source: this.activeSource },
|
|
4704
|
+
intent: options?.intent ?? null,
|
|
4705
|
+
actor: options?.actor ?? null
|
|
4706
|
+
}) });
|
|
4707
|
+
const before = this.doc.oplogFrontiers();
|
|
4708
|
+
return (kind === "Undo" ? this.manager.undo() : this.manager.redo()) && this.doc.cmpWithFrontiers(before) !== 0;
|
|
4709
|
+
} finally {
|
|
4710
|
+
this.doc.clearNextCommitOptions();
|
|
4711
|
+
this.activeSource = null;
|
|
4712
|
+
this.scheduleNotification();
|
|
4713
|
+
}
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
dispose() {
|
|
4717
|
+
if (this.disposed) return;
|
|
4718
|
+
this.disposed = true;
|
|
4719
|
+
this.unsubscribe();
|
|
4720
|
+
this.manager.setOnPush(void 0);
|
|
4721
|
+
this.manager.setOnPop(void 0);
|
|
4722
|
+
this.manager.free();
|
|
4723
|
+
}
|
|
4724
|
+
scheduleNotification() {
|
|
4725
|
+
if (this.notificationQueued || this.disposed) return;
|
|
4726
|
+
this.notificationQueued = true;
|
|
4727
|
+
queueMicrotask(() => {
|
|
4728
|
+
this.notificationQueued = false;
|
|
4729
|
+
if (!this.disposed) this.publish();
|
|
4730
|
+
});
|
|
4731
|
+
}
|
|
4732
|
+
publish() {
|
|
4733
|
+
if (this.disposed) return;
|
|
4734
|
+
const state = this.getState();
|
|
4735
|
+
if (state.canUndo === this.published.canUndo && state.canRedo === this.published.canRedo) return;
|
|
4736
|
+
this.published = state;
|
|
4737
|
+
this.onChange();
|
|
4738
|
+
}
|
|
4739
|
+
assertActive() {
|
|
4740
|
+
if (this.disposed) throw new Error("mengine doc undo manager is disposed");
|
|
4741
|
+
}
|
|
4742
|
+
};
|
|
4743
|
+
//#endregion
|
|
4570
4744
|
//#region src/session/mengine-doc-session.ts
|
|
4571
4745
|
/**
|
|
4572
4746
|
* Raised by {@link MengineDocSession.waitForServerAck} when the local edits it
|
|
@@ -4602,13 +4776,9 @@ var MengineAckFailedError = class extends Error {
|
|
|
4602
4776
|
* A live editing session for one Medeo document — the single entry point clients
|
|
4603
4777
|
* (FE draft driver, Agent, embeds) use to open, edit, and observe a document.
|
|
4604
4778
|
*
|
|
4605
|
-
*
|
|
4606
|
-
*
|
|
4607
|
-
*
|
|
4608
|
-
* ├─ ClientServerSynchronizer ── DocManager ── LoroDoc
|
|
4609
|
-
* MedeoHttpDocStorage┘ │
|
|
4610
|
-
* (remote = Rust mengine-server) ▼
|
|
4611
|
-
* MirrorVideoDocumentAdapter + SemanticEditor
|
|
4779
|
+
* SemanticEditor writes through MirrorVideoDocumentAdapter directly, while
|
|
4780
|
+
* DocumentUndoManager observes the same LoroDoc independently. Both mutation
|
|
4781
|
+
* paths use the session's DocumentMutationGuard, closed before teardown.
|
|
4612
4782
|
*
|
|
4613
4783
|
* `DocManager` owns the `LoroDoc`: local edits committed on the adapter are
|
|
4614
4784
|
* picked up via `subscribeLocalUpdates`, saved to local storage, then pushed to
|
|
@@ -4629,11 +4799,20 @@ var MengineDocSession = class {
|
|
|
4629
4799
|
manager;
|
|
4630
4800
|
events = new EventBus();
|
|
4631
4801
|
disposables = new DisposableSet();
|
|
4802
|
+
mutationGuard = new DocumentMutationGuard();
|
|
4632
4803
|
adapterValue = null;
|
|
4633
4804
|
editorValue = null;
|
|
4805
|
+
undoManagerValue = null;
|
|
4634
4806
|
started = false;
|
|
4807
|
+
startTask = null;
|
|
4808
|
+
destroyed = false;
|
|
4809
|
+
lifetime = null;
|
|
4810
|
+
hasConnected = false;
|
|
4811
|
+
initialVersionValue = null;
|
|
4812
|
+
localVersionValue = null;
|
|
4813
|
+
pushErrorValue = null;
|
|
4635
4814
|
/**
|
|
4636
|
-
* Latest server oplog version seen on
|
|
4815
|
+
* Latest server oplog version seen on an authoritative HTTP response. Lets `waitForServerAck`
|
|
4637
4816
|
* return without waiting when the server is already known to cover the local
|
|
4638
4817
|
* doc (e.g. nothing was edited since the last confirmed push).
|
|
4639
4818
|
*/
|
|
@@ -4662,14 +4841,14 @@ var MengineDocSession = class {
|
|
|
4662
4841
|
return this.server.subscribePushOutcome(callback);
|
|
4663
4842
|
}
|
|
4664
4843
|
/**
|
|
4665
|
-
* Resolve once the server has durably accepted the local
|
|
4844
|
+
* Resolve once the server has durably accepted the local commits as of
|
|
4666
4845
|
* *now* — the capability an Agent tool needs to answer "did my edit land?"
|
|
4667
4846
|
* before reporting success.
|
|
4668
4847
|
*
|
|
4669
4848
|
* Call it right after the editor ops whose durability matters. It snapshots the
|
|
4670
|
-
* local
|
|
4671
|
-
*
|
|
4672
|
-
*
|
|
4849
|
+
* latest local commit's version (or the loaded baseline before any edit) and
|
|
4850
|
+
* resolves when an authoritative server version covers it. Remote-only
|
|
4851
|
+
* imports do not extend this target, matching the displayed confirmation state.
|
|
4673
4852
|
*
|
|
4674
4853
|
* Version coverage, not byte identity, is the acceptance test — for two reasons:
|
|
4675
4854
|
* the ops may reach the server inside a merged blob rather than as the exact
|
|
@@ -4684,31 +4863,39 @@ var MengineDocSession = class {
|
|
|
4684
4863
|
* @throws {MengineAckFailedError} on timeout, rejection, or transport failure.
|
|
4685
4864
|
*/
|
|
4686
4865
|
async waitForServerAck(options = {}) {
|
|
4687
|
-
if (this.adapterValue == null) throw new Error("mengine doc session is not started");
|
|
4866
|
+
if (this.adapterValue == null || this.lifetime == null) throw new Error("mengine doc session is not started");
|
|
4688
4867
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
4689
|
-
const target = this.manager.connectDoc(this.docId).version();
|
|
4868
|
+
const target = this.confirmationTarget() ?? this.manager.connectDoc(this.docId).version();
|
|
4869
|
+
const signal = this.lifetime.signal;
|
|
4690
4870
|
if (this.serverVVValue != null && serverCovers(this.serverVVValue, target)) return;
|
|
4691
4871
|
await new Promise((resolve, reject) => {
|
|
4692
|
-
|
|
4872
|
+
const cleanup = [];
|
|
4693
4873
|
let done = false;
|
|
4694
4874
|
const settle = (error) => {
|
|
4695
4875
|
if (done) return;
|
|
4696
4876
|
done = true;
|
|
4697
4877
|
clearTimeout(timer);
|
|
4698
|
-
off();
|
|
4878
|
+
cleanup.forEach((off) => off());
|
|
4699
4879
|
if (error == null) resolve();
|
|
4700
4880
|
else reject(error);
|
|
4701
4881
|
};
|
|
4702
4882
|
const timer = setTimeout(() => {
|
|
4703
4883
|
settle(new MengineAckFailedError("timeout", void 0, void 0, `mengine did not acknowledge the update within ${timeoutMs}ms`));
|
|
4704
4884
|
}, timeoutMs);
|
|
4705
|
-
|
|
4885
|
+
const onAbort = () => settle(new MengineAckFailedError("failed", void 0, void 0, "mengine doc session destroyed"));
|
|
4886
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4887
|
+
cleanup.push(() => signal.removeEventListener("abort", onAbort));
|
|
4888
|
+
cleanup.push(this.events.on("syncState", () => {
|
|
4889
|
+
if (this.serverVVValue != null && serverCovers(this.serverVVValue, target)) settle(void 0);
|
|
4890
|
+
}));
|
|
4891
|
+
cleanup.push(this.server.subscribePushOutcome((outcome) => {
|
|
4706
4892
|
if (outcome.kind === "rejected" || outcome.kind === "failed") {
|
|
4707
4893
|
settle(new MengineAckFailedError(outcome.kind, outcome.code, outcome.error, outcome.error?.message ?? `mengine push ${outcome.kind}`));
|
|
4708
4894
|
return;
|
|
4709
4895
|
}
|
|
4710
|
-
|
|
4711
|
-
|
|
4896
|
+
}));
|
|
4897
|
+
if (signal.aborted) onAbort();
|
|
4898
|
+
else this.synchronizer.requestSync(this.docId);
|
|
4712
4899
|
});
|
|
4713
4900
|
}
|
|
4714
4901
|
/**
|
|
@@ -4744,44 +4931,154 @@ var MengineDocSession = class {
|
|
|
4744
4931
|
if (this.adapterValue == null) throw new Error("mengine doc session is not started");
|
|
4745
4932
|
return this.adapterValue.snapshot();
|
|
4746
4933
|
}
|
|
4934
|
+
/** Current-session undo/redo availability. Loading and closed sessions report false. */
|
|
4935
|
+
getUndoState() {
|
|
4936
|
+
return this.undoManagerValue?.getState() ?? {
|
|
4937
|
+
canUndo: false,
|
|
4938
|
+
canRedo: false
|
|
4939
|
+
};
|
|
4940
|
+
}
|
|
4941
|
+
/**
|
|
4942
|
+
* Supplies current availability immediately, then coalesces stack changes in a
|
|
4943
|
+
* microtask. Session teardown publishes empty availability synchronously.
|
|
4944
|
+
*/
|
|
4945
|
+
subscribeUndoState(cb) {
|
|
4946
|
+
const notify = () => cb(this.getUndoState());
|
|
4947
|
+
const off = this.destroyed ? () => {} : this.events.on("undoState", notify);
|
|
4948
|
+
notify();
|
|
4949
|
+
return off;
|
|
4950
|
+
}
|
|
4951
|
+
/**
|
|
4952
|
+
* Undo a local edit immediately; server confirmation remains asynchronous.
|
|
4953
|
+
* On a shared LWW field this writes its prior value, potentially overwriting a
|
|
4954
|
+
* later remote edit to that same field. Returns false if no update is produced.
|
|
4955
|
+
*/
|
|
4956
|
+
undo(options) {
|
|
4957
|
+
if (this.undoManagerValue == null) throw new Error("mengine doc undo manager is not ready");
|
|
4958
|
+
return this.undoManagerValue.undo(options);
|
|
4959
|
+
}
|
|
4960
|
+
/**
|
|
4961
|
+
* Restore the state visible before undo without rerunning external side effects.
|
|
4962
|
+
* Returns false if no update is produced; durable confirmation is separate.
|
|
4963
|
+
*/
|
|
4964
|
+
redo(options) {
|
|
4965
|
+
if (this.undoManagerValue == null) throw new Error("mengine doc undo manager is not ready");
|
|
4966
|
+
return this.undoManagerValue.redo(options);
|
|
4967
|
+
}
|
|
4968
|
+
/**
|
|
4969
|
+
* Combine existing semantic operations (e.g. trim a clip and adjust its volume)
|
|
4970
|
+
* or repeated commits during one drag gesture into a single undo step. A gesture
|
|
4971
|
+
* that only previews locally and commits once on release needs no group.
|
|
4972
|
+
* Each commit is immediately visible locally and enters sync independently;
|
|
4973
|
+
* a later failure does not roll it back.
|
|
4974
|
+
* Always pair with endUndoGroup in a finally block. Conflicting remote imports
|
|
4975
|
+
* can end/split the group early, and unrelated local edits would join it.
|
|
4976
|
+
* Complete asynchronous asset preparation before opening the group.
|
|
4977
|
+
*/
|
|
4978
|
+
beginUndoGroup() {
|
|
4979
|
+
if (this.undoManagerValue == null) throw new Error("mengine doc undo manager is not ready");
|
|
4980
|
+
this.undoManagerValue.beginGroup();
|
|
4981
|
+
}
|
|
4982
|
+
/** End a gesture, including one already split by a conflicting remote import. */
|
|
4983
|
+
endUndoGroup() {
|
|
4984
|
+
if (this.undoManagerValue == null) throw new Error("mengine doc undo manager is not ready");
|
|
4985
|
+
this.undoManagerValue.endGroup();
|
|
4986
|
+
}
|
|
4747
4987
|
/**
|
|
4748
4988
|
* Start the sync stack and connect the document.
|
|
4749
4989
|
*
|
|
4750
|
-
* Returns
|
|
4751
|
-
*
|
|
4752
|
-
*
|
|
4990
|
+
* Returns after DocManager loads a local snapshot, or after the first remote
|
|
4991
|
+
* sync when no local snapshot exists. A valid empty document is ready too.
|
|
4992
|
+
* Cached documents can open before SSE connects; subsequent remote changes
|
|
4993
|
+
* surface through `subscribe`.
|
|
4753
4994
|
*/
|
|
4754
|
-
|
|
4755
|
-
if (this.
|
|
4995
|
+
start() {
|
|
4996
|
+
if (this.destroyed) return Promise.reject(/* @__PURE__ */ new Error("mengine doc session destroyed"));
|
|
4997
|
+
if (this.startTask != null) return this.startTask;
|
|
4756
4998
|
this.started = true;
|
|
4757
|
-
this.
|
|
4758
|
-
this.
|
|
4999
|
+
this.lifetime = new AbortController();
|
|
5000
|
+
this.startTask = this.open().catch((error) => {
|
|
5001
|
+
const wasDestroyed = this.destroyed;
|
|
5002
|
+
this.destroy();
|
|
5003
|
+
if (wasDestroyed) throw new Error("mengine doc session destroyed");
|
|
5004
|
+
throw error;
|
|
5005
|
+
});
|
|
5006
|
+
return this.startTask;
|
|
5007
|
+
}
|
|
5008
|
+
async open() {
|
|
5009
|
+
this.disposables.add(this.server.subscribeResync(() => this.synchronizer.requestSync(this.docId)));
|
|
5010
|
+
this.disposables.add(this.server.subscribeServerVersion((version) => {
|
|
5011
|
+
this.serverVVValue = version;
|
|
5012
|
+
if (this.getSyncState().confirmation === "confirmed") this.pushErrorValue = null;
|
|
5013
|
+
this.emitSyncState();
|
|
5014
|
+
}));
|
|
5015
|
+
this.disposables.add(this.server.subscribePushOutcome((outcome) => {
|
|
5016
|
+
this.pushErrorValue = outcome.kind === "failed" || outcome.kind === "rejected" ? outcome.error ?? /* @__PURE__ */ new Error(`mengine push ${outcome.kind}`) : null;
|
|
5017
|
+
this.emitSyncState();
|
|
5018
|
+
}));
|
|
5019
|
+
this.disposables.add(this.server.connection.onStatusChanged((status) => {
|
|
5020
|
+
if (status === "connected") this.hasConnected = true;
|
|
5021
|
+
this.emitSyncState();
|
|
5022
|
+
}));
|
|
4759
5023
|
this.disposables.add(() => this.local.connection.disconnect());
|
|
4760
5024
|
this.disposables.add(() => this.server.connection.disconnect());
|
|
5025
|
+
this.local.connection.connect();
|
|
5026
|
+
await this.local.connection.waitForConnected().abortOn(this.lifetime.signal);
|
|
5027
|
+
if (this.destroyed) throw new Error("mengine doc session destroyed");
|
|
5028
|
+
this.server.connection.connect();
|
|
4761
5029
|
this.synchronizer.start();
|
|
4762
5030
|
this.manager.start();
|
|
4763
5031
|
this.disposables.add(() => this.synchronizer.stop());
|
|
4764
5032
|
this.disposables.add(() => this.manager.stop());
|
|
4765
5033
|
const doc = this.manager.connectDoc(this.docId);
|
|
4766
|
-
if (this.options.peerId != null) doc.setPeerId(this.options.peerId);
|
|
4767
|
-
this.adapterValue = new MirrorVideoDocumentAdapter(doc);
|
|
4768
|
-
this.editorValue = new SemanticEditor(this.adapterValue);
|
|
4769
5034
|
this.disposables.add(() => this.manager.disconnectDoc(this.docId));
|
|
5035
|
+
if (this.options.peerId != null) doc.setPeerId(this.options.peerId);
|
|
5036
|
+
this.adapterValue = new MirrorVideoDocumentAdapter(doc, this.mutationGuard);
|
|
4770
5037
|
const unsubscribe = doc.subscribe((event) => {
|
|
4771
5038
|
if (this.adapterValue == null) return;
|
|
5039
|
+
if (event.by === "local") this.localVersionValue = doc.version();
|
|
5040
|
+
this.emitSyncState();
|
|
4772
5041
|
this.events.emit("update", {
|
|
4773
5042
|
source: event.by === "local" ? "local" : "remote",
|
|
4774
5043
|
snapshot: this.adapterValue.snapshot()
|
|
4775
5044
|
});
|
|
4776
5045
|
});
|
|
4777
5046
|
this.disposables.add(unsubscribe);
|
|
4778
|
-
this.disposables.add(this.manager.onDocStateChange(this.docId, (state) =>
|
|
4779
|
-
|
|
4780
|
-
|
|
5047
|
+
this.disposables.add(this.manager.onDocStateChange(this.docId, (state) => {
|
|
5048
|
+
this.events.emit("state", state);
|
|
5049
|
+
this.emitSyncState();
|
|
4781
5050
|
}));
|
|
4782
|
-
await this.
|
|
5051
|
+
await this.waitForInitialLoad();
|
|
5052
|
+
if (this.destroyed) throw new Error("mengine doc session destroyed");
|
|
5053
|
+
assertValidVideoDocument(this.snapshot());
|
|
5054
|
+
this.undoManagerValue = new DocumentUndoManager(doc, this.mutationGuard, () => this.events.emit("undoState"));
|
|
5055
|
+
this.editorValue = new SemanticEditor(this.adapterValue);
|
|
5056
|
+
this.initialVersionValue = doc.version();
|
|
5057
|
+
this.emitSyncState();
|
|
4783
5058
|
return this.snapshot();
|
|
4784
5059
|
}
|
|
5060
|
+
getSyncState() {
|
|
5061
|
+
const state = this.getState();
|
|
5062
|
+
const target = this.confirmationTarget();
|
|
5063
|
+
const confirmation = this.serverVVValue == null || target == null ? this.localVersionValue == null ? "unknown" : "pending" : serverCovers(this.serverVVValue, target) ? "confirmed" : "pending";
|
|
5064
|
+
const connection = !this.started ? "disconnected" : this.server.connection.status === "connected" ? "connected" : this.hasConnected || this.server.connection.error != null ? "disconnected" : "connecting";
|
|
5065
|
+
return {
|
|
5066
|
+
confirmation,
|
|
5067
|
+
phase: connection === "disconnected" ? "offline" : this.pushErrorValue != null || state.error != null ? "failed" : connection === "connecting" ? "connecting" : state.syncing ? "syncing" : "idle"
|
|
5068
|
+
};
|
|
5069
|
+
}
|
|
5070
|
+
confirmationTarget() {
|
|
5071
|
+
return this.localVersionValue ?? this.initialVersionValue;
|
|
5072
|
+
}
|
|
5073
|
+
/** Immediately supplies current state; unsubscribe when the host changes docs. */
|
|
5074
|
+
subscribeSyncState(cb) {
|
|
5075
|
+
const off = this.events.on("syncState", cb);
|
|
5076
|
+
cb(this.getSyncState());
|
|
5077
|
+
return off;
|
|
5078
|
+
}
|
|
5079
|
+
emitSyncState() {
|
|
5080
|
+
this.events.emit("syncState", this.getSyncState());
|
|
5081
|
+
}
|
|
4785
5082
|
subscribe(cb) {
|
|
4786
5083
|
return this.events.on("update", cb);
|
|
4787
5084
|
}
|
|
@@ -4792,40 +5089,48 @@ var MengineDocSession = class {
|
|
|
4792
5089
|
return this.manager.getDocState(this.docId);
|
|
4793
5090
|
}
|
|
4794
5091
|
destroy() {
|
|
5092
|
+
if (this.destroyed) return;
|
|
5093
|
+
this.destroyed = true;
|
|
5094
|
+
this.mutationGuard.close();
|
|
5095
|
+
this.started = false;
|
|
5096
|
+
this.lifetime?.abort();
|
|
5097
|
+
this.undoManagerValue?.dispose();
|
|
5098
|
+
this.undoManagerValue = null;
|
|
5099
|
+
this.events.emit("undoState");
|
|
4795
5100
|
this.disposables.dispose();
|
|
5101
|
+
this.emitSyncState();
|
|
4796
5102
|
this.events.clearAll();
|
|
5103
|
+
this.adapterValue?.dispose();
|
|
4797
5104
|
this.adapterValue = null;
|
|
4798
5105
|
this.editorValue = null;
|
|
4799
5106
|
this.started = false;
|
|
4800
5107
|
}
|
|
4801
|
-
/**
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
5108
|
+
/** Local absence needs a remote sync; empty business content does not. */
|
|
5109
|
+
async waitForInitialLoad() {
|
|
5110
|
+
const signal = this.lifetime.signal;
|
|
5111
|
+
await new Promise((resolve, reject) => {
|
|
5112
|
+
let off = () => {};
|
|
5113
|
+
let offConnection = () => {};
|
|
5114
|
+
const finish = (error) => {
|
|
5115
|
+
off();
|
|
5116
|
+
offConnection();
|
|
5117
|
+
signal.removeEventListener("abort", onAbort);
|
|
5118
|
+
if (error != null) reject(error);
|
|
5119
|
+
else resolve();
|
|
5120
|
+
};
|
|
5121
|
+
const onAbort = () => finish(/* @__PURE__ */ new Error("mengine doc session destroyed"));
|
|
5122
|
+
const check = (state) => {
|
|
5123
|
+
if (state.loaded && (state.loadedFromLocal || state.synced)) finish();
|
|
5124
|
+
else if (state.error != null) finish(new Error(state.error));
|
|
5125
|
+
else if (state.loaded && this.server.connection.error != null) finish(this.server.connection.error);
|
|
5126
|
+
};
|
|
5127
|
+
off = this.onStateChange(check);
|
|
5128
|
+
offConnection = this.server.connection.onStatusChanged(() => check(this.getState()));
|
|
5129
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5130
|
+
if (signal.aborted) onAbort();
|
|
5131
|
+
else check(this.getState());
|
|
4819
5132
|
});
|
|
4820
5133
|
}
|
|
4821
|
-
hasContent() {
|
|
4822
|
-
if (this.adapterValue == null) return false;
|
|
4823
|
-
try {
|
|
4824
|
-
return this.adapterValue.hasContent();
|
|
4825
|
-
} catch {
|
|
4826
|
-
return false;
|
|
4827
|
-
}
|
|
4828
|
-
}
|
|
4829
5134
|
};
|
|
4830
5135
|
/**
|
|
4831
5136
|
* True when the *encoded* server version `serverVV` includes everything in
|
|
@@ -4849,4 +5154,4 @@ function serverCovers(serverVV, target) {
|
|
|
4849
5154
|
return covers(server, target);
|
|
4850
5155
|
}
|
|
4851
5156
|
//#endregion
|
|
4852
|
-
export { DEFAULT_UNIT_TIME_MS,
|
|
5157
|
+
export { DEFAULT_UNIT_TIME_MS, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, LORO_ENTITY_SCHEMA, LoroEntityDocument, LoroEntityDraft, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, ValidationError, VideoDocumentValidationError, applyEntityRows, applyFieldChanges, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertFieldChanges, assertMediaAssetWritePolicy, assertValidVideoDocument, audioScriptAssetContent, audioScriptAssetFields, audioScriptAssetHash, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, compileEntityRows, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hasAudioScriptAsset, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, prepareCaptionContent, projectEntityTimeline, readDocumentAudioScript, readEntityDocument, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
|