@mengine/medeo-client 2.0.1-alpha.1 → 2.0.1-alpha.3

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
@@ -41,3 +41,9 @@ The session assembles SemanticEditor with MirrorVideoDocumentAdapter directly. D
41
41
  Undo/redo append ordinary synchronized updates and retain audit metadata (`Undo`/`Redo`, actor, and `payload.source` identifying an original local change). Existing revision bindings remain immutable. Redo reuses already materialized assets without repeating generation, billing, or attachment requests. Network failures do not automatically reverse history.
42
42
 
43
43
  Loro history uses new writes: undo on a shared LWW field restores its value before the local edit, potentially overwriting a later remote write to the same field; redo restores the value visible before undo. Unrelated remote fields remain intact. Undoing a local insertion removes the object and makes dependent remote edits invisible until redo restores the object. Caption creation includes its style container, even when empty, so a later style edit only changes fields and undo preserves another peer's independently edited fields. Initial document seeding preserves supplied styles and creates empty maps for captions without styles. No write-time repair or migration of older CRDT documents is provided.
44
+
45
+ ## Large-update resynchronization
46
+
47
+ `MengineDocSession` handles SSE `event: resync` by scheduling the existing version-vector sync on the current connection. The event carries no Loro bytes and does not create an edit or undo step. A hint arriving while an HTTP sync is in flight schedules another diff so changes newer than that response are still fetched. Low-level users of `readMengineEventStream` can handle the same hint with `onResync`; `MedeoHttpDocStorage` exposes `subscribeResync`.
48
+
49
+ Deploy this client before enabling server-side resync notifications. Older clients ignore the event. Large HTTP pushes remain valid: the server chooses inline update or resync notification after persistence, rather than requiring client-side fragmentation. Base64 encoding uses `Uint8Array.toBase64()` where available, with a chunked fallback preserving the same padded wire representation.
@@ -3,9 +3,11 @@ import { z } from "zod";
3
3
  import { LoroDoc } from "loro-crdt";
4
4
  //#region src/client/base64.ts
5
5
  function bytesToBase64(bytes) {
6
- let binary = "";
7
- for (const byte of bytes) binary += String.fromCharCode(byte);
8
- return btoa(binary);
6
+ const native = bytes.toBase64;
7
+ if (typeof native === "function") return native.call(bytes);
8
+ const chunks = [];
9
+ for (let offset = 0; offset < bytes.length; offset += 8192) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 8192)));
10
+ return btoa(chunks.join(""));
9
11
  }
