@mengine/medeo-client 1.0.1 → 1.1.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.
@@ -648,6 +648,42 @@ function seedAggregations(mainItems, speechItems) {
648
648
  return order.map((host) => byHost.get(host));
649
649
  }
650
650
  /**
651
+ * The four lanes in top-to-bottom stack order — the order a `tracks` list holds
652
+ * them in (see {@link laneRank}).
653
+ *
654
+ * Exported so a document can be seeded with all four lanes up front. That seed is
655
+ * not cosmetic: {@link ensureLaneTrack} is find-then-mint over a `LoroMovableList`,
656
+ * so two concurrent writers that each mint the same absent lane both keep their
657
+ * row, and the merged document holds two tracks for one lane. For the main lane
658
+ * that is fatal — `videoDocumentSchema` allows at most one `video_clip` track, so
659
+ * the merged document stops being projectable at all, symmetrically on both
660
+ * replicas. Pre-seeding every lane makes `ensureLaneTrack` always take its find
661
+ * branch, which removes the race by construction rather than by detection.
662
+ *
663
+ * What closes the race is that a track with the lane's `parts_kind` EXISTS — the
664
+ * lookup is by kind, not by id. So a seed is only safe if it covers every lane:
665
+ * a partial seed leaves the uncovered lanes exactly as exposed as before.
666
+ */
667
+ const LANE_KINDS_IN_STACK_ORDER = [
668
+ "caption",
669
+ "video_clip",
670
+ "speech",
671
+ "bgm"
672
+ ];
673
+ /**
674
+ * The conventional track id for a lane (`main_track`, `<kind>_track`). Shared by
675
+ * the up-front seed and {@link ensureLaneTrack}'s lazy mint so a document's lane
676
+ * ids do not depend on which of the two created the track.
677
+ *
678
+ * Ids are cosmetic to the merge itself — lane lookup goes by `parts_kind`, so
679
+ * drifting them apart would not reopen the concurrent-mint race. They matter to
680
+ * readers that address a lane by id (the FE editor's panes, fixtures), which is
681
+ * why there is one convention rather than two.
682
+ */
683
+ function laneTrackId(kind) {
684
+ return kind === "video_clip" ? "main_track" : `${kind}_track`;
685
+ }
686
+ /**
651
687
  * Lane-stacking rank for the single `tracks` list (reference/17 §4): caption
652
688
  * (above) sits before the video_clip main track, which sits before speech / bgm
653
689
  * (below). The `tracks` array is kept in this top-to-bottom order so a freshly
@@ -678,14 +714,18 @@ function insertTrackByLaneOrder(tracks, track) {
678
714
  * Locate a lane's track row in the single `tracks` list by kind, minting an empty
679
715
  * row in lane-stacking order if absent (reference/17 §4: lane = `parts_kind`).
680
716
  * Ops use this to write authoritative items onto the right lane. The track id
681
- * mirrors the seed convention (`<kind>_track`).
717
+ * comes from {@link laneTrackId}, shared with the up-front seed.
718
+ *
719
+ * The mint branch is a concurrency hazard, not a convenience: see
720
+ * {@link LANE_KINDS_IN_STACK_ORDER}. A document seeded with all four lanes never
721
+ * reaches it.
682
722
  */
