@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.
- package/dist/{loro-relay-doc-Br-ZJBHa.js → document-C98vSu7J.js} +94 -59
- package/dist/{index-DLchEQG7.d.ts → index-BUKA3L7o.d.ts} +44 -1
- package/dist/index.d.ts +56 -76
- package/dist/index.js +25 -22
- package/dist/loro-relay-doc-ssdYpuef.js +58 -0
- package/dist/relay.d.ts +37 -0
- package/dist/relay.js +2 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -1
- package/package.json +5 -4
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
2
|
-
import {
|
|
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
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
504
|
+
* This is what an "is my write durable" waiter keys on, and it is deliberately
|
|
505
|
+
* the ONLY correlation handle here. An earlier revision also carried the pushed
|
|
506
|
+
* bytes so a waiter could match its own update; that was removed because byte
|
|
507
|
+
* identity is not a reliable match — the same ops can reach the server either
|
|
508
|
+
* as the individual blob the push job carried or as a merged
|
|
509
|
+
* `export({from: serverVV})` blob produced by the synchronizer's sync path.
|
|
510
|
+
* Version coverage is reliable, and it makes `duplicate` satisfy a waiter
|
|
511
|
+
* correctly: the bytes appended nothing precisely because the server already
|
|
512
|
+
* held them. Offering both invited the wrong one.
|
|
572
513
|
*/
|
|
573
514
|
serverVV?: Uint8Array | undefined;
|
|
574
515
|
/** Server machine code for `rejected` (e.g. `missing_dependency`). */
|
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = "
|
|
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 {
|
|
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 &&
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
-
*
|
|
2752
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2755
|
-
*
|
|
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
|
|
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
|
-
|
|
2766
|
-
return true;
|
|
2769
|
+
return covers(server, target);
|
|
2767
2770
|
}
|
|
2768
2771
|
//#endregion
|
|
2769
|
-
export { IMPLEMENTED_SEMANTIC_OP_KINDS, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage,
|
|
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 };
|
package/dist/relay.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LoroDoc } from "loro-crdt";
|
|
2
|
+
|
|
3
|
+
//#region src/relay/loro-relay-doc.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Loro OpLog-relay primitives: the doc shape and update classification an update
|
|
6
|
+
* *host* needs, as opposed to an editing client.
|
|
7
|
+
*
|
|
8
|
+
* A relay never materializes DocState (`docs/concepts/oplog_docstate` §Relay
|
|
9
|
+
* Server) — it keeps a detached `LoroDoc` that validates and accumulates updates
|
|
10
|
+
* and exports diffs by version vector.
|
|
11
|
+
*
|
|
12
|
+
* These live in `medeo-client` rather than in `mengine-server` because two
|
|
13
|
+
* separate hosts must classify identically: the real server (`apps/mengine-server`,
|
|
14
|
+
* both storage implementations) and the in-process test double
|
|
15
|
+
* (`testing/in-memory-mengine-server.ts`) that client-side tests push against. A
|
|
16
|
+
* double with its own verdict logic drifts from the server it stands in for, and
|
|
17
|
+
* the four push outcomes it produces are exactly what those tests assert on — so
|
|
18
|
+
* the classification is shared code, not duplicated code.
|
|
19
|
+
*/
|
|
20
|
+
/** Build a fresh detached relay doc (no DocState materialization). */
|
|
21
|
+
declare function newRelayDoc(): LoroDoc;
|
|
22
|
+
/** Load a detached relay doc from a stored snapshot. */
|
|
23
|
+
declare function relayDocFromSnapshot(snapshot: Uint8Array): LoroDoc;
|
|
24
|
+
type ImportVerdict = 'accepted' | 'duplicate' | 'corrupt_update' | 'missing_dependency';
|
|
25
|
+
/**
|
|
26
|
+
* Classify an incoming update against `doc` without mutating it.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately version-delta based, not `ImportStatus`-shape based: in loro-crdt
|
|
29
|
+
* 1.13.x the JS `import()` returns `success`/`pending` as objects whose ranges
|
|
30
|
+
* are not reliably populated, and a pending (out-of-order) import still buffers
|
|
31
|
+
* the orphan op into the oplog. So this imports into a throwaway snapshot-clone
|
|
32
|
+
* and inspects whether the oplog frontiers advanced — letting the caller import
|
|
33
|
+
* into its canonical doc only on `accepted`, keeping orphan bytes out of the log.
|
|
34
|
+
*/
|
|
35
|
+
declare function classifyUpdate(doc: LoroDoc, update: Uint8Array): ImportVerdict;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { type ImportVerdict, classifyUpdate, newRelayDoc, relayDocFromSnapshot };
|
package/dist/relay.js
ADDED
package/dist/testing.d.ts
CHANGED
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { L as base64ToBytes, M as VIDEO_DOCUMENT_SCHEMA_VERSION, R as bytesToBase64,
|
|
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
|
|
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
|
|
28
|
-
"@mengine/sync": "1.0
|
|
29
|
-
"@mengine/utils": "1.0
|
|
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",
|