@mengine/medeo-client 2.0.1 → 2.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/README.md CHANGED
@@ -8,6 +8,20 @@ This package owns the shared `VideoDocument` contract, semantic editing layer, a
8
8
 
9
9
  `getState().loaded` means the first local load completed. Once loaded, `loadedFromLocal` reports whether that load imported a snapshot, including a valid empty document. It stays unchanged by later edits and remote updates. Document readiness does not depend on metadata or nonempty tracks.
10
10
 
11
+ ## Local persistence
12
+
13
+ Browser hosts can inject `IndexedDBDocStorage` through `localStorage`. An optional `initialSnapshot` merges a freshly fetched server snapshot into that storage before DocManager loads it; it never replaces locally saved, unconfirmed edits. The host can require a successful online fetch before constructing the session.
14
+
15
+ `getLocalSaveState()` / `subscribeLocalSaveState()` report `{ status: 'saving' | 'saved' | 'failed', error }` for this session's local commits. This is separate from `getSyncState()` and server confirmation. The storage adapter determines durability: IndexedDB resolves only after its transaction commits; MemoryDocStorage remains in memory. Failed saves retain their causal order and retry without blocking other documents.
16
+
17
+ `waitForLocalSave({ timeoutMs? })` waits until the local save queue drains, including in-flight writes and edits made while waiting (default deadline: 15 seconds). Continuous editing may keep it pending. Timing out only stops the wait; persistence continues. This does not wait for server acknowledgement.
18
+
19
+ For normal teardown, use `await session.close()`. It immediately rejects new edits and stops synchronization, then keeps local persistence alive until pending commits are saved before releasing connections. Storage failures remain retryable while closing. `destroy()` is immediate cancellation for exceptional teardown, including failed startup; it can discard unpersisted in-memory work. Neither API can guarantee a save after the browser process is forcibly terminated. Only completed local saves survive reopening.
20
+
21
+ Edits received from another local-storage peer also remain unconfirmed until covered by an authoritative server version. Receiving a cross-tab notification is not a server acknowledgement.
22
+
23
+ Reopening uses the stored Loro history and the server's version vector to derive missing updates, without replaying semantic operations or their external side effects. Undo history remains limited to the current session.
24
+
11
25
  ## Session undo/redo
12
26
 
13
27
  After `await session.start()`, `session.undo(options?)` and `session.redo(options?)` synchronously apply local history and return whether they produced a document update. `options` accepts the same actor/intent metadata as semantic edits. Read the resulting document through `session.snapshot()`; use `waitForServerAck()` only when durable confirmation is required.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { $ as VideoDraft, A as assertValidVideoDocument, B as CaptionPart, C as index_d_exports, D as VideoDocumentMirrorSchema, E as VideoDocumentDraft, F as buildSpeechHostMap, G as Timeline, H as PartKind, I as derivePositionFromAbs, J as TrackItemTimePosition, K as Track, L as fromVideoDocument, M as buildInitialVideoDocument, N as DerivedItemPosition, O as videoDocumentMirrorSchema, P as SpeechHostMap, Q as VideoDocumentValidationIssueCode, R as toVideoDocument, S as ValidationError, T as TrackItemDraft, U as PartUnion, V as DEFAULT_UNIT_TIME_MS, W as SpeechPart, X as VideoDocument, Y as VideoClipPart, Z as VideoDocumentValidationIssue, _ as PlannedSemanticOpKind, a as MirrorVideoDocumentOptions, at as PartAggregation, b as SchemaValidator, c as CommitOptions, ct as Track$1, d as SemanticEditor, et as VideoDraftContent, f as SemanticOpInput, g as ImplementedSemanticOpKind, h as IMPLEMENTED_SEMANTIC_OP_KINDS, i as MirrorVideoDocumentAdapter, it as CaptionStyle, j as validateVideoDocument, k as VideoDocumentValidationError, l as OpActor, lt as TrackItem$1, m as TransactAudit, n as videoDocumentSchema, nt as Attachment, o as createMirrorVideoDocument, ot as SpeedShift, p as SemanticOpName, q as TrackItem, r as readVideoDocumentFromDraft, rt as CaptionPart$1, s as createMirrorVideoDocumentAdapter, st as Timeline$1, t as partUnionSchema, tt as VideoDraftPartUnion, u as SemanticDocumentAdapter, v as SemanticOpKind, w as TrackDraft, x as SnapshotReadable, y as isImplementedSemanticOpKind, z as BgmPart } from "./index-CnZ9l3rb.js";
2
2
  import { LoroDoc, PeerID } from "loro-crdt";
