@mengine/medeo-client 2.1.1-dsl.2 → 2.1.1-dsl.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.
@@ -21,29 +21,22 @@ function resourceId(kind, system, key) {
21
21
  function relationId(table, value) {
22
22
  const v = value;
23
23
  switch (table) {
24
- case "clip-source-relations": return `cs:${v.clipId}`;
25
- case "time-anchor-relations": return `ta:${v.from}`;
26
- case "caption-content-relations": return `cc:${v.captionId}`;
27
- case "caption-alignment-relations": return `ca:${v.captionId}`;
28
24
  case "voice-timbre-relations": return `voice-timbre:${tupleHash([v.recordingId, v.voiceId])}`;
29
25
  case "derived-from-relations": return `derived-from:${tupleHash([v.entityId, v.sourceId])}`;
30
- case "from-asset-relations": return `from-asset:${v.audioScriptId}`;
31
- case "to-asset-relations": return `to-asset:${v.audioScriptId}`;
32
26
  }
33
27
  throw new Error(`Unknown relation table: ${table}`);
34
28
  }
35
29
  const defaultIdFactory = (kind) => `${kind}_${globalThis.crypto.randomUUID()}`;
36
30
  const resourceKinds = {
37
31
  "media-entities": "media",
38
- "voice-entities": "voice",
39
- "asset-entities": "asset"
32
+ "voice-entities": "voice"
40
33
  };
41
34
  /** A resource row's ID is derived from its locator, so one external resource is one entity. */
42
35
  function assertResourceIdentity(table, id, value) {
43
36
  const kind = resourceKinds[table];
44
37
  if (kind && id !== resourceId(kind, value.system, value.key)) throw new Error("Resource identity does not match locator");
45
38
  }
46
- /** A relation row's ID is derived from its uniqueness key. */
39
+ /** A relation row's ID is derived from its two ends. */
47
40
  function assertRelationIdentity(table, id, value) {
48
41
  if (id !== relationId(table, value)) throw new Error("Relation ID does not match endpoints");
49
42
  }
@@ -61,16 +54,19 @@ const optional$1 = (check) => ({
61
54
  check,
62
55
  optional: true
63
56
  });
57
+ const reservedIds = [
58
+ "$cid",
59
+ "__proto__",
60
+ "constructor",
61
+ "prototype"
62
+ ];
63
+ const id = (v) => nonempty(v) && !reservedIds.includes(v);
64
+ const cursor = (v) => v instanceof Cursor;
64
65
  function isRecord(value) {
65
66
  return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Cursor);
66
67
  }
67
68
  function assertId(value) {
68
- if (!nonempty(value) || [
69
- "$cid",
70
- "__proto__",
71
- "constructor",
72
- "prototype"
73
- ].includes(value)) throw new Error("Expected a nonempty, non-reserved ID");
69
+ if (!id(value)) throw new Error("Expected a nonempty, non-reserved ID");
74
70
  }