10
12
  function base64ToBytes(base64) {
11
13
  const binary = atob(base64);
package/dist/index.d.ts CHANGED
@@ -173,6 +173,8 @@ interface MengineEventStreamOptions {
173
173
  /** Fired once the stream response is established (HTTP ok, body readable). */
174
174
  onOpen?(): void;
175
175
  onUpdate(event: MengineSseUpdateEvent): void;
176
+ /** A committed update omitted from live transport; fetch the missing operations by VV. */
177
+ onResync?(): void;
176
178
  }
177
179
  declare function readMengineEventStream(options: MengineEventStreamOptions): Promise<void>;
178
180
  //#endregion
@@ -574,6 +576,8 @@ declare class MedeoHttpDocStorage implements DocStorage {
574
576
  * tool-level ack wait) subscribe here.
575
577
  */
576
578
  subscribePushOutcome(callback: (outcome: PushOutcome) => void): () => void;
579
+ /** Observe live hints that require VV catch-up without replacing the SSE connection. */
580
+ subscribeResync(callback: () => void): () => void;
577
581
  /** Authoritative HTTP reads and accepted pushes, never inferred from SSE bytes. */
578
582
  subscribeServerVersion(callback: (version: Uint8Array) => void): () => void;
579
583
  private reportServerVersion;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-D7D4PA-g.js";
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-CmjJwOba.js";
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
4
  import { LoroDoc, UndoManager, VersionVector, decodeImportBlobMeta } from "loro-crdt";
5
5
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
@@ -163,9 +163,6 @@ async function safeReadJson(response) {
163
163
  //#endregion
164
164
  //#region src/client/sse.ts
165
165
  async function readMengineEventStream(options) {
166
- const onUpdate = (event) => {
167
- options.onUpdate(event);
168
- };
169
166
  const response = await options.client.fetch(options.client.eventsUrl(), {
170
167
  headers: options.client.headers(),
171
168
  signal: options.signal
@@ -184,15 +181,19 @@ async function readMengineEventStream(options) {
184
181
  while (splitAt >= 0) {
185
182
  const frame = buffer.slice(0, splitAt);
186
183
  buffer = buffer.slice(buffer[splitAt] === "\r" ? splitAt + 4 : splitAt + 2);
187
- handleSseFrame(frame, onUpdate);
184
+ handleSseFrame(frame, options);
188
185
  splitAt = findSseFrameBoundary(buffer);
189
186
  }
190
187
  }
191
188
  }
192
- function handleSseFrame(frame, onUpdate) {
189
+ function handleSseFrame(frame, options) {
193
190
  const event = parseSseFrame(frame);
191
+ if (event.event === "resync") {
192
+ options.onResync?.();
193
+ return;
194
+ }
194
195
  if (event.event !== "update" || event.data.length === 0) return;
195
- onUpdate(JSON.parse(event.data.join("\n")));
196
+ options.onUpdate(JSON.parse(event.data.join("\n")));
196
197
  }
197
198
  function parseSseFrame(frame) {
198
199
  let event = null;
@@ -2199,22 +2200,24 @@ var ManualSyncDoc = class ManualSyncDoc {
2199
2200
  * recovers whatever the stream missed while it was down. Recovery therefore
2200
2201
  * lives in the synchronizer (keyed on the real doc version), not here.
2201
2202
  *
2202
- * Each SSE frame's updates are forwarded verbatim via `onUpdate`; this
2203
+ * Update frames are forwarded via `onUpdate`, resync hints via `onResync`; this
2203
2204
  * connection keeps no version cursor and does no catch-up of its own. Dedup is
2204
2205
  * unnecessary because `LoroDoc.import` is idempotent by OpId/VV.
2205
2206
  */
2206
2207
  var SseConnection = class {
2207
2208
  client;
2208
2209
  onUpdate;
2210
+ onResync;
2209
2211
  reconnectDelayMs;
2210
2212
  inner = void 0;
2211
2213
  event = new EventBus();
2212
2214
  streamTask = null;
2213
2215
  _status = "idle";
2214
2216
  _error;
2215
- constructor(client, onUpdate, reconnectDelayMs) {
2217
+ constructor(client, onUpdate, onResync, reconnectDelayMs) {
2216
2218
  this.client = client;
2217
2219
  this.onUpdate = onUpdate;
2220
+ this.onResync = onResync;
2218
2221
  this.reconnectDelayMs = reconnectDelayMs;
2219
2222
  }
2220
2223
  get status() {
@@ -2275,6 +2278,7 @@ var SseConnection = class {
2275
2278
  client: this.client,
2276
2279
  signal,
2277
2280
  onOpen: () => this.setStatus("connected"),
2281
+ onResync: this.onResync,
2278
2282
  onUpdate: (event) => {
2279
2283
  for (const update of event.updates) this.onUpdate(update);
2280
2284
  }
@@ -2324,7 +2328,7 @@ var MedeoHttpDocStorage = class {
2324
2328
  this.options = options;
2325
2329
  this.client = options.client;
2326
2330
  this.docId = options.docId;
2327
- this.connection = new SseConnection(this.client, (update) => this.emitUpdate(update), options.sseReconnectDelayMs ?? 500);
2331
+ this.connection = new SseConnection(this.client, (update) => this.emitUpdate(update), () => this.events.emit("resync"), options.sseReconnectDelayMs ?? 500);
2328
2332
  }
2329
2333
  get isReadonly() {
2330
2334
  return this.options.readonlyMode ?? false;
@@ -2403,6 +2407,10 @@ var MedeoHttpDocStorage = class {
2403
2407
  subscribePushOutcome(callback) {
2404
2408
  return this.events.on("pushOutcome", callback);
2405
2409
  }
2410
+ /** Observe live hints that require VV catch-up without replacing the SSE connection. */
2411
+ subscribeResync(callback) {
2412
+ return this.events.on("resync", callback);
2413
+ }
2406
2414
  /** Authoritative HTTP reads and accepted pushes, never inferred from SSE bytes. */
2407
2415
  subscribeServerVersion(callback) {
2408
2416
  return this.events.on("serverVersion", callback);
@@ -2887,6 +2895,7 @@ var MengineDocSession = class {
2887
2895
  return this.startTask;
2888
2896
  }
2889
2897
  async open() {
2898
+ this.disposables.add(this.server.subscribeResync(() => this.synchronizer.requestSync(this.docId)));
2890
2899
  this.disposables.add(this.server.subscribeServerVersion((version) => {
2891
2900
  this.serverVVValue = version;
2892
2901
  if (this.getSyncState().confirmation === "confirmed") this.pushErrorValue = null;
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { R as base64ToBytes, r as createMirrorVideoDocumentAdapter, z as bytesToBase64 } from "./document-CmjJwOba.js";
1
+ import { R as base64ToBytes, r as createMirrorVideoDocumentAdapter, z as bytesToBase64 } from "./document-B_JQwrC5.js";
2
2
  import { t as classifyUpdate } from "./loro-relay-doc-cJSY-uau.js";
3
3
  import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
4
4
  //#region src/testing/in-memory-mengine-server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mengine/medeo-client",
3
- "version": "2.0.1-alpha.1",
3
+ "version": "2.0.1-alpha.3",
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-alpha.1",
29
- "@mengine/sync": "2.0.1-alpha.1",
30
- "@mengine/utils": "2.0.1-alpha.1"
28
+ "@mengine/storage": "2.0.1-alpha.3",
29
+ "@mengine/sync": "2.0.1-alpha.3",
30
+ "@mengine/utils": "2.0.1-alpha.3"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",