3
- import { DocState } from "@mengine/sync";
3
+ import { DocState, LocalSaveState } from "@mengine/sync";
4
4
  import { BaseDocStorage, Connection, DocDiff, DocPushReceipt, DocSnapshotRecord, DocStorage, DocStorageOptions, DocUpdate, DocUpdateRecord } from "@mengine/storage";
5
5
 
6
6
  //#region src/client/base64.d.ts
@@ -632,13 +632,18 @@ declare class MengineAckFailedError extends Error {
632
632
  readonly cause: Error | undefined;
633
633
  constructor(reason: 'timeout' | 'rejected' | 'failed', code: string | undefined, cause: Error | undefined, message: string);
634
634
  }
635
+ type MengineLocalSaveState = LocalSaveState;
636
+ interface WaitForLocalSaveOptions {
637
+ /** Only limits waiting, never cancels persistence. */
638
+ timeoutMs?: number;
639
+ }
635
640
  interface WaitForServerAckOptions {
636
641
  /** Deadline in ms. Rejects with reason `timeout` when it elapses. */
637
642
  timeoutMs?: number;
638
643
  }
639
644
  /** Document-level confirmation is independent of local operation completion. */
640
645
  interface MengineDocSyncState {
641
- /** Remote-only imports do not create a new local confirmation obligation. */
646
+ /** Server-originated imports do not create a new local confirmation obligation. */
642
647
  readonly confirmation: 'unknown' | 'pending' | 'confirmed';
643
648
  /** Transport/activity phase. Idle does not imply confirmation; failure details
644
649
  * remain available through push outcomes and confirmation wait errors. */
@@ -653,6 +658,9 @@ interface MengineDocSessionOptions {
653
658
  * Browsers should pass an `IndexedDBDocStorage` for refresh/cross-tab support.
654
659
  */
655
660
  localStorage?: DocStorageLike;
661
+ /** A freshly fetched remote snapshot to merge into storage before loading.
662
+ * Existing local changes are retained; the host owns any online startup gate. */
663
+ initialSnapshot?: Uint8Array;
656
664
  sseReconnectDelayMs?: number;
657
665
  }
658
666
  /**
@@ -689,6 +697,7 @@ declare class MengineDocSession {
689
697
  private started;
690
698
  private startTask;
691
699
  private destroyed;
700
+ private closeTask;
692
701
  private lifetime;
693
702
  private hasConnected;
694
703
  private initialVersionValue;
@@ -782,7 +791,17 @@ declare class MengineDocSession {
782
791
  */
783
792
  start(): Promise<VideoDocument>;
784
793
  private open;
794
+ getLocalSaveState(): MengineLocalSaveState;
795
+ /** Immediately reports local transaction state; independent of server confirmation. */
796
+ subscribeLocalSaveState(cb: (state: MengineLocalSaveState) => void): () => void;
797
+ /** Waits until all local saves finish, including edits made while waiting. Does not wait for the network. */
798
+ waitForLocalSave(options?: WaitForLocalSaveOptions): Promise<void>;
799
+ /** Stop accepting edits and drain local persistence before releasing resources.
800
+ * A failed local write keeps retrying, even after the host has unmounted.
801
+ * Hosts should use this for normal teardown; destroy() is immediate cancellation. */
802
+ close(): Promise<void>;
785
803
  getSyncState(): MengineDocSyncState;
804
+ private trackLocalVersion;
786
805
  private confirmationTarget;
787
806
  /** Immediately supplies current state; unsubscribe when the host changes docs. */
788
807
  subscribeSyncState(cb: (state: MengineDocSyncState) => void): () => void;
@@ -1119,4 +1138,4 @@ declare function hostForAbsMs(ranges: MainClipRange[], absMs: number): MainClipR
1119
1138
  */
1120
1139
  declare function relativePositionForAbs(ranges: MainClipRange[], absMs: number): TrackItemTimePosition;
1121
1140
  //#endregion
1122
- 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, 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 MengineDocSyncState, type MengineDocumentVersion, type MengineEventStreamOptions, MengineHttpClient, type MengineHttpClientOptions, MengineHttpRequestError, MenginePushRejectedError, type MenginePushResponse, type MenginePushUpdateResponse, type MengineRejectedResponse, type MengineSnapshotResponse, type MengineSseUpdateEvent, type MengineSyncResponse, type MengineUndoState, 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, ValidationError, type VideoClipPart, type VideoDocument, type VideoDocumentDraft, type VideoDocumentMirrorSchema, VideoDocumentValidationError, type VideoDocumentValidationIssue, type VideoDocumentValidationIssueCode, type VideoDraft, type CaptionPart$1 as VideoDraftCaptionPart, type VideoDraftContent, 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 };
1141
+ 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, 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 MengineDocSyncState, type MengineDocumentVersion, type MengineEventStreamOptions, MengineHttpClient, type MengineHttpClientOptions, MengineHttpRequestError, type MengineLocalSaveState, MenginePushRejectedError, type MenginePushResponse, type MenginePushUpdateResponse, type MengineRejectedResponse, type MengineSnapshotResponse, type MengineSseUpdateEvent, type MengineSyncResponse, type MengineUndoState, 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, ValidationError, type VideoClipPart, type VideoDocument, type VideoDocumentDraft, type VideoDocumentMirrorSchema, VideoDocumentValidationError, type VideoDocumentValidationIssue, type VideoDocumentValidationIssueCode, type VideoDraft, type CaptionPart$1 as VideoDraftCaptionPart, type VideoDraftContent, type VideoDraftPartUnion, type Timeline$1 as VideoDraftTimeline, type Track$1 as VideoDraftTrack, type TrackItem$1 as VideoDraftTrackItem, type WaitForLocalSaveOptions, 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,7 +1,7 @@
1
1
  import { t as __exportAll } from "./chunk-D7D4PA-g.js";
