@mengine/medeo-client 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{loro-relay-doc-Br-ZJBHa.js → document-C0o_cjAH.js} +120 -61
- package/dist/{index-DLchEQG7.d.ts → index-DWVYLTjv.d.ts} +68 -1
- package/dist/index.d.ts +70 -85
- package/dist/index.js +42 -30
- package/dist/loro-relay-doc-ssdYpuef.js +58 -0
- package/dist/relay.d.ts +37 -0
- package/dist/relay.js +2 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -1
- package/package.json +5 -4
|
@@ -230,6 +230,16 @@ function effectiveVideoClipDurationMs(clip) {
|
|
|
230
230
|
const effective = Math.round(sourceMs / speed);
|
|
231
231
|
return Number.isFinite(effective) && effective > 0 ? effective : 0;
|
|
232
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* The `timeline.unit_time_ms` the read-view states when the document states none.
|
|
235
|
+
*
|
|
236
|
+
* One frame at 30fps. A document legitimately has no such fact (display
|
|
237
|
+
* granularity is the editor's, not the timeline's), but the read-view IDL marks
|
|
238
|
+
* the field `@required`, so the projection has to name a value. This one is a
|
|
239
|
+
* usable granularity; `0` — which a downstream consumer had been defaulting to —
|
|
240
|
+
* is not, and passes any `>= 0` guard on its way to an editor that cannot use it.
|
|
241
|
+
*/
|
|
242
|
+
const DEFAULT_UNIT_TIME_MS = 33.333;
|
|
233
243
|
//#endregion
|
|
234
244
|
//#region src/timeline-core/types.ts
|
|
235
245
|
/** Total document duration when there is no real content (matches FE bgm fallback). */
|
|
@@ -648,6 +658,42 @@ function seedAggregations(mainItems, speechItems) {
|
|
|
648
658
|
return order.map((host) => byHost.get(host));
|
|
649
659
|
}
|
|
650
660
|
/**
|
|
661
|
+
* The four lanes in top-to-bottom stack order — the order a `tracks` list holds
|
|
662
|
+
* them in (see {@link laneRank}).
|
|
663
|
+
*
|
|
664
|
+
* Exported so a document can be seeded with all four lanes up front. That seed is
|
|
665
|
+
* not cosmetic: {@link ensureLaneTrack} is find-then-mint over a `LoroMovableList`,
|
|
666
|
+
* so two concurrent writers that each mint the same absent lane both keep their
|
|
667
|
+
* row, and the merged document holds two tracks for one lane. For the main lane
|
|
668
|
+
* that is fatal — `videoDocumentSchema` allows at most one `video_clip` track, so
|
|
669
|
+
* the merged document stops being projectable at all, symmetrically on both
|
|
670
|
+
* replicas. Pre-seeding every lane makes `ensureLaneTrack` always take its find
|
|
671
|
+
* branch, which removes the race by construction rather than by detection.
|
|
672
|
+
*
|
|
673
|
+
* What closes the race is that a track with the lane's `parts_kind` EXISTS — the
|
|
674
|
+
* lookup is by kind, not by id. So a seed is only safe if it covers every lane:
|
|
675
|
+
* a partial seed leaves the uncovered lanes exactly as exposed as before.
|
|
676
|
+
*/
|
|
677
|
+
const LANE_KINDS_IN_STACK_ORDER = [
|
|
678
|
+
"caption",
|
|
679
|
+
"video_clip",
|
|
680
|
+
"speech",
|
|
681
|
+
"bgm"
|
|
682
|
+
];
|
|
683
|
+
/**
|
|
684
|
+
* The conventional track id for a lane (`main_track`, `<kind>_track`). Shared by
|
|
685
|
+
* the up-front seed and {@link ensureLaneTrack}'s lazy mint so a document's lane
|
|
686
|
+
* ids do not depend on which of the two created the track.
|
|
687
|
+
*
|
|
688
|
+
* Ids are cosmetic to the merge itself — lane lookup goes by `parts_kind`, so
|
|
689
|
+
* drifting them apart would not reopen the concurrent-mint race. They matter to
|
|
690
|
+
* readers that address a lane by id (the FE editor's panes, fixtures), which is
|
|
691
|
+
* why there is one convention rather than two.
|
|
692
|
+
*/
|
|
693
|
+
function laneTrackId(kind) {
|
|
694
|
+
return kind === "video_clip" ? "main_track" : `${kind}_track`;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
651
697
|
* Lane-stacking rank for the single `tracks` list (reference/17 §4): caption
|
|
652
698
|
* (above) sits before the video_clip main track, which sits before speech / bgm
|
|
653
699
|
* (below). The `tracks` array is kept in this top-to-bottom order so a freshly
|
|
@@ -678,14 +724,18 @@ function insertTrackByLaneOrder(tracks, track) {
|
|
|
678
724
|
* Locate a lane's track row in the single `tracks` list by kind, minting an empty
|
|
679
725
|
* row in lane-stacking order if absent (reference/17 §4: lane = `parts_kind`).
|
|
680
726
|
* Ops use this to write authoritative items onto the right lane. The track id
|
|
681
|
-
*
|
|
727
|
+
* comes from {@link laneTrackId}, shared with the up-front seed.
|
|
728
|
+
*
|
|
729
|
+
* The mint branch is a concurrency hazard, not a convenience: see
|
|
730
|
+
* {@link LANE_KINDS_IN_STACK_ORDER}. A document seeded with all four lanes never
|
|
731
|
+
* reaches it.
|
|
682
732
|
*/
|
|
683
733
|
function ensureLaneTrack(draft, kind) {
|
|
684
734
|
draft.tracks ??= [];
|
|
685
735
|
let track = draft.tracks.find((t) => t?.parts_kind === kind);
|
|
686
736
|
if (track == null) {
|
|
687
737
|
track = {
|
|
688
|
-
id: kind
|
|
738
|
+
id: laneTrackId(kind),
|
|
689
739
|
parts_kind: kind,
|
|
690
740
|
is_hidden: void 0,
|
|
691
741
|
items: []
|
|
@@ -1239,6 +1289,20 @@ function deriveItem(item, isMain, speechHost, partLibrary) {
|
|
|
1239
1289
|
* Project the authoritative `VideoDocument` back into the legacy `VideoDraft`
|
|
1240
1290
|
* read-view, solving each item's absolute position, the `part_aggregations`, and
|
|
1241
1291
|
* the total duration via the timeline-core cascade.
|
|
1292
|
+
*
|
|
1293
|
+
* The read-view is a compatibility contract, and the IDL declares
|
|
1294
|
+
* `Timeline.unit_time_ms` and `Track.is_hidden` `@required`
|
|
1295
|
+
* (`video_draft_comp.smithy`) — so a projection that omitted them handed every
|
|
1296
|
+
* consumer a payload that violated the shape it claims to satisfy, leaving each
|
|
1297
|
+
* one to invent its own default. Two already had, and they disagreed: the agent
|
|
1298
|
+
* harness used `unit_time_ms: 0`, director used a frame at 30fps. Defaulting here
|
|
1299
|
+
* makes the contract true at the one place that produces it.
|
|
1300
|
+
*
|
|
1301
|
+
* These two are defaultable because the projection knows their values on its own:
|
|
1302
|
+
* absent `unit_time_ms` means "no display granularity was ever stated" and absent
|
|
1303
|
+
* `is_hidden` means "this track was never hidden". `version` is NOT defaultable
|
|
1304
|
+
* here — its only honest value is server state (`update_seq`) that a
|
|
1305
|
+
* `VideoDocument` cannot see, so it stays absent and the HTTP layer fills it.
|
|
1242
1306
|
*/
|
|
1243
1307
|
function fromVideoDocument(document) {
|
|
1244
1308
|
assertValidVideoDocument(document);
|
|
@@ -1254,7 +1318,7 @@ function fromVideoDocument(document) {
|
|
|
1254
1318
|
thumbnail_storage_key: document.meta.thumbnail_storage_key,
|
|
1255
1319
|
timeline: {
|
|
1256
1320
|
duration_ms: view.durationMs,
|
|
1257
|
-
unit_time_ms: document.timeline?.unit_time_ms
|
|
1321
|
+
unit_time_ms: document.timeline?.unit_time_ms ?? 33.333
|
|
1258
1322
|
},
|
|
1259
1323
|
video_creation_settings: clone(document.meta.video_creation_settings),
|
|
1260
1324
|
chat_session_id: document.meta.chat_session_id,
|
|
@@ -1350,7 +1414,7 @@ function draftTrack(track, absByPartId) {
|
|
|
1350
1414
|
return {
|
|
1351
1415
|
id: track.id,
|
|
1352
1416
|
parts_kind: track.parts_kind,
|
|
1353
|
-
is_hidden: track.is_hidden,
|
|
1417
|
+
is_hidden: track.is_hidden ?? false,
|
|
1354
1418
|
items: (track.items ?? []).map((item) => ({
|
|
1355
1419
|
part_id: item.part_id,
|
|
1356
1420
|
abs_time_position: absByPartId.get(item.part_id) ?? 0
|
|
@@ -1367,6 +1431,57 @@ function clone(value) {
|
|
|
1367
1431
|
return value;
|
|
1368
1432
|
}
|
|
1369
1433
|
//#endregion
|
|
1434
|
+
//#region src/document/initial-document.ts
|
|
1435
|
+
/**
|
|
1436
|
+
* Build the `VideoDocument` a newly created project starts from: the caller's
|
|
1437
|
+
* project facts, no parts, and one empty track per lane.
|
|
1438
|
+
*
|
|
1439
|
+
* The empty lane tracks are the reason this function exists rather than callers
|
|
1440
|
+
* assembling a document inline. Lane tracks are otherwise minted lazily by the
|
|
1441
|
+
* first op that needs one (`ensureLaneTrack`), which is find-then-mint over a
|
|
1442
|
+
* CRDT list — so if the Agent's first generation and a user edit race on an
|
|
1443
|
+
* empty document, each mints its own copy of the same lane and the merge keeps
|
|
1444
|
+
* both. Two video_clip tracks violate the at-most-one-main-track rule, and the
|
|
1445
|
+
* merged document becomes unprojectable for *every* reader (editor, render,
|
|
1446
|
+
* share) with no winning replica to fall back on. Seeding all four lanes up
|
|
1447
|
+
* front means `ensureLaneTrack` only ever finds, never mints, so the race cannot
|
|
1448
|
+
* happen on a document created here.
|
|
1449
|
+
*
|
|
1450
|
+
* All four, not just the main lane: the secondary lanes fail less loudly (a
|
|
1451
|
+
* duplicate pane rather than an unreadable document), but they fail by the same
|
|
1452
|
+
* mechanism, and covering only the fatal one would leave three live races.
|
|
1453
|
+
*
|
|
1454
|
+
* `timeline` is left absent: `unit_time_ms` is a display concern the editor
|
|
1455
|
+
* supplies, and total duration is derived on read (RFC 02 §6), so a new document
|
|
1456
|
+
* has no timeline fact to state.
|
|
1457
|
+
*/
|
|
1458
|
+
function buildInitialVideoDocument(facts) {
|
|
1459
|
+
return {
|
|
1460
|
+
meta: {
|
|
1461
|
+
schema_version: VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
1462
|
+
draft_id: facts.draftId,
|
|
1463
|
+
project_id: facts.projectId,
|
|
1464
|
+
owner_id: facts.ownerId,
|
|
1465
|
+
thumbnail_storage_key: void 0,
|
|
1466
|
+
chat_session_id: facts.chatSessionId,
|
|
1467
|
+
video_creation_settings: facts.videoCreationSettings,
|
|
1468
|
+
version: void 0
|
|
1469
|
+
},
|
|
1470
|
+
timeline: void 0,
|
|
1471
|
+
tracks: emptyLaneTracks(),
|
|
1472
|
+
part_library: {}
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
/** One empty track per lane, in lane-stacking order, with the conventional ids. */
|
|
1476
|
+
function emptyLaneTracks() {
|
|
1477
|
+
return LANE_KINDS_IN_STACK_ORDER.map((kind) => ({
|
|
1478
|
+
id: laneTrackId(kind),
|
|
1479
|
+
parts_kind: kind,
|
|
1480
|
+
is_hidden: void 0,
|
|
1481
|
+
items: []
|
|
1482
|
+
}));
|
|
1483
|
+
}
|
|
1484
|
+
//#endregion
|
|
1370
1485
|
//#region src/document/mirror-read.ts
|
|
1371
1486
|
/**
|
|
1372
1487
|
* Project the mirror state (`VideoDocumentDraft`) into the authoritative
|
|
@@ -1536,60 +1651,4 @@ function toTrackRow(track) {
|
|
|
1536
1651
|
};
|
|
1537
1652
|
}
|
|
1538
1653
|
//#endregion
|
|
1539
|
-
|
|
1540
|
-
/**
|
|
1541
|
-
* Loro OpLog-relay primitives: the doc shape and update classification an update
|
|
1542
|
-
* *host* needs, as opposed to an editing client.
|
|
1543
|
-
*
|
|
1544
|
-
* A relay never materializes DocState (`docs/concepts/oplog_docstate` §Relay
|
|
1545
|
-
* Server) — it keeps a detached `LoroDoc` that validates and accumulates updates
|
|
1546
|
-
* and exports diffs by version vector.
|
|
1547
|
-
*
|
|
1548
|
-
* These live in `medeo-client` rather than in `mengine-server` because two
|
|
1549
|
-
* separate hosts must classify identically: the real server (`apps/mengine-server`,
|
|
1550
|
-
* both storage implementations) and the in-process test double
|
|
1551
|
-
* (`testing/in-memory-mengine-server.ts`) that client-side tests push against. A
|
|
1552
|
-
* double with its own verdict logic drifts from the server it stands in for, and
|
|
1553
|
-
* the four push outcomes it produces are exactly what those tests assert on — so
|
|
1554
|
-
* the classification is shared code, not duplicated code.
|
|
1555
|
-
*/
|
|
1556
|
-
/** Build a fresh detached relay doc (no DocState materialization). */
|
|
1557
|
-
function newRelayDoc() {
|
|
1558
|
-
const doc = new LoroDoc();
|
|
1559
|
-
doc.detach();
|
|
1560
|
-
return doc;
|
|
1561
|
-
}
|
|
1562
|
-
/** Load a detached relay doc from a stored snapshot. */
|
|
1563
|
-
function relayDocFromSnapshot(snapshot) {
|
|
1564
|
-
const doc = newRelayDoc();
|
|
1565
|
-
doc.import(snapshot);
|
|
1566
|
-
return doc;
|
|
1567
|
-
}
|
|
1568
|
-
/** A full-oplog clone (via snapshot round-trip) for non-destructive probing. */
|
|
1569
|
-
function cloneRelayDoc(doc) {
|
|
1570
|
-
return relayDocFromSnapshot(doc.export({ mode: "snapshot" }));
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Classify an incoming update against `doc` without mutating it.
|
|
1574
|
-
*
|
|
1575
|
-
* Deliberately version-delta based, not `ImportStatus`-shape based: in loro-crdt
|
|
1576
|
-
* 1.13.x the JS `import()` returns `success`/`pending` as objects whose ranges
|
|
1577
|
-
* are not reliably populated, and a pending (out-of-order) import still buffers
|
|
1578
|
-
* the orphan op into the oplog. So this imports into a throwaway snapshot-clone
|
|
1579
|
-
* and inspects whether the oplog frontiers advanced — letting the caller import
|
|
1580
|
-
* into its canonical doc only on `accepted`, keeping orphan bytes out of the log.
|
|
1581
|
-
*/
|
|
1582
|
-
function classifyUpdate(doc, update) {
|
|
1583
|
-
const probe = cloneRelayDoc(doc);
|
|
1584
|
-
let status;
|
|
1585
|
-
try {
|
|
1586
|
-
status = probe.import(update);
|
|
1587
|
-
} catch {
|
|
1588
|
-
return "corrupt_update";
|
|
1589
|
-
}
|
|
1590
|
-
if (status?.pending != null) return "missing_dependency";
|
|
1591
|
-
if (probe.cmpWithFrontiers(doc.oplogFrontiers()) !== 1) return "duplicate";
|
|
1592
|
-
return "accepted";
|
|
1593
|
-
}
|
|
1594
|
-
//#endregion
|
|
1595
|
-
export { partDurationMs as A, reassignSpeechesToVideoClipsByTime as C, syncAggregatedClipsTimePosition as D, resolveSpeechOverlapByShiftingVideos as E, recordEntries as F, videoDocumentMirrorSchema as I, base64ToBytes as L, VIDEO_DOCUMENT_SCHEMA_VERSION as M, effectiveVideoClipDurationMs as N, TIMELINE_SKELETON_DURATION_MS as O, partUnionToDraft as P, bytesToBase64 as R, fillMainTrackTimeGaps as S, resolveAllSpeechOverlaps as T, ensureLaneTrack as _, createMirrorVideoDocument as a, cascadeAfterVideoClipChanges as b, buildSpeechHostMap as c, toVideoDocument as d, VideoDocumentValidationError as f, videoDocumentSchema as g, partUnionSchema as h, MirrorVideoDocumentAdapter as i, safeDurationMs as j, isEmptyVideoClip as k, derivePositionFromAbs as l, validateVideoDocument as m, newRelayDoc as n, createMirrorVideoDocumentAdapter as o, assertValidVideoDocument as p, relayDocFromSnapshot as r, readVideoDocumentFromDraft as s, classifyUpdate as t, fromVideoDocument as u, findLaneTrack as v, recalculateTimelineDuration as w, arrangeMainTrackSeamlessly as x, solveVideoDocument as y };
|
|
1654
|
+
export { partDurationMs as A, reassignSpeechesToVideoClipsByTime as C, syncAggregatedClipsTimePosition as D, resolveSpeechOverlapByShiftingVideos as E, partUnionToDraft as F, recordEntries as I, videoDocumentMirrorSchema as L, DEFAULT_UNIT_TIME_MS as M, VIDEO_DOCUMENT_SCHEMA_VERSION as N, TIMELINE_SKELETON_DURATION_MS as O, effectiveVideoClipDurationMs as P, base64ToBytes as R, fillMainTrackTimeGaps as S, resolveAllSpeechOverlaps as T, findLaneTrack as _, buildInitialVideoDocument as a, cascadeAfterVideoClipChanges as b, fromVideoDocument as c, assertValidVideoDocument as d, validateVideoDocument as f, ensureLaneTrack as g, LANE_KINDS_IN_STACK_ORDER as h, readVideoDocumentFromDraft as i, safeDurationMs as j, isEmptyVideoClip as k, toVideoDocument as l, videoDocumentSchema as m, createMirrorVideoDocument as n, buildSpeechHostMap as o, partUnionSchema as p, createMirrorVideoDocumentAdapter as r, derivePositionFromAbs as s, MirrorVideoDocumentAdapter as t, VideoDocumentValidationError as u, laneTrackId as v, recalculateTimelineDuration as w, arrangeMainTrackSeamlessly as x, solveVideoDocument as y, bytesToBase64 as z };
|
|
@@ -632,6 +632,16 @@ type VideoDraft = Omit<VideoDraft$1, 'part_library' | 'video_creation_settings'>
|
|
|
632
632
|
interface Timeline {
|
|
633
633
|
unit_time_ms: number | undefined;
|
|
634
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* The `timeline.unit_time_ms` the read-view states when the document states none.
|
|
637
|
+
*
|
|
638
|
+
* One frame at 30fps. A document legitimately has no such fact (display
|
|
639
|
+
* granularity is the editor's, not the timeline's), but the read-view IDL marks
|
|
640
|
+
* the field `@required`, so the projection has to name a value. This one is a
|
|
641
|
+
* usable granularity; `0` — which a downstream consumer had been defaulting to —
|
|
642
|
+
* is not, and passes any `>= 0` guard on its way to an editor that cannot use it.
|
|
643
|
+
*/
|
|
644
|
+
declare const DEFAULT_UNIT_TIME_MS = 33.333;
|
|
635
645
|
/**
|
|
636
646
|
* Project-level scalars that do not participate in track ordering. Grouped under
|
|
637
647
|
* `meta` because the loro-mirror `schema()` root only accepts container schemas,
|
|
@@ -748,9 +758,66 @@ declare function derivePositionFromAbs(partId: string, abs: number, isMain: bool
|
|
|
748
758
|
* Project the authoritative `VideoDocument` back into the legacy `VideoDraft`
|
|
749
759
|
* read-view, solving each item's absolute position, the `part_aggregations`, and
|
|
750
760
|
* the total duration via the timeline-core cascade.
|
|
761
|
+
*
|
|
762
|
+
* The read-view is a compatibility contract, and the IDL declares
|
|
763
|
+
* `Timeline.unit_time_ms` and `Track.is_hidden` `@required`
|
|
764
|
+
* (`video_draft_comp.smithy`) — so a projection that omitted them handed every
|
|
765
|
+
* consumer a payload that violated the shape it claims to satisfy, leaving each
|
|
766
|
+
* one to invent its own default. Two already had, and they disagreed: the agent
|
|
767
|
+
* harness used `unit_time_ms: 0`, director used a frame at 30fps. Defaulting here
|
|
768
|
+
* makes the contract true at the one place that produces it.
|
|
769
|
+
*
|
|
770
|
+
* These two are defaultable because the projection knows their values on its own:
|
|
771
|
+
* absent `unit_time_ms` means "no display granularity was ever stated" and absent
|
|
772
|
+
* `is_hidden` means "this track was never hidden". `version` is NOT defaultable
|
|
773
|
+
* here — its only honest value is server state (`update_seq`) that a
|
|
774
|
+
* `VideoDocument` cannot see, so it stays absent and the HTTP layer fills it.
|
|
751
775
|
*/
|
|
752
776
|
declare function fromVideoDocument(document: VideoDocument): VideoDraft;
|
|
753
777
|
//#endregion
|
|
778
|
+
//#region src/document/initial-document.d.ts
|
|
779
|
+
/**
|
|
780
|
+
* The project facts a freshly created document is built from — exactly the five
|
|
781
|
+
* fields Director sets when it creates a draft row, and nothing else.
|
|
782
|
+
*
|
|
783
|
+
* Deliberately not a whole `VideoDraft`: tracks and the part library of a new
|
|
784
|
+
* document are empty by definition, so accepting them would mean accepting
|
|
785
|
+
* values that must always be empty, and every such field is a place for a future
|
|
786
|
+
* caller to send something that is silently dropped. The narrow shape makes the
|
|
787
|
+
* "a new document has no content" rule structural instead of a convention.
|
|
788
|
+
*/
|
|
789
|
+
interface InitialDocumentFacts {
|
|
790
|
+
draftId: string;
|
|
791
|
+
projectId: string | undefined;
|
|
792
|
+
ownerId: string | undefined;
|
|
793
|
+
chatSessionId: string | undefined;
|
|
794
|
+
videoCreationSettings: VideoCreationSettings | undefined;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Build the `VideoDocument` a newly created project starts from: the caller's
|
|
798
|
+
* project facts, no parts, and one empty track per lane.
|
|
799
|
+
*
|
|
800
|
+
* The empty lane tracks are the reason this function exists rather than callers
|
|
801
|
+
* assembling a document inline. Lane tracks are otherwise minted lazily by the
|
|
802
|
+
* first op that needs one (`ensureLaneTrack`), which is find-then-mint over a
|
|
803
|
+
* CRDT list — so if the Agent's first generation and a user edit race on an
|
|
804
|
+
* empty document, each mints its own copy of the same lane and the merge keeps
|
|
805
|
+
* both. Two video_clip tracks violate the at-most-one-main-track rule, and the
|
|
806
|
+
* merged document becomes unprojectable for *every* reader (editor, render,
|
|
807
|
+
* share) with no winning replica to fall back on. Seeding all four lanes up
|
|
808
|
+
* front means `ensureLaneTrack` only ever finds, never mints, so the race cannot
|
|
809
|
+
* happen on a document created here.
|
|
810
|
+
*
|
|
811
|
+
* All four, not just the main lane: the secondary lanes fail less loudly (a
|
|
812
|
+
* duplicate pane rather than an unreadable document), but they fail by the same
|
|
813
|
+
* mechanism, and covering only the fatal one would leave three live races.
|
|
814
|
+
*
|
|
815
|
+
* `timeline` is left absent: `unit_time_ms` is a display concern the editor
|
|
816
|
+
* supplies, and total duration is derived on read (RFC 02 §6), so a new document
|
|
817
|
+
* has no timeline fact to state.
|
|
818
|
+
*/
|
|
819
|
+
declare function buildInitialVideoDocument(facts: InitialDocumentFacts): VideoDocument;
|
|
820
|
+
//#endregion
|
|
754
821
|
//#region src/document/validation.d.ts
|
|
755
822
|
/**
|
|
756
823
|
* Business-level schema guard for `VideoDocument` (RFC 03 §9). It is the gate
|
|
@@ -2029,4 +2096,4 @@ declare const videoDocumentSchema: z.ZodObject<{
|
|
|
2029
2096
|
}, z.core.$loose>]>>>;
|
|
2030
2097
|
}, z.core.$loose>;
|
|
2031
2098
|
//#endregion
|
|
2032
|
-
export {
|
|
2099
|
+
export { VideoDocumentSchemaVersion as $, assertValidVideoDocument as A, BgmPart as B, index_d_exports as C, VideoDocumentMirrorSchema as D, VideoDocumentDraft as E, SpeechHostMap as F, SpeechPart as G, DEFAULT_UNIT_TIME_MS as H, buildSpeechHostMap as I, TrackItem as J, Timeline as K, derivePositionFromAbs as L, InitialDocumentFacts as M, buildInitialVideoDocument as N, videoDocumentMirrorSchema as O, DerivedItemPosition as P, VideoDocument as Q, fromVideoDocument as R, ValidationError as S, TrackItemDraft as T, PartKind as U, CaptionPart as V, PartUnion as W, VIDEO_DOCUMENT_SCHEMA_VERSION as X, TrackItemTimePosition as Y, VideoClipPart as Z, PlannedSemanticOpKind as _, MirrorVideoDocumentOptions as a, CaptionPart$1 as at, SchemaValidator as b, CommitOptions as c, SpeedShift as ct, SemanticEditor as d, TrackItem$1 as dt, VideoDocumentValidationIssue as et, SemanticOpInput as f, ImplementedSemanticOpKind as g, IMPLEMENTED_SEMANTIC_OP_KINDS as h, MirrorVideoDocumentAdapter as i, Attachment as it, validateVideoDocument as j, VideoDocumentValidationError as k, OpActor as l, Timeline$1 as lt, TransactAudit as m, videoDocumentSchema as n, VideoDraft as nt, createMirrorVideoDocument as o, CaptionStyle as ot, SemanticOpName as p, Track as q, readVideoDocumentFromDraft as r, VideoDraftPartUnion as rt, createMirrorVideoDocumentAdapter as s, PartAggregation as st, partUnionSchema as t, VideoDocumentValidationIssueCode as tt, SemanticDocumentAdapter as u, Track$1 as ut, SemanticOpKind as v, TrackDraft as w, SnapshotReadable as x, isImplementedSemanticOpKind as y, toVideoDocument as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import {
|
|
1
|
+
import { $ as VideoDocumentSchemaVersion, A as assertValidVideoDocument, B as BgmPart, C as index_d_exports, D as VideoDocumentMirrorSchema, E as VideoDocumentDraft, F as SpeechHostMap, G as SpeechPart, H as DEFAULT_UNIT_TIME_MS, I as buildSpeechHostMap, J as TrackItem, K as Timeline, L as derivePositionFromAbs, M as InitialDocumentFacts, N as buildInitialVideoDocument, O as videoDocumentMirrorSchema, P as DerivedItemPosition, Q as VideoDocument, R as fromVideoDocument, S as ValidationError, T as TrackItemDraft, U as PartKind, V as CaptionPart, W as PartUnion, X as VIDEO_DOCUMENT_SCHEMA_VERSION, Y as TrackItemTimePosition, Z as VideoClipPart, _ as PlannedSemanticOpKind, a as MirrorVideoDocumentOptions, at as CaptionPart$1, b as SchemaValidator, c as CommitOptions, ct as SpeedShift, d as SemanticEditor, dt as TrackItem$1, et as VideoDocumentValidationIssue, f as SemanticOpInput, g as ImplementedSemanticOpKind, h as IMPLEMENTED_SEMANTIC_OP_KINDS, i as MirrorVideoDocumentAdapter, it as Attachment, j as validateVideoDocument, k as VideoDocumentValidationError, l as OpActor, lt as Timeline$1, m as TransactAudit, n as videoDocumentSchema, nt as VideoDraft, o as createMirrorVideoDocument, ot as CaptionStyle, p as SemanticOpName, q as Track, r as readVideoDocumentFromDraft, rt as VideoDraftPartUnion, s as createMirrorVideoDocumentAdapter, st as PartAggregation, t as partUnionSchema, tt as VideoDocumentValidationIssueCode, u as SemanticDocumentAdapter, ut as Track$1, v as SemanticOpKind, w as TrackDraft, x as SnapshotReadable, y as isImplementedSemanticOpKind, z as toVideoDocument } from "./index-DWVYLTjv.js";
|
|
2
|
+
import { PeerID } from "loro-crdt";
|
|
3
3
|
import { DocState } from "@mengine/sync";
|
|
4
|
-
import { BaseDocStorage, Connection, DocDiff, DocSnapshotRecord, DocStorage, DocStorageOptions, DocUpdate, DocUpdateRecord } from "@mengine/storage";
|
|
4
|
+
import { BaseDocStorage, Connection, DocDiff, DocPushReceipt, DocSnapshotRecord, DocStorage, DocStorageOptions, DocUpdate, DocUpdateRecord } from "@mengine/storage";
|
|
5
5
|
|
|
6
6
|
//#region src/client/base64.d.ts
|
|
7
7
|
declare function bytesToBase64(bytes: Uint8Array): string;
|
|
@@ -474,66 +474,6 @@ declare class ManualSyncDoc {
|
|
|
474
474
|
private serverVVFrom;
|
|
475
475
|
}
|
|
476
476
|
//#endregion
|
|
477
|
-
//#region src/manual-sync/version-coverage.d.ts
|
|
478
|
-
/**
|
|
479
|
-
* Version-vector coverage: "does `outer` contain everything in `inner`?"
|
|
480
|
-
*
|
|
481
|
-
* This one predicate answers three different questions in the manual-sync document, which
|
|
482
|
-
* is why it is factored out rather than inlined three times:
|
|
483
|
-
*
|
|
484
|
-
* | question | call |
|
|
485
|
-
* | ------------------------------------- | ------------------------------- |
|
|
486
|
-
* | is there anything left to push? | `covers(watermark, localOplog)` |
|
|
487
|
-
* | did someone else write concurrently? | `covers(localOplog, serverVV)` |
|
|
488
|
-
* | has the doc moved since I last read? | `covers(seenVersion, localOplog)`|
|
|
489
|
-
*
|
|
490
|
-
* `VersionVector.compare` cannot be used for any of them: it returns `undefined`
|
|
491
|
-
* for concurrent vectors, and concurrency is the NORMAL case here — the server
|
|
492
|
-
* routinely holds peers the local doc has never seen, and after a collaborative
|
|
493
|
-
* merge the local doc holds ops the watermark predates. Treating "concurrent" as
|
|
494
|
-
* "not covered" is right for some of these and wrong for others, so the per-peer
|
|
495
|
-
* counter check is the only formulation that stays correct for all three.
|
|
496
|
-
*
|
|
497
|
-
* Equality must NOT be used as a substitute either: once collaboration happens
|
|
498
|
-
* the watermark legitimately *leads* the local doc (it carries other peers'
|
|
499
|
-
* counters), so an equality test reports "still has ops to push" forever.
|
|
500
|
-
*/
|
|
501
|
-
declare function covers(outer: VersionVector, inner: VersionVector): boolean;
|
|
502
|
-
//#endregion
|
|
503
|
-
//#region src/relay/loro-relay-doc.d.ts
|
|
504
|
-
/**
|
|
505
|
-
* Loro OpLog-relay primitives: the doc shape and update classification an update
|
|
506
|
-
* *host* needs, as opposed to an editing client.
|
|
507
|
-
*
|
|
508
|
-
* A relay never materializes DocState (`docs/concepts/oplog_docstate` §Relay
|
|
509
|
-
* Server) — it keeps a detached `LoroDoc` that validates and accumulates updates
|
|
510
|
-
* and exports diffs by version vector.
|
|
511
|
-
*
|
|
512
|
-
* These live in `medeo-client` rather than in `mengine-server` because two
|
|
513
|
-
* separate hosts must classify identically: the real server (`apps/mengine-server`,
|
|
514
|
-
* both storage implementations) and the in-process test double
|
|
515
|
-
* (`testing/in-memory-mengine-server.ts`) that client-side tests push against. A
|
|
516
|
-
* double with its own verdict logic drifts from the server it stands in for, and
|
|
517
|
-
* the four push outcomes it produces are exactly what those tests assert on — so
|
|
518
|
-
* the classification is shared code, not duplicated code.
|
|
519
|
-
*/
|
|
520
|
-
/** Build a fresh detached relay doc (no DocState materialization). */
|
|
521
|
-
declare function newRelayDoc(): LoroDoc;
|
|
522
|
-
/** Load a detached relay doc from a stored snapshot. */
|
|
523
|
-
declare function relayDocFromSnapshot(snapshot: Uint8Array): LoroDoc;
|
|
524
|
-
type ImportVerdict = 'accepted' | 'duplicate' | 'corrupt_update' | 'missing_dependency';
|
|
525
|
-
/**
|
|
526
|
-
* Classify an incoming update against `doc` without mutating it.
|
|
527
|
-
*
|
|
528
|
-
* Deliberately version-delta based, not `ImportStatus`-shape based: in loro-crdt
|
|
529
|
-
* 1.13.x the JS `import()` returns `success`/`pending` as objects whose ranges
|
|
530
|
-
* are not reliably populated, and a pending (out-of-order) import still buffers
|
|
531
|
-
* the orphan op into the oplog. So this imports into a throwaway snapshot-clone
|
|
532
|
-
* and inspects whether the oplog frontiers advanced — letting the caller import
|
|
533
|
-
* into its canonical doc only on `accepted`, keeping orphan bytes out of the log.
|
|
534
|
-
*/
|
|
535
|
-
declare function classifyUpdate(doc: LoroDoc, update: Uint8Array): ImportVerdict;
|
|
536
|
-
//#endregion
|
|
537
477
|
//#region src/storage/medeo-http-doc-storage.d.ts
|
|
538
478
|
interface MedeoHttpDocStorageOptions {
|
|
539
479
|
docId: string;
|
|
@@ -555,20 +495,21 @@ interface MedeoHttpDocStorageOptions {
|
|
|
555
495
|
type PushOutcomeKind = 'ack' | 'duplicate' | 'rejected' | 'failed';
|
|
556
496
|
interface PushOutcome {
|
|
557
497
|
kind: PushOutcomeKind;
|
|
558
|
-
/** The bytes this outcome describes — lets a waiter match its own update. */
|
|
559
|
-
update: Uint8Array;
|
|
560
498
|
/** Server-allocated sequence number; only present for `ack`. */
|
|
561
499
|
updateSeq?: number | undefined;
|
|
562
500
|
/**
|
|
563
501
|
* The server oplog version vector *after* handling this push (`ack` and
|
|
564
502
|
* `duplicate` both carry it; encoded `VersionVector`).
|
|
565
503
|
*
|
|
566
|
-
* This
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
504
|
+
* This is what an "is my write durable" waiter keys on, and it is deliberately
|
|
505
|
+
* the ONLY correlation handle here. An earlier revision also carried the pushed
|
|
506
|
+
* bytes so a waiter could match its own update; that was removed because byte
|
|
507
|
+
* identity is not a reliable match — the same ops can reach the server either
|
|
508
|
+
* as the individual blob the push job carried or as a merged
|
|
509
|
+
* `export({from: serverVV})` blob produced by the synchronizer's sync path.
|
|
510
|
+
* Version coverage is reliable, and it makes `duplicate` satisfy a waiter
|
|
511
|
+
* correctly: the bytes appended nothing precisely because the server already
|
|
512
|
+
* held them. Offering both invited the wrong one.
|
|
572
513
|
*/
|
|
573
514
|
serverVV?: Uint8Array | undefined;
|
|
574
515
|
/** Server machine code for `rejected` (e.g. `missing_dependency`). */
|
|
@@ -608,19 +549,24 @@ declare class MedeoHttpDocStorage implements DocStorage {
|
|
|
608
549
|
getDoc(docId: string): Promise<DocSnapshotRecord | null>;
|
|
609
550
|
getDocDiff(docId: string, knownVersion?: Uint8Array): Promise<DocDiff | null>;
|
|
610
551
|
/**
|
|
611
|
-
* Forward one update to the server.
|
|
552
|
+
* Forward one update to the server, returning what the server says it now holds.
|
|
553
|
+
*
|
|
554
|
+
* The returned `server_vv` is the server's own statement about itself, computed
|
|
555
|
+
* inside the write transaction. A caller tracking "what the remote has" can
|
|
556
|
+
* adopt it directly, which is strictly better than inferring that bound from
|
|
557
|
+
* the pushed blob: it also covers ops other peers wrote, so those stop being
|
|
558
|
+
* re-sent on every later push. `ack` and `duplicate` both carry it.
|
|
612
559
|
*
|
|
613
|
-
* The
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
* (bytes contributed nothing) was indistinguishable from a successful write.
|
|
560
|
+
* The verdict itself stays on {@link subscribePushOutcome}, which reports
|
|
561
|
+
* failures too — a return value cannot. Previously the verdict was read and
|
|
562
|
+
* dropped, so a `duplicate` (bytes contributed nothing) was indistinguishable
|
|
563
|
+
* from a successful write.
|
|
618
564
|
*
|
|
619
565
|
* The error is still rethrown after being published: the synchronizer treats a
|
|
620
566
|
* throw as "retry this cycle", and swallowing it here would strand the update.
|
|
621
567
|
* Publishing is therefore additive observability, not error handling.
|
|
622
568
|
*/
|
|
623
|
-
pushDocUpdate(update: DocUpdate, _origin: unknown): Promise<
|
|
569
|
+
pushDocUpdate(update: DocUpdate, _origin: unknown): Promise<DocPushReceipt>;
|
|
624
570
|
/**
|
|
625
571
|
* Observe the server's verdict for every pushed update, including failures.
|
|
626
572
|
*
|
|
@@ -651,7 +597,11 @@ interface MengineDocSessionUpdateEvent {
|
|
|
651
597
|
}
|
|
652
598
|
/**
|
|
653
599
|
* Raised by {@link MengineDocSession.waitForServerAck} when the local edits it
|
|
654
|
-
* was asked to confirm
|
|
600
|
+
* was asked to confirm were not acknowledged as durable.
|
|
601
|
+
*
|
|
602
|
+
* Named `...AckFailed`, not `...AckTimeout`: two of the three `reason` values are
|
|
603
|
+
* not timeouts, and the earlier name made callers reach for a retry-after-delay
|
|
604
|
+
* that is wrong for `rejected`.
|
|
655
605
|
*
|
|
656
606
|
* The distinction the caller needs is "was it written": if this throws, treat the
|
|
657
607
|
* write as NOT durable. `reason` says which failure it was, and `code` carries the
|
|
@@ -663,7 +613,7 @@ interface MengineDocSessionUpdateEvent {
|
|
|
663
613
|
* catch-up; `corrupt_update` never is.
|
|
664
614
|
* - `failed` — transport/network failure; the server's answer is unknown.
|
|
665
615
|
*/
|
|
666
|
-
declare class
|
|
616
|
+
declare class MengineAckFailedError extends Error {
|
|
667
617
|
readonly reason: 'timeout' | 'rejected' | 'failed';
|
|
668
618
|
readonly code: string | undefined;
|
|
669
619
|
readonly cause: Error | undefined;
|
|
@@ -753,7 +703,7 @@ declare class MengineDocSession {
|
|
|
753
703
|
* Returns early when the server is already known to be current, so a caller
|
|
754
704
|
* with nothing outstanding does not block.
|
|
755
705
|
*
|
|
756
|
-
* @throws {
|
|
706
|
+
* @throws {MengineAckFailedError} on timeout, rejection, or transport failure.
|
|
757
707
|
*/
|
|
758
708
|
waitForServerAck(options?: WaitForServerAckOptions): Promise<void>;
|
|
759
709
|
/**
|
|
@@ -805,7 +755,7 @@ declare class MemoryDocStorage extends BaseDocStorage {
|
|
|
805
755
|
readonly connection: Connection;
|
|
806
756
|
private readonly entries;
|
|
807
757
|
constructor(options?: DocStorageOptions);
|
|
808
|
-
pushDocUpdate(update: DocUpdate, origin: unknown): Promise<
|
|
758
|
+
pushDocUpdate(update: DocUpdate, origin: unknown): Promise<DocPushReceipt>;
|
|
809
759
|
deleteDoc(docId: string): Promise<void>;
|
|
810
760
|
protected getDocSnapshot(docId: string): Promise<DocSnapshotRecord | null>;
|
|
811
761
|
protected setDocSnapshot(snapshot: DocSnapshotRecord): Promise<boolean>;
|
|
@@ -1026,13 +976,48 @@ interface SolvedVideoDocument {
|
|
|
1026
976
|
declare function solveVideoDocument(document: VideoDocument): SolvedVideoDocument;
|
|
1027
977
|
/** The named container a secondary lane lives in. */
|
|
1028
978
|
type SecondaryLane = 'speech' | 'caption' | 'bgm';
|
|
979
|
+
/** A lane's track kind: the `video_clip` main lane plus the three secondary lanes. */
|
|
980
|
+
type LaneKind = SecondaryLane | 'video_clip';
|
|
981
|
+
/**
|
|
982
|
+
* The four lanes in top-to-bottom stack order — the order a `tracks` list holds
|
|
983
|
+
* them in (see {@link laneRank}).
|
|
984
|
+
*
|
|
985
|
+
* Exported so a document can be seeded with all four lanes up front. That seed is
|
|
986
|
+
* not cosmetic: {@link ensureLaneTrack} is find-then-mint over a `LoroMovableList`,
|
|
987
|
+
* so two concurrent writers that each mint the same absent lane both keep their
|
|
988
|
+
* row, and the merged document holds two tracks for one lane. For the main lane
|
|
989
|
+
* that is fatal — `videoDocumentSchema` allows at most one `video_clip` track, so
|
|
990
|
+
* the merged document stops being projectable at all, symmetrically on both
|
|
991
|
+
* replicas. Pre-seeding every lane makes `ensureLaneTrack` always take its find
|
|
992
|
+
* branch, which removes the race by construction rather than by detection.
|
|
993
|
+
*
|
|
994
|
+
* What closes the race is that a track with the lane's `parts_kind` EXISTS — the
|
|
995
|
+
* lookup is by kind, not by id. So a seed is only safe if it covers every lane:
|
|
996
|
+
* a partial seed leaves the uncovered lanes exactly as exposed as before.
|
|
997
|
+
*/
|
|
998
|
+
declare const LANE_KINDS_IN_STACK_ORDER: readonly LaneKind[];
|
|
999
|
+
/**
|
|
1000
|
+
* The conventional track id for a lane (`main_track`, `<kind>_track`). Shared by
|
|
1001
|
+
* the up-front seed and {@link ensureLaneTrack}'s lazy mint so a document's lane
|
|
1002
|
+
* ids do not depend on which of the two created the track.
|
|
1003
|
+
*
|
|
1004
|
+
* Ids are cosmetic to the merge itself — lane lookup goes by `parts_kind`, so
|
|
1005
|
+
* drifting them apart would not reopen the concurrent-mint race. They matter to
|
|
1006
|
+
* readers that address a lane by id (the FE editor's panes, fixtures), which is
|
|
1007
|
+
* why there is one convention rather than two.
|
|
1008
|
+
*/
|
|
1009
|
+
declare function laneTrackId(kind: LaneKind): string;
|
|
1029
1010
|
/**
|
|
1030
1011
|
* Locate a lane's track row in the single `tracks` list by kind, minting an empty
|
|
1031
1012
|
* row in lane-stacking order if absent (reference/17 §4: lane = `parts_kind`).
|
|
1032
1013
|
* Ops use this to write authoritative items onto the right lane. The track id
|
|
1033
|
-
*
|
|
1014
|
+
* comes from {@link laneTrackId}, shared with the up-front seed.
|
|
1015
|
+
*
|
|
1016
|
+
* The mint branch is a concurrency hazard, not a convenience: see
|
|
1017
|
+
* {@link LANE_KINDS_IN_STACK_ORDER}. A document seeded with all four lanes never
|
|
1018
|
+
* reaches it.
|
|
1034
1019
|
*/
|
|
1035
|
-
declare function ensureLaneTrack(draft: VideoDocumentDraft, kind:
|
|
1020
|
+
declare function ensureLaneTrack(draft: VideoDocumentDraft, kind: LaneKind): TrackDraft;
|
|
1036
1021
|
/** Find a secondary lane's track row without minting it. */
|
|
1037
1022
|
declare function findLaneTrack(draft: VideoDocumentDraft, kind: SecondaryLane): TrackDraft | undefined;
|
|
1038
1023
|
//#endregion
|
|
@@ -1079,4 +1064,4 @@ declare function hostForAbsMs(ranges: MainClipRange[], absMs: number): MainClipR
|
|
|
1079
1064
|
*/
|
|
1080
1065
|
declare function relativePositionForAbs(ranges: MainClipRange[], absMs: number): TrackItemTimePosition;
|
|
1081
1066
|
//#endregion
|
|
1082
|
-
export { type Aggregation, type Attachment, type BgmPart, type CaptionPart, type CaptionStyle, type CommitOptions, type DerivedItemPosition, type DocStorageLike, type DocVersionMark, IMPLEMENTED_SEMANTIC_OP_KINDS, type ImplementedSemanticOpKind, type
|
|
1067
|
+
export { type Aggregation, type Attachment, type BgmPart, type CaptionPart, type CaptionStyle, type CommitOptions, DEFAULT_UNIT_TIME_MS, type DerivedItemPosition, type DocStorageLike, type DocVersionMark, IMPLEMENTED_SEMANTIC_OP_KINDS, type ImplementedSemanticOpKind, type InitialDocumentFacts, LANE_KINDS_IN_STACK_ORDER, type LaneKind, type MainClipRange, type MakeEmptyPart, ManualSyncDoc, type ManualSyncDocOptions, MedeoHttpDocStorage, type MedeoHttpDocStorageOptions, MemoryDocStorage, MengineAckFailedError, type MengineAuditEntry, type MengineAuditResponse, MengineDocSession, type MengineDocSessionOptions, type MengineDocSessionUpdateEvent, type MengineDocumentVersion, type MengineEventStreamOptions, MengineHttpClient, type MengineHttpClientOptions, MengineHttpRequestError, MenginePushRejectedError, type MenginePushResponse, type MenginePushUpdateResponse, type MengineRejectedResponse, type MengineSnapshotResponse, type MengineSseUpdateEvent, type MengineSyncResponse, type MengineUpdateMeta, MirrorVideoDocumentAdapter, type MirrorVideoDocumentOptions, type OpActor, type PartAggregation, type PartKind, type PartUnion, type PlannedSemanticOpKind, type PullFailureReason, type PullResult, type PushOutcome, type PushOutcomeKind, type PushResult, type PushResultKind, SchemaValidator, type SemanticDocumentAdapter, SemanticEditor, type SemanticOpInput, type SemanticOpKind, type SemanticOpName, type SnapshotReadable, type SolvedVideoDocument, type SpeechHostMap, type SpeechPart, type SpeedShift, TIMELINE_SKELETON_DURATION_MS, type Timeline, type TimelineDoc, type TimelineItem, type Track, type TrackDraft, type TrackItem, type TrackItemDraft, type TrackItemTimePosition, type TransactAudit, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, type VideoClipPart, type VideoDocument, type VideoDocumentDraft, type VideoDocumentMirrorSchema, type VideoDocumentSchemaVersion, VideoDocumentValidationError, type VideoDocumentValidationIssue, type VideoDocumentValidationIssueCode, type VideoDraft, type CaptionPart$1 as VideoDraftCaptionPart, type VideoDraftPartUnion, type Timeline$1 as VideoDraftTimeline, type Track$1 as VideoDraftTrack, type TrackItem$1 as VideoDraftTrackItem, type WaitForServerAckOptions, arrangeMainTrackSeamlessly, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, decodeDocVersionMark, derivePositionFromAbs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, partDurationMs, partUnionSchema, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, resolveAllSpeechOverlaps, resolveSpeechOverlapByShiftingVideos, safeDurationMs, index_d_exports as schemas, snapshotToPlain, solveVideoDocument, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-D7D4PA-g.js";
|
|
2
|
-
import { A as partDurationMs, C as reassignSpeechesToVideoClipsByTime, D as syncAggregatedClipsTimePosition, E as resolveSpeechOverlapByShiftingVideos, F as
|
|
2
|
+
import { A as partDurationMs, C as reassignSpeechesToVideoClipsByTime, D as syncAggregatedClipsTimePosition, E as resolveSpeechOverlapByShiftingVideos, F as partUnionToDraft, I as recordEntries, L as videoDocumentMirrorSchema, M as DEFAULT_UNIT_TIME_MS, N as VIDEO_DOCUMENT_SCHEMA_VERSION, O as TIMELINE_SKELETON_DURATION_MS, P as effectiveVideoClipDurationMs, R as base64ToBytes, S as fillMainTrackTimeGaps, T as resolveAllSpeechOverlaps, _ as findLaneTrack, a as buildInitialVideoDocument, b as cascadeAfterVideoClipChanges, c as fromVideoDocument, d as assertValidVideoDocument, f as validateVideoDocument, g as ensureLaneTrack, h as LANE_KINDS_IN_STACK_ORDER, i as readVideoDocumentFromDraft, j as safeDurationMs, k as isEmptyVideoClip, l as toVideoDocument, m as videoDocumentSchema, n as createMirrorVideoDocument, o as buildSpeechHostMap, p as partUnionSchema, r as createMirrorVideoDocumentAdapter, s as derivePositionFromAbs, t as MirrorVideoDocumentAdapter, u as VideoDocumentValidationError, v as laneTrackId, w as recalculateTimelineDuration, x as arrangeMainTrackSeamlessly, y as solveVideoDocument, z as bytesToBase64 } from "./document-C0o_cjAH.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { LoroDoc, VersionVector } from "loro-crdt";
|
|
5
5
|
import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
|
|
@@ -2348,13 +2348,18 @@ var MedeoHttpDocStorage = class {
|
|
|
2348
2348
|
};
|
|
2349
2349
|
}
|
|
2350
2350
|
/**
|
|
2351
|
-
* Forward one update to the server.
|
|
2351
|
+
* Forward one update to the server, returning what the server says it now holds.
|
|
2352
2352
|
*
|
|
2353
|
-
* The `
|
|
2354
|
-
* the
|
|
2355
|
-
*
|
|
2356
|
-
*
|
|
2357
|
-
*
|
|
2353
|
+
* The returned `server_vv` is the server's own statement about itself, computed
|
|
2354
|
+
* inside the write transaction. A caller tracking "what the remote has" can
|
|
2355
|
+
* adopt it directly, which is strictly better than inferring that bound from
|
|
2356
|
+
* the pushed blob: it also covers ops other peers wrote, so those stop being
|
|
2357
|
+
* re-sent on every later push. `ack` and `duplicate` both carry it.
|
|
2358
|
+
*
|
|
2359
|
+
* The verdict itself stays on {@link subscribePushOutcome}, which reports
|
|
2360
|
+
* failures too — a return value cannot. Previously the verdict was read and
|
|
2361
|
+
* dropped, so a `duplicate` (bytes contributed nothing) was indistinguishable
|
|
2362
|
+
* from a successful write.
|
|
2358
2363
|
*
|
|
2359
2364
|
* The error is still rethrown after being published: the synchronizer treats a
|
|
2360
2365
|
* throw as "retry this cycle", and swallowing it here would strand the update.
|
|
@@ -2362,21 +2367,22 @@ var MedeoHttpDocStorage = class {
|
|
|
2362
2367
|
*/
|
|
2363
2368
|
async pushDocUpdate(update, _origin) {
|
|
2364
2369
|
this.assertDocId(update.docId);
|
|
2365
|
-
if (this.isReadonly
|
|
2370
|
+
if (this.isReadonly) throw new Error(`MedeoHttpDocStorage is readonly; refusing to push ${update.docId}`);
|
|
2371
|
+
if (update.data.byteLength === 0) return {};
|
|
2366
2372
|
try {
|
|
2367
2373
|
const response = await this.client.pushUpdate(update.data);
|
|
2374
|
+
const serverVV = decodeServerVV(response.version?.server_vv);
|
|
2368
2375
|
this.events.emit("pushOutcome", {
|
|
2369
2376
|
kind: response.kind,
|
|
2370
|
-
update: update.data,
|
|
2371
2377
|
updateSeq: response.update_seq ?? void 0,
|
|
2372
|
-
serverVV
|
|
2378
|
+
serverVV
|
|
2373
2379
|
});
|
|
2380
|
+
return { version: serverVV };
|
|
2374
2381
|
} catch (error) {
|
|
2375
2382
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
2376
2383
|
const rejected = error instanceof MenginePushRejectedError;
|
|
2377
2384
|
this.events.emit("pushOutcome", {
|
|
2378
2385
|
kind: rejected ? "rejected" : "failed",
|
|
2379
|
-
update: update.data,
|
|
2380
2386
|
code: rejected ? error.code : void 0,
|
|
2381
2387
|
error: failure
|
|
2382
2388
|
});
|
|
@@ -2469,6 +2475,7 @@ var MemoryDocStorage = class extends BaseDocStorage {
|
|
|
2469
2475
|
},
|
|
2470
2476
|
origin
|
|
2471
2477
|
});
|
|
2478
|
+
return {};
|
|
2472
2479
|
}
|
|
2473
2480
|
async deleteDoc(docId) {
|
|
2474
2481
|
this.entries.delete(docId);
|
|
@@ -2511,7 +2518,11 @@ var MemoryDocStorage = class extends BaseDocStorage {
|
|
|
2511
2518
|
//#region src/session/mengine-doc-session.ts
|
|
2512
2519
|
/**
|
|
2513
2520
|
* Raised by {@link MengineDocSession.waitForServerAck} when the local edits it
|
|
2514
|
-
* was asked to confirm
|
|
2521
|
+
* was asked to confirm were not acknowledged as durable.
|
|
2522
|
+
*
|
|
2523
|
+
* Named `...AckFailed`, not `...AckTimeout`: two of the three `reason` values are
|
|
2524
|
+
* not timeouts, and the earlier name made callers reach for a retry-after-delay
|
|
2525
|
+
* that is wrong for `rejected`.
|
|
2515
2526
|
*
|
|
2516
2527
|
* The distinction the caller needs is "was it written": if this throws, treat the
|
|
2517
2528
|
* write as NOT durable. `reason` says which failure it was, and `code` carries the
|
|
@@ -2523,7 +2534,7 @@ var MemoryDocStorage = class extends BaseDocStorage {
|
|
|
2523
2534
|
* catch-up; `corrupt_update` never is.
|
|
2524
2535
|
* - `failed` — transport/network failure; the server's answer is unknown.
|
|
2525
2536
|
*/
|
|
2526
|
-
var
|
|
2537
|
+
var MengineAckFailedError = class extends Error {
|
|
2527
2538
|
reason;
|
|
2528
2539
|
code;
|
|
2529
2540
|
cause;
|
|
@@ -2532,7 +2543,7 @@ var MengineAckTimeoutError = class extends Error {
|
|
|
2532
2543
|
this.reason = reason;
|
|
2533
2544
|
this.code = code;
|
|
2534
2545
|
this.cause = cause;
|
|
2535
|
-
this.name = "
|
|
2546
|
+
this.name = "MengineAckFailedError";
|
|
2536
2547
|
}
|
|
2537
2548
|
};
|
|
2538
2549
|
/**
|
|
@@ -2617,13 +2628,13 @@ var MengineDocSession = class {
|
|
|
2617
2628
|
* Returns early when the server is already known to be current, so a caller
|
|
2618
2629
|
* with nothing outstanding does not block.
|
|
2619
2630
|
*
|
|
2620
|
-
* @throws {
|
|
2631
|
+
* @throws {MengineAckFailedError} on timeout, rejection, or transport failure.
|
|
2621
2632
|
*/
|
|
2622
2633
|
async waitForServerAck(options = {}) {
|
|
2623
2634
|
if (this.adapterValue == null) throw new Error("mengine doc session is not started");
|
|
2624
2635
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
2625
2636
|
const target = this.manager.connectDoc(this.docId).version();
|
|
2626
|
-
if (this.serverVVValue != null &&
|
|
2637
|
+
if (this.serverVVValue != null && serverCovers(this.serverVVValue, target)) return;
|
|
2627
2638
|
await new Promise((resolve, reject) => {
|
|
2628
2639
|
let off = () => {};
|
|
2629
2640
|
let done = false;
|
|
@@ -2636,14 +2647,14 @@ var MengineDocSession = class {
|
|
|
2636
2647
|
else reject(error);
|
|
2637
2648
|
};
|
|
2638
2649
|
const timer = setTimeout(() => {
|
|
2639
|
-
settle(new
|
|
2650
|
+
settle(new MengineAckFailedError("timeout", void 0, void 0, `mengine did not acknowledge the update within ${timeoutMs}ms`));
|
|
2640
2651
|
}, timeoutMs);
|
|
2641
2652
|
off = this.server.subscribePushOutcome((outcome) => {
|
|
2642
2653
|
if (outcome.kind === "rejected" || outcome.kind === "failed") {
|
|
2643
|
-
settle(new
|
|
2654
|
+
settle(new MengineAckFailedError(outcome.kind, outcome.code, outcome.error, outcome.error?.message ?? `mengine push ${outcome.kind}`));
|
|
2644
2655
|
return;
|
|
2645
2656
|
}
|
|
2646
|
-
if (outcome.serverVV != null &&
|
|
2657
|
+
if (outcome.serverVV != null && serverCovers(outcome.serverVV, target)) settle(void 0);
|
|
2647
2658
|
});
|
|
2648
2659
|
});
|
|
2649
2660
|
}
|
|
@@ -2746,24 +2757,25 @@ var MengineDocSession = class {
|
|
|
2746
2757
|
}
|
|
2747
2758
|
};
|
|
2748
2759
|
/**
|
|
2749
|
-
* True when the server version `serverVV` includes everything in
|
|
2760
|
+
* True when the *encoded* server version `serverVV` includes everything in
|
|
2761
|
+
* `target`. Thin decode wrapper over the shared {@link covers} predicate — an
|
|
2762
|
+
* undecodable blob is treated as "does not cover", so a corrupt server response
|
|
2763
|
+
* leaves a waiter waiting rather than falsely acking it.
|
|
2750
2764
|
*
|
|
2751
|
-
*
|
|
2752
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2755
|
-
*
|
|
2756
|
-
* irrelevant to whether *our* ops landed.
|
|
2765
|
+
* The coverage rule itself (per-peer counters, never `VersionVector.compare`,
|
|
2766
|
+
* never equality) lives in `manual-sync/version-coverage.ts` with the reasoning.
|
|
2767
|
+
* An earlier revision reimplemented it here with a near-identical comment block;
|
|
2768
|
+
* two copies of one rule is exactly the duplication this project has been bitten
|
|
2769
|
+
* by repeatedly.
|
|
2757
2770
|
*/
|
|
2758
|
-
function
|
|
2771
|
+
function serverCovers(serverVV, target) {
|
|
2759
2772
|
let server;
|
|
2760
2773
|
try {
|
|
2761
2774
|
server = VersionVector.decode(serverVV);
|
|
2762
2775
|
} catch {
|
|
2763
2776
|
return false;
|
|
2764
2777
|
}
|
|
2765
|
-
|
|
2766
|
-
return true;
|
|
2778
|
+
return covers(server, target);
|
|
2767
2779
|
}
|
|
2768
2780
|
//#endregion
|
|
2769
|
-
export { IMPLEMENTED_SEMANTIC_OP_KINDS, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage,
|
|
2781
|
+
export { DEFAULT_UNIT_TIME_MS, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, SchemaValidator, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, arrangeMainTrackSeamlessly, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, decodeDocVersionMark, derivePositionFromAbs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, partDurationMs, partUnionSchema, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, resolveAllSpeechOverlaps, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, snapshotToPlain, solveVideoDocument, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { LoroDoc } from "loro-crdt";
|
|
2
|
+
//#region src/relay/loro-relay-doc.ts
|
|
3
|
+
/**
|
|
4
|
+
* Loro OpLog-relay primitives: the doc shape and update classification an update
|
|
5
|
+
* *host* needs, as opposed to an editing client.
|
|
6
|
+
*
|
|
7
|
+
* A relay never materializes DocState (`docs/concepts/oplog_docstate` §Relay
|
|
8
|
+
* Server) — it keeps a detached `LoroDoc` that validates and accumulates updates
|
|
9
|
+
* and exports diffs by version vector.
|
|
10
|
+
*
|
|
11
|
+
* These live in `medeo-client` rather than in `mengine-server` because two
|
|
12
|
+
* separate hosts must classify identically: the real server (`apps/mengine-server`,
|
|
13
|
+
* both storage implementations) and the in-process test double
|
|
14
|
+
* (`testing/in-memory-mengine-server.ts`) that client-side tests push against. A
|
|
15
|
+
* double with its own verdict logic drifts from the server it stands in for, and
|
|
16
|
+
* the four push outcomes it produces are exactly what those tests assert on — so
|
|
17
|
+
* the classification is shared code, not duplicated code.
|
|
18
|
+
*/
|
|
19
|
+
/** Build a fresh detached relay doc (no DocState materialization). */
|
|
20
|
+
function newRelayDoc() {
|
|
21
|
+
const doc = new LoroDoc();
|
|
22
|
+
doc.detach();
|
|
23
|
+
return doc;
|
|
24
|
+
}
|
|
25
|
+
/** Load a detached relay doc from a stored snapshot. */
|
|
26
|
+
function relayDocFromSnapshot(snapshot) {
|
|
27
|
+
const doc = newRelayDoc();
|
|
28
|
+
doc.import(snapshot);
|
|
29
|
+
return doc;
|
|
30
|
+
}
|
|
31
|
+
/** A full-oplog clone (via snapshot round-trip) for non-destructive probing. */
|
|
32
|
+
function cloneRelayDoc(doc) {
|
|
33
|
+
return relayDocFromSnapshot(doc.export({ mode: "snapshot" }));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Classify an incoming update against `doc` without mutating it.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately version-delta based, not `ImportStatus`-shape based: in loro-crdt
|
|
39
|
+
* 1.13.x the JS `import()` returns `success`/`pending` as objects whose ranges
|
|
40
|
+
* are not reliably populated, and a pending (out-of-order) import still buffers
|
|
41
|
+
* the orphan op into the oplog. So this imports into a throwaway snapshot-clone
|
|
42
|
+
* and inspects whether the oplog frontiers advanced — letting the caller import
|
|
43
|
+
* into its canonical doc only on `accepted`, keeping orphan bytes out of the log.
|
|
44
|
+
*/
|
|
45
|
+
function classifyUpdate(doc, update) {
|
|
46
|
+
const probe = cloneRelayDoc(doc);
|
|
47
|
+
let status;
|
|
48
|
+
try {
|
|
49
|
+
status = probe.import(update);
|
|
50
|
+
} catch {
|
|
51
|
+
return "corrupt_update";
|
|
52
|
+
}
|
|
53
|
+
if (status?.pending != null) return "missing_dependency";
|
|
54
|
+
if (probe.cmpWithFrontiers(doc.oplogFrontiers()) !== 1) return "duplicate";
|
|
55
|
+
return "accepted";
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { newRelayDoc as n, relayDocFromSnapshot as r, classifyUpdate as t };
|
package/dist/relay.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LoroDoc } from "loro-crdt";
|
|
2
|
+
|
|
3
|
+
//#region src/relay/loro-relay-doc.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Loro OpLog-relay primitives: the doc shape and update classification an update
|
|
6
|
+
* *host* needs, as opposed to an editing client.
|
|
7
|
+
*
|
|
8
|
+
* A relay never materializes DocState (`docs/concepts/oplog_docstate` §Relay
|
|
9
|
+
* Server) — it keeps a detached `LoroDoc` that validates and accumulates updates
|
|
10
|
+
* and exports diffs by version vector.
|
|
11
|
+
*
|
|
12
|
+
* These live in `medeo-client` rather than in `mengine-server` because two
|
|
13
|
+
* separate hosts must classify identically: the real server (`apps/mengine-server`,
|
|
14
|
+
* both storage implementations) and the in-process test double
|
|
15
|
+
* (`testing/in-memory-mengine-server.ts`) that client-side tests push against. A
|
|
16
|
+
* double with its own verdict logic drifts from the server it stands in for, and
|
|
17
|
+
* the four push outcomes it produces are exactly what those tests assert on — so
|
|
18
|
+
* the classification is shared code, not duplicated code.
|
|
19
|
+
*/
|
|
20
|
+
/** Build a fresh detached relay doc (no DocState materialization). */
|
|
21
|
+
declare function newRelayDoc(): LoroDoc;
|
|
22
|
+
/** Load a detached relay doc from a stored snapshot. */
|
|
23
|
+
declare function relayDocFromSnapshot(snapshot: Uint8Array): LoroDoc;
|
|
24
|
+
type ImportVerdict = 'accepted' | 'duplicate' | 'corrupt_update' | 'missing_dependency';
|
|
25
|
+
/**
|
|
26
|
+
* Classify an incoming update against `doc` without mutating it.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately version-delta based, not `ImportStatus`-shape based: in loro-crdt
|
|
29
|
+
* 1.13.x the JS `import()` returns `success`/`pending` as objects whose ranges
|
|
30
|
+
* are not reliably populated, and a pending (out-of-order) import still buffers
|
|
31
|
+
* the orphan op into the oplog. So this imports into a throwaway snapshot-clone
|
|
32
|
+
* and inspects whether the oplog frontiers advanced — letting the caller import
|
|
33
|
+
* into its canonical doc only on `accepted`, keeping orphan bytes out of the log.
|
|
34
|
+
*/
|
|
35
|
+
declare function classifyUpdate(doc: LoroDoc, update: Uint8Array): ImportVerdict;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type ImportVerdict, classifyUpdate, newRelayDoc, relayDocFromSnapshot };
|
package/dist/relay.js
ADDED
package/dist/testing.d.ts
CHANGED
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { N as VIDEO_DOCUMENT_SCHEMA_VERSION, R as base64ToBytes, r as createMirrorVideoDocumentAdapter, z as bytesToBase64 } from "./document-C0o_cjAH.js";
|
|
2
|
+
import { t as classifyUpdate } from "./loro-relay-doc-ssdYpuef.js";
|
|
2
3
|
import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
|
|
3
4
|
//#region src/testing/in-memory-mengine-server.ts
|
|
4
5
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mengine/medeo-client",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./dist/index.js",
|
|
17
|
+
"./relay": "./dist/relay.js",
|
|
17
18
|
"./testing": "./dist/testing.js",
|
|
18
19
|
"./package.json": "./package.json"
|
|
19
20
|
},
|
|
@@ -24,9 +25,9 @@
|
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"loro-mirror": "^2.2.0",
|
|
26
27
|
"zod": "^4.4.3",
|
|
27
|
-
"@mengine/
|
|
28
|
-
"@mengine/
|
|
29
|
-
"@mengine/utils": "1.0
|
|
28
|
+
"@mengine/sync": "1.2.0",
|
|
29
|
+
"@mengine/storage": "1.2.0",
|
|
30
|
+
"@mengine/utils": "1.2.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^25.9.1",
|