@mengine/medeo-client 2.0.1-alpha.6 → 2.0.1-alpha.7
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-Bnxm1p5E.js} +150 -181
- package/dist/{index-B31KeDzA.d.ts → index-CDrPUhBH.d.ts} +276 -181
- package/dist/index.d.ts +94 -134
- package/dist/index.js +368 -104
- 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/README.md
CHANGED
|
@@ -112,3 +112,34 @@ Loro version vector remains the transport-sync cursor.
|
|
|
112
112
|
`entities.get/list` return complete assembled fields. Consumers do not query base IDs or merge source payloads. `entities.update` patches supplied fields, preserves omitted fields, and routes inherited fields to their declaring entity. Explicit own-field declarations use `entities.declareFields`; these override unambiguous base fields without changing the base. Persisted entity rows and command payloads contain owned fields only. Base collisions are validated before own overrides.
|
|
113
113
|
|
|
114
114
|
`prepareCaptionContent` accepts caption authoring intent and handles source composition, visible text selection, and preservation of existing intrinsic timing inside the SDK.
|
|
115
|
+
This package owns the shared `VideoDocument` contract, semantic editing layer, and runtime-neutral HTTP/SSE client used by medeo-web, agent-harness, and server-side validation code. UI adapters such as `DraftMutationDriver` stay in product repositories.
|
|
116
|
+
|
|
117
|
+
`MengineDocSession.start()` waits for DocManager to load a local snapshot, or for the first remote sync if no local snapshot exists, then validates the content. A cached document can open before SSE connects; a cold start waits for SSE and the existing sync cycle. Session does not prefetch or import an initial snapshot itself.
|
|
118
|
+
|
|
119
|
+
`getState().loaded` means the first local load completed. Once loaded, `loadedFromLocal` reports whether that load imported a snapshot, including a valid empty document. It stays unchanged by later edits and remote updates. Document readiness does not depend on metadata or nonempty tracks.
|
|
120
|
+
|
|
121
|
+
## Session undo/redo
|
|
122
|
+
|
|
123
|
+
After `await session.start()`, `session.undo(options?)` and `session.redo(options?)` synchronously apply local history and return whether they produced a document update. `options` accepts the same actor/intent metadata as semantic edits. Read the resulting document through `session.snapshot()`; use `waitForServerAck()` only when durable confirmation is required.
|
|
124
|
+
|
|
125
|
+
`getUndoState()` returns the current `{ canUndo, canRedo }` immediately. `subscribeUndoState(callback)` immediately provides that state, then coalesces subsequent stack changes in a microtask after mutations settle. Several synchronous commands may produce only one availability notification; use the getter when reading directly after a command. Teardown publishes empty availability synchronously and cancels pending notifications. Dispose the returned subscription with the host. Loading and closed sessions report an empty history; history commands require a ready session.
|
|
126
|
+
|
|
127
|
+
History belongs to one document/session peer. The loaded baseline and remote imports are not recorded. Each semantic commit is a separate step (up to 100); a new local edit invalidates redo. Reconnecting preserves history, while reopening starts empty. Empty or ineffective history actions return false. Never reuse a peer ID across concurrent sessions.
|
|
128
|
+
|
|
129
|
+
Use `beginUndoGroup()` / `endUndoGroup()` to combine existing semantic operations into one undo step, or to combine repeated commits during one gesture. Always end in `finally`. Every constituent commit is immediately visible locally and enters synchronization independently; if a later operation fails, earlier commits remain applied. Conflicting remote imports may split/end a group early, and unrelated local edits in the same session would join an open group.
|
|
130
|
+
|
|
131
|
+
Mirror's draft callback and commit are synchronous. SemanticEditor methods return promises, but their document mutations currently finish synchronously before the promise resolves. Await external asset generation before opening a group, then compose the local semantic operations. Keeping a group open across asynchronous work does not provide transaction isolation, deferred publication, or rollback. If several changes must be atomic, implement one semantic operation with one synchronous draft callback. A gesture that uses only local previews and commits once on release already needs no group.
|
|
132
|
+
|
|
133
|
+
Flush pending local preview/debounce work before undo/redo. Mutating again from a document-update observer during a mutation is rejected; schedule the next command after the current one returns.
|
|
134
|
+
|
|
135
|
+
The session assembles SemanticEditor with MirrorVideoDocumentAdapter directly. DocumentUndoManager independently observes the same LoroDoc and owns undo/redo, grouping and audit metadata. Both write paths share a synchronous DocumentMutationGuard; the session closes it before teardown callbacks, and each component releases its own resources. Standalone adapters create their own guard by default and reject use after disposal while leaving the borrowed LoroDoc usable.
|
|
136
|
+
|
|
137
|
+
Undo/redo append ordinary synchronized updates and retain audit metadata (`Undo`/`Redo`, actor, and `payload.source` identifying an original local change). Existing revision bindings remain immutable. Redo reuses already materialized assets without repeating generation, billing, or attachment requests. Network failures do not automatically reverse history.
|
|
138
|
+
|
|
139
|
+
Loro history uses new writes: undo on a shared LWW field restores its value before the local edit, potentially overwriting a later remote write to the same field; redo restores the value visible before undo. Unrelated remote fields remain intact. Undoing a local insertion removes the object and makes dependent remote edits invisible until redo restores the object. Caption creation includes its style container, even when empty, so a later style edit only changes fields and undo preserves another peer's independently edited fields. Initial document seeding preserves supplied styles and creates empty maps for captions without styles. No write-time repair or migration of older CRDT documents is provided.
|
|
140
|
+
|
|
141
|
+
## Large-update resynchronization
|
|
142
|
+
|
|
143
|
+
`MengineDocSession` handles SSE `event: resync` by scheduling the existing version-vector sync on the current connection. The event carries no Loro bytes and does not create an edit or undo step. A hint arriving while an HTTP sync is in flight schedules another diff so changes newer than that response are still fetched. Low-level users of `readMengineEventStream` can handle the same hint with `onResync`; `MedeoHttpDocStorage` exposes `subscribeResync`.
|
|
144
|
+
|
|
145
|
+
Deploy this client before enabling server-side resync notifications. Older clients ignore the event. Large HTTP pushes remain valid: the server chooses inline update or resync notification after persistence, rather than requiring client-side fragmentation. Base64 encoding uses `Uint8Array.toBase64()` where available, with a chunked fallback preserving the same padded wire representation.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { u as volumeSchema } from "./shared-iWnU2osE.js";
|
|
2
|
-
import { Mirror, schema } from "loro-mirror";
|
|
2
|
+
import { Mirror, schema, validateSchema } from "loro-mirror";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { LoroDoc, LoroMap, LoroMovableList, LoroText, UndoManager } from "loro-crdt";
|
|
5
5
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
@@ -7,9 +7,11 @@ import { bytesToHex } from "@noble/hashes/utils.js";
|
|
|
7
7
|
import { produce } from "immer";
|
|
8
8
|
//#region src/client/base64.ts
|
|
9
9
|
function bytesToBase64(bytes) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const native = bytes.toBase64;
|
|
11
|
+
if (typeof native === "function") return native.call(bytes);
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for (let offset = 0; offset < bytes.length; offset += 8192) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 8192)));
|
|
14
|
+
return btoa(chunks.join(""));
|
|
13
15
|
}
|
|
14
16
|
function base64ToBytes(base64) {
|
|
15
17
|
const binary = atob(base64);
|
|
@@ -47,10 +49,6 @@ function base64ToBytes(base64) {
|
|
|
47
49
|
* value, last-writer-wins). The derived `fallback_abs_ms` snapshot sits beside
|
|
48
50
|
* it as its own number field — a separate LWW unit so a projection refresh
|
|
49
51
|
* never clobbers a `time_position` edit.
|
|
50
|
-
* - meta lives in a `meta` LoroMap: the mirror root only accepts container
|
|
51
|
-
* schemas, so meta scalars can't sit at the root as bare values. The domain
|
|
52
|
-
* `VideoDocument` keeps the same `meta` wrapper, so the shapes are isomorphic
|
|
53
|
-
* and the read/seed mapping is a near-identity.
|
|
54
52
|
* - `part_library` values are nested `LoroMap`s: the value is a `LoroMap` with
|
|
55
53
|
* four mutually-exclusive optional part sub-maps (`video_clip` / `speech` /
|
|
56
54
|
* `caption` / `bgm`), each a `LoroMap` whose fields are stored as real Loro
|
|
@@ -64,16 +62,10 @@ function base64ToBytes(base64) {
|
|
|
64
62
|
* as opaque JSON-blob sub-keys via `transform`. The four sub-keys are optional,
|
|
65
63
|
* so the "exactly one part kind" union invariant is not enforced by the schema
|
|
66
64
|
* type; it is rebuilt by `draftToPartUnion` on read and gated by zod on write.
|
|
67
|
-
* - a caption's `style` is
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* other nested maps are keyed by a unique id (`part_library`) or created
|
|
72
|
-
* atomically with their parent (`partValue`'s part sub-map), so they never hit
|
|
73
|
-
* that concurrent-first-create fork and stay on plain `setContainer`. See the
|
|
74
|
-
* note on `captionPart` below.
|
|
75
|
-
* - `video_creation_settings` is an opaque JSON blob carried in a string via
|
|
76
|
-
* `transform`: a whole value, last-writer-wins (no field-level concurrent edits).
|
|
65
|
+
* - a caption's `style` is a mergeable child map for independent style fields.
|
|
66
|
+
* Creation paths materialize it with the caption, even when empty, so undoing
|
|
67
|
+
* a later style edit never removes the parent reference. Other nested maps
|
|
68
|
+
* keep their regular container identities from creation.
|
|
77
69
|
*/
|
|
78
70
|
/**
|
|
79
71
|
* JSON-blob transform for an opaque, last-writer-wins value carried in a Loro
|
|
@@ -154,16 +146,6 @@ const partValue = schema.LoroMap({
|
|
|
154
146
|
bgm: bgmPart
|
|
155
147
|
});
|
|
156
148
|
const videoDocumentMirrorSchema = schema({
|
|
157
|
-
meta: schema.LoroMap({
|
|
158
|
-
schema_version: schema.String({ required: false }),
|
|
159
|
-
draft_id: schema.String({ required: false }),
|
|
160
|
-
project_id: schema.String({ required: false }),
|
|
161
|
-
owner_id: schema.String({ required: false }),
|
|
162
|
-
thumbnail_storage_key: schema.String({ required: false }),
|
|
163
|
-
chat_session_id: schema.String({ required: false }),
|
|
164
|
-
video_creation_settings: schema.String({ required: false }).transform(jsonTransform()),
|
|
165
|
-
version: schema.Number({ required: false })
|
|
166
|
-
}),
|
|
167
149
|
timeline: schema.LoroMap({ unit_time_ms: schema.Number({ required: false }) }),
|
|
168
150
|
tracks: schema.LoroMovableList(track, (t) => t.id),
|
|
169
151
|
part_library: schema.LoroMapRecord(partValue)
|
|
@@ -213,9 +195,6 @@ function recordEntries(record) {
|
|
|
213
195
|
}
|
|
214
196
|
//#endregion
|
|
215
197
|
//#region src/document/types.ts
|
|
216
|
-
const VIDEO_DOCUMENT_SCHEMA_VERSION = "video-document/v0";
|
|
217
|
-
/** A server-derived compatibility view, never an editor write authority. */
|
|
218
|
-
const ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION = "video-document/entity-projection-v1";
|
|
219
198
|
/** The linear speed multiplier of a `speed_shift`, defaulting to 1 (original). */
|
|
220
199
|
function speedOf(speedShift) {
|
|
221
200
|
const speed = speedShift?.config?.linear?.speed;
|
|
@@ -549,12 +528,12 @@ function cascadeAfterVideoClipChanges(doc, makeEmptyPart) {
|
|
|
549
528
|
* read-view: absolute time per item, `part_aggregations`, and total duration.
|
|
550
529
|
* This is the read side of the single-directional flow — never written back.
|
|
551
530
|
*/
|
|
552
|
-
function solveVideoDocument(document) {
|
|
531
|
+
function solveVideoDocument(document, options = {}) {
|
|
553
532
|
const doc = videoDocumentToTimelineDoc(document);
|
|
554
533
|
let counter = 0;
|
|
555
534
|
const derivedFillerPartIds = /* @__PURE__ */ new Set();
|
|
556
|
-
if (
|
|
557
|
-
|
|
535
|
+
if (options.placementResolved === true) {
|
|
536
|
+
assertResolvedPlacement(document);
|
|
558
537
|
recalculateTimelineDuration(doc);
|
|
559
538
|
} else cascadeAfterVideoClipChanges(doc, () => {
|
|
560
539
|
const partId = `empty_${counter++}`;
|
|
@@ -759,6 +738,19 @@ function ensureLaneTrack(draft, kind) {
|
|
|
759
738
|
function findLaneTrack(draft, kind) {
|
|
760
739
|
return (draft.tracks ?? []).find((t) => t?.parts_kind === kind);
|
|
761
740
|
}
|
|
741
|
+
/**
|
|
742
|
+
* Whether every item already states a position that can be read without running
|
|
743
|
+
* the cascade: nothing `sequential`, and every `anchored` item carrying the
|
|
744
|
+
* absolute landing its writer stamped. This is a verification predicate, not a
|
|
745
|
+
* provenance test — see `SolveVideoDocumentOptions.placementResolved`.
|
|
746
|
+
*/
|
|
747
|
+
function hasResolvedPlacement(document) {
|
|
748
|
+
return (document.tracks ?? []).every((track) => (track.items ?? []).every((item) => item.time_position.mode !== "sequential" && (item.time_position.mode !== "anchored" || Number.isSafeInteger(item.fallback_abs_ms))));
|
|
749
|
+
}
|
|
750
|
+
/** A producer that claims resolved placement must actually have resolved it. */
|
|
751
|
+
function assertResolvedPlacement(document) {
|
|
752
|
+
if (!hasResolvedPlacement(document)) throw new Error("An entity timeline projection must contain resolved placement facts");
|
|
753
|
+
}
|
|
762
754
|
//#endregion
|
|
763
755
|
//#region src/document/zod-schema.ts
|
|
764
756
|
const partKindSchema = z.enum([
|
|
@@ -859,18 +851,7 @@ const partUnionSchema = z.union([
|
|
|
859
851
|
"bgm"
|
|
860
852
|
].filter((k) => p[k] != null).length === 1;
|
|
861
853
|
}, { message: "A part_library value must have exactly one of video_clip / speech / caption / bgm" });
|
|
862
|
-
const videoDocumentMetaSchema = z.object({
|
|
863
|
-
schema_version: z.enum([VIDEO_DOCUMENT_SCHEMA_VERSION, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION]),
|
|
864
|
-
draft_id: z.string().optional(),
|
|
865
|
-
project_id: z.string().optional(),
|
|
866
|
-
owner_id: z.string().optional(),
|
|
867
|
-
thumbnail_storage_key: z.string().optional(),
|
|
868
|
-
chat_session_id: z.string().optional(),
|
|
869
|
-
video_creation_settings: z.unknown().optional(),
|
|
870
|
-
version: finiteNumber.optional()
|
|
871
|
-
}).passthrough();
|
|
872
854
|
const videoDocumentSchema = z.object({
|
|
873
|
-
meta: videoDocumentMetaSchema,
|
|
874
855
|
timeline: z.object({ unit_time_ms: finiteNumber.optional() }).passthrough().optional(),
|
|
875
856
|
tracks: z.array(trackSchema).optional(),
|
|
876
857
|
part_library: z.record(z.string(), partUnionSchema).optional()
|
|
@@ -1140,13 +1121,13 @@ function zodPath(path) {
|
|
|
1140
1121
|
//#endregion
|
|
1141
1122
|
//#region src/document/projection.ts
|
|
1142
1123
|
/**
|
|
1143
|
-
* Projection between
|
|
1144
|
-
*
|
|
1124
|
+
* Projection between authoritative `VideoDocument` and compatible content
|
|
1125
|
+
* layouts (RFC 02 §5/§7). Both directions live here:
|
|
1145
1126
|
*
|
|
1146
1127
|
* - `toVideoDocument` ingests a `VideoDraft`, deriving each item's `position`
|
|
1147
1128
|
* from the legacy absolute layout + aggregations; derived values (abs time,
|
|
1148
1129
|
* `part_aggregations`, total duration) are dropped.
|
|
1149
|
-
* - `fromVideoDocument` solves a `VideoDocument`
|
|
1130
|
+
* - `fromVideoDocument` solves a `VideoDocument` into `VideoDraftContent` via the
|
|
1150
1131
|
* timeline-core cascade, re-deriving exactly those values.
|
|
1151
1132
|
*
|
|
1152
1133
|
* Business validation lives in `validation.ts`; `fromVideoDocument` asserts a
|
|
@@ -1177,16 +1158,6 @@ function toVideoDocument(draft) {
|
|
|
1177
1158
|
...(draft.below_main_tracks ?? []).map((t) => toTrack(t, false))
|
|
1178
1159
|
];
|
|
1179
1160
|
return {
|
|
1180
|
-
meta: {
|
|
1181
|
-
schema_version: VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
1182
|
-
draft_id: draft.id,
|
|
1183
|
-
project_id: draft.project_id,
|
|
1184
|
-
owner_id: draft.owner_id,
|
|
1185
|
-
thumbnail_storage_key: draft.thumbnail_storage_key,
|
|
1186
|
-
chat_session_id: draft.chat_session_id,
|
|
1187
|
-
video_creation_settings: clone$1(draft.video_creation_settings),
|
|
1188
|
-
version: draft.version
|
|
1189
|
-
},
|
|
1190
1161
|
timeline: draft.timeline == null ? void 0 : { unit_time_ms: draft.timeline.unit_time_ms },
|
|
1191
1162
|
tracks,
|
|
1192
1163
|
part_library: toAuthoritativePartLibrary(draft.part_library)
|
|
@@ -1296,8 +1267,8 @@ function deriveItem(item, isMain, speechHost, partLibrary) {
|
|
|
1296
1267
|
};
|
|
1297
1268
|
}
|
|
1298
1269
|
/**
|
|
1299
|
-
* Project
|
|
1300
|
-
*
|
|
1270
|
+
* Project `VideoDocument` into `VideoDraftContent`, solving each item's
|
|
1271
|
+
* absolute position, the `part_aggregations`, and
|
|
1301
1272
|
* the total duration via the timeline-core cascade.
|
|
1302
1273
|
*
|
|
1303
1274
|
* The read-view is a compatibility contract, and the IDL declares
|
|
@@ -1311,33 +1282,26 @@ function deriveItem(item, isMain, speechHost, partLibrary) {
|
|
|
1311
1282
|
* These two are defaultable because the projection knows their values on its own:
|
|
1312
1283
|
* absent `unit_time_ms` means "no display granularity was ever stated" and absent
|
|
1313
1284
|
* `is_hidden` means "this track was never hidden". `version` is NOT defaultable
|
|
1314
|
-
* here —
|
|
1315
|
-
*
|
|
1285
|
+
* here — it belongs to Director assembly using the server envelope update_seq.
|
|
1286
|
+
* Neither business metadata nor a revision counter belongs in content projection.
|
|
1316
1287
|
*/
|
|
1317
|
-
function fromVideoDocument(document) {
|
|
1288
|
+
function fromVideoDocument(document, options = {}) {
|
|
1318
1289
|
assertValidVideoDocument(document);
|
|
1319
|
-
const view = solveVideoDocument(document);
|
|
1290
|
+
const view = solveVideoDocument(document, options);
|
|
1320
1291
|
const tracks = document.tracks ?? [];
|
|
1321
1292
|
const mainTrack = tracks.find((t) => t.parts_kind === "video_clip");
|
|
1322
1293
|
const aboveTracks = tracks.filter((t) => t.parts_kind === "caption");
|
|
1323
1294
|
const belowTracks = tracks.filter((t) => t.parts_kind === "speech" || t.parts_kind === "bgm");
|
|
1324
1295
|
return {
|
|
1325
|
-
id: document.meta.draft_id,
|
|
1326
|
-
project_id: document.meta.project_id,
|
|
1327
|
-
owner_id: document.meta.owner_id,
|
|
1328
|
-
thumbnail_storage_key: document.meta.thumbnail_storage_key,
|
|
1329
1296
|
timeline: {
|
|
1330
1297
|
duration_ms: view.durationMs,
|
|
1331
1298
|
unit_time_ms: document.timeline?.unit_time_ms ?? 33.333
|
|
1332
1299
|
},
|
|
1333
|
-
video_creation_settings: clone$1(document.meta.video_creation_settings),
|
|
1334
|
-
chat_session_id: document.meta.chat_session_id,
|
|
1335
1300
|
main_track: draftTrack(mainTrack, view.absByPartId),
|
|
1336
1301
|
above_main_tracks: aboveTracks.map((t) => draftTrack(t, view.absByPartId)),
|
|
1337
1302
|
below_main_tracks: belowTracks.map((t) => draftTrack(t, view.absByPartId)),
|
|
1338
1303
|
part_aggregations: view.aggregations,
|
|
1339
|
-
part_library: toReadViewPartLibrary(view.partLibrary, view.durationMs, view.derivedFillerPartIds)
|
|
1340
|
-
version: document.meta.version
|
|
1304
|
+
part_library: toReadViewPartLibrary(view.partLibrary, view.durationMs, view.derivedFillerPartIds)
|
|
1341
1305
|
};
|
|
1342
1306
|
}
|
|
1343
1307
|
/**
|
|
@@ -1444,7 +1408,7 @@ function clone$1(value) {
|
|
|
1444
1408
|
//#region src/document/initial-document.ts
|
|
1445
1409
|
/**
|
|
1446
1410
|
* Build the `VideoDocument` a newly created project starts from: the caller's
|
|
1447
|
-
*
|
|
1411
|
+
* no parts and one empty track per lane. Business facts remain in Director.
|
|
1448
1412
|
*
|
|
1449
1413
|
* The empty lane tracks are the reason this function exists rather than callers
|
|
1450
1414
|
* assembling a document inline. Lane tracks are otherwise minted lazily by the
|
|
@@ -1465,18 +1429,8 @@ function clone$1(value) {
|
|
|
1465
1429
|
* supplies, and total duration is derived on read (RFC 02 §6), so a new document
|
|
1466
1430
|
* has no timeline fact to state.
|
|
1467
1431
|
*/
|
|
1468
|
-
function buildInitialVideoDocument(
|
|
1432
|
+
function buildInitialVideoDocument() {
|
|
1469
1433
|
return {
|
|
1470
|
-
meta: {
|
|
1471
|
-
schema_version: VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
1472
|
-
draft_id: facts.draftId,
|
|
1473
|
-
project_id: facts.projectId,
|
|
1474
|
-
owner_id: facts.ownerId,
|
|
1475
|
-
thumbnail_storage_key: void 0,
|
|
1476
|
-
chat_session_id: facts.chatSessionId,
|
|
1477
|
-
video_creation_settings: facts.videoCreationSettings,
|
|
1478
|
-
version: void 0
|
|
1479
|
-
},
|
|
1480
1434
|
timeline: void 0,
|
|
1481
1435
|
tracks: emptyLaneTracks(),
|
|
1482
1436
|
part_library: {}
|
|
@@ -3094,7 +3048,7 @@ function resolveGraphLayout(graph) {
|
|
|
3094
3048
|
* consulted as an editing fact. Peer IDs appear only in this derived read
|
|
3095
3049
|
* contract, reconstructed from relations; they are not stored in payloads.
|
|
3096
3050
|
*/
|
|
3097
|
-
function projectEntityTimeline(rows
|
|
3051
|
+
function projectEntityTimeline(rows) {
|
|
3098
3052
|
const graph = new EntityProjectionGraph(rows);
|
|
3099
3053
|
const layout = resolveGraphLayout(graph);
|
|
3100
3054
|
for (const track of layout.tracks) {
|
|
@@ -3183,10 +3137,6 @@ function projectEntityTimeline(rows, meta) {
|
|
|
3183
3137
|
}
|
|
3184
3138
|
}
|
|
3185
3139
|
const document = {
|
|
3186
|
-
meta: {
|
|
3187
|
-
...structuredClone(meta),
|
|
3188
|
-
schema_version: ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION
|
|
3189
|
-
},
|
|
3190
3140
|
timeline: graph.timeline.payload.unit_time_ms === void 0 ? void 0 : { unit_time_ms: positive(graph.timeline.payload.unit_time_ms, "unit_time_ms") },
|
|
3191
3141
|
tracks: layout.tracks.map((track) => ({
|
|
3192
3142
|
id: track.entityId,
|
|
@@ -4271,13 +4221,37 @@ function materialize(doc, allowSelectionConflicts = false) {
|
|
|
4271
4221
|
for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
|
|
4272
4222
|
}
|
|
4273
4223
|
//#endregion
|
|
4224
|
+
//#region src/document/document-mutation-guard.ts
|
|
4225
|
+
/**
|
|
4226
|
+
* @internal
|
|
4227
|
+
* Shared by synchronous mutation entry points for one document. The session
|
|
4228
|
+
* closes it before teardown callbacks can attempt another write.
|
|
4229
|
+
*/
|
|
4230
|
+
var DocumentMutationGuard = class {
|
|
4231
|
+
running = false;
|
|
4232
|
+
closed = false;
|
|
4233
|
+
run(mutation) {
|
|
4234
|
+
if (this.closed) throw new Error("mengine document is disposed");
|
|
4235
|
+
if (this.running) throw new Error("mengine document mutation is already running");
|
|
4236
|
+
this.running = true;
|
|
4237
|
+
try {
|
|
4238
|
+
return mutation();
|
|
4239
|
+
} finally {
|
|
4240
|
+
this.running = false;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
close() {
|
|
4244
|
+
this.closed = true;
|
|
4245
|
+
}
|
|
4246
|
+
};
|
|
4247
|
+
//#endregion
|
|
4274
4248
|
//#region src/document/mirror-read.ts
|
|
4275
4249
|
/**
|
|
4276
4250
|
* Project the mirror state (`VideoDocumentDraft`) into the authoritative
|
|
4277
4251
|
* `VideoDocument`. The storage shape is isomorphic to the domain shape (RFC 03
|
|
4278
|
-
* §4, reference/17 §4:
|
|
4252
|
+
* §4, reference/17 §4: timeline + a single `tracks` list + part_library), so this
|
|
4279
4253
|
* is a near-identity — it reads `time_position` / `fallback_abs_ms` JSON blobs
|
|
4280
|
-
* back into structured values and
|
|
4254
|
+
* back into structured values and selects content fields. Legacy meta is ignored.
|
|
4281
4255
|
*
|
|
4282
4256
|
* It maps only authoritative facts (RFC 02 §6): `part_id` + `time_position`. The
|
|
4283
4257
|
* projection-derived `VideoDraft` read-view (absolute time, `part_aggregations`,
|
|
@@ -4294,20 +4268,8 @@ function readVideoDocumentFromDraft(draft) {
|
|
|
4294
4268
|
if (part != null) partLibrary[partId] = part;
|
|
4295
4269
|
}
|
|
4296
4270
|
const timeline = draft.timeline;
|
|
4297
|
-
const hasTimeline = timeline?.unit_time_ms != null;
|
|
4298
|
-
const meta = draft.meta;
|
|
4299
4271
|
return {
|
|
4300
|
-
|
|
4301
|
-
schema_version: meta?.schema_version === "video-document/entity-projection-v1" ? ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION : VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
4302
|
-
draft_id: emptyToUndefined(meta?.draft_id),
|
|
4303
|
-
project_id: emptyToUndefined(meta?.project_id),
|
|
4304
|
-
owner_id: emptyToUndefined(meta?.owner_id),
|
|
4305
|
-
thumbnail_storage_key: emptyToUndefined(meta?.thumbnail_storage_key),
|
|
4306
|
-
chat_session_id: emptyToUndefined(meta?.chat_session_id),
|
|
4307
|
-
video_creation_settings: meta?.video_creation_settings ?? void 0,
|
|
4308
|
-
version: meta?.version ?? void 0
|
|
4309
|
-
},
|
|
4310
|
-
timeline: hasTimeline ? { unit_time_ms: timeline.unit_time_ms } : void 0,
|
|
4272
|
+
timeline: timeline?.unit_time_ms != null ? { unit_time_ms: timeline.unit_time_ms } : void 0,
|
|
4311
4273
|
tracks: rowsToTracks(draft.tracks),
|
|
4312
4274
|
part_library: partLibrary
|
|
4313
4275
|
};
|
|
@@ -4331,9 +4293,6 @@ function rowToTrack(row) {
|
|
|
4331
4293
|
})
|
|
4332
4294
|
};
|
|
4333
4295
|
}
|
|
4334
|
-
function emptyToUndefined(value) {
|
|
4335
|
-
return value == null || value === "" ? void 0 : value;
|
|
4336
|
-
}
|
|
4337
4296
|
//#endregion
|
|
4338
4297
|
//#region src/document/mirror-adapter.ts
|
|
4339
4298
|
/**
|
|
@@ -4346,33 +4305,50 @@ function emptyToUndefined(value) {
|
|
|
4346
4305
|
* - `transact(edit, audit)` runs the whole op in one `mirror.setState` callback:
|
|
4347
4306
|
* one diff, one `doc.commit` carrying the audit message. The callback edits an
|
|
4348
4307
|
* immer draft, so a throw inside it discards the draft and never touches Loro
|
|
4349
|
-
* (natural rollback)
|
|
4308
|
+
* (natural rollback) without a document checkout or compensating commit.
|
|
4350
4309
|
* - mirror's `idSelector` (track items keyed by `part_id`) diffs reorders to
|
|
4351
4310
|
* real Loro `move` ops on every lane, so per-item CRDT identity survives on
|
|
4352
4311
|
* main and secondary tracks alike.
|
|
4353
4312
|
*/
|
|
4354
4313
|
var MirrorVideoDocumentAdapter = class {
|
|
4355
4314
|
doc;
|
|
4315
|
+
mutationGuard;
|
|
4356
4316
|
mirror;
|
|
4357
|
-
|
|
4317
|
+
disposed = false;
|
|
4318
|
+
constructor(doc, mutationGuard = new DocumentMutationGuard()) {
|
|
4358
4319
|
this.doc = doc;
|
|
4320
|
+
this.mutationGuard = mutationGuard;
|
|
4359
4321
|
this.mirror = new Mirror({
|
|
4360
4322
|
doc,
|
|
4361
|
-
schema: videoDocumentMirrorSchema
|
|
4323
|
+
schema: videoDocumentMirrorSchema,
|
|
4324
|
+
validateUpdates: false
|
|
4362
4325
|
});
|
|
4363
4326
|
}
|
|
4364
4327
|
snapshot() {
|
|
4365
|
-
|
|
4366
|
-
if (this.
|
|
4367
|
-
return
|
|
4328
|
+
this.assertActive();
|
|
4329
|
+
if (this.isEntityDocument()) return projectEntityTimeline(readEntityDocument(this.doc).rows);
|
|
4330
|
+
return readVideoDocumentFromDraft(this.mirror.getState());
|
|
4368
4331
|
}
|
|
4369
4332
|
/**
|
|
4370
|
-
*
|
|
4371
|
-
*
|
|
4372
|
-
*
|
|
4333
|
+
* Native Entity authority: placement is already solved by the Entity graph.
|
|
4334
|
+
*
|
|
4335
|
+
* The root check goes through `getShallowValue` first because `getMap` creates
|
|
4336
|
+
* the container on access — probing a legacy document for `medeo` would
|
|
4337
|
+
* otherwise mutate it, which a read must never do.
|
|
4373
4338
|
*/
|
|
4374
|
-
|
|
4375
|
-
|
|
4339
|
+
isEntityDocument() {
|
|
4340
|
+
const roots = this.doc.getShallowValue();
|
|
4341
|
+
if (!Object.hasOwn(roots, "medeo")) return false;
|
|
4342
|
+
return this.doc.getMap("medeo").get("schema") === LORO_ENTITY_SCHEMA;
|
|
4343
|
+
}
|
|
4344
|
+
/**
|
|
4345
|
+
* Release mirror subscriptions; do not reuse this adapter afterwards.
|
|
4346
|
+
* The caller still owns the borrowed LoroDoc.
|
|
4347
|
+
*/
|
|
4348
|
+
dispose() {
|
|
4349
|
+
if (this.disposed) return;
|
|
4350
|
+
this.disposed = true;
|
|
4351
|
+
this.mirror.dispose();
|
|
4376
4352
|
}
|
|
4377
4353
|
/**
|
|
4378
4354
|
* Apply one op as a single transaction. `edit` mutates the immer draft; mirror
|
|
@@ -4381,80 +4357,71 @@ var MirrorVideoDocumentAdapter = class {
|
|
|
4381
4357
|
* skips the commit — matching the prior "empty op leaves no audit" behavior.
|
|
4382
4358
|
*/
|
|
4383
4359
|
transact(edit, audit) {
|
|
4384
|
-
|
|
4385
|
-
this.
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4360
|
+
this.assertActive();
|
|
4361
|
+
if (this.isEntityDocument()) throw new Error("Entity documents are read-only through this adapter; edit the Medeo entities instead");
|
|
4362
|
+
this.mutationGuard.run(() => {
|
|
4363
|
+
this.mirror.setState((draft) => {
|
|
4364
|
+
edit(draft);
|
|
4365
|
+
const result = validateSchema(videoDocumentMirrorSchema, {
|
|
4366
|
+
timeline: draft.timeline,
|
|
4367
|
+
tracks: draft.tracks,
|
|
4368
|
+
part_library: draft.part_library
|
|
4369
|
+
});
|
|
4370
|
+
if (!result.valid) throw new Error(`Invalid content update: ${result.errors?.join("; ")}`);
|
|
4371
|
+
}, {
|
|
4372
|
+
origin: "mengine.semantic_editor",
|
|
4373
|
+
message: JSON.stringify({
|
|
4374
|
+
semantic_op: audit.kind,
|
|
4375
|
+
payload: audit.payload,
|
|
4376
|
+
intent: audit.intent ?? null,
|
|
4377
|
+
actor: audit.actor ?? null
|
|
4378
|
+
})
|
|
4379
|
+
});
|
|
4395
4380
|
});
|
|
4396
4381
|
}
|
|
4382
|
+
assertActive() {
|
|
4383
|
+
if (this.disposed) throw new Error("mengine document adapter is disposed");
|
|
4384
|
+
}
|
|
4397
4385
|
};
|
|
4398
4386
|
/** Build a fresh Loro doc seeded with `document` through the mirror. */
|
|
4399
4387
|
function createMirrorVideoDocument(document, options = {}) {
|
|
4400
4388
|
assertValidVideoDocument(document);
|
|
4401
4389
|
const doc = new LoroDoc();
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4390
|
+
try {
|
|
4391
|
+
if (options.peerId != null) doc.setPeerId(options.peerId);
|
|
4392
|
+
const mirror = new Mirror({
|
|
4393
|
+
doc,
|
|
4394
|
+
schema: videoDocumentMirrorSchema
|
|
4395
|
+
});
|
|
4396
|
+
try {
|
|
4397
|
+
mirror.setState((draft) => {
|
|
4398
|
+
seedDraft(draft, document);
|
|
4399
|
+
}, { origin: options.origin ?? "mengine.bootstrap" });
|
|
4400
|
+
return doc;
|
|
4401
|
+
} finally {
|
|
4402
|
+
mirror.dispose();
|
|
4403
|
+
}
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
doc.free();
|
|
4406
|
+
throw error;
|
|
4407
|
+
}
|
|
4410
4408
|
}
|
|
4411
4409
|
/** Build a `MirrorVideoDocumentAdapter` over a fresh doc seeded with `document`. */
|
|
4412
4410
|
function createMirrorVideoDocumentAdapter(document, options) {
|
|
4413
4411
|
return new MirrorVideoDocumentAdapter(createMirrorVideoDocument(document, options));
|
|
4414
4412
|
}
|
|
4415
|
-
/**
|
|
4416
|
-
function
|
|
4417
|
-
if (projection.meta.schema_version !== "video-document/entity-projection-v1") throw new Error("Expected an entity timeline projection");
|
|
4418
|
-
if (!Number.isSafeInteger(entityRevision) || entityRevision < 1) throw new Error("Expected a positive entity revision");
|
|
4419
|
-
assertValidVideoDocument(projection);
|
|
4420
|
-
const from = doc.oplogVersion();
|
|
4421
|
-
new Mirror({
|
|
4422
|
-
doc,
|
|
4423
|
-
schema: videoDocumentMirrorSchema
|
|
4424
|
-
}).setState((draft) => writeVideoDocumentToDraft(draft, {
|
|
4425
|
-
...projection,
|
|
4426
|
-
meta: {
|
|
4427
|
-
...projection.meta,
|
|
4428
|
-
version: entityRevision
|
|
4429
|
-
}
|
|
4430
|
-
}), {
|
|
4431
|
-
origin: "mengine.entity_projection",
|
|
4432
|
-
message: JSON.stringify({
|
|
4433
|
-
semantic_op: "EntityTimelineProjection",
|
|
4434
|
-
entity_revision: entityRevision
|
|
4435
|
-
})
|
|
4436
|
-
});
|
|
4437
|
-
return doc.export({
|
|
4438
|
-
mode: "update",
|
|
4439
|
-
from
|
|
4440
|
-
});
|
|
4441
|
-
}
|
|
4442
|
-
/** Write a whole `VideoDocument` into a draft (seed a fresh doc / plain-memory state). */
|
|
4443
|
-
function writeVideoDocumentToDraft(draft, document) {
|
|
4444
|
-
draft.meta = {
|
|
4445
|
-
schema_version: document.meta.schema_version,
|
|
4446
|
-
draft_id: document.meta.draft_id ?? void 0,
|
|
4447
|
-
project_id: document.meta.project_id ?? void 0,
|
|
4448
|
-
owner_id: document.meta.owner_id ?? void 0,
|
|
4449
|
-
thumbnail_storage_key: document.meta.thumbnail_storage_key ?? void 0,
|
|
4450
|
-
chat_session_id: document.meta.chat_session_id ?? void 0,
|
|
4451
|
-
video_creation_settings: document.meta.video_creation_settings ?? void 0,
|
|
4452
|
-
version: document.meta.version ?? void 0
|
|
4453
|
-
};
|
|
4413
|
+
/** Write a whole `VideoDocument` into the mirror draft (seed a fresh doc / plain-memory state). */
|
|
4414
|
+
function seedDraft(draft, document) {
|
|
4454
4415
|
draft.timeline = { unit_time_ms: document.timeline?.unit_time_ms ?? void 0 };
|
|
4455
4416
|
draft.tracks = (document.tracks ?? []).map(toTrackRow);
|
|
4456
4417
|
draft.part_library = {};
|
|
4457
|
-
for (const [partId, part] of Object.entries(document.part_library ?? {}))
|
|
4418
|
+
for (const [partId, part] of Object.entries(document.part_library ?? {})) {
|
|
4419
|
+
const createdPart = part.caption != null ? { caption: {
|
|
4420
|
+
...part.caption,
|
|
4421
|
+
style: part.caption.style ?? {}
|
|
4422
|
+
} } : part;
|
|
4423
|
+
draft.part_library[partId] = partUnionToDraft(createdPart);
|
|
4424
|
+
}
|
|
4458
4425
|
}
|
|
4459
4426
|
/** Map a domain `Track` to a draft track row (no lane/lane_order — see schema). */
|
|
4460
4427
|
function toTrackRow(track) {
|
|
@@ -4538,10 +4505,12 @@ var PlainMemoryAdapter = class {
|
|
|
4538
4505
|
* use this so every minted id is appended to the current transact's list.
|
|
4539
4506
|
*/
|
|
4540
4507
|
idFactory;
|
|
4508
|
+
readOnly;
|
|
4541
4509
|
constructor(document, options) {
|
|
4542
4510
|
assertValidVideoDocument(document);
|
|
4511
|
+
this.readOnly = options?.readOnly === true;
|
|
4543
4512
|
const draft = {};
|
|
4544
|
-
|
|
4513
|
+
seedDraft(draft, document);
|
|
4545
4514
|
this.state = draft;
|
|
4546
4515
|
this.baseIdFactory = options?.idFactory ?? generatePartId;
|
|
4547
4516
|
this.idFactory = (prefix) => {
|
|
@@ -4554,7 +4523,7 @@ var PlainMemoryAdapter = class {
|
|
|
4554
4523
|
return this._journal;
|
|
4555
4524
|
}
|
|
4556
4525
|
hasContent() {
|
|
4557
|
-
return
|
|
4526
|
+
return this.state.tracks !== void 0 || this.state.timeline !== void 0;
|
|
4558
4527
|
}
|
|
4559
4528
|
snapshot() {
|
|
4560
4529
|
return readVideoDocumentFromDraft(this.state);
|
|
@@ -4565,7 +4534,7 @@ var PlainMemoryAdapter = class {
|
|
|
4565
4534
|
* no structural change — skip journal, matching mirror "no change, no commit".
|
|
4566
4535
|
*/
|
|
4567
4536
|
transact(edit, audit) {
|
|
4568
|
-
if (this.
|
|
4537
|
+
if (this.readOnly) throw new Error("Entity documents are read-only here; edit the Medeo entities instead");
|
|
4569
4538
|
this.pendingIds = [];
|
|
4570
4539
|
try {
|
|
4571
4540
|
const next = produce(this.state, edit);
|
|
@@ -4586,4 +4555,4 @@ function createPlainMemoryAdapter(document, options) {
|
|
|
4586
4555
|
return new PlainMemoryAdapter(document, options);
|
|
4587
4556
|
}
|
|
4588
4557
|
//#endregion
|
|
4589
|
-
export {
|
|
4558
|
+
export { findLaneTrack as $, assemblePhoneticScriptContent as A, isMediaAssetVariantKind as B, EntityProjectionGraph as C, ScriptCompositionError as D, decodeEntityRelationRows as E, variantBaseEntityIds as F, toVideoDocument as G, buildSpeechHostMap as H, isJsonObject as I, validateVideoDocument as J, VideoDocumentValidationError as K, createEntityId as L, findComposedAudioScript as M, selectAudioScriptSegment as N, assembleCaptionContent as O, updateEntityFields as P, ensureLaneTrack as Q, createRelationId as R, projectEntityTimeline as S, bytesToBase64 as St, EntityTimelineProjectionError as T, derivePositionFromAbs as U, buildInitialVideoDocument as V, fromVideoDocument as W, videoDocumentSchema as X, partUnionSchema as Y, LANE_KINDS_IN_STACK_ORDER as Z, hasAudioScriptAsset as _, speedOf as _t, createMirrorVideoDocument as a, fillMainTrackTimeGaps as at, assertFieldChanges as b, videoDocumentMirrorSchema as bt, DocumentMutationGuard as c, resolveAllSpeechOverlaps as ct, LoroEntityDraft as d, TIMELINE_SKELETON_DURATION_MS as dt, hasResolvedPlacement as et, readEntityDocument as f, isEmptyVideoClip as ft, audioScriptAssetHash as g, effectiveVideoClipDurationMs as gt, audioScriptAssetFields as h, DEFAULT_UNIT_TIME_MS as ht, MirrorVideoDocumentAdapter as i, arrangeMainTrackSeamlessly as it, assembleScriptText as j, assembleEntityContent as k, LORO_ENTITY_SCHEMA as l, resolveSpeechOverlapByShiftingVideos as lt, audioScriptAssetContent as m, safeDurationMs as mt, createPlainMemoryAdapter as n, solveVideoDocument as nt, createMirrorVideoDocumentAdapter as o, reassignSpeechesToVideoClipsByTime as ot, entityOrderGroups as p, partDurationMs as pt, assertValidVideoDocument as q, generatePartId as r, cascadeAfterVideoClipChanges as rt, readVideoDocumentFromDraft as s, recalculateTimelineDuration as st, PlainMemoryAdapter as t, laneTrackId as tt, LoroEntityDocument as u, syncAggregatedClipsTimePosition as ut, equalJson as v, partUnionToDraft as vt, resolveEntityTimelineLayout as w, resolveFieldPath as x, base64ToBytes as xt, applyFieldChanges as y, recordEntries as yt, hasSequence as z };
|