2
2
  import { A as isEmptyVideoClip, C as fillMainTrackTimeGaps, D as resolveSpeechOverlapByShiftingVideos, E as resolveAllSpeechOverlaps, F as partUnionToDraft, I as recordEntries, L as videoDocumentMirrorSchema, M as safeDurationMs, N as DEFAULT_UNIT_TIME_MS, O as syncAggregatedClipsTimePosition, P as effectiveVideoClipDurationMs, R as base64ToBytes, S as arrangeMainTrackSeamlessly, T as recalculateTimelineDuration, _ as ensureLaneTrack, a as DocumentMutationGuard, b as solveVideoDocument, c as derivePositionFromAbs, d as VideoDocumentValidationError, f as assertValidVideoDocument, g as LANE_KINDS_IN_STACK_ORDER, h as videoDocumentSchema, i as readVideoDocumentFromDraft, j as partDurationMs, k as TIMELINE_SKELETON_DURATION_MS, l as fromVideoDocument, m as partUnionSchema, n as createMirrorVideoDocument, o as buildInitialVideoDocument, p as validateVideoDocument, r as createMirrorVideoDocumentAdapter, s as buildSpeechHostMap, t as MirrorVideoDocumentAdapter, u as toVideoDocument, v as findLaneTrack, w as reassignSpeechesToVideoClipsByTime, x as cascadeAfterVideoClipChanges, y as laneTrackId, z as bytesToBase64 } from "./document-B_JQwrC5.js";
3
3
  import { z } from "zod";
4
- import { LoroDoc, UndoManager, VersionVector } from "loro-crdt";
4
+ import { LoroDoc, UndoManager, VersionVector, decodeImportBlobMeta } from "loro-crdt";
5
5
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
6
6
  import { DisposableSet, EventBus, Task } from "@mengine/utils";
7
7
  import { BaseDocStorage, DummyConnection } from "@mengine/storage";
