@mengine/medeo-client 1.0.0 → 1.0.1-alpha.1

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.
@@ -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.1",
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.1",
30
+ "@mengine/sync": "1.0.1-alpha.1",
31
+ "@mengine/utils": "1.0.1-alpha.1"
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",