@mengine/medeo-client 1.0.0 → 1.0.1-alpha.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/document-DgffKwRw.js +1593 -0
- package/dist/index-6e5cbdM3.d.ts +475 -0
- package/dist/index-DRUGsbm2.d.ts +1421 -0
- package/dist/index.d.ts +28 -1813
- package/dist/index.js +124 -1900
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +378 -0
- package/dist/testing.d.ts +46 -0
- package/dist/testing.js +339 -0
- package/package.json +8 -4
package/dist/testing.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { B as bytesToBase64, N as VIDEO_DOCUMENT_SCHEMA_VERSION, o as createMirrorVideoDocumentAdapter, z as base64ToBytes } from "./document-DgffKwRw.js";
|
|
2
|
+
import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
|
|
3
|
+
//#region src/testing/in-memory-mengine-server.ts
|
|
4
|
+
/**
|
|
5
|
+
* In-memory stand-in for `mengine-server`, exposed as a `fetchImpl`.
|
|
6
|
+
*
|
|
7
|
+
* It models the surface the client runtime uses under the Loro-native protocol
|
|
8
|
+
* (Phase 5 §A1.5): snapshot, VV-diff pull (`GET .../sync?from=<vv b64>`), update
|
|
9
|
+
* push with an `update_seq` ack, and an SSE `events` stream that broadcasts each
|
|
10
|
+
* new update with its metadata — so multiple runtimes can converge through one
|
|
11
|
+
* shared server without a real HTTP host. It is a test double, not a persistence
|
|
12
|
+
* model: state lives in process and dies with it.
|
|
13
|
+
*/
|
|
14
|
+
var InMemoryMengineServer = class {
|
|
15
|
+
doc = new LoroDoc();
|
|
16
|
+
updates = [];
|
|
17
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
18
|
+
seq = 0;
|
|
19
|
+
hasSnapshot = false;
|
|
20
|
+
/**
|
|
21
|
+
* Close every open SSE stream, simulating a network drop. Clients reconnect
|
|
22
|
+
* (a fresh `/events`) and catch up via their next sync — used to exercise
|
|
23
|
+
* disconnect recovery.
|
|
24
|
+
*/
|
|
25
|
+
dropStreams() {
|
|
26
|
+
for (const subscriber of this.subscribers) subscriber.close();
|
|
27
|
+
this.subscribers.clear();
|
|
28
|
+
}
|
|
29
|
+
fetchImpl = async (input, init) => {
|
|
30
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
31
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
32
|
+
const path = this.parsePath(url);
|
|
33
|
+
if (path === "snapshot" && method === "GET") return this.handleSnapshot();
|
|
34
|
+
if (path.startsWith("bootstrap") && method === "POST") return this.handleBootstrap(init);
|
|
35
|
+
if (path.startsWith("sync") && method === "GET") return this.handleSync(url);
|
|
36
|
+
if (path === "updates" && method === "POST") return this.handlePush(init);
|
|
37
|
+
if (path === "events" && method === "GET") return this.handleEvents(init);
|
|
38
|
+
if (path === "audit" && method === "GET") return Response.json({ entries: this.updates.map((u) => ({
|
|
39
|
+
...u.meta,
|
|
40
|
+
update_seq: u.updateSeq
|
|
41
|
+
})) });
|
|
42
|
+
return new Response("not found", { status: 404 });
|
|
43
|
+
};
|
|
44
|
+
parsePath(url) {
|
|
45
|
+
const start = url.indexOf("/api/mengine/v1/docs/");
|
|
46
|
+
if (start < 0) return "";
|
|
47
|
+
const afterDocId = url.slice(start + 21);
|
|
48
|
+
return afterDocId.slice(afterDocId.indexOf("/") + 1);
|
|
49
|
+
}
|
|
50
|
+
version() {
|
|
51
|
+
return {
|
|
52
|
+
update_seq: this.seq,
|
|
53
|
+
server_vv: bytesToBase64(this.doc.oplogVersion().encode()),
|
|
54
|
+
frontiers: bytesToBase64(encodeFrontiers(this.doc.oplogFrontiers()))
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
handleSnapshot() {
|
|
58
|
+
if (!this.hasSnapshot) return Response.json({
|
|
59
|
+
kind: "rejected",
|
|
60
|
+
code: "not_found",
|
|
61
|
+
message: "no snapshot"
|
|
62
|
+
}, { status: 404 });
|
|
63
|
+
return Response.json({
|
|
64
|
+
snapshot: bytesToBase64(this.doc.export({ mode: "snapshot" })),
|
|
65
|
+
version: this.version()
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async handleBootstrap(init) {
|
|
69
|
+
const body = JSON.parse(typeof init?.body === "string" ? init.body : "{}");
|
|
70
|
+
if (body.snapshot != null && !this.hasSnapshot) {
|
|
71
|
+
this.doc.import(base64ToBytes(body.snapshot));
|
|
72
|
+
this.hasSnapshot = true;
|
|
73
|
+
}
|
|
74
|
+
return this.handleSnapshot();
|
|
75
|
+
}
|
|
76
|
+
handleSync(url) {
|
|
77
|
+
const fromParam = new URL(url).searchParams.get("from");
|
|
78
|
+
const from = fromParam != null && fromParam !== "" ? VersionVector.decode(base64ToBytes(fromParam)) : void 0;
|
|
79
|
+
const update = this.doc.export({
|
|
80
|
+
mode: "update",
|
|
81
|
+
from
|
|
82
|
+
});
|
|
83
|
+
return Response.json({
|
|
84
|
+
update: bytesToBase64(update),
|
|
85
|
+
server_vv: bytesToBase64(this.doc.oplogVersion().encode())
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async handlePush(init) {
|
|
89
|
+
const body = JSON.parse(typeof init?.body === "string" ? init.body : "{}");
|
|
90
|
+
if (body.update == null) return Response.json({
|
|
91
|
+
kind: "rejected",
|
|
92
|
+
code: "bad_request",
|
|
93
|
+
message: "no update"
|
|
94
|
+
}, { status: 400 });
|
|
95
|
+
const data = base64ToBytes(body.update);
|
|
96
|
+
const baseVV = this.doc.oplogVersion();
|
|
97
|
+
this.doc.import(data);
|
|
98
|
+
this.hasSnapshot = true;
|
|
99
|
+
this.seq += 1;
|
|
100
|
+
const meta = extractMeta(this.doc, baseVV);
|
|
101
|
+
const stored = {
|
|
102
|
+
updateSeq: this.seq,
|
|
103
|
+
data,
|
|
104
|
+
meta
|
|
105
|
+
};
|
|
106
|
+
this.updates.push(stored);
|
|
107
|
+
for (const subscriber of this.subscribers) subscriber.push(stored);
|
|
108
|
+
return Response.json({
|
|
109
|
+
kind: "ack",
|
|
110
|
+
update_seq: this.seq,
|
|
111
|
+
version: this.version()
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
handleEvents(init) {
|
|
115
|
+
const subscribers = this.subscribers;
|
|
116
|
+
const encoder = new TextEncoder();
|
|
117
|
+
const version = () => this.version();
|
|
118
|
+
let subscriber = null;
|
|
119
|
+
const stream = new ReadableStream({
|
|
120
|
+
start(controller) {
|
|
121
|
+
subscriber = {
|
|
122
|
+
push(update) {
|
|
123
|
+
const payload = JSON.stringify({
|
|
124
|
+
update_seq: update.updateSeq,
|
|
125
|
+
updates: [bytesToBase64(update.data)],
|
|
126
|
+
meta: update.meta,
|
|
127
|
+
version: version()
|
|
128
|
+
});
|
|
129
|
+
controller.enqueue(encoder.encode(`event: update\ndata: ${payload}\n\n`));
|
|
130
|
+
},
|
|
131
|
+
close() {
|
|
132
|
+
try {
|
|
133
|
+
controller.close();
|
|
134
|
+
} catch {}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
subscribers.add(subscriber);
|
|
138
|
+
},
|
|
139
|
+
cancel() {
|
|
140
|
+
if (subscriber) subscribers.delete(subscriber);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
const signal = init?.signal;
|
|
144
|
+
if (signal) signal.addEventListener("abort", () => {
|
|
145
|
+
if (subscriber) {
|
|
146
|
+
subscribers.delete(subscriber);
|
|
147
|
+
subscriber.close();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return new Response(stream, { headers: { "content-type": "text/event-stream" } });
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
/** Read the newest Change introduced since `baseVV` into wire metadata. */
|
|
154
|
+
function extractMeta(doc, baseVV) {
|
|
155
|
+
const frontiers = bytesToBase64(encodeFrontiers(doc.oplogFrontiers()));
|
|
156
|
+
const json = doc.exportJsonUpdates(baseVV);
|
|
157
|
+
const parsed = typeof json === "string" ? JSON.parse(json) : json;
|
|
158
|
+
const peers = parsed.peers ?? [];
|
|
159
|
+
const change = (parsed.changes ?? []).at(-1);
|
|
160
|
+
if (change == null) return {
|
|
161
|
+
semantic_op: null,
|
|
162
|
+
payload: null,
|
|
163
|
+
intent: null,
|
|
164
|
+
message: null,
|
|
165
|
+
parse_error: false,
|
|
166
|
+
peer: "0",
|
|
167
|
+
counter: 0,
|
|
168
|
+
lamport: 0,
|
|
169
|
+
timestamp: 0,
|
|
170
|
+
frontiers
|
|
171
|
+
};
|
|
172
|
+
const [counterPart, peerPart] = change.id.split("@");
|
|
173
|
+
const message = change.msg ?? change.message ?? null;
|
|
174
|
+
const { semantic_op, payload, intent, parse_error } = parseMessage(message);
|
|
175
|
+
return {
|
|
176
|
+
semantic_op,
|
|
177
|
+
payload,
|
|
178
|
+
intent,
|
|
179
|
+
message,
|
|
180
|
+
parse_error,
|
|
181
|
+
peer: peers[Number(peerPart)] ?? peerPart ?? "0",
|
|
182
|
+
counter: Number(counterPart),
|
|
183
|
+
lamport: change.lamport ?? 0,
|
|
184
|
+
timestamp: change.timestamp ?? 0,
|
|
185
|
+
frontiers
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function parseMessage(message) {
|
|
189
|
+
if (message == null || message === "") return {
|
|
190
|
+
semantic_op: null,
|
|
191
|
+
payload: null,
|
|
192
|
+
intent: null,
|
|
193
|
+
parse_error: false
|
|
194
|
+
};
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(message);
|
|
197
|
+
if (parsed == null || typeof parsed !== "object" || !("semantic_op" in parsed)) return {
|
|
198
|
+
semantic_op: null,
|
|
199
|
+
payload: null,
|
|
200
|
+
intent: null,
|
|
201
|
+
parse_error: true
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
semantic_op: typeof parsed.semantic_op === "string" ? parsed.semantic_op : null,
|
|
205
|
+
payload: parsed.payload ?? null,
|
|
206
|
+
intent: typeof parsed.intent === "string" ? parsed.intent : null,
|
|
207
|
+
parse_error: false
|
|
208
|
+
};
|
|
209
|
+
} catch {
|
|
210
|
+
return {
|
|
211
|
+
semantic_op: null,
|
|
212
|
+
payload: null,
|
|
213
|
+
intent: null,
|
|
214
|
+
parse_error: true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/testing/index.ts
|
|
220
|
+
const ACTOR_TEST = "1001";
|
|
221
|
+
function createTestDraft() {
|
|
222
|
+
return {
|
|
223
|
+
id: "draft_test",
|
|
224
|
+
project_id: "project_test",
|
|
225
|
+
owner_id: "owner_test",
|
|
226
|
+
thumbnail_storage_key: "thumb.jpg",
|
|
227
|
+
timeline: {
|
|
228
|
+
duration_ms: 3e3,
|
|
229
|
+
unit_time_ms: 33.33
|
|
230
|
+
},
|
|
231
|
+
video_creation_settings: {
|
|
232
|
+
aspect_ratio: "16:9",
|
|
233
|
+
voice_id: "voice_test",
|
|
234
|
+
video_style_id: null,
|
|
235
|
+
asset_sources: []
|
|
236
|
+
},
|
|
237
|
+
chat_session_id: "chat_test",
|
|
238
|
+
main_track: {
|
|
239
|
+
id: "main_track",
|
|
240
|
+
parts_kind: "video_clip",
|
|
241
|
+
is_hidden: false,
|
|
242
|
+
items: [{
|
|
243
|
+
abs_time_position: 0,
|
|
244
|
+
part_id: "clip_a"
|
|
245
|
+
}, {
|
|
246
|
+
abs_time_position: 1e3,
|
|
247
|
+
part_id: "clip_b"
|
|
248
|
+
}]
|
|
249
|
+
},
|
|
250
|
+
above_main_tracks: [],
|
|
251
|
+
below_main_tracks: [],
|
|
252
|
+
part_aggregations: [],
|
|
253
|
+
part_library: {
|
|
254
|
+
clip_a: { video_clip: {
|
|
255
|
+
id: "clip_a",
|
|
256
|
+
kind: "video_clip",
|
|
257
|
+
duration_ms: 1e3,
|
|
258
|
+
play_in: 0,
|
|
259
|
+
play_out: 1e3,
|
|
260
|
+
volume: 1,
|
|
261
|
+
origin_media_id: "media_a"
|
|
262
|
+
} },
|
|
263
|
+
clip_b: { video_clip: {
|
|
264
|
+
id: "clip_b",
|
|
265
|
+
kind: "video_clip",
|
|
266
|
+
duration_ms: 2e3,
|
|
267
|
+
play_in: 0,
|
|
268
|
+
play_out: 2e3,
|
|
269
|
+
volume: 1,
|
|
270
|
+
origin_media_id: "media_b"
|
|
271
|
+
} }
|
|
272
|
+
},
|
|
273
|
+
version: 1
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function createSeededDoc(docId, opts = {}, actor = ACTOR_TEST) {
|
|
277
|
+
return createMirrorVideoDocumentAdapter({
|
|
278
|
+
meta: {
|
|
279
|
+
schema_version: VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
280
|
+
draft_id: docId,
|
|
281
|
+
project_id: "project-test",
|
|
282
|
+
owner_id: "owner-test",
|
|
283
|
+
thumbnail_storage_key: "thumb-test",
|
|
284
|
+
chat_session_id: "chat-test",
|
|
285
|
+
video_creation_settings: {
|
|
286
|
+
aspect_ratio: "16:9",
|
|
287
|
+
voice_id: "test-voice",
|
|
288
|
+
video_style_id: null,
|
|
289
|
+
asset_sources: []
|
|
290
|
+
},
|
|
291
|
+
version: 1
|
|
292
|
+
},
|
|
293
|
+
timeline: { unit_time_ms: 33.33 },
|
|
294
|
+
tracks: [{
|
|
295
|
+
id: "main_track",
|
|
296
|
+
parts_kind: "video_clip",
|
|
297
|
+
is_hidden: false,
|
|
298
|
+
items: (opts.mainTrack ?? []).map((partId) => ({
|
|
299
|
+
part_id: partId,
|
|
300
|
+
time_position: { mode: "sequential" }
|
|
301
|
+
}))
|
|
302
|
+
}],
|
|
303
|
+
part_library: Object.fromEntries(Object.entries(opts.parts ?? {}).map(([partId, payload]) => [partId, toPartUnion(partId, payload)]))
|
|
304
|
+
}, { peerId: actor });
|
|
305
|
+
}
|
|
306
|
+
function mainTrackPartIds(doc) {
|
|
307
|
+
return ((doc.snapshot().tracks ?? []).find((t) => t.parts_kind === "video_clip")?.items ?? []).map((it) => it.part_id).filter((partId) => partId != null);
|
|
308
|
+
}
|
|
309
|
+
function toPartUnion(partId, payload) {
|
|
310
|
+
const partKind = payload.part_kind;
|
|
311
|
+
const durationMs = numberValue(payload.duration_ms);
|
|
312
|
+
if (partKind === "speech") return { speech: {
|
|
313
|
+
id: partId,
|
|
314
|
+
kind: "speech",
|
|
315
|
+
media_duration_ms: durationMs ?? 0,
|
|
316
|
+
audio_script: stringValue(payload.audio_script) ?? "s",
|
|
317
|
+
volume: numberValue(payload.volume) ?? 1,
|
|
318
|
+
audio_storage_key: stringValue(payload.audio_storage_key) ?? `${partId}.mp3`,
|
|
319
|
+
origin_speech_id: stringValue(payload.origin_speech_id) ?? `o_${partId}`,
|
|
320
|
+
voice: void 0,
|
|
321
|
+
caption_ids: []
|
|
322
|
+
} };
|
|
323
|
+
return { video_clip: {
|
|
324
|
+
id: partId,
|
|
325
|
+
kind: "video_clip",
|
|
326
|
+
play_in: numberValue(payload.play_in) ?? 0,
|
|
327
|
+
play_out: numberValue(payload.play_out) ?? durationMs ?? 0,
|
|
328
|
+
volume: numberValue(payload.volume) ?? 1,
|
|
329
|
+
origin_media_id: stringValue(payload.origin_media_id) ?? `media_${partId}`
|
|
330
|
+
} };
|
|
331
|
+
}
|
|
332
|
+
function numberValue(value) {
|
|
333
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
334
|
+
}
|
|
335
|
+
function stringValue(value) {
|
|
336
|
+
return typeof value === "string" ? value : void 0;
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
export { ACTOR_TEST, InMemoryMengineServer, createSeededDoc, createTestDraft, mainTrackPartIds };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mengine/medeo-client",
|
|
3
|
-
"version": "1.0.0",
|
|
3
|
+
"version": "1.0.1-alpha.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./dist/index.js",
|
|
17
|
+
"./schemas": "./dist/schemas.js",
|
|
18
|
+
"./testing": "./dist/testing.js",
|
|
17
19
|
"./package.json": "./package.json"
|
|
18
20
|
},
|
|
19
21
|
"publishConfig": {
|
|
@@ -21,16 +23,18 @@
|
|
|
21
23
|
"registry": "https://registry.npmjs.org/"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
26
|
+
"immer": "^10.2.0",
|
|
24
27
|
"loro-mirror": "^2.2.0",
|
|
25
28
|
"zod": "^4.4.3",
|
|
26
|
-
"@mengine/storage": "1.0.0",
|
|
27
|
-
"@mengine/sync": "1.0.0",
|
|
28
|
-
"@mengine/utils": "1.0.0"
|
|
29
|
+
"@mengine/storage": "1.0.1-alpha.0",
|
|
30
|
+
"@mengine/sync": "1.0.1-alpha.0",
|
|
31
|
+
"@mengine/utils": "1.0.1-alpha.0"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@types/node": "^25.9.1",
|
|
32
35
|
"@typescript/native-preview": "7.0.0-dev.20260521.1",
|
|
33
36
|
"loro-crdt": "^1.13.6",
|
|
37
|
+
"tsx": "^4.22.3",
|
|
34
38
|
"typescript": "^6.0.3",
|
|
35
39
|
"vite-plugin-wasm": "^3.6.0",
|
|
36
40
|
"vite-plus": "^0.1.23",
|