@@ -2713,6 +2713,7 @@ var MengineDocSession = class {
2713
2713
  started = false;
2714
2714
  startTask = null;
2715
2715
  destroyed = false;
2716
+ closeTask = null;
2716
2717
  lifetime = null;
2717
2718
  hasConnected = false;
2718
2719
  initialVersionValue = null;
@@ -2913,6 +2914,13 @@ var MengineDocSession = class {
2913
2914
  this.local.connection.connect();
2914
2915
  await this.local.connection.waitForConnected().abortOn(this.lifetime.signal);
2915
2916
  if (this.destroyed) throw new Error("mengine doc session destroyed");
2917
+ if (this.options.initialSnapshot) {
2918
+ await this.local.pushDocUpdate({
2919
+ docId: this.docId,
2920
+ data: this.options.initialSnapshot
2921
+ }, "initial-snapshot");
2922
+ if (this.destroyed) throw new Error("mengine doc session destroyed");
2923
+ }
2916
2924
  this.server.connection.connect();
2917
2925
  this.synchronizer.start();
2918
2926
  this.manager.start();
@@ -2924,7 +2932,7 @@ var MengineDocSession = class {
2924
2932
  this.adapterValue = new MirrorVideoDocumentAdapter(doc, this.mutationGuard);
2925
2933
  const unsubscribe = doc.subscribe((event) => {
2926
2934
  if (this.adapterValue == null) return;
2927
- if (event.by === "local") this.localVersionValue = doc.version();
2935
+ if (event.by === "local") this.trackLocalVersion(doc.version());
2928
2936
  this.emitSyncState();
2929
2937
  this.events.emit("update", {
2930
2938
  source: event.by === "local" ? "local" : "remote",
@@ -2932,6 +2940,10 @@ var MengineDocSession = class {
2932
2940
  });
2933
2941
  });
2934
2942
  this.disposables.add(unsubscribe);
2943
+ this.disposables.add(this.manager.onLocalUpdateReceived(this.docId, (update) => {
2944
+ this.trackLocalVersion(decodeImportBlobMeta(update, true).partialEndVersionVector);
2945
+ this.emitSyncState();
2946
+ }));
2935
2947
  this.disposables.add(this.manager.onDocStateChange(this.docId, (state) => {
2936
2948
  this.events.emit("state", state);
2937
2949
  this.emitSyncState();
@@ -2942,9 +2954,39 @@ var MengineDocSession = class {
2942
2954
  this.undoManagerValue = new DocumentUndoManager(doc, this.mutationGuard, () => this.events.emit("undoState"));
2943
2955
  this.editorValue = new SemanticEditor(this.adapterValue);
2944
2956
  this.initialVersionValue = doc.version();
2957
+ if (this.localVersionValue) this.trackLocalVersion(this.initialVersionValue);
2945
2958
  this.emitSyncState();
2946
2959
  return this.snapshot();
2947
2960
  }
2961
+ getLocalSaveState() {
2962
+ return this.getState().localSave;
2963
+ }
2964
+ /** Immediately reports local transaction state; independent of server confirmation. */
2965
+ subscribeLocalSaveState(cb) {
2966
+ const off = this.events.on("state", () => cb(this.getLocalSaveState()));
2967
+ cb(this.getLocalSaveState());
2968
+ return off;
2969
+ }
2970
+ /** Waits until all local saves finish, including edits made while waiting. Does not wait for the network. */
2971
+ async waitForLocalSave(options = {}) {
2972
+ if (!this.editorValue || this.destroyed) throw new Error("mengine doc session is not started");
2973
+ await this.manager.waitForLocalSave(this.docId).timeout(options.timeoutMs ?? 15e3);
2974
+ }
2975
+ /** Stop accepting edits and drain local persistence before releasing resources.
2976
+ * A failed local write keeps retrying, even after the host has unmounted.
2977
+ * Hosts should use this for normal teardown; destroy() is immediate cancellation. */
2978
+ close() {
2979
+ if (this.closeTask) return this.closeTask;
2980
+ if (this.destroyed || !this.editorValue) {
2981
+ this.destroy();
2982
+ return Promise.resolve();
2983
+ }
2984
+ this.mutationGuard.close();
2985
+ this.synchronizer.stop();
2986
+ this.server.connection.disconnect();
2987
+ this.closeTask = this.manager.waitForLocalSave(this.docId).then(() => this.destroy());
2988
+ return this.closeTask;
2989
+ }
2948
2990
  getSyncState() {
2949
2991
  const state = this.getState();
2950
2992
  const target = this.confirmationTarget();
@@ -2955,6 +2997,14 @@ var MengineDocSession = class {
2955
2997
  phase: connection === "disconnected" ? "offline" : this.pushErrorValue != null || state.error != null ? "failed" : connection === "connecting" ? "connecting" : state.syncing ? "syncing" : "idle"
2956
2998
  };
2957
2999
  }
3000
+ trackLocalVersion(version) {
3001
+ const target = VersionVector.parseJSON(this.confirmationTarget()?.toJSON() ?? /* @__PURE__ */ new Map());
3002
+ for (const [peer, counter] of version.toJSON()) if ((target.get(peer) ?? 0) < counter) target.setEnd({
3003
+ peer,
3004
+ counter
3005
+ });
3006
+ this.localVersionValue = target;
3007
+ }
2958
3008
  confirmationTarget() {
2959
3009
  return this.localVersionValue ?? this.initialVersionValue;
2960
3010
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mengine/medeo-client",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,9 +25,9 @@
25
25
  "dependencies": {
26
26
  "loro-mirror": "^2.3.3",
27
27
  "zod": "^4.4.3",
28
- "@mengine/storage": "2.0.1",
29
- "@mengine/sync": "2.0.1",
30
- "@mengine/utils": "2.0.1"
28
+ "@mengine/sync": "2.1.0",
29
+ "@mengine/storage": "2.1.0",
30
+ "@mengine/utils": "2.1.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",