683
723
  function ensureLaneTrack(draft, kind) {
684
724
  draft.tracks ??= [];
685
725
  let track = draft.tracks.find((t) => t?.parts_kind === kind);
686
726
  if (track == null) {
687
727
  track = {
688
- id: kind === "video_clip" ? "main_track" : `${kind}_track`,
728
+ id: laneTrackId(kind),
689
729
  parts_kind: kind,
690
730
  is_hidden: void 0,
691
731
  items: []
@@ -1367,6 +1407,57 @@ function clone(value) {
1367
1407
  return value;
1368
1408
  }
1369
1409
  //#endregion
1410
+ //#region src/document/initial-document.ts
1411
+ /**
1412
+ * Build the `VideoDocument` a newly created project starts from: the caller's
1413
+ * project facts, no parts, and one empty track per lane.
1414
+ *
1415
+ * The empty lane tracks are the reason this function exists rather than callers
1416
+ * assembling a document inline. Lane tracks are otherwise minted lazily by the
1417
+ * first op that needs one (`ensureLaneTrack`), which is find-then-mint over a
1418
+ * CRDT list — so if the Agent's first generation and a user edit race on an
1419
+ * empty document, each mints its own copy of the same lane and the merge keeps
1420
+ * both. Two video_clip tracks violate the at-most-one-main-track rule, and the
1421
+ * merged document becomes unprojectable for *every* reader (editor, render,
1422
+ * share) with no winning replica to fall back on. Seeding all four lanes up
1423
+ * front means `ensureLaneTrack` only ever finds, never mints, so the race cannot
1424
+ * happen on a document created here.
1425
+ *
1426
+ * All four, not just the main lane: the secondary lanes fail less loudly (a
1427
+ * duplicate pane rather than an unreadable document), but they fail by the same
1428
+ * mechanism, and covering only the fatal one would leave three live races.
1429
+ *
1430
+ * `timeline` is left absent: `unit_time_ms` is a display concern the editor
1431
+ * supplies, and total duration is derived on read (RFC 02 §6), so a new document
1432
+ * has no timeline fact to state.
1433
+ */
1434
+ function buildInitialVideoDocument(facts) {
1435
+ return {
1436
+ meta: {
1437
+ schema_version: VIDEO_DOCUMENT_SCHEMA_VERSION,
1438
+ draft_id: facts.draftId,
1439
+ project_id: facts.projectId,
1440
+ owner_id: facts.ownerId,
1441
+ thumbnail_storage_key: void 0,
1442
+ chat_session_id: facts.chatSessionId,
1443
+ video_creation_settings: facts.videoCreationSettings,
1444
+ version: void 0
1445
+ },
1446
+ timeline: void 0,
1447
+ tracks: emptyLaneTracks(),
1448
+ part_library: {}
1449
+ };
1450
+ }
1451
+ /** One empty track per lane, in lane-stacking order, with the conventional ids. */
1452
+ function emptyLaneTracks() {
1453
+ return LANE_KINDS_IN_STACK_ORDER.map((kind) => ({
1454
+ id: laneTrackId(kind),
1455
+ parts_kind: kind,
1456
+ is_hidden: void 0,
1457
+ items: []
1458
+ }));
1459
+ }
1460
+ //#endregion
1370
1461
  //#region src/document/mirror-read.ts
1371
1462
  /**
1372
1463
  * Project the mirror state (`VideoDocumentDraft`) into the authoritative
@@ -1536,60 +1627,4 @@ function toTrackRow(track) {
1536
1627
  };
1537
1628
  }
1538
1629
  //#endregion
1539
- //#region src/relay/loro-relay-doc.ts
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 };
1630
+ 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, 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 };
@@ -751,6 +751,49 @@ declare function derivePositionFromAbs(partId: string, abs: number, isMain: bool
751
751
  */
752
752
  declare function fromVideoDocument(document: VideoDocument): VideoDraft;
753
753
  //#endregion
754
+ //#region src/document/initial-document.d.ts
755
+ /**
756
+ * The project facts a freshly created document is built from — exactly the five
757
+ * fields Director sets when it creates a draft row, and nothing else.
758
+ *
759
+ * Deliberately not a whole `VideoDraft`: tracks and the part library of a new
760
+ * document are empty by definition, so accepting them would mean accepting
761
+ * values that must always be empty, and every such field is a place for a future
762
+ * caller to send something that is silently dropped. The narrow shape makes the
763
+ * "a new document has no content" rule structural instead of a convention.
764
+ */
765
+ interface InitialDocumentFacts {
766
+ draftId: string;
767
+ projectId: string | undefined;
768
+ ownerId: string | undefined;
769
+ chatSessionId: string | undefined;
770
+ videoCreationSettings: VideoCreationSettings | undefined;
771
+ }
772
+ /**
773
+ * Build the `VideoDocument` a newly created project starts from: the caller's
774
+ * project facts, no parts, and one empty track per lane.
775
+ *
776
+ * The empty lane tracks are the reason this function exists rather than callers
777
+ * assembling a document inline. Lane tracks are otherwise minted lazily by the
778
+ * first op that needs one (`ensureLaneTrack`), which is find-then-mint over a
779
+ * CRDT list — so if the Agent's first generation and a user edit race on an
780
+ * empty document, each mints its own copy of the same lane and the merge keeps
781
+ * both. Two video_clip tracks violate the at-most-one-main-track rule, and the
782
+ * merged document becomes unprojectable for *every* reader (editor, render,
783
+ * share) with no winning replica to fall back on. Seeding all four lanes up
784
+ * front means `ensureLaneTrack` only ever finds, never mints, so the race cannot
785
+ * happen on a document created here.
786
+ *
787
+ * All four, not just the main lane: the secondary lanes fail less loudly (a
788
+ * duplicate pane rather than an unreadable document), but they fail by the same
789
+ * mechanism, and covering only the fatal one would leave three live races.
790
+ *
791
+ * `timeline` is left absent: `unit_time_ms` is a display concern the editor
792
+ * supplies, and total duration is derived on read (RFC 02 §6), so a new document
793
+ * has no timeline fact to state.
794
+ */
795
+ declare function buildInitialVideoDocument(facts: InitialDocumentFacts): VideoDocument;
796
+ //#endregion
754
797
  //#region src/document/validation.d.ts
755
798
  /**
756
799
  * Business-level schema guard for `VideoDocument` (RFC 03 §9). It is the gate
@@ -2029,4 +2072,4 @@ declare const videoDocumentSchema: z.ZodObject<{
2029
2072
  }, z.core.$loose>]>>>;
2030
2073
  }, z.core.$loose>;
2031
2074
  //#endregion
2032
- export { VideoDraft as $, assertValidVideoDocument as A, PartKind as B, index_d_exports as C, VideoDocumentMirrorSchema as D, VideoDocumentDraft as E, derivePositionFromAbs as F, TrackItem as G, SpeechPart as H, fromVideoDocument as I, VideoClipPart as J, TrackItemTimePosition as K, toVideoDocument as L, DerivedItemPosition as M, SpeechHostMap as N, videoDocumentMirrorSchema as O, buildSpeechHostMap as P, VideoDocumentValidationIssueCode as Q, BgmPart as R, ValidationError as S, TrackItemDraft as T, Timeline as U, PartUnion as V, Track as W, VideoDocumentSchemaVersion as X, VideoDocument as Y, VideoDocumentValidationIssue as Z, PlannedSemanticOpKind as _, MirrorVideoDocumentOptions as a, SpeedShift as at, SchemaValidator as b, CommitOptions as c, TrackItem$1 as ct, SemanticEditor as d, VideoDraftPartUnion as et, SemanticOpInput as f, ImplementedSemanticOpKind as g, IMPLEMENTED_SEMANTIC_OP_KINDS as h, MirrorVideoDocumentAdapter as i, PartAggregation as it, validateVideoDocument as j, VideoDocumentValidationError as k, OpActor as l, TransactAudit as m, videoDocumentSchema as n, CaptionPart$1 as nt, createMirrorVideoDocument as o, Timeline$1 as ot, SemanticOpName as p, VIDEO_DOCUMENT_SCHEMA_VERSION as q, readVideoDocumentFromDraft as r, CaptionStyle as rt, createMirrorVideoDocumentAdapter as s, Track$1 as st, partUnionSchema as t, Attachment as tt, SemanticDocumentAdapter as u, SemanticOpKind as v, TrackDraft as w, SnapshotReadable as x, isImplementedSemanticOpKind as y, CaptionPart as z };
2075
+ export { VideoDocumentValidationIssue as $, assertValidVideoDocument as A, BgmPart as B, index_d_exports as C, VideoDocumentMirrorSchema as D, VideoDocumentDraft as E, SpeechHostMap as F, Timeline as G, PartKind as H, buildSpeechHostMap as I, TrackItemTimePosition as J, Track as K, derivePositionFromAbs as L, InitialDocumentFacts as M, buildInitialVideoDocument as N, videoDocumentMirrorSchema as O, DerivedItemPosition as P, VideoDocumentSchemaVersion as Q, fromVideoDocument as R, ValidationError as S, TrackItemDraft as T, PartUnion as U, CaptionPart as V, SpeechPart as W, VideoClipPart as X, VIDEO_DOCUMENT_SCHEMA_VERSION as Y, VideoDocument as Z, PlannedSemanticOpKind as _, MirrorVideoDocumentOptions as a, CaptionStyle as at, SchemaValidator as b, CommitOptions as c, Timeline$1 as ct, SemanticEditor as d, VideoDocumentValidationIssueCode as et, SemanticOpInput as f, ImplementedSemanticOpKind as g, IMPLEMENTED_SEMANTIC_OP_KINDS as h, MirrorVideoDocumentAdapter as i, CaptionPart$1 as it, validateVideoDocument as j, VideoDocumentValidationError as k, OpActor as l, Track$1 as lt, TransactAudit as m, videoDocumentSchema as n, VideoDraftPartUnion as nt, createMirrorVideoDocument as o, PartAggregation as ot, SemanticOpName as p, TrackItem as q, readVideoDocumentFromDraft as r, Attachment as rt, createMirrorVideoDocumentAdapter as s, SpeedShift as st, partUnionSchema as t, VideoDraft as tt, SemanticDocumentAdapter as u, TrackItem$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,5 +1,5 @@
1
- import { $ as VideoDraft, A as assertValidVideoDocument, B as PartKind, C as index_d_exports, D as VideoDocumentMirrorSchema, E as VideoDocumentDraft, F as derivePositionFromAbs, G as TrackItem, H as SpeechPart, I as fromVideoDocument, J as VideoClipPart, K as TrackItemTimePosition, L as toVideoDocument, M as DerivedItemPosition, N as SpeechHostMap, O as videoDocumentMirrorSchema, P as buildSpeechHostMap, Q as VideoDocumentValidationIssueCode, R as BgmPart, S as ValidationError, T as TrackItemDraft, U as Timeline, V as PartUnion, W as Track, X as VideoDocumentSchemaVersion, Y as VideoDocument, Z as VideoDocumentValidationIssue, _ as PlannedSemanticOpKind, a as MirrorVideoDocumentOptions, at as SpeedShift, b as SchemaValidator, c as CommitOptions, ct as TrackItem$1, d as SemanticEditor, et as VideoDraftPartUnion, f as SemanticOpInput, g as ImplementedSemanticOpKind, h as IMPLEMENTED_SEMANTIC_OP_KINDS, i as MirrorVideoDocumentAdapter, it as PartAggregation, j as validateVideoDocument, k as VideoDocumentValidationError, l as OpActor, m as TransactAudit, n as videoDocumentSchema, nt as CaptionPart$1, o as createMirrorVideoDocument, ot as Timeline$1, p as SemanticOpName, q as VIDEO_DOCUMENT_SCHEMA_VERSION, r as readVideoDocumentFromDraft, rt as CaptionStyle, s as createMirrorVideoDocumentAdapter, st as Track$1, t as partUnionSchema, tt as Attachment, u as SemanticDocumentAdapter, v as SemanticOpKind, w as TrackDraft, x as SnapshotReadable, y as isImplementedSemanticOpKind, z as CaptionPart } from "./index-DLchEQG7.js";
2
- import { LoroDoc, PeerID, VersionVector } from "loro-crdt";
1
+ import { $ as VideoDocumentValidationIssue, A as assertValidVideoDocument, B as BgmPart, C as index_d_exports, D as VideoDocumentMirrorSchema, E as VideoDocumentDraft, F as SpeechHostMap, G as Timeline, H as PartKind, I as buildSpeechHostMap, J as TrackItemTimePosition, K as Track, L as derivePositionFromAbs, M as InitialDocumentFacts, N as buildInitialVideoDocument, O as videoDocumentMirrorSchema, P as DerivedItemPosition, Q as VideoDocumentSchemaVersion, R as fromVideoDocument, S as ValidationError, T as TrackItemDraft, U as PartUnion, V as CaptionPart, W as SpeechPart, X as VideoClipPart, Y as VIDEO_DOCUMENT_SCHEMA_VERSION, Z as VideoDocument, _ as PlannedSemanticOpKind, a as MirrorVideoDocumentOptions, at as CaptionStyle, b as SchemaValidator, c as CommitOptions, ct as Timeline$1, d as SemanticEditor, et as VideoDocumentValidationIssueCode, f as SemanticOpInput, g as ImplementedSemanticOpKind, h as IMPLEMENTED_SEMANTIC_OP_KINDS, i as MirrorVideoDocumentAdapter, it as CaptionPart$1, j as validateVideoDocument, k as VideoDocumentValidationError, l as OpActor, lt as Track$1, m as TransactAudit, n as videoDocumentSchema, nt as VideoDraftPartUnion, o as createMirrorVideoDocument, ot as PartAggregation, p as SemanticOpName, q as TrackItem, r as readVideoDocumentFromDraft, rt as Attachment, s as createMirrorVideoDocumentAdapter, st as SpeedShift, t as partUnionSchema, tt as VideoDraft, u as SemanticDocumentAdapter, ut as TrackItem$1, v as SemanticOpKind, w as TrackDraft, x as SnapshotReadable, y as isImplementedSemanticOpKind, z as toVideoDocument } from "./index-BUKA3L7o.js";
2
+ import { PeerID } from "loro-crdt";
3
3
  import { DocState } from "@mengine/sync";
4
4
  import { BaseDocStorage, Connection, DocDiff, DocSnapshotRecord, DocStorage, DocStorageOptions, DocUpdate, DocUpdateRecord } from "@mengine/storage";
5
5
 
@@ -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 — not the pushed bytes — is what an "is my write durable" waiter should
567
- * key on. The same ops can reach the server either as the individual blob the
568
- * push job carried or as a merged `export({from: serverVV})` blob produced by
569
- * the synchronizer's sync path, so byte identity is not a reliable match.
570
- * Version coverage is, and it makes `duplicate` satisfy a waiter correctly: the
571
- * bytes appended nothing precisely because the server already held them.
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`). */
@@ -651,7 +592,11 @@ interface MengineDocSessionUpdateEvent {
651
592
  }
652
593
  /**
653
594
  * Raised by {@link MengineDocSession.waitForServerAck} when the local edits it
654
- * was asked to confirm did not reach the server in time, or were refused.
595
+ * was asked to confirm were not acknowledged as durable.
596
+ *
597
+ * Named `...AckFailed`, not `...AckTimeout`: two of the three `reason` values are
598
+ * not timeouts, and the earlier name made callers reach for a retry-after-delay
599
+ * that is wrong for `rejected`.
655
600
  *
656
601
  * The distinction the caller needs is "was it written": if this throws, treat the
657
602
  * write as NOT durable. `reason` says which failure it was, and `code` carries the
@@ -663,7 +608,7 @@ interface MengineDocSessionUpdateEvent {
663
608
  * catch-up; `corrupt_update` never is.
664
609
  * - `failed` — transport/network failure; the server's answer is unknown.
665
610
  */
666
- declare class MengineAckTimeoutError extends Error {
611
+ declare class MengineAckFailedError extends Error {
667
612
  readonly reason: 'timeout' | 'rejected' | 'failed';
668
613
  readonly code: string | undefined;
669
614
  readonly cause: Error | undefined;
@@ -753,7 +698,7 @@ declare class MengineDocSession {
753
698
  * Returns early when the server is already known to be current, so a caller
754
699
  * with nothing outstanding does not block.
755
700
  *
756
- * @throws {MengineAckTimeoutError} on timeout, rejection, or transport failure.
701
+ * @throws {MengineAckFailedError} on timeout, rejection, or transport failure.
757
702
  */
758
703
  waitForServerAck(options?: WaitForServerAckOptions): Promise<void>;
759
704
  /**
@@ -1026,13 +971,48 @@ interface SolvedVideoDocument {
1026
971
  declare function solveVideoDocument(document: VideoDocument): SolvedVideoDocument;
1027
972
  /** The named container a secondary lane lives in. */
1028
973
  type SecondaryLane = 'speech' | 'caption' | 'bgm';
974
+ /** A lane's track kind: the `video_clip` main lane plus the three secondary lanes. */
975
+ type LaneKind = SecondaryLane | 'video_clip';
976
+ /**
977
+ * The four lanes in top-to-bottom stack order — the order a `tracks` list holds
978
+ * them in (see {@link laneRank}).
979
+ *
980
+ * Exported so a document can be seeded with all four lanes up front. That seed is
981
+ * not cosmetic: {@link ensureLaneTrack} is find-then-mint over a `LoroMovableList`,
982
+ * so two concurrent writers that each mint the same absent lane both keep their
983
+ * row, and the merged document holds two tracks for one lane. For the main lane
984
+ * that is fatal — `videoDocumentSchema` allows at most one `video_clip` track, so
985
+ * the merged document stops being projectable at all, symmetrically on both
986
+ * replicas. Pre-seeding every lane makes `ensureLaneTrack` always take its find
987
+ * branch, which removes the race by construction rather than by detection.
988
+ *
989
+ * What closes the race is that a track with the lane's `parts_kind` EXISTS — the
990
+ * lookup is by kind, not by id. So a seed is only safe if it covers every lane:
991
+ * a partial seed leaves the uncovered lanes exactly as exposed as before.
992
+ */
993
+ declare const LANE_KINDS_IN_STACK_ORDER: readonly LaneKind[];
994
+ /**
995
+ * The conventional track id for a lane (`main_track`, `<kind>_track`). Shared by
996
+ * the up-front seed and {@link ensureLaneTrack}'s lazy mint so a document's lane
997
+ * ids do not depend on which of the two created the track.
998
+ *
999
+ * Ids are cosmetic to the merge itself — lane lookup goes by `parts_kind`, so
1000
+ * drifting them apart would not reopen the concurrent-mint race. They matter to
1001
+ * readers that address a lane by id (the FE editor's panes, fixtures), which is
1002
+ * why there is one convention rather than two.
1003
+ */
1004
+ declare function laneTrackId(kind: LaneKind): string;
1029
1005
  /**
1030
1006
  * Locate a lane's track row in the single `tracks` list by kind, minting an empty
1031
1007
  * row in lane-stacking order if absent (reference/17 §4: lane = `parts_kind`).
1032
1008
  * Ops use this to write authoritative items onto the right lane. The track id
1033
- * mirrors the seed convention (`<kind>_track`).
1009
+ * comes from {@link laneTrackId}, shared with the up-front seed.
1010
+ *
1011
+ * The mint branch is a concurrency hazard, not a convenience: see
1012
+ * {@link LANE_KINDS_IN_STACK_ORDER}. A document seeded with all four lanes never
1013
+ * reaches it.
1034
1014
  */
1035
- declare function ensureLaneTrack(draft: VideoDocumentDraft, kind: SecondaryLane | 'video_clip'): TrackDraft;
1015
+ declare function ensureLaneTrack(draft: VideoDocumentDraft, kind: LaneKind): TrackDraft;
1036
1016
  /** Find a secondary lane's track row without minting it. */
1037
1017
  declare function findLaneTrack(draft: VideoDocumentDraft, kind: SecondaryLane): TrackDraft | undefined;
1038
1018
  //#endregion
@@ -1079,4 +1059,4 @@ declare function hostForAbsMs(ranges: MainClipRange[], absMs: number): MainClipR
1079
1059
  */
1080
1060
  declare function relativePositionForAbs(ranges: MainClipRange[], absMs: number): TrackItemTimePosition;
1081
1061
  //#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 ImportVerdict, type MainClipRange, type MakeEmptyPart, ManualSyncDoc, type ManualSyncDocOptions, MedeoHttpDocStorage, type MedeoHttpDocStorageOptions, MemoryDocStorage, MengineAckTimeoutError, 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, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, classifyUpdate, covers, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, decodeDocVersionMark, derivePositionFromAbs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, mainTrackRanges, newRelayDoc, partDurationMs, partUnionSchema, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, relayDocFromSnapshot, resolveAllSpeechOverlaps, resolveSpeechOverlapByShiftingVideos, safeDurationMs, index_d_exports as schemas, snapshotToPlain, solveVideoDocument, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
1062
+ 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 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 recordEntries, I as videoDocumentMirrorSchema, L as base64ToBytes, M as VIDEO_DOCUMENT_SCHEMA_VERSION, N as effectiveVideoClipDurationMs, O as TIMELINE_SKELETON_DURATION_MS, P as partUnionToDraft, R as bytesToBase64, S as fillMainTrackTimeGaps, T as resolveAllSpeechOverlaps, _ as ensureLaneTrack, a as createMirrorVideoDocument, b as cascadeAfterVideoClipChanges, c as buildSpeechHostMap, d as toVideoDocument, f as VideoDocumentValidationError, g as videoDocumentSchema, h as partUnionSchema, i as MirrorVideoDocumentAdapter, j as safeDurationMs, k as isEmptyVideoClip, l as derivePositionFromAbs, m as validateVideoDocument, n as newRelayDoc, o as createMirrorVideoDocumentAdapter, p as assertValidVideoDocument, r as relayDocFromSnapshot, s as readVideoDocumentFromDraft, t as classifyUpdate, u as fromVideoDocument, v as findLaneTrack, w as recalculateTimelineDuration, x as arrangeMainTrackSeamlessly, y as solveVideoDocument } from "./loro-relay-doc-Br-ZJBHa.js";
2
+ import { A as partDurationMs, C as reassignSpeechesToVideoClipsByTime, D as syncAggregatedClipsTimePosition, E as resolveSpeechOverlapByShiftingVideos, F as recordEntries, I as videoDocumentMirrorSchema, L as base64ToBytes, M as VIDEO_DOCUMENT_SCHEMA_VERSION, N as effectiveVideoClipDurationMs, O as TIMELINE_SKELETON_DURATION_MS, P as partUnionToDraft, R as bytesToBase64, 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 } from "./document-C98vSu7J.js";
3
3
  import { z } from "zod";
4
4
  import { LoroDoc, VersionVector } from "loro-crdt";
5
5
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
@@ -2367,7 +2367,6 @@ var MedeoHttpDocStorage = class {
2367
2367
  const response = await this.client.pushUpdate(update.data);
2368
2368
  this.events.emit("pushOutcome", {
2369
2369
  kind: response.kind,
2370
- update: update.data,
2371
2370
  updateSeq: response.update_seq ?? void 0,
2372
2371
  serverVV: decodeServerVV(response.version?.server_vv)
2373
2372
  });
@@ -2376,7 +2375,6 @@ var MedeoHttpDocStorage = class {
2376
2375
  const rejected = error instanceof MenginePushRejectedError;
2377
2376
  this.events.emit("pushOutcome", {
2378
2377
  kind: rejected ? "rejected" : "failed",
2379
- update: update.data,
2380
2378
  code: rejected ? error.code : void 0,
2381
2379
  error: failure
2382
2380
  });
@@ -2511,7 +2509,11 @@ var MemoryDocStorage = class extends BaseDocStorage {
2511
2509
  //#region src/session/mengine-doc-session.ts
2512
2510
  /**
2513
2511
  * Raised by {@link MengineDocSession.waitForServerAck} when the local edits it
2514
- * was asked to confirm did not reach the server in time, or were refused.
2512
+ * was asked to confirm were not acknowledged as durable.
2513
+ *
2514
+ * Named `...AckFailed`, not `...AckTimeout`: two of the three `reason` values are
2515
+ * not timeouts, and the earlier name made callers reach for a retry-after-delay
2516
+ * that is wrong for `rejected`.
2515
2517
  *
2516
2518
  * The distinction the caller needs is "was it written": if this throws, treat the
2517
2519
  * write as NOT durable. `reason` says which failure it was, and `code` carries the
@@ -2523,7 +2525,7 @@ var MemoryDocStorage = class extends BaseDocStorage {
2523
2525
  * catch-up; `corrupt_update` never is.
2524
2526
  * - `failed` — transport/network failure; the server's answer is unknown.
2525
2527
  */
2526
- var MengineAckTimeoutError = class extends Error {
2528
+ var MengineAckFailedError = class extends Error {
2527
2529
  reason;
2528
2530
  code;
2529
2531
  cause;
@@ -2532,7 +2534,7 @@ var MengineAckTimeoutError = class extends Error {
2532
2534
  this.reason = reason;
2533
2535
  this.code = code;
2534
2536
  this.cause = cause;
2535
- this.name = "MengineAckTimeoutError";
2537
+ this.name = "MengineAckFailedError";
2536
2538
  }
2537
2539
  };
2538
2540
  /**
@@ -2617,13 +2619,13 @@ var MengineDocSession = class {
2617
2619
  * Returns early when the server is already known to be current, so a caller
2618
2620
  * with nothing outstanding does not block.
2619
2621
  *
2620
- * @throws {MengineAckTimeoutError} on timeout, rejection, or transport failure.
2622
+ * @throws {MengineAckFailedError} on timeout, rejection, or transport failure.
2621
2623
  */
2622
2624
  async waitForServerAck(options = {}) {
2623
2625
  if (this.adapterValue == null) throw new Error("mengine doc session is not started");
2624
2626
  const timeoutMs = options.timeoutMs ?? 15e3;
2625
2627
  const target = this.manager.connectDoc(this.docId).version();
2626
- if (this.serverVVValue != null && covers$1(this.serverVVValue, target)) return;
2628
+ if (this.serverVVValue != null && serverCovers(this.serverVVValue, target)) return;
2627
2629
  await new Promise((resolve, reject) => {
2628
2630
  let off = () => {};
2629
2631
  let done = false;
@@ -2636,14 +2638,14 @@ var MengineDocSession = class {
2636
2638
  else reject(error);
2637
2639
  };
2638
2640
  const timer = setTimeout(() => {
2639
- settle(new MengineAckTimeoutError("timeout", void 0, void 0, `mengine did not acknowledge the update within ${timeoutMs}ms`));
2641
+ settle(new MengineAckFailedError("timeout", void 0, void 0, `mengine did not acknowledge the update within ${timeoutMs}ms`));
2640
2642
  }, timeoutMs);
2641
2643
  off = this.server.subscribePushOutcome((outcome) => {
2642
2644
  if (outcome.kind === "rejected" || outcome.kind === "failed") {
2643
- settle(new MengineAckTimeoutError(outcome.kind, outcome.code, outcome.error, outcome.error?.message ?? `mengine push ${outcome.kind}`));
2645
+ settle(new MengineAckFailedError(outcome.kind, outcome.code, outcome.error, outcome.error?.message ?? `mengine push ${outcome.kind}`));
2644
2646
  return;
2645
2647
  }
2646
- if (outcome.serverVV != null && covers$1(outcome.serverVV, target)) settle(void 0);
2648
+ if (outcome.serverVV != null && serverCovers(outcome.serverVV, target)) settle(void 0);
2647
2649
  });
2648
2650
  });
2649
2651
  }
@@ -2746,24 +2748,25 @@ var MengineDocSession = class {
2746
2748
  }
2747
2749
  };
2748
2750
  /**
2749
- * True when the server version `serverVV` includes everything in `target`.
2751
+ * True when the *encoded* server version `serverVV` includes everything in
2752
+ * `target`. Thin decode wrapper over the shared {@link covers} predicate — an
2753
+ * undecodable blob is treated as "does not cover", so a corrupt server response
2754
+ * leaves a waiter waiting rather than falsely acking it.
2750
2755
  *
2751
- * `VersionVector.compare` returns `undefined` for concurrent vectors which is
2752
- * the normal case here, since the server usually holds ops from other peers that
2753
- * the local target does not. Concurrency alone must therefore NOT be read as "not
2754
- * yet acked", so compare per-peer counters instead: every peer in `target` must
2755
- * have advanced at least as far on the server. Extra server-side peers are
2756
- * irrelevant to whether *our* ops landed.
2756
+ * The coverage rule itself (per-peer counters, never `VersionVector.compare`,
2757
+ * never equality) lives in `manual-sync/version-coverage.ts` with the reasoning.
2758
+ * An earlier revision reimplemented it here with a near-identical comment block;
2759
+ * two copies of one rule is exactly the duplication this project has been bitten
2760
+ * by repeatedly.
2757
2761
  */
2758
- function covers$1(serverVV, target) {
2762
+ function serverCovers(serverVV, target) {
2759
2763
  let server;
2760
2764
  try {
2761
2765
  server = VersionVector.decode(serverVV);
2762
2766
  } catch {
2763
2767
  return false;
2764
2768
  }
2765
- for (const [peer, counter] of target.toJSON()) if ((server.get(peer) ?? 0) < counter) return false;
2766
- return true;
2769
+ return covers(server, target);
2767
2770
  }
2768
2771
  //#endregion
2769
- export { IMPLEMENTED_SEMANTIC_OP_KINDS, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckTimeoutError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, SchemaValidator, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, arrangeMainTrackSeamlessly, assertValidVideoDocument, base64ToBytes, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, classifyUpdate, covers, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, decodeDocVersionMark, derivePositionFromAbs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, mainTrackRanges, newRelayDoc, partDurationMs, partUnionSchema, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, relayDocFromSnapshot, resolveAllSpeechOverlaps, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, snapshotToPlain, solveVideoDocument, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
2772
+ export { 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 };
@@ -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
@@ -0,0 +1,2 @@
1
+ import { n as newRelayDoc, r as relayDocFromSnapshot, t as classifyUpdate } from "./loro-relay-doc-ssdYpuef.js";
2
+ export { classifyUpdate, newRelayDoc, relayDocFromSnapshot };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as VideoDraft, i as MirrorVideoDocumentAdapter } from "./index-DLchEQG7.js";
1
+ import { i as MirrorVideoDocumentAdapter, tt as VideoDraft } from "./index-BUKA3L7o.js";
2
2
 
3
3
  //#region src/testing/in-memory-mengine-server.d.ts
4
4
  /**
package/dist/testing.js CHANGED
@@ -1,4 +1,5 @@
1
- import { L as base64ToBytes, M as VIDEO_DOCUMENT_SCHEMA_VERSION, R as bytesToBase64, o as createMirrorVideoDocumentAdapter, t as classifyUpdate } from "./loro-relay-doc-Br-ZJBHa.js";
1
+ import { L as base64ToBytes, M as VIDEO_DOCUMENT_SCHEMA_VERSION, R as bytesToBase64, r as createMirrorVideoDocumentAdapter } from "./document-C98vSu7J.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.1",
3
+ "version": "1.1.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/storage": "1.0.1",
28
- "@mengine/sync": "1.0.1",
29
- "@mengine/utils": "1.0.1"
28
+ "@mengine/storage": "1.1.0",
29
+ "@mengine/sync": "1.1.0",
30
+ "@mengine/utils": "1.1.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^25.9.1",