@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.
@@ -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
- * mirrors the seed convention (`<kind>_track`).
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 === "video_clip" ? "main_track" : `${kind}_track`,
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
- //#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 };
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 { 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 };
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 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 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 — 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`). */
@@ -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 `DocStorage` contract returns `void`, so the server's verdict cannot be
614
- * the return value it is published on {@link subscribePushOutcome} instead,
615
- * for BOTH outcomes and failures. That channel is what makes the push
616
- * observable; previously the verdict was read and dropped, so a `duplicate`
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<void>;
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 did not reach the server in time, or were refused.
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 MengineAckTimeoutError extends Error {
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 {MengineAckTimeoutError} on timeout, rejection, or transport failure.
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<void>;
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
- * mirrors the seed convention (`<kind>_track`).
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: SecondaryLane | 'video_clip'): TrackDraft;
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 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 };
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 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 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 `DocStorage` contract returns `void`, so the server's verdict cannot be
2354
- * the return value it is published on {@link subscribePushOutcome} instead,
2355
- * for BOTH outcomes and failures. That channel is what makes the push
2356
- * observable; previously the verdict was read and dropped, so a `duplicate`
2357
- * (bytes contributed nothing) was indistinguishable from a successful write.
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 || update.data.byteLength === 0) return;
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: decodeServerVV(response.version?.server_vv)
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 did not reach the server in time, or were refused.
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 MengineAckTimeoutError = class extends Error {
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 = "MengineAckTimeoutError";
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 {MengineAckTimeoutError} on timeout, rejection, or transport failure.
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 && covers$1(this.serverVVValue, target)) return;
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 MengineAckTimeoutError("timeout", void 0, void 0, `mengine did not acknowledge the update within ${timeoutMs}ms`));
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 MengineAckTimeoutError(outcome.kind, outcome.code, outcome.error, outcome.error?.message ?? `mengine push ${outcome.kind}`));
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 && covers$1(outcome.serverVV, target)) settle(void 0);
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 `target`.
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
- * `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.
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 covers$1(serverVV, target) {
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
- for (const [peer, counter] of target.toJSON()) if ((server.get(peer) ?? 0) < counter) return false;
2766
- return true;
2778
+ return covers(server, target);
2767
2779
  }
2768
2780
  //#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 };
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 };
@@ -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, nt as VideoDraft } from "./index-DWVYLTjv.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 { 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.1",
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/storage": "1.0.1",
28
- "@mengine/sync": "1.0.1",
29
- "@mengine/utils": "1.0.1"
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",