@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 +6 -0
- package/dist/{document-CmjJwOba.js → document-B_JQwrC5.js} +5 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +19 -10
- package/dist/testing.js +1 -1
- package/package.json +4 -4
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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-
|
|
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,
|
|
184
|
+
handleSseFrame(frame, options);
|
|
188
185
|
splitAt = findSseFrameBoundary(buffer);
|
|
189
186
|
}
|
|
190
187
|
}
|
|
191
188
|
}
|
|
192
|
-
function handleSseFrame(frame,
|
|
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
|
-
*
|
|
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-
|
|
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.
|
|
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.
|
|
29
|
-
"@mengine/sync": "2.0.1-alpha.
|
|
30
|
-
"@mengine/utils": "2.0.1-alpha.
|
|
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",
|