75
71
  function validLanguage(value) {
76
72
  if (typeof value !== "string") return false;
@@ -126,6 +122,30 @@ const seqMarkerFields = {
126
122
  function assertSeqMarker(value) {
127
123
  checkFields(value, seqMarkerFields);
128
124
  }
125
+ const timeAnchorFields = {
126
+ to: required(id),
127
+ offsetMs: required(integer),
128
+ fallback_abs_ms: required(nonnegative)
129
+ };
130
+ /** A time anchor's own fields; what it points at is checked by `assertReferenceAdmitted`. */
131
+ function assertTimeAnchor(value) {
132
+ checkFields(value, timeAnchorFields);
133
+ }
134
+ const captionContentFields = {
135
+ audioScriptId: required(id),
136
+ start: required(cursor),
137
+ end: required(cursor)
138
+ };
139
+ function assertCaptionContent(value) {
140
+ checkFields(value, captionContentFields);
141
+ }
142
+ const phonemeFields = {
143
+ audioScriptId: required(id),
144
+ phonemeScript: optional$1(string)
145
+ };
146
+ function assertPhoneme(value) {
147
+ checkFields(value, phonemeFields);
148
+ }
129
149
  const locator = {
130
150
  system: required(oneOf("memota", "memota-speech")),
131
151
  key: required(nonempty)
@@ -138,7 +158,9 @@ const entityFields = {
138
158
  },
139
159
  "clip-entities": {
140
160
  seqMarker: required(object(seqMarkerFields)),
141
- volume: optional$1(finite)
161
+ volume: optional$1(finite),
162
+ source: required(id),
163
+ anchor: optional$1(object(timeAnchorFields))
142
164
  },
143
165
  "media-entities": {
144
166
  ...locator,
@@ -151,7 +173,9 @@ const entityFields = {
151
173
  body: required(string),
152
174
  defaultLanguage: optional$1(validLanguage)
153
175
  },
176
+ "phonetic-script-entities": { phoneme: required(object(phonemeFields)) },
154
177
  "caption-entities": {
178
+ selection: optional$1(object(captionContentFields)),
155
179
  text: optional$1(string),
156
180
  style: optional$1(object(styleFields))
157
181
  },
@@ -159,52 +183,26 @@ const entityFields = {
159
183
  system: required(oneOf("voice-library")),
160
184
  key: required(nonempty),
161
185
  name: optional$1(string)
162
- },
163
- "asset-entities": {
164
- ...locator,
165
- storageKey: optional$1(nonempty),
166
- digest: optional$1(nonempty)
167
186
  }
168
187
  };
169
188
  const entityTables = Object.keys(entityFields);
189
+ /**
190
+ * Fields stored as one encoded string: written whole, never merged field by field. A patch to one
191
+ * is merged into the current value first, then the whole value is written.
192
+ */
193
+ const atomicFields = {
194
+ "clip-entities": ["seqMarker", "anchor"],
195
+ "phonetic-script-entities": ["phoneme"],
196
+ "caption-entities": ["selection"]
197
+ };
170
198
  const relationFields = {
171
- "clip-source-relations": {
172
- clipId: required(nonempty),
173
- sourceId: required(nonempty)
174
- },
175
- "time-anchor-relations": {
176
- from: required(nonempty),
177
- to: required(nonempty),
178
- offsetMs: required(integer),
179
- fallback_abs_ms: required(nonnegative)
180
- },
181
- "caption-content-relations": {
182
- captionId: required(nonempty),
183
- audioScriptId: required(nonempty),
184
- start: required((v) => v instanceof Cursor),
185
- end: required((v) => v instanceof Cursor)
186
- },
187
- "caption-alignment-relations": {
188
- captionId: required(nonempty),
189
- mediaId: required(nonempty),
190
- startMs: required(nonnegative),
191
- endMs: required(nonnegative)
192
- },
193
199
  "voice-timbre-relations": {
194
- recordingId: required(nonempty),
195
- voiceId: required(nonempty)
200
+ recordingId: required(id),
201
+ voiceId: required(id)
196
202
  },
197
203
  "derived-from-relations": {
198
- entityId: required(nonempty),
199
- sourceId: required(nonempty)
200
- },
201
- "from-asset-relations": {
202
- audioScriptId: required(nonempty),
203
- assetId: required(nonempty)
204
- },
205
- "to-asset-relations": {
206
- audioScriptId: required(nonempty),
207
- assetId: required(nonempty)
204
+ entityId: required(id),
205
+ sourceId: required(id)
208
206
  }
209
207
  };
210
208
  const relationTables = Object.keys(relationFields);
@@ -222,12 +220,12 @@ function assertEntityPatch(table, patch, current) {
222
220
  if (table === "audio-script-entities" && Object.hasOwn(patch, "body")) throw new Error("Edit body through the Text adapter");
223
221
  if (table === "caption-entities" && current.style !== void 0 && Object.hasOwn(patch, "style") && patch.style === void 0) throw new Error("Reset individual style fields; retain the style Map");
224
222
  if (table === "caption-entities" && current.text !== void 0 && patch.text !== void 0) throw new Error("Edit Caption text through the Text adapter");
225
- const next = mergeFields(current, patch);
223
+ const next = patchRow(table, current, patch);
226
224
  for (const key of Object.keys(patch)) {
227
225
  const field = entityFields[table][key];
228
226
  if (!field || !field.optional && next[key] === void 0 || next[key] !== void 0 && !field.check(next[key])) throw new Error(`Invalid patch field: ${key}`);
229
227
  }
230
- if (table === "media-entities" || table === "voice-entities" || table === "asset-entities") {
228
+ if (table === "media-entities" || table === "voice-entities") {
231
229
  for (const key of [
232
230
  "system",
233
231
  "key",
@@ -243,18 +241,25 @@ function assertEntityPatch(table, patch, current) {
243
241
  }
244
242
  function assertRelation(table, value) {
245
243
  checkFields(value, relationFields[table]);
246
- for (const [key, endpoint] of Object.entries(value)) if (key.endsWith("Id") || key === "from" || key === "to") assertId(endpoint);
244
+ }
245
+ /**
246
+ * The row after a patch. An atomic field merges into its current value, so a partial patch keeps
247
+ * the rest of it; an unreadable stored value is replaced instead, so writing a whole value repairs it.
248
+ */
249
+ function patchRow(table, current, patch) {
250
+ const next = mergeFields(current, patch);
251
+ for (const key of atomicFields[table] ?? []) {
252
+ const old = current[key];
253
+ const value = patch[key];
254
+ if (value !== void 0 && isRecord(old) && old.invalid === true) next[key] = isRecord(value) ? mergeFields({}, value) : value;
255
+ }
256
+ return next;
247
257
  }
248
258
  /** Undefined deletes; nested patches retain sibling fields and an existing empty style Map. */
249
259
  function mergeFields(target, patch) {
250
260
  const result = { ...target };
251
261
  for (const [key, value] of Object.entries(patch)) {
252
- if ([
253
- "$cid",
254
- "__proto__",
255
- "constructor",
256
- "prototype"
257
- ].includes(key)) throw new Error(`Reserved field: ${key}`);
262
+ if (reservedIds.includes(key)) throw new Error(`Reserved field: ${key}`);
258
263
  if (value === void 0) delete result[key];
259
264
  else result[key] = isRecord(value) ? mergeFields(isRecord(result[key]) ? result[key] : {}, value) : value;
260
265
  }
@@ -1252,23 +1257,23 @@ function codec(encode, decode) {
1252
1257
  }
1253
1258
  };
1254
1259
  }
1260
+ /** A Clip's time anchor; the Clip holding it is the anchored end. */
1255
1261
  const timeAnchorCodec = codec((v) => {
1256
- assertRelation("time-anchor-relations", v);
1262
+ assertTimeAnchor(v);
1257
1263
  return JSON.stringify({
1258
- from: v.from,
1259
1264
  to: v.to,
1260
1265
  offsetMs: v.offsetMs,
1261
1266
  fallback_abs_ms: v.fallback_abs_ms
1262
1267
  });
1263
1268
  }, (raw) => {
1264
1269
  const v = JSON.parse(raw);
1265
- assertRelation("time-anchor-relations", v);
1270
+ assertTimeAnchor(v);
1266
1271
  return v;
1267
1272
  });
1273
+ /** A Caption's selection; Cursors are stored as their native bytes in unpadded base64url. */
1268
1274
  const captionContentCodec = codec((v) => {
1269
- assertRelation("caption-content-relations", v);
1275
+ assertCaptionContent(v);
1270
1276
  return JSON.stringify({
1271
- captionId: v.captionId,
1272
1277
  audioScriptId: v.audioScriptId,
1273
1278
  start: bytesToBase64url(v.start.encode()),
1274
1279
  end: bytesToBase64url(v.end.encode())
@@ -1281,9 +1286,21 @@ const captionContentCodec = codec((v) => {
1281
1286
  start: Cursor.decode(base64urlToBytes(v.start)),
1282
1287
  end: Cursor.decode(base64urlToBytes(v.end))
1283
1288
  };
1284
- assertRelation("caption-content-relations", value);
1289
+ assertCaptionContent(value);
1285
1290
  return value;
1286
1291
  });
1292
+ /** A phonetic script's base script and pronunciation, stored as one string so they move together. */
1293
+ const phonemeCodec = codec((v) => {
1294
+ assertPhoneme(v);
1295
+ return JSON.stringify({
1296
+ audioScriptId: v.audioScriptId,
1297
+ ...v.phonemeScript === void 0 ? {} : { phonemeScript: v.phonemeScript }
1298
+ });
1299
+ }, (raw) => {
1300
+ const v = JSON.parse(raw);
1301
+ assertPhoneme(v);
1302
+ return v;
1303
+ });
1287
1304
  /** A Clip's sequence marker is stored as one string, so it is taken or replaced whole. */
1288
1305
  const seqMarkerCodec = codec((v) => {
1289
1306
  assertSeqMarker(v);
@@ -1304,13 +1321,19 @@ const optional = { required: false };
1304
1321
  const mergeable = { mergeableMapChildContainers: true };
1305
1322
  const strings = (keys) => Object.fromEntries(keys.map((key) => [key, schema.String()]));
1306
1323
  const relation = (keys) => schema.LoroMapRecord(schema.LoroMap(strings(keys)), mergeable);
1307
- const atomicValue = (codec) => schema.String().transform({
1324
+ const transform = (codec) => ({
1308
1325
  ...codec,
1309
1326
  isEqual: "encoded-value-equality",
1310
1327
  validateEncodedType: true
1311
1328
  });
1312
- const atomic = (codec) => schema.LoroMapRecord(atomicValue(codec));
1313
- /** Only direct children inherit mergeable identity. Body and ordinary entity rows keep normal identities. */
1329
+ /** One field stored as one encoded string, so it is taken or replaced whole. */
1330
+ const atomicValue = (codec) => schema.String().transform(transform(codec));
1331
+ const optionalAtomicValue = (codec) => schema.String(optional).transform(transform(codec));
1332
+ /**
1333
+ * Only direct children inherit mergeable identity. Body and ordinary entity rows keep normal
1334
+ * identities, so deleting a row also drops what a peer wrote into it concurrently: a relation an
1335
+ * entity holds at most one of is a field of that row and never outlives it.
1336
+ */
1314
1337
  const medeoDslLoroShape = schema({
1315
1338
  "timeline-entities": schema.LoroMapRecord(schema.LoroMap({})),
1316
1339
  "track-entities": schema.LoroMapRecord(schema.LoroMap({
@@ -1319,7 +1342,9 @@ const medeoDslLoroShape = schema({
1319
1342
  })),
1320
1343
  "clip-entities": schema.LoroMapRecord(schema.LoroMap({
1321
1344
  seqMarker: atomicValue(seqMarkerCodec),
1322
- volume: schema.Number(optional)
1345
+ volume: schema.Number(optional),
1346
+ source: schema.String(),
1347
+ anchor: optionalAtomicValue(timeAnchorCodec)
1323
1348
  })),
1324
1349
  "media-entities": schema.LoroMapRecord(schema.LoroMap({
1325
1350
  kind: schema.String(),
@@ -1333,7 +1358,9 @@ const medeoDslLoroShape = schema({
1333
1358
  body: schema.LoroText(),
1334
1359
  defaultLanguage: schema.String(optional)
1335
1360
  })),
1361
+ "phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({ phoneme: atomicValue(phonemeCodec) })),
1336
1362
  "caption-entities": schema.LoroMapRecord(schema.LoroMap({
1363
+ selection: optionalAtomicValue(captionContentCodec),
1337
1364
  text: schema.LoroText(optional),
1338
1365
  style: schema.LoroMap({
1339
1366
  font: schema.LoroMap({
@@ -1359,26 +1386,9 @@ const medeoDslLoroShape = schema({
1359
1386
  key: schema.String(),
1360
1387
  name: schema.String(optional)
1361
1388
  }), mergeable),
1362
- "asset-entities": schema.LoroMapRecord(schema.LoroMap({
1363
- system: schema.String(),
1364
- key: schema.String(),
1365
- storageKey: schema.String(optional),
1366
- digest: schema.String(optional)
1367
- }), mergeable),
1368
1389
  "contain-relations": schema.LoroTree(schema.LoroMap({ entityId: schema.String() })),
1369
- "clip-source-relations": relation(["clipId", "sourceId"]),
1370
- "time-anchor-relations": atomic(timeAnchorCodec),
1371
- "caption-content-relations": atomic(captionContentCodec),
1372
- "caption-alignment-relations": schema.LoroMapRecord(schema.LoroMap({
1373
- captionId: schema.String(),
1374
- mediaId: schema.String(),
1375
- startMs: schema.Number(),
1376
- endMs: schema.Number()
1377
- }), mergeable),
1378
1390
  "voice-timbre-relations": relation(["recordingId", "voiceId"]),
1379
- "derived-from-relations": relation(["entityId", "sourceId"]),
1380
- "from-asset-relations": relation(["audioScriptId", "assetId"]),
1381
- "to-asset-relations": relation(["audioScriptId", "assetId"])
1391
+ "derived-from-relations": relation(["entityId", "sourceId"])
1382
1392
  });
1383
1393
  //#endregion
1384
1394
  //#region ../medeo-dsl/src/schema/document-format.ts
@@ -1407,13 +1417,10 @@ function assertDslDocument(doc) {
1407
1417
  }
1408
1418
  //#endregion
1409
1419
  //#region ../medeo-dsl/src/schema/text-styles.ts
1410
- /** How rich-text marks extend at their edges: a language runs on into new text, a pause stays on its space. */
1411
- const textStyles = {
1412
- language: { expand: "after" },
1413
- pauseMs: { expand: "none" }
1414
- };
1420
+ /** How rich-text marks extend at their edges: a language runs on into text typed after it. */
1421
+ const textStyles = { language: { expand: "after" } };
1415
1422
  //#endregion
1416
1423
  //#region src/dsl/projection/video-draft-types.ts
1417
1424
  const DEFAULT_UNIT_TIME_MS = 33.333;
1418
1425
  //#endregion
1419
- export { assertRelation as $, hardenCidDescriptors as A, safeStringify as B, applySchemaToInferOptions as C, deepEqual as D, decodeNestedJsonValues as E, isObject as F, valuesEqual as G, stripUndefined as H, isObjectLike as I, getChildSchema as J, CID_KEY as K, isStateAndSchemaOfType as L, inferContainerTypeFromValue as M, insertChildToMap as N, defineCidProperty as O, isArrayLike as P, assertId as Q, isTreeID as R, applyEncode as S, containerIdToContainerType as T, tryInferContainerType as U, schemaToContainerType as V, tryUpdateToContainer as W, assertEntity as X, getMapFieldSchema as Y, assertEntityPatch as Z, isLoroTreeSchema as _, documentFormat as a, assertRelationIdentity as at, validateSchema as b, isInvalidAtomic as c, relationId as ct, getDefaultValue as d, entityTables as et, isContainerSchema as f, isLoroMovableListSchema as g, isLoroMapSchema as h, assertDslDocument as i, validLanguage as it, hasTransform as j, getRootContainerByType as k, timeAnchorCodec as l, resourceId as lt, isLoroListSchema as m, textStyles as n, mergeFields as nt, medeoDslLoroShape as o, assertResourceIdentity as ot, isListLikeSchema as p, getChildContainerSchema as q, UnsupportedDocumentFormatError as r, relationTables as rt, captionContentCodec as s, defaultIdFactory as st, DEFAULT_UNIT_TIME_MS as t, isRecord as tt, schema as u, tupleHash as ut, isRootSchemaType as v, cidsEqual as w, applyDecode as x, schemaContainsIgnoreField as y, isValueOfContainerType as z };
1426
+ export { assertId as $, hardenCidDescriptors as A, safeStringify as B, applySchemaToInferOptions as C, deepEqual as D, decodeNestedJsonValues as E, isObject as F, valuesEqual as G, stripUndefined as H, isObjectLike as I, getChildSchema as J, CID_KEY as K, isStateAndSchemaOfType as L, inferContainerTypeFromValue as M, insertChildToMap as N, defineCidProperty as O, isArrayLike as P, assertEntityPatch as Q, isTreeID as R, applyEncode as S, containerIdToContainerType as T, tryInferContainerType as U, schemaToContainerType as V, tryUpdateToContainer as W, assertCaptionContent as X, getMapFieldSchema as Y, assertEntity as Z, isLoroTreeSchema as _, documentFormat as a, isRecord as at, validateSchema as b, isInvalidAtomic as c, relationTables as ct, getDefaultValue as d, assertResourceIdentity as dt, assertPhoneme as et, isContainerSchema as f, defaultIdFactory as ft, isLoroMovableListSchema as g, isLoroMapSchema as h, tupleHash as ht, assertDslDocument as i, entityTables as it, hasTransform as j, getRootContainerByType as k, timeAnchorCodec as l, validLanguage as lt, isLoroListSchema as m, resourceId as mt, textStyles as n, assertTimeAnchor as nt, medeoDslLoroShape as o, mergeFields as ot, isListLikeSchema as p, relationId as pt, getChildContainerSchema as q, UnsupportedDocumentFormatError as r, atomicFields as rt, captionContentCodec as s, patchRow as st, DEFAULT_UNIT_TIME_MS as t, assertRelation as tt, schema as u, assertRelationIdentity as ut, isRootSchemaType as v, cidsEqual as w, applyDecode as x, schemaContainsIgnoreField as y, isValueOfContainerType as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mengine/medeo-client",
3
- "version": "2.1.1-dsl.2",
3
+ "version": "2.1.1-dsl.3",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,9 +29,9 @@
29
29
  "@noble/hashes": "^2.4.0",
30
30
  "immer": "^10.2.0",
31
31
  "zod": "^4.4.3",
32
- "@mengine/storage": "2.1.1-dsl.2",
33
- "@mengine/sync": "2.1.1-dsl.2",
34
- "@mengine/utils": "2.1.1-dsl.2"
32
+ "@mengine/storage": "2.1.1-dsl.3",
33
+ "@mengine/sync": "2.1.1-dsl.3",
34
+ "@mengine/utils": "2.1.1-dsl.3"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.9.1",
@@ -42,8 +42,8 @@
42
42
  "typescript": "^6.0.3",
43
43
  "vite-plugin-wasm": "^3.6.0",
44
44
  "vite-plus": "^0.1.23",
45
- "@mengine/idl-codegen": "0.0.0",
46
- "@mengine/medeo-dsl": "0.0.0"
45
+ "@mengine/medeo-dsl": "0.0.0",
46
+ "@mengine/idl-codegen": "0.0.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "loro-crdt": "^1.16.1"