js-bao 0.5.0 → 0.5.2

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/node.js CHANGED
@@ -1,5 +1,11 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
+ }) : x)(function(x) {
6
+ if (typeof require !== "undefined") return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
3
9
  var __esm = (fn, res) => function __init() {
4
10
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
11
  };
@@ -344,21 +350,36 @@ var init_CursorManager = __esm({
344
350
  return fields;
345
351
  }
346
352
  /**
347
- * Build ORDER BY clause from sort specification
353
+ * Build ORDER BY clause from sort specification.
354
+ *
355
+ * `direction` is the pagination direction (1 = forward, -1 = backward).
356
+ * For backward pagination each field's effective ORDER BY direction is
357
+ * reversed so the cursor's flipped inequality (built by
358
+ * `buildPaginationConditions`) returns the page adjacent to the cursor,
359
+ * not the globally smallest/largest window. Without this,
360
+ * `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
361
+ * rows overall instead of the n rows just below the cursor.
362
+ *
363
+ * The returned `directions` array carries the *declared* per-field sort
364
+ * directions (unaffected by `direction`) — `buildPaginationConditions`
365
+ * needs the declared directions to pick each field's comparison operator.
348
366
  */
349
- static buildOrderClause(sort, fieldFormatter = (field) => field) {
367
+ static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
350
368
  const fields = this.extractSortFields(sort);
351
369
  const clauses = [];
352
370
  const directions = [];
371
+ const sqlDir = (declared) => {
372
+ const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
373
+ return effective === 1 ? "ASC" : "DESC";
374
+ };
353
375
  if (sort && Object.keys(sort).length > 0) {
354
376
  for (const [field, dir] of Object.entries(sort)) {
355
- const sqlDirection = dir === 1 ? "ASC" : "DESC";
356
- clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
377
+ clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
357
378
  directions.push(dir);
358
379
  }
359
380
  }
360
381
  if (!sort || !sort.hasOwnProperty("id")) {
361
- clauses.push(`${fieldFormatter("id")} ASC`);
382
+ clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
362
383
  directions.push(1);
363
384
  } else if (sort && sort.hasOwnProperty("id")) {
364
385
  directions.push(sort["id"]);
@@ -381,18 +402,27 @@ var init_CursorManager = __esm({
381
402
  /**
382
403
  * Generate next and previous cursors from result set
383
404
  */
384
- static generateResultCursors(results, sortFields, _requestedDirection, hasMore, isFirstPage = false) {
405
+ static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
385
406
  if (results.length === 0) {
386
407
  return {};
387
408
  }
388
409
  const cursors = {};
389
410
  if (hasMore && results.length > 0) {
390
411
  const lastResult = results[results.length - 1];
391
- cursors.nextCursor = this.generateCursor(lastResult, sortFields, 1);
412
+ cursors.nextCursor = this.generateCursor(
413
+ lastResult,
414
+ sortFields,
415
+ requestedDirection
416
+ );
392
417
  }
393
418
  if (results.length > 0 && !isFirstPage) {
394
419
  const firstResult = results[0];
395
- cursors.prevCursor = this.generateCursor(firstResult, sortFields, -1);
420
+ const opposite = requestedDirection === 1 ? -1 : 1;
421
+ cursors.prevCursor = this.generateCursor(
422
+ firstResult,
423
+ sortFields,
424
+ opposite
425
+ );
396
426
  }
397
427
  return cursors;
398
428
  }
@@ -525,7 +555,8 @@ var init_DocumentQueryTranslator = __esm({
525
555
  const whereClause = this.translateFilter(filter);
526
556
  const orderClause = CursorManager.buildOrderClause(
527
557
  options?.sort,
528
- (field) => this.getQuotedField(field)
558
+ (field) => this.getQuotedField(field),
559
+ options?.direction || 1
529
560
  );
530
561
  const limitClause = this.buildLimitClause(options);
531
562
  const paginationClause = this.buildPaginationClause(
@@ -1161,10 +1192,244 @@ var init_DocumentQueryTranslator = __esm({
1161
1192
  }
1162
1193
  });
1163
1194
 
1164
- // src/models/metaSync.ts
1195
+ // src/utils/yjsValues.ts
1165
1196
  import * as Y from "yjs";
1197
+ function isYjsComposite(value) {
1198
+ return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
1199
+ }
1200
+ function isStringSetYMap(value) {
1201
+ if (!(value instanceof Y.Map)) return false;
1202
+ for (const v of value.values()) {
1203
+ if (v !== true) return false;
1204
+ }
1205
+ return true;
1206
+ }
1207
+ function normalizeYjsValue(value) {
1208
+ if (value instanceof Y.Text) {
1209
+ return value.toString();
1210
+ }
1211
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1212
+ return value.toJSON();
1213
+ }
1214
+ return value;
1215
+ }
1216
+ function normalizeYjsValueForStorage(value) {
1217
+ if (value instanceof Y.Text) {
1218
+ return value.toString();
1219
+ }
1220
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1221
+ return JSON.stringify(value.toJSON());
1222
+ }
1223
+ return value;
1224
+ }
1225
+ var init_yjsValues = __esm({
1226
+ "src/utils/yjsValues.ts"() {
1227
+ "use strict";
1228
+ }
1229
+ });
1230
+
1231
+ // src/models/writeLifecycle.ts
1232
+ import * as Y2 from "yjs";
1233
+ import { ulid } from "ulid";
1234
+ function assertNever(x) {
1235
+ throw new Error(`Unhandled field type: ${String(x)}`);
1236
+ }
1237
+ function fieldWriteKind(type) {
1238
+ switch (type) {
1239
+ case "string":
1240
+ case "number":
1241
+ case "boolean":
1242
+ case "date":
1243
+ case "id":
1244
+ return "scalar";
1245
+ case "stringset":
1246
+ return "stringset";
1247
+ default:
1248
+ return assertNever(type);
1249
+ }
1250
+ }
1251
+ function isKnownFieldType(type) {
1252
+ return KNOWN_FIELD_TYPES.has(type);
1253
+ }
1254
+ function ensureStringSetYMap(rm, fieldName) {
1255
+ const nested = rm.get(fieldName);
1256
+ if (nested instanceof Y2.Map) return nested;
1257
+ const fresh = new Y2.Map();
1258
+ if (nested && typeof nested === "object") {
1259
+ for (const [member, marker] of Object.entries(
1260
+ nested
1261
+ )) {
1262
+ if (Boolean(marker)) fresh.set(member, true);
1263
+ }
1264
+ }
1265
+ rm.set(fieldName, fresh);
1266
+ return fresh;
1267
+ }
1268
+ function applyStringSetDelta(target, delta) {
1269
+ if (delta.clear) {
1270
+ for (const member of Array.from(target.keys())) target.delete(member);
1271
+ }
1272
+ if (delta.remove) {
1273
+ for (const member of delta.remove) target.delete(String(member));
1274
+ }
1275
+ if (delta.add) {
1276
+ for (const member of delta.add) {
1277
+ const key = String(member);
1278
+ if (target.get(key) !== true) target.set(key, true);
1279
+ }
1280
+ }
1281
+ }
1282
+ function replaceStringSet(rm, fieldName, members) {
1283
+ const target = ensureStringSetYMap(rm, fieldName);
1284
+ const desired = /* @__PURE__ */ new Set();
1285
+ for (const member of members) desired.add(String(member));
1286
+ for (const existing of Array.from(target.keys())) {
1287
+ if (!desired.has(existing)) target.delete(existing);
1288
+ }
1289
+ for (const member of desired) {
1290
+ if (target.get(member) !== true) target.set(member, true);
1291
+ }
1292
+ }
1293
+ function applyStringSetChange(recordYMap, fieldName, change) {
1294
+ const target = ensureStringSetYMap(recordYMap, fieldName);
1295
+ applyStringSetDelta(target, {
1296
+ add: change.additions,
1297
+ remove: change.removals
1298
+ });
1299
+ }
1300
+ function parseStringSetOp(value) {
1301
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Y2.AbstractType) {
1302
+ return null;
1303
+ }
1304
+ const keys = Object.keys(value);
1305
+ const opKeys = keys.filter((k) => k.startsWith("$"));
1306
+ if (opKeys.length === 0) return null;
1307
+ const known = /* @__PURE__ */ new Set(["$add", "$remove", "$clear"]);
1308
+ const unknown = keys.filter((k) => !known.has(k));
1309
+ if (unknown.length > 0) {
1310
+ throw new Error(
1311
+ `Invalid stringset op: unexpected key(s) [${unknown.join(", ")}]. Only $add, $remove and $clear are supported.`
1312
+ );
1313
+ }
1314
+ const obj = value;
1315
+ const toMembers = (raw, opName) => {
1316
+ if (raw === void 0) return [];
1317
+ if (!Array.isArray(raw)) {
1318
+ throw new Error(`Invalid stringset op: ${opName} must be an array.`);
1319
+ }
1320
+ return raw.map((m) => String(m));
1321
+ };
1322
+ return {
1323
+ add: toMembers(obj.$add, "$add"),
1324
+ remove: toMembers(obj.$remove, "$remove"),
1325
+ clear: obj.$clear === true
1326
+ };
1327
+ }
1328
+ function writeField(rm, declaredType, key, value, modelName) {
1329
+ const kind = declaredType && isKnownFieldType(declaredType) ? fieldWriteKind(declaredType) : "scalar";
1330
+ if (Array.isArray(value)) {
1331
+ if (kind !== "stringset") {
1332
+ throw new Error(
1333
+ `Cannot write array value to field "${key}" on model "${modelName}": its declared type could not be resolved to a collection (stringset). Collection fields require schema metadata in the document (_meta_${modelName}); the model must be saved by a client at least once before a workflow writes one of its collection fields.`
1334
+ );
1335
+ }
1336
+ replaceStringSet(rm, key, value);
1337
+ return;
1338
+ }
1339
+ const op = parseStringSetOp(value);
1340
+ if (op) {
1341
+ if (kind !== "stringset") {
1342
+ throw new Error(
1343
+ `Cannot apply a stringset op ({ $add/$remove/$clear }) to field "${key}" on model "${modelName}": its declared type is ${declaredType ? `"${declaredType}"` : "unresolved"}, not a collection (stringset). Op-objects are only valid for stringset / refersToMany fields with schema metadata (_meta_${modelName}).`
1344
+ );
1345
+ }
1346
+ applyStringSetDelta(ensureStringSetYMap(rm, key), op);
1347
+ return;
1348
+ }
1349
+ if (kind === "stringset") {
1350
+ throw new Error(
1351
+ `Field "${key}" on model "${modelName}" is a stringset and requires an array (replace) or { $add/$remove/$clear } (delta) value; received ${value === null ? "null" : typeof value}.`
1352
+ );
1353
+ }
1354
+ rm.set(key, value);
1355
+ }
1356
+ function registerFunctionDefaultGenerator(name, fn) {
1357
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
1358
+ }
1359
+ function decodeFunctionDefault(name) {
1360
+ return FUNCTION_DEFAULT_GENERATORS.get(name);
1361
+ }
1362
+ function resolveLiteralOrFunctionDefault(rawDefault) {
1363
+ return typeof rawDefault === "function" ? rawDefault() : rawDefault;
1364
+ }
1365
+ function resolveDefaultValue(rawDefault, opts = {}) {
1366
+ if (typeof rawDefault === "function") return rawDefault();
1367
+ if (typeof rawDefault === "string" && rawDefault.startsWith("$")) {
1368
+ if (rawDefault.startsWith("$$")) return rawDefault.slice(1);
1369
+ const name = rawDefault.slice(1);
1370
+ const gen = opts.decode?.(name) ?? decodeFunctionDefault(name);
1371
+ if (!gen) {
1372
+ throw new Error(
1373
+ `Unknown function default "${rawDefault}" for field "${opts.fieldName ?? "?"}"${opts.modelName ? ` on model "${opts.modelName}"` : ""}: no generator is registered for "${name}". Register it via registerFunctionDefaultGenerator("${name}", fn) so the server can populate the field the same way the client does.`
1374
+ );
1375
+ }
1376
+ return gen();
1377
+ }
1378
+ return rawDefault;
1379
+ }
1380
+ function applyDefaultsFromSchema(fields, data, opts = {}) {
1381
+ for (const [fieldName, field] of Object.entries(fields)) {
1382
+ if (fieldName === "id") continue;
1383
+ if (!field || field.default === void 0) continue;
1384
+ if (field.default === UNKNOWN_FUNCTION_DEFAULT) {
1385
+ continue;
1386
+ }
1387
+ if (Object.prototype.hasOwnProperty.call(data, fieldName) && data[fieldName] !== void 0) {
1388
+ continue;
1389
+ }
1390
+ data[fieldName] = resolveDefaultValue(field.default, {
1391
+ fieldName,
1392
+ modelName: opts.modelName,
1393
+ decode: opts.decodeFunctionDefault
1394
+ });
1395
+ }
1396
+ return data;
1397
+ }
1398
+ function applyAutoStampsFromSchema(fields, data, opts) {
1399
+ const { isNew, now } = opts;
1400
+ for (const [fieldName, field] of Object.entries(fields)) {
1401
+ const stamp = field?.autoStamp;
1402
+ if (!stamp) continue;
1403
+ const explicit = Object.prototype.hasOwnProperty.call(data, fieldName) && data[fieldName] !== null && data[fieldName] !== void 0;
1404
+ if (explicit) continue;
1405
+ if (stamp === "create" && !isNew) continue;
1406
+ data[fieldName] = now;
1407
+ }
1408
+ return data;
1409
+ }
1410
+ var KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
1411
+ var init_writeLifecycle = __esm({
1412
+ "src/models/writeLifecycle.ts"() {
1413
+ "use strict";
1414
+ KNOWN_FIELD_TYPE_MAP = {
1415
+ string: true,
1416
+ number: true,
1417
+ boolean: true,
1418
+ date: true,
1419
+ id: true,
1420
+ stringset: true
1421
+ };
1422
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
1423
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
1424
+ registerFunctionDefaultGenerator("generate_ulid", () => ulid());
1425
+ UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
1426
+ }
1427
+ });
1428
+
1429
+ // src/models/metaSync.ts
1430
+ import * as Y3 from "yjs";
1166
1431
  function inferFieldType(value) {
1167
- if (value instanceof Y.Map) return "stringset";
1432
+ if (isStringSetYMap(value)) return "stringset";
1168
1433
  switch (typeof value) {
1169
1434
  case "string":
1170
1435
  return "string";
@@ -1183,7 +1448,10 @@ function encodeDefault(value) {
1183
1448
  if (value === void 0 || value === null) return void 0;
1184
1449
  if (typeof value === "function") {
1185
1450
  const name = KNOWN_FUNCTION_DEFAULTS.get(value);
1186
- return name ? `$${name}` : "$unknown_function";
1451
+ return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
1452
+ }
1453
+ if (typeof value === "string" && value.startsWith("$")) {
1454
+ return `$${value}`;
1187
1455
  }
1188
1456
  return value;
1189
1457
  }
@@ -1209,7 +1477,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1209
1477
  if (compoundConstraints.length > 0) {
1210
1478
  let constraints = meta.get("_constraints");
1211
1479
  if (!constraints) {
1212
- constraints = new Y.Map();
1480
+ constraints = new Y3.Map();
1213
1481
  meta.set("_constraints", constraints);
1214
1482
  }
1215
1483
  for (const constraint of compoundConstraints) {
@@ -1220,7 +1488,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1220
1488
  if (relationships && Object.keys(relationships).length > 0) {
1221
1489
  let rels = meta.get("_relationships");
1222
1490
  if (!rels) {
1223
- rels = new Y.Map();
1491
+ rels = new Y3.Map();
1224
1492
  meta.set("_relationships", rels);
1225
1493
  }
1226
1494
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -1232,7 +1500,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1232
1500
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1233
1501
  let fieldMeta = metaMap.get(fieldName);
1234
1502
  if (!fieldMeta) {
1235
- fieldMeta = new Y.Map();
1503
+ fieldMeta = new Y3.Map();
1236
1504
  metaMap.set(fieldName, fieldMeta);
1237
1505
  }
1238
1506
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -1240,6 +1508,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1240
1508
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
1241
1509
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
1242
1510
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
1511
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
1243
1512
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
1244
1513
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
1245
1514
  const encoded = encodeDefault(fieldOpts.default);
@@ -1248,7 +1517,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1248
1517
  function syncConstraintMeta(constraintsMap, constraint) {
1249
1518
  let cMeta = constraintsMap.get(constraint.name);
1250
1519
  if (!cMeta) {
1251
- cMeta = new Y.Map();
1520
+ cMeta = new Y3.Map();
1252
1521
  constraintsMap.set(constraint.name, cMeta);
1253
1522
  }
1254
1523
  setIfChanged(cMeta, "type", "unique");
@@ -1258,7 +1527,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
1258
1527
  function syncRelationshipMeta(relsMap, relName, relConfig) {
1259
1528
  let relMeta = relsMap.get(relName);
1260
1529
  if (!relMeta) {
1261
- relMeta = new Y.Map();
1530
+ relMeta = new Y3.Map();
1262
1531
  relsMap.set(relName, relMeta);
1263
1532
  }
1264
1533
  for (const [key, value] of Object.entries(relConfig)) {
@@ -1273,7 +1542,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
1273
1542
  if (fieldName.startsWith("_")) continue;
1274
1543
  let fieldMeta = meta.get(fieldName);
1275
1544
  if (!fieldMeta) {
1276
- fieldMeta = new Y.Map();
1545
+ fieldMeta = new Y3.Map();
1277
1546
  meta.set(fieldName, fieldMeta);
1278
1547
  }
1279
1548
  if (!fieldMeta.has("type")) {
@@ -1293,12 +1562,52 @@ var KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1293
1562
  var init_metaSync = __esm({
1294
1563
  "src/models/metaSync.ts"() {
1295
1564
  "use strict";
1565
+ init_yjsValues();
1566
+ init_writeLifecycle();
1296
1567
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
1297
1568
  _syncedCache = /* @__PURE__ */ new WeakMap();
1298
1569
  }
1299
1570
  });
1300
1571
 
1572
+ // src/utils/uniqueIndex.ts
1573
+ import * as Y4 from "yjs";
1574
+ function buildUniqueKey(recordData, fields) {
1575
+ const keyParts = [];
1576
+ for (const field of fields) {
1577
+ const value = recordData instanceof Y4.Map ? recordData.get(field) : recordData[field];
1578
+ if (value === null || value === void 0) {
1579
+ return null;
1580
+ }
1581
+ keyParts.push(String(value));
1582
+ }
1583
+ return keyParts.join("|");
1584
+ }
1585
+ function uniqueIndexMapName(modelName, constraintName) {
1586
+ return `_uniqueIdx_${modelName}_${constraintName}`;
1587
+ }
1588
+ function fieldUniqueConstraintName(modelName, fieldName) {
1589
+ return `${modelName}_${fieldName}_unique`;
1590
+ }
1591
+ var init_uniqueIndex = __esm({
1592
+ "src/utils/uniqueIndex.ts"() {
1593
+ "use strict";
1594
+ }
1595
+ });
1596
+
1301
1597
  // src/models/relationshipManager.ts
1598
+ function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
1599
+ if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
1600
+ const schema = modelClass.getSchema?.();
1601
+ const fields = schema?.fields;
1602
+ if (!fields || typeof fields.get !== "function") return;
1603
+ const fieldOptions = fields.get(orderByField);
1604
+ if (!fieldOptions) return;
1605
+ if (fieldOptions.required !== true) {
1606
+ Logger.warn(
1607
+ `[RelationshipManager] ${relationshipDescription} orders by '${orderByField}', which is not marked required on ${modelClass.name}. The composite (${orderByField}, id) cursor paginates deterministically, but a row whose '${orderByField}' is actually NULL has no stable cursor position and may drop from the tail. Mark '${orderByField}' as required, or order by a required field such as 'id', to remove the risk. (Full NULL-ordering support is tracked in #1316.)`
1608
+ );
1609
+ }
1610
+ }
1302
1611
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1303
1612
  return async function() {
1304
1613
  const relatedId = this[config.relatedIdField];
@@ -1307,6 +1616,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1307
1616
  };
1308
1617
  }
1309
1618
  function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
1619
+ const {
1620
+ relatedIdField,
1621
+ orderByField = "id",
1622
+ orderDirection = "ASC"
1623
+ } = config;
1624
+ warnIfOrderFieldNullable(
1625
+ targetModelClass,
1626
+ orderByField,
1627
+ `hasMany(${targetModelClass.name})`
1628
+ );
1310
1629
  return async function(paginationArgs) {
1311
1630
  const {
1312
1631
  limit,
@@ -1314,71 +1633,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
1314
1633
  beforeCursor,
1315
1634
  direction = "forward"
1316
1635
  } = paginationArgs || {};
1317
- const {
1318
- relatedIdField,
1319
- orderByField = "id",
1320
- orderDirection = "ASC"
1321
- } = config;
1322
1636
  const targetSchema = targetModelClass.getSchema();
1323
1637
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1324
1638
  Logger.error(
1325
1639
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
1326
1640
  );
1327
- return { data: [], nextCursor: null, prevCursor: null };
1641
+ return { data: [], hasMore: false };
1328
1642
  }
1329
- const filter = { [relatedIdField]: this.id };
1643
+ const forward = direction !== "backward";
1644
+ const cursor = forward ? afterCursor : beforeCursor;
1330
1645
  const queryOptions = {
1331
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
1646
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
1647
+ direction: forward ? 1 : -1
1332
1648
  };
1333
- if (limit !== void 0) {
1334
- queryOptions.limit = limit + 1;
1335
- }
1336
- if (direction === "forward" && afterCursor) {
1337
- filter[orderByField] = {
1338
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1339
- };
1340
- } else if (direction === "backward") {
1341
- if (beforeCursor) {
1342
- filter[orderByField] = {
1343
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1344
- };
1345
- }
1346
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
1347
- }
1348
- const result = await targetModelClass.query(filter, queryOptions);
1349
- const results = result.data;
1350
- let nextCursor = null;
1351
- let prevCursor = null;
1352
- const isFirstPage = direction === "forward" && !afterCursor;
1353
- if (limit !== void 0) {
1354
- if (direction === "forward") {
1355
- if (results.length > limit) {
1356
- nextCursor = results[limit - 1][orderByField];
1357
- results.pop();
1358
- }
1359
- if (results.length > 0 && !isFirstPage) {
1360
- prevCursor = results[0][orderByField];
1361
- }
1362
- } else {
1363
- if (results.length > limit) {
1364
- prevCursor = results[limit - 1][orderByField];
1365
- results.pop();
1366
- }
1367
- results.reverse();
1368
- if (results.length > 0 && beforeCursor) {
1369
- nextCursor = results[results.length - 1][orderByField];
1370
- }
1371
- if (results.length > 0 && !beforeCursor) {
1372
- if (results.length === limit) {
1373
- prevCursor = results[0][orderByField];
1374
- }
1375
- }
1376
- }
1377
- }
1378
- return { data: results, nextCursor, prevCursor };
1649
+ if (limit !== void 0) queryOptions.limit = limit;
1650
+ if (cursor != null) queryOptions.uniqueStartKey = cursor;
1651
+ return targetModelClass.query(
1652
+ { [relatedIdField]: this.id },
1653
+ queryOptions
1654
+ );
1379
1655
  };
1380
1656
  }
1381
1657
  function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
1658
+ const {
1659
+ joinModelLocalField,
1660
+ joinModelRelatedField,
1661
+ joinModelOrderByField = "id",
1662
+ joinModelOrderDirection = "ASC"
1663
+ } = config;
1664
+ warnIfOrderFieldNullable(
1665
+ joinModelClass,
1666
+ joinModelOrderByField,
1667
+ `hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
1668
+ );
1382
1669
  return async function(paginationArgs) {
1383
1670
  const {
1384
1671
  limit,
@@ -1392,87 +1679,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
1392
1679
  Logger.error(
1393
1680
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
1394
1681
  );
1395
- return { data: [], nextCursor: null, prevCursor: null };
1682
+ return { data: [], hasMore: false };
1396
1683
  }
1397
1684
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1398
1685
  Logger.error(
1399
1686
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
1400
1687
  );
1401
- return { data: [], nextCursor: null, prevCursor: null };
1688
+ return { data: [], hasMore: false };
1402
1689
  }
1403
- const {
1404
- joinModelLocalField,
1405
- joinModelRelatedField,
1406
- joinModelOrderByField = "id",
1407
- joinModelOrderDirection = "ASC"
1408
- } = config;
1409
- const joinFilter = { [joinModelLocalField]: this.id };
1690
+ const forward = direction !== "backward";
1691
+ const cursor = forward ? afterCursor : beforeCursor;
1410
1692
  const joinQueryOptions = {
1411
1693
  sort: {
1412
1694
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
1413
1695
  },
1414
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
1696
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
1697
+ direction: forward ? 1 : -1
1415
1698
  };
1416
- if (limit !== void 0) {
1417
- joinQueryOptions.limit = limit + 1;
1418
- }
1419
- if (direction === "forward" && afterCursor) {
1420
- joinFilter[joinModelOrderByField] = {
1421
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1422
- };
1423
- } else if (direction === "backward") {
1424
- if (beforeCursor) {
1425
- joinFilter[joinModelOrderByField] = {
1426
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1427
- };
1428
- }
1429
- joinQueryOptions.sort = {
1430
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
1431
- };
1432
- }
1699
+ if (limit !== void 0) joinQueryOptions.limit = limit;
1700
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
1433
1701
  const joinResult = await joinModelClass.query(
1434
- joinFilter,
1702
+ { [joinModelLocalField]: this.id },
1435
1703
  joinQueryOptions
1436
1704
  );
1437
- const joinResults = joinResult.data;
1438
- let nextCursorFromJoin = null;
1439
- let prevCursorFromJoin = null;
1440
- const isFirstPage = direction === "forward" && !afterCursor;
1441
- if (limit !== void 0) {
1442
- if (direction === "forward") {
1443
- if (joinResults.length > limit) {
1444
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1445
- joinResults.pop();
1446
- }
1447
- if (joinResults.length > 0 && !isFirstPage) {
1448
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1449
- }
1450
- } else {
1451
- if (joinResults.length > limit) {
1452
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1453
- joinResults.pop();
1454
- }
1455
- joinResults.reverse();
1456
- if (joinResults.length > 0 && beforeCursor) {
1457
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
1458
- }
1459
- if (joinResults.length > 0 && !beforeCursor) {
1460
- if (joinResults.length === limit) {
1461
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1462
- }
1463
- }
1464
- }
1705
+ const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1706
+ let targetData = [];
1707
+ if (relatedIds.length > 0) {
1708
+ const targetResult = await targetModelClass.query({
1709
+ id: { $in: relatedIds }
1710
+ });
1711
+ targetData = targetResult.data;
1465
1712
  }
1466
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1467
- if (relatedIds.length === 0)
1468
- return { data: [], nextCursor: null, prevCursor: null };
1469
- const targetResult = await targetModelClass.query({
1470
- id: { $in: relatedIds }
1471
- });
1472
1713
  return {
1473
- data: targetResult.data,
1474
- nextCursor: nextCursorFromJoin,
1475
- prevCursor: prevCursorFromJoin
1714
+ data: targetData,
1715
+ nextCursor: joinResult.nextCursor,
1716
+ prevCursor: joinResult.prevCursor,
1717
+ hasMore: joinResult.hasMore
1476
1718
  };
1477
1719
  };
1478
1720
  }
@@ -1596,10 +1838,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
1596
1838
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
1597
1839
  };
1598
1840
  }
1841
+ var ALWAYS_PRESENT_ORDER_FIELDS;
1599
1842
  var init_relationshipManager = __esm({
1600
1843
  "src/models/relationshipManager.ts"() {
1601
1844
  "use strict";
1602
1845
  init_BaseModel();
1846
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1603
1847
  }
1604
1848
  });
1605
1849
 
@@ -2370,7 +2614,13 @@ var init_IncludeResolver = __esm({
2370
2614
  }
2371
2615
  return null;
2372
2616
  }
2373
- /** Apply projection to a single record. */
2617
+ /**
2618
+ * Apply a projection to a single record. Assumes the projection was already
2619
+ * validated upstream (mixed inclusion+exclusion rejected by the translator):
2620
+ * `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
2621
+ * are system-identity fields and are always kept in BOTH modes (mirrors the
2622
+ * DatabaseDO `_applyProjection`); `_meta` stays excludable.
2623
+ */
2374
2624
  _applyProjection(record, projection) {
2375
2625
  const fields = Object.entries(projection);
2376
2626
  if (fields.length === 0) return record;
@@ -2389,7 +2639,7 @@ var init_IncludeResolver = __esm({
2389
2639
  } else {
2390
2640
  const result = { ...record };
2391
2641
  for (const [f, v] of fields) {
2392
- if (v === 0) delete result[f];
2642
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
2393
2643
  }
2394
2644
  return result;
2395
2645
  }
@@ -2408,10 +2658,10 @@ __export(BaseModel_exports, {
2408
2658
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
2409
2659
  generateULID: () => generateULID
2410
2660
  });
2411
- import * as Y2 from "yjs";
2412
- import { ulid } from "ulid";
2661
+ import * as Y5 from "yjs";
2662
+ import { ulid as ulid2 } from "ulid";
2413
2663
  function generateULID() {
2414
- return ulid();
2664
+ return ulid2();
2415
2665
  }
2416
2666
  var LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2417
2667
  var init_BaseModel = __esm({
@@ -2423,6 +2673,9 @@ var init_BaseModel = __esm({
2423
2673
  init_CursorManager();
2424
2674
  init_sql();
2425
2675
  init_metaSync();
2676
+ init_writeLifecycle();
2677
+ init_yjsValues();
2678
+ init_uniqueIndex();
2426
2679
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2427
2680
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
2428
2681
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2807,7 +3060,9 @@ var init_BaseModel = __esm({
2807
3060
  this.setValue(fieldKey, data[fieldKey]);
2808
3061
  this._constructorProvidedFields.add(fieldKey);
2809
3062
  } else if (fieldOptions.default !== void 0) {
2810
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
3063
+ const defaultValue = resolveLiteralOrFunctionDefault(
3064
+ fieldOptions.default
3065
+ );
2811
3066
  this.setValue(fieldKey, defaultValue);
2812
3067
  }
2813
3068
  }
@@ -2931,7 +3186,30 @@ var init_BaseModel = __esm({
2931
3186
  if (verboseEnabled) {
2932
3187
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2933
3188
  }
2934
- return fieldValue;
3189
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
3190
+ }
3191
+ /**
3192
+ * Defensive normalization for the read/sync boundary (issue #625).
3193
+ *
3194
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
3195
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
3196
+ * through untouched. For any other field, a composite Yjs primitive
3197
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
3198
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
3199
+ * warn, rather than throwing. Scalars pass through unchanged.
3200
+ */
3201
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
3202
+ const fieldOptions = schema?.fields?.get(fieldKey);
3203
+ if (fieldOptions?.type === "stringset") {
3204
+ return value;
3205
+ }
3206
+ if (isYjsComposite(value)) {
3207
+ Logger.warn(
3208
+ `[${modelName ?? this.constructor.name}] Field '${fieldKey}' holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions?.type ?? "unknown"}'. Normalizing to plain JS \u2014 the writer disagreed with the schema.`
3209
+ );
3210
+ return normalizeYjsValue(value);
3211
+ }
3212
+ return value;
2935
3213
  }
2936
3214
  getValue(fieldKey) {
2937
3215
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -3076,7 +3354,7 @@ var init_BaseModel = __esm({
3076
3354
  }
3077
3355
  getStringSetFromYjs(fieldName) {
3078
3356
  const yjsData = this.getFromYjs(fieldName);
3079
- if (yjsData instanceof Y2.Map) {
3357
+ if (yjsData instanceof Y5.Map) {
3080
3358
  return Array.from(yjsData.keys());
3081
3359
  }
3082
3360
  if (yjsData && typeof yjsData === "object") {
@@ -3184,7 +3462,7 @@ var init_BaseModel = __esm({
3184
3462
  }
3185
3463
  const extractStringSetValues = (value) => {
3186
3464
  if (!value) return [];
3187
- if (value instanceof Y2.Map) {
3465
+ if (value instanceof Y5.Map) {
3188
3466
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
3189
3467
  }
3190
3468
  if (Array.isArray(value)) {
@@ -3201,7 +3479,7 @@ var init_BaseModel = __esm({
3201
3479
  if (!recordId) continue;
3202
3480
  let itemData;
3203
3481
  const stringSetValuesByField = {};
3204
- if (recordData instanceof Y2.Map) {
3482
+ if (recordData instanceof Y5.Map) {
3205
3483
  itemData = {};
3206
3484
  const unknownFields = [];
3207
3485
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3317,20 +3595,10 @@ var init_BaseModel = __esm({
3317
3595
  Logger.verbose(
3318
3596
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
3319
3597
  );
3320
- const buildUniqueKey = (recordData, fields) => {
3321
- const keyParts = [];
3322
- for (const field of fields) {
3323
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
3324
- if (value === null || value === void 0) {
3325
- return null;
3326
- }
3327
- keyParts.push(String(value));
3328
- }
3329
- return keyParts.join("|");
3330
- };
3598
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
3331
3599
  const extractItemData = (key, recordData) => {
3332
3600
  let itemData;
3333
- if (recordData instanceof Y2.Map) {
3601
+ if (recordData instanceof Y5.Map) {
3334
3602
  itemData = {};
3335
3603
  const unknownFields = [];
3336
3604
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3343,7 +3611,14 @@ var init_BaseModel = __esm({
3343
3611
  continue;
3344
3612
  }
3345
3613
  if (value !== void 0) {
3346
- itemData[fieldKey] = value;
3614
+ if (isYjsComposite(value)) {
3615
+ Logger.warn(
3616
+ `[${this.name}] Field '${fieldKey}' on record ${key} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
3617
+ );
3618
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
3619
+ } else {
3620
+ itemData[fieldKey] = value;
3621
+ }
3347
3622
  }
3348
3623
  }
3349
3624
  if (unknownFields.length > 0) {
@@ -3366,7 +3641,14 @@ var init_BaseModel = __esm({
3366
3641
  continue;
3367
3642
  }
3368
3643
  if (value !== void 0) {
3369
- filteredData[fieldKey] = value;
3644
+ if (isYjsComposite(value)) {
3645
+ Logger.warn(
3646
+ `[${this.name}] Field '${fieldKey}' on legacy record ${key} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
3647
+ );
3648
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
3649
+ } else {
3650
+ filteredData[fieldKey] = value;
3651
+ }
3370
3652
  }
3371
3653
  }
3372
3654
  if (unknownFields.length > 0) {
@@ -3400,7 +3682,7 @@ var init_BaseModel = __esm({
3400
3682
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
3401
3683
  for (const [recordId, recordData] of allRecords.entries()) {
3402
3684
  if (recordIdsToDiscard.has(recordId)) continue;
3403
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3685
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3404
3686
  if (uniqueKey === null) continue;
3405
3687
  if (!recordsByUniqueKey.has(uniqueKey)) {
3406
3688
  recordsByUniqueKey.set(uniqueKey, []);
@@ -3446,7 +3728,7 @@ var init_BaseModel = __esm({
3446
3728
  if (!recordData || !key) continue;
3447
3729
  const itemData = extractItemData(key, recordData);
3448
3730
  if (!itemData) continue;
3449
- if (change.action === "add" && recordData instanceof Y2.Map) {
3731
+ if (change.action === "add" && recordData instanceof Y5.Map) {
3450
3732
  Logger.verbose(
3451
3733
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
3452
3734
  );
@@ -3544,9 +3826,12 @@ var init_BaseModel = __esm({
3544
3826
  const recordData = documentYMap.get(idToDiscard);
3545
3827
  if (!recordData) continue;
3546
3828
  for (const constraint of schema.resolvedUniqueConstraints) {
3547
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3829
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3548
3830
  if (uniqueKey === null) continue;
3549
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3831
+ const constraintMapName = uniqueIndexMapName(
3832
+ modelName,
3833
+ constraint.name
3834
+ );
3550
3835
  const constraintMap = yDoc.getMap(constraintMapName);
3551
3836
  const currentIndexValue = constraintMap.get(uniqueKey);
3552
3837
  if (currentIndexValue === idToDiscard) {
@@ -3564,11 +3849,14 @@ var init_BaseModel = __esm({
3564
3849
  }, `conflict-resolution-${modelName}-${docId}`);
3565
3850
  yDoc.transact(() => {
3566
3851
  for (const constraint of schema.resolvedUniqueConstraints) {
3567
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3852
+ const constraintMapName = uniqueIndexMapName(
3853
+ modelName,
3854
+ constraint.name
3855
+ );
3568
3856
  const constraintMap = yDoc.getMap(constraintMapName);
3569
3857
  for (const [recordId, recordData] of documentYMap.entries()) {
3570
3858
  if (!recordData) continue;
3571
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3859
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3572
3860
  if (uniqueKey === null) continue;
3573
3861
  const currentIndexValue = constraintMap.get(uniqueKey);
3574
3862
  if (currentIndexValue !== recordId) {
@@ -3591,7 +3879,7 @@ var init_BaseModel = __esm({
3591
3879
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
3592
3880
  );
3593
3881
  for (const [recordId, recordData] of documentYMap.entries()) {
3594
- if (recordData instanceof Y2.Map) {
3882
+ if (recordData instanceof Y5.Map) {
3595
3883
  Logger.verbose(
3596
3884
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
3597
3885
  );
@@ -3776,6 +4064,11 @@ var init_BaseModel = __esm({
3776
4064
  Logger.debug(
3777
4065
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3778
4066
  );
4067
+ if (isYjsComposite(yjsValue)) {
4068
+ Logger.warn(
4069
+ `[${modelConstructor.name}] Field '${key}' holds a composite Yjs value (${yjsValue?.constructor?.name}) in a non-stringset slot; the local scalar change will overwrite it (last-writer-wins).`
4070
+ );
4071
+ }
3779
4072
  if (yjsValue === void 0) {
3780
4073
  Logger.debug(
3781
4074
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3812,26 +4105,7 @@ var init_BaseModel = __esm({
3812
4105
  * if the field hasn't been touched since the wire-format change in #561.
3813
4106
  */
3814
4107
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3815
- let nested = recordYMap.get(fieldName);
3816
- if (!(nested instanceof Y2.Map)) {
3817
- const migrated = new Y2.Map();
3818
- if (nested && typeof nested === "object") {
3819
- for (const [member, marker] of Object.entries(
3820
- nested
3821
- )) {
3822
- if (Boolean(marker)) migrated.set(member, true);
3823
- }
3824
- }
3825
- recordYMap.set(fieldName, migrated);
3826
- nested = migrated;
3827
- }
3828
- const target = nested;
3829
- for (const member of change.additions) {
3830
- target.set(member, true);
3831
- }
3832
- for (const member of change.removals) {
3833
- target.delete(member);
3834
- }
4108
+ applyStringSetChange(recordYMap, fieldName, change);
3835
4109
  }
3836
4110
  /**
3837
4111
  * Deep equality check for comparing field values
@@ -4001,6 +4275,29 @@ var init_BaseModel = __esm({
4001
4275
  );
4002
4276
  throw new Error("Cannot save item without an id. Ensure id is set.");
4003
4277
  }
4278
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
4279
+ {
4280
+ const existingRecord = targetYMap.get(this.id);
4281
+ const isNew = !existingRecord;
4282
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
4283
+ const stamp = fieldOptions.autoStamp;
4284
+ if (!stamp) continue;
4285
+ const callerExplicit = this.hasLocalChange(fieldKey);
4286
+ if (callerExplicit) {
4287
+ const localValue = this._localChanges[fieldKey];
4288
+ if (localValue !== null && localValue !== void 0) continue;
4289
+ }
4290
+ if (stamp === "create") {
4291
+ if (!isNew) continue;
4292
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
4293
+ if (persisted !== null && persisted !== void 0) continue;
4294
+ }
4295
+ this[fieldKey] = Date.now();
4296
+ if (stamp === "create") {
4297
+ preTransactCreateStamps.add(fieldKey);
4298
+ }
4299
+ }
4300
+ }
4004
4301
  this.validateBeforeSave();
4005
4302
  Logger.debug(`[${modelName}.save] About to get current JS state`);
4006
4303
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -4010,6 +4307,34 @@ var init_BaseModel = __esm({
4010
4307
  `[${modelName}.save] getCurrentJSState() returned:`,
4011
4308
  dataToSave
4012
4309
  );
4310
+ const uniqueMissConflicts = /* @__PURE__ */ new Map();
4311
+ for (const constraint of schema.resolvedUniqueConstraints) {
4312
+ const newUniqueKey = this._buildUniqueKey(
4313
+ constraint.fields,
4314
+ dataToSave,
4315
+ modelName,
4316
+ constraint.name
4317
+ );
4318
+ if (newUniqueKey === null) {
4319
+ continue;
4320
+ }
4321
+ const constraintMap = yDoc.getMap(
4322
+ uniqueIndexMapName(modelName, constraint.name)
4323
+ );
4324
+ if (!constraintMap.get(newUniqueKey)) {
4325
+ const constraintKeyValues = constraint.fields.map(
4326
+ (field) => dataToSave[field]
4327
+ );
4328
+ const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
4329
+ constraint,
4330
+ constraintKeyValues,
4331
+ { excludeId: this.id }
4332
+ );
4333
+ if (conflictingRecord) {
4334
+ uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
4335
+ }
4336
+ }
4337
+ }
4013
4338
  await yDoc.transact(async () => {
4014
4339
  if (upsertOnConstraint && upsertOnKey) {
4015
4340
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -4031,6 +4356,14 @@ var init_BaseModel = __esm({
4031
4356
  }
4032
4357
  }
4033
4358
  }
4359
+ for (const fieldKey of preTransactCreateStamps) {
4360
+ if (this._localChanges && fieldKey in this._localChanges) {
4361
+ delete this._localChanges[fieldKey];
4362
+ }
4363
+ if (fieldKey in dataToSave) {
4364
+ delete dataToSave[fieldKey];
4365
+ }
4366
+ }
4034
4367
  }
4035
4368
  }
4036
4369
  if (!targetYMap) {
@@ -4043,7 +4376,7 @@ var init_BaseModel = __esm({
4043
4376
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
4044
4377
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
4045
4378
  if (!recordYMap) {
4046
- recordYMap = new Y2.Map();
4379
+ recordYMap = new Y5.Map();
4047
4380
  Logger.verbose(
4048
4381
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
4049
4382
  );
@@ -4095,7 +4428,10 @@ var init_BaseModel = __esm({
4095
4428
  );
4096
4429
  continue;
4097
4430
  }
4098
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
4431
+ const constraintMapName = uniqueIndexMapName(
4432
+ modelName,
4433
+ constraint.name
4434
+ );
4099
4435
  const constraintMap = yDoc.getMap(constraintMapName);
4100
4436
  Logger.verbose(
4101
4437
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -4124,6 +4460,26 @@ var init_BaseModel = __esm({
4124
4460
  this.id,
4125
4461
  existingRecordIdWithNewKey
4126
4462
  );
4463
+ } else if (!existingRecordIdWithNewKey) {
4464
+ const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
4465
+ if (conflictingRecordId && conflictingRecordId !== this.id) {
4466
+ Logger.verbose(
4467
+ `[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
4468
+ );
4469
+ throw new UniqueConstraintViolationError(
4470
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
4471
+ ", "
4472
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
4473
+ 0,
4474
+ 50
4475
+ )}...' already exist for index-less record ID ${conflictingRecordId}.`,
4476
+ modelName,
4477
+ constraint.name,
4478
+ constraint.fields,
4479
+ this.id,
4480
+ conflictingRecordId
4481
+ );
4482
+ }
4127
4483
  }
4128
4484
  }
4129
4485
  if (isUpdate && oldData) {
@@ -4467,34 +4823,46 @@ var init_BaseModel = __esm({
4467
4823
  const modelName = schema.options.name;
4468
4824
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
4469
4825
  const hasIncludes = options?.include && options.include.length > 0;
4470
- const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4826
+ const requestedLimit = options?.limit;
4827
+ const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
4828
+ let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4829
+ if (overFetch) {
4830
+ translatorOptions = {
4831
+ ...translatorOptions || {},
4832
+ limit: requestedLimit + 1
4833
+ };
4834
+ }
4471
4835
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
4472
4836
  const translatedQuery = translator.translateFind(filter, translatorOptions);
4473
4837
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
4474
4838
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
4475
- const results = await _BaseModel.dbInstance.query(
4839
+ let results = await _BaseModel.dbInstance.query(
4476
4840
  translatedQuery.sql,
4477
4841
  translatedQuery.params
4478
4842
  );
4479
4843
  Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
4844
+ let hasMore = false;
4845
+ if (overFetch && results.length > requestedLimit) {
4846
+ hasMore = true;
4847
+ results = results.slice(0, requestedLimit);
4848
+ }
4480
4849
  if (options?.include && options.include.length > 0) {
4481
4850
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
4482
4851
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
4483
4852
  await resolver.resolve(results, options.include, 0, modelName);
4484
4853
  }
4485
- const requestedLimit = options?.limit;
4486
- const hasMore = CursorManager.hasMoreResults(
4487
- requestedLimit,
4488
- results.length
4489
- );
4490
4854
  const isFirstPage = !options?.uniqueStartKey;
4855
+ const direction = options?.direction || 1;
4491
4856
  const cursors = CursorManager.generateResultCursors(
4492
4857
  results,
4493
4858
  translatedQuery.sortFields,
4494
- options?.direction || 1,
4859
+ direction,
4495
4860
  hasMore,
4496
4861
  isFirstPage
4497
4862
  );
4863
+ if (direction === -1) {
4864
+ results.reverse();
4865
+ }
4498
4866
  let transformedData;
4499
4867
  if (options?.projection && Object.keys(options.projection).length > 0) {
4500
4868
  transformedData = results.map((row) => {
@@ -5070,7 +5438,7 @@ var init_BaseModel = __esm({
5070
5438
  );
5071
5439
  }
5072
5440
  for (const [recordId, recordYMap] of documentYMap.entries()) {
5073
- if (recordYMap instanceof Y2.Map) {
5441
+ if (recordYMap instanceof Y5.Map) {
5074
5442
  const instance = new this({ id: recordId });
5075
5443
  instance._metaDocId = docId;
5076
5444
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -5095,6 +5463,50 @@ var init_BaseModel = __esm({
5095
5463
  }
5096
5464
  return allInstances;
5097
5465
  }
5466
+ /**
5467
+ * Resolve a record by its unique-constraint field values via an engine query.
5468
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
5469
+ * entry for a key.
5470
+ *
5471
+ * The index map is a cache over the SQLite projection, not the source of
5472
+ * truth: every record is loaded into the projection on `initializeForDocument`
5473
+ * regardless of index presence, but a record written by a client predating
5474
+ * index maintenance (legacy data) is missing from the index. Such a record
5475
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
5476
+ * check. A query over the constraint fields finds it reliably.
5477
+ *
5478
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
5479
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
5480
+ * whether to heal the missing index entry (write paths) and do their own
5481
+ * logging — this method never mutates the index, so read paths stay
5482
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
5483
+ * constraint field means the record does not participate in the constraint,
5484
+ * so no query runs.
5485
+ */
5486
+ static async _resolveByUniqueFallback(constraint, keyValues, options) {
5487
+ if (!_BaseModel.dbInstance) {
5488
+ return null;
5489
+ }
5490
+ const filter = {};
5491
+ for (let i = 0; i < constraint.fields.length; i++) {
5492
+ const value = keyValues[i];
5493
+ if (value === null || value === void 0) {
5494
+ return null;
5495
+ }
5496
+ filter[constraint.fields[i]] = value;
5497
+ }
5498
+ if (options?.excludeId) {
5499
+ if ("id" in filter) {
5500
+ if (filter.id === options.excludeId) {
5501
+ return null;
5502
+ }
5503
+ } else {
5504
+ filter.id = { $ne: options.excludeId };
5505
+ }
5506
+ }
5507
+ const found = await this.queryOne(filter);
5508
+ return found ?? null;
5509
+ }
5098
5510
  static async findByUnique(constraintName, value) {
5099
5511
  const modelConstructor = this;
5100
5512
  const schema = modelConstructor.getSchema();
@@ -5133,7 +5545,10 @@ var init_BaseModel = __esm({
5133
5545
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
5134
5546
  if (!documentInfo) continue;
5135
5547
  const yDoc = documentInfo.yDoc;
5136
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5548
+ const constraintMapName2 = uniqueIndexMapName(
5549
+ currentModelName,
5550
+ constraint.name
5551
+ );
5137
5552
  const constraintMap2 = yDoc.getMap(constraintMapName2);
5138
5553
  const recordId2 = constraintMap2.get(uniqueKeyString);
5139
5554
  if (recordId2) {
@@ -5141,7 +5556,7 @@ var init_BaseModel = __esm({
5141
5556
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
5142
5557
  if (documentYMap) {
5143
5558
  const recordYMap2 = documentYMap.get(recordId2);
5144
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
5559
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
5145
5560
  const instance = new modelConstructor({
5146
5561
  id: recordId2
5147
5562
  });
@@ -5155,6 +5570,16 @@ var init_BaseModel = __esm({
5155
5570
  }
5156
5571
  }
5157
5572
  }
5573
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5574
+ constraint,
5575
+ keyValues
5576
+ );
5577
+ if (fallbackRecord) {
5578
+ Logger.verbose(
5579
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5580
+ );
5581
+ return fallbackRecord;
5582
+ }
5158
5583
  Logger.verbose(
5159
5584
  `[${currentModelName}.findByUnique] Record not found in any connected document`
5160
5585
  );
@@ -5168,17 +5593,30 @@ var init_BaseModel = __esm({
5168
5593
  `[${currentModelName}] No Y.Doc is connected for this model. Connect a document via initJsBao(...).connectDocument or call initializeForDocument(yDoc, db, docId, permissionHint) before using legacy findByUnique.`
5169
5594
  );
5170
5595
  }
5171
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5596
+ const constraintMapName = uniqueIndexMapName(
5597
+ currentModelName,
5598
+ constraint.name
5599
+ );
5172
5600
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
5173
5601
  const recordId = constraintMap.get(uniqueKeyString);
5174
5602
  if (!recordId) {
5603
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5604
+ constraint,
5605
+ keyValues
5606
+ );
5607
+ if (fallbackRecord) {
5608
+ Logger.verbose(
5609
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5610
+ );
5611
+ return fallbackRecord;
5612
+ }
5175
5613
  return null;
5176
5614
  }
5177
5615
  const recordYMap = legacyDocumentMap.get(recordId);
5178
5616
  if (!recordYMap) {
5179
5617
  return null;
5180
5618
  }
5181
- if (recordYMap instanceof Y2.Map) {
5619
+ if (recordYMap instanceof Y5.Map) {
5182
5620
  const instance = new modelConstructor({
5183
5621
  id: recordId
5184
5622
  });
@@ -5236,7 +5674,10 @@ var init_BaseModel = __esm({
5236
5674
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
5237
5675
  );
5238
5676
  }
5239
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5677
+ const constraintMapName = uniqueIndexMapName(
5678
+ currentModelName,
5679
+ constraint.name
5680
+ );
5240
5681
  let existingRecord = null;
5241
5682
  if (connectedDocuments && connectedDocuments.size > 0) {
5242
5683
  for (const docId of connectedDocuments.keys()) {
@@ -5254,6 +5695,33 @@ var init_BaseModel = __esm({
5254
5695
  }
5255
5696
  } else {
5256
5697
  }
5698
+ if (!existingRecord) {
5699
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5700
+ constraint,
5701
+ keyValuesForLookup
5702
+ );
5703
+ if (fallbackRecord) {
5704
+ existingRecord = fallbackRecord;
5705
+ const healDocId = fallbackRecord._metaDocId;
5706
+ const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
5707
+ const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
5708
+ if (healDocInfo && canHeal) {
5709
+ healDocInfo.yDoc.transact(() => {
5710
+ const healMap = healDocInfo.yDoc.getMap(constraintMapName);
5711
+ if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
5712
+ healMap.set(uniqueKeyString, fallbackRecord.id);
5713
+ }
5714
+ }, `heal-unique-index-${currentModelName}-${healDocId}`);
5715
+ Logger.verbose(
5716
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
5717
+ );
5718
+ } else {
5719
+ Logger.verbose(
5720
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; skipped index heal (owning document is read-only or not connected).`
5721
+ );
5722
+ }
5723
+ }
5724
+ }
5257
5725
  if (options?.objectMustExist) {
5258
5726
  if (!existingRecord) {
5259
5727
  throw new RecordNotFoundError(
@@ -5410,7 +5878,7 @@ var init_BaseModel = __esm({
5410
5878
  for (const [fieldKey, fieldOptions] of schema.fields) {
5411
5879
  if (fieldOptions?.type !== "stringset") continue;
5412
5880
  const value = recordYMap.get(fieldKey);
5413
- if (value instanceof Y2.Map) {
5881
+ if (value instanceof Y5.Map) {
5414
5882
  this.observeStringSetMapOnce(value);
5415
5883
  }
5416
5884
  }
@@ -5457,7 +5925,14 @@ var init_BaseModel = __esm({
5457
5925
  continue;
5458
5926
  }
5459
5927
  if (value !== void 0) {
5460
- updatedData[key] = value;
5928
+ if (isYjsComposite(value)) {
5929
+ Logger.warn(
5930
+ `[${modelName}] Field '${key}' on record ${recordId} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
5931
+ );
5932
+ updatedData[key] = normalizeYjsValueForStorage(value);
5933
+ } else {
5934
+ updatedData[key] = value;
5935
+ }
5461
5936
  }
5462
5937
  }
5463
5938
  if (unknownFields.length > 0) {
@@ -5537,7 +6012,14 @@ var init_BaseModel = __esm({
5537
6012
  continue;
5538
6013
  }
5539
6014
  if (value !== void 0) {
5540
- updatedData[key] = value;
6015
+ if (isYjsComposite(value)) {
6016
+ Logger.warn(
6017
+ `[${modelName}] Field '${key}' on record ${recordId} in document ${docId} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
6018
+ );
6019
+ updatedData[key] = normalizeYjsValueForStorage(value);
6020
+ } else {
6021
+ updatedData[key] = value;
6022
+ }
5541
6023
  }
5542
6024
  }
5543
6025
  if (unknownFields.length > 0) {
@@ -5581,16 +6063,170 @@ var init_BaseModel = __esm({
5581
6063
  }
5582
6064
  });
5583
6065
 
6066
+ // src/utils/environment.ts
6067
+ var environment_exports = {};
6068
+ __export(environment_exports, {
6069
+ constants: () => constants,
6070
+ detectEnvironment: () => detectEnvironment,
6071
+ features: () => features,
6072
+ isBrowser: () => isBrowser,
6073
+ isNode: () => isNode,
6074
+ isNodeModuleAvailable: () => isNodeModuleAvailable
6075
+ });
6076
+ function detectEnvironment() {
6077
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
6078
+ return "node";
6079
+ }
6080
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
6081
+ return "browser";
6082
+ }
6083
+ if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6084
+ return "browser";
6085
+ }
6086
+ return "unknown";
6087
+ }
6088
+ function isNode() {
6089
+ return detectEnvironment() === "node";
6090
+ }
6091
+ function isBrowser() {
6092
+ return detectEnvironment() === "browser";
6093
+ }
6094
+ async function isNodeModuleAvailable(moduleName) {
6095
+ if (!isNode()) {
6096
+ return false;
6097
+ }
6098
+ try {
6099
+ await import(
6100
+ /* @vite-ignore */
6101
+ moduleName
6102
+ );
6103
+ return true;
6104
+ } catch {
6105
+ return false;
6106
+ }
6107
+ }
6108
+ var features, constants;
6109
+ var init_environment = __esm({
6110
+ "src/utils/environment.ts"() {
6111
+ "use strict";
6112
+ features = {
6113
+ get hasWebWorkers() {
6114
+ return isBrowser() && typeof Worker !== "undefined";
6115
+ },
6116
+ get hasFileSystem() {
6117
+ return isNode();
6118
+ },
6119
+ get hasWebAssembly() {
6120
+ return typeof WebAssembly !== "undefined";
6121
+ },
6122
+ async hasBetterSqlite3() {
6123
+ return await isNodeModuleAvailable("better-sqlite3");
6124
+ }
6125
+ };
6126
+ constants = {
6127
+ DEFAULT_NODE_SQLITE_PATH: ":memory:",
6128
+ DEFAULT_BROWSER_WASM_PATH: "/sql-wasm.wasm"
6129
+ };
6130
+ }
6131
+ });
6132
+
5584
6133
  // src/engines/SqljsEngine.ts
5585
6134
  import initSqlJs from "sql.js";
5586
6135
  import { Mutex } from "async-mutex";
5587
- var SQL_WASM_URL, TransactionalSQLJSOperations, SqljsEngine;
6136
+ async function resolveNodeWasmPath() {
6137
+ if (!isNode()) {
6138
+ return null;
6139
+ }
6140
+ try {
6141
+ const cjsRequire = typeof __require !== "undefined" ? __require : void 0;
6142
+ if (cjsRequire) {
6143
+ return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
6144
+ }
6145
+ } catch {
6146
+ }
6147
+ try {
6148
+ const metaUrl = import.meta.url;
6149
+ if (metaUrl) {
6150
+ const { createRequire } = await import(
6151
+ /* @vite-ignore */
6152
+ "module"
6153
+ );
6154
+ const req = createRequire(metaUrl);
6155
+ return req.resolve("sql.js/dist/sql-wasm.wasm");
6156
+ }
6157
+ } catch {
6158
+ }
6159
+ return null;
6160
+ }
6161
+ async function preflightNodeWasm(wasmPath) {
6162
+ if (!isNode()) {
6163
+ return;
6164
+ }
6165
+ if (/^[a-z]+:\/\//i.test(wasmPath)) {
6166
+ return;
6167
+ }
6168
+ const { existsSync } = await import(
6169
+ /* @vite-ignore */
6170
+ "fs"
6171
+ );
6172
+ if (!existsSync(wasmPath)) {
6173
+ throw new Error(
6174
+ `[SqljsEngine] sql.js wasm not found at "${wasmPath}". Pass \`locateFile\`/\`wasmURL\` pointing at sql-wasm.wasm, or install \`better-sqlite3\` and use the \`node-sqlite\` engine (see getRecommendedNodeEngine()).`
6175
+ );
6176
+ }
6177
+ }
6178
+ async function loadSqlJsModule(locateFile) {
6179
+ let abortReject;
6180
+ const toError = (reason) => reason instanceof Error ? reason : new Error(
6181
+ `[SqljsEngine] sql.js aborted during initialization: ${String(
6182
+ reason
6183
+ )}`
6184
+ );
6185
+ const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
6186
+ const onUnhandled = (reason) => {
6187
+ const err = toError(reason);
6188
+ if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
6189
+ abortReject?.(err);
6190
+ return;
6191
+ }
6192
+ if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
6193
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6194
+ void Promise.reject(reason);
6195
+ }
6196
+ };
6197
+ if (nodeProcess) {
6198
+ nodeProcess.on("unhandledRejection", onUnhandled);
6199
+ }
6200
+ const abortGuard = new Promise((_resolve, reject) => {
6201
+ abortReject = reject;
6202
+ });
6203
+ abortGuard.catch(() => {
6204
+ });
6205
+ try {
6206
+ const initPromise = initSqlJs({
6207
+ locateFile,
6208
+ // Layer 1: emscripten onAbort.
6209
+ onAbort: (reason) => abortReject?.(toError(reason))
6210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6211
+ });
6212
+ return await Promise.race([initPromise, abortGuard]);
6213
+ } finally {
6214
+ if (nodeProcess) {
6215
+ setTimeout(() => {
6216
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6217
+ }, 0);
6218
+ }
6219
+ }
6220
+ }
6221
+ var SQL_WASM_URL, SQL_WASM_FILENAME, TransactionalSQLJSOperations, SqljsEngine;
5588
6222
  var init_SqljsEngine = __esm({
5589
6223
  "src/engines/SqljsEngine.ts"() {
5590
6224
  "use strict";
5591
6225
  init_BaseModel();
5592
6226
  init_sql();
6227
+ init_environment();
5593
6228
  SQL_WASM_URL = "/sql-wasm.wasm";
6229
+ SQL_WASM_FILENAME = "sql-wasm.wasm";
5594
6230
  TransactionalSQLJSOperations = class {
5595
6231
  db;
5596
6232
  engine;
@@ -5656,22 +6292,37 @@ var init_SqljsEngine = __esm({
5656
6292
  }
5657
6293
  async initialize(engineOptions) {
5658
6294
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6295
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5659
6296
  try {
5660
6297
  if (!_SqljsEngine.SQL) {
5661
- const locateFileConfig = engineOptions?.locateFile || ((_file) => engineOptions?.wasmURL || SQL_WASM_URL);
6298
+ let locateFileConfig;
6299
+ if (engineOptions?.locateFile) {
6300
+ locateFileConfig = engineOptions.locateFile;
6301
+ } else if (engineOptions?.wasmURL) {
6302
+ const url = engineOptions.wasmURL;
6303
+ locateFileConfig = (_file) => url;
6304
+ } else {
6305
+ const nodeWasmPath = await resolveNodeWasmPath();
6306
+ const resolved = nodeWasmPath ?? SQL_WASM_URL;
6307
+ locateFileConfig = (_file) => resolved;
6308
+ }
6309
+ const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
5662
6310
  Logger.debug(
5663
6311
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5664
- locateFileConfig("sql-wasm.wasm")
6312
+ wasmPath
5665
6313
  );
5666
- _SqljsEngine.SQL = await initSqlJs({
5667
- locateFile: locateFileConfig
5668
- });
6314
+ await preflightNodeWasm(wasmPath);
6315
+ const SQL = await loadSqlJsModule(locateFileConfig);
6316
+ _SqljsEngine.SQL = SQL;
5669
6317
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5670
6318
  }
5671
6319
  this.db = new _SqljsEngine.SQL.Database();
5672
6320
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5673
6321
  this.updateTableNames();
5674
6322
  } catch (error) {
6323
+ if (loadingModuleThisCall) {
6324
+ _SqljsEngine.SQL = null;
6325
+ }
5675
6326
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5676
6327
  throw error;
5677
6328
  }
@@ -5959,73 +6610,6 @@ var init_SqljsEngine = __esm({
5959
6610
  }
5960
6611
  });
5961
6612
 
5962
- // src/utils/environment.ts
5963
- var environment_exports = {};
5964
- __export(environment_exports, {
5965
- constants: () => constants,
5966
- detectEnvironment: () => detectEnvironment,
5967
- features: () => features,
5968
- isBrowser: () => isBrowser,
5969
- isNode: () => isNode,
5970
- isNodeModuleAvailable: () => isNodeModuleAvailable
5971
- });
5972
- function detectEnvironment() {
5973
- if (typeof process !== "undefined" && process.versions && process.versions.node) {
5974
- return "node";
5975
- }
5976
- if (typeof window !== "undefined" && typeof document !== "undefined") {
5977
- return "browser";
5978
- }
5979
- if (typeof self !== "undefined" && typeof self.importScripts === "function") {
5980
- return "browser";
5981
- }
5982
- return "unknown";
5983
- }
5984
- function isNode() {
5985
- return detectEnvironment() === "node";
5986
- }
5987
- function isBrowser() {
5988
- return detectEnvironment() === "browser";
5989
- }
5990
- async function isNodeModuleAvailable(moduleName) {
5991
- if (!isNode()) {
5992
- return false;
5993
- }
5994
- try {
5995
- await import(
5996
- /* @vite-ignore */
5997
- moduleName
5998
- );
5999
- return true;
6000
- } catch {
6001
- return false;
6002
- }
6003
- }
6004
- var features, constants;
6005
- var init_environment = __esm({
6006
- "src/utils/environment.ts"() {
6007
- "use strict";
6008
- features = {
6009
- get hasWebWorkers() {
6010
- return isBrowser() && typeof Worker !== "undefined";
6011
- },
6012
- get hasFileSystem() {
6013
- return isNode();
6014
- },
6015
- get hasWebAssembly() {
6016
- return typeof WebAssembly !== "undefined";
6017
- },
6018
- async hasBetterSqlite3() {
6019
- return await isNodeModuleAvailable("better-sqlite3");
6020
- }
6021
- };
6022
- constants = {
6023
- DEFAULT_NODE_SQLITE_PATH: ":memory:",
6024
- DEFAULT_BROWSER_WASM_PATH: "/sql-wasm.wasm"
6025
- };
6026
- }
6027
- });
6028
-
6029
6613
  // src/engines/node/NodeSqliteEngine.ts
6030
6614
  var NodeSqliteEngine_exports = {};
6031
6615
  __export(NodeSqliteEngine_exports, {
@@ -6969,6 +7553,7 @@ init_StringSet();
6969
7553
  // src/models/schema.ts
6970
7554
  init_BaseModel();
6971
7555
  init_ModelRegistry();
7556
+ init_uniqueIndex();
6972
7557
  function defineModelSchema(input) {
6973
7558
  const { name, fields } = input;
6974
7559
  const options = {
@@ -7041,7 +7626,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
7041
7626
  for (const [fieldName, options] of fields.entries()) {
7042
7627
  if (options.unique) {
7043
7628
  resolved.push({
7044
- name: `${modelName}_${fieldName}_unique`,
7629
+ name: fieldUniqueConstraintName(modelName, fieldName),
7045
7630
  fields: [fieldName]
7046
7631
  });
7047
7632
  }
@@ -7105,7 +7690,8 @@ function attachAndRegisterModel(modelClass, schema) {
7105
7690
  }
7106
7691
 
7107
7692
  // src/utils/yDocSchema.ts
7108
- import * as Y3 from "yjs";
7693
+ init_yjsValues();
7694
+ import * as Y6 from "yjs";
7109
7695
  function discoverSchema(yDoc) {
7110
7696
  const models = {};
7111
7697
  const metaNames = /* @__PURE__ */ new Set();
@@ -7139,7 +7725,7 @@ function discoverModelNames(yDoc) {
7139
7725
  function materializeMap(yDoc, key) {
7140
7726
  try {
7141
7727
  const map = yDoc.getMap(key);
7142
- return map instanceof Y3.Map ? map : null;
7728
+ return map instanceof Y6.Map ? map : null;
7143
7729
  } catch {
7144
7730
  return null;
7145
7731
  }
@@ -7149,11 +7735,11 @@ function readModelMeta(metaMap) {
7149
7735
  let constraints;
7150
7736
  let relationships;
7151
7737
  for (const [key, value] of metaMap.entries()) {
7152
- if (key === "_constraints" && value instanceof Y3.Map) {
7738
+ if (key === "_constraints" && value instanceof Y6.Map) {
7153
7739
  constraints = readConstraints(value);
7154
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7740
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
7155
7741
  relationships = readRelationships(value);
7156
- } else if (value instanceof Y3.Map) {
7742
+ } else if (value instanceof Y6.Map) {
7157
7743
  fields[key] = readFieldMeta(value);
7158
7744
  }
7159
7745
  }
@@ -7172,6 +7758,10 @@ function readFieldMeta(fieldMap) {
7172
7758
  if (fieldMap.get("unique") === true) field.unique = true;
7173
7759
  if (fieldMap.get("required") === true) field.required = true;
7174
7760
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7761
+ const autoStamp = fieldMap.get("autoStamp");
7762
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7763
+ field.autoStamp = autoStamp;
7764
+ }
7175
7765
  const def = fieldMap.get("default");
7176
7766
  if (def !== void 0) field.default = def;
7177
7767
  const maxLength = fieldMap.get("maxLength");
@@ -7184,7 +7774,7 @@ function inferModelFromData(dataMap) {
7184
7774
  const fields = {};
7185
7775
  let sampled = 0;
7186
7776
  for (const [_recordId, recordValue] of dataMap.entries()) {
7187
- if (!(recordValue instanceof Y3.Map)) continue;
7777
+ if (!(recordValue instanceof Y6.Map)) continue;
7188
7778
  if (++sampled > 5) break;
7189
7779
  for (const [fieldName, value] of recordValue.entries()) {
7190
7780
  if (fieldName.startsWith("_")) continue;
@@ -7197,7 +7787,7 @@ function inferModelFromData(dataMap) {
7197
7787
  return { fields };
7198
7788
  }
7199
7789
  function inferTypeFromValue(value) {
7200
- if (value instanceof Y3.Map) return "stringset";
7790
+ if (isStringSetYMap(value)) return "stringset";
7201
7791
  switch (typeof value) {
7202
7792
  case "string":
7203
7793
  return "string";
@@ -7212,7 +7802,7 @@ function inferTypeFromValue(value) {
7212
7802
  function readConstraints(constraintsMap) {
7213
7803
  const out = {};
7214
7804
  for (const [name, value] of constraintsMap.entries()) {
7215
- if (!(value instanceof Y3.Map)) continue;
7805
+ if (!(value instanceof Y6.Map)) continue;
7216
7806
  let fields = [];
7217
7807
  const rawFields = value.get("fields");
7218
7808
  if (typeof rawFields === "string") {
@@ -7231,7 +7821,7 @@ function readConstraints(constraintsMap) {
7231
7821
  function readRelationships(relsMap) {
7232
7822
  const out = {};
7233
7823
  for (const [name, value] of relsMap.entries()) {
7234
- if (!(value instanceof Y3.Map)) continue;
7824
+ if (!(value instanceof Y6.Map)) continue;
7235
7825
  const rel = {};
7236
7826
  for (const [k, v] of value.entries()) {
7237
7827
  rel[k] = v;
@@ -7242,6 +7832,7 @@ function readRelationships(relsMap) {
7242
7832
  }
7243
7833
  var CAMEL_TO_SNAKE = {
7244
7834
  autoAssign: "auto_assign",
7835
+ autoStamp: "auto_stamp",
7245
7836
  maxLength: "max_length",
7246
7837
  maxCount: "max_count",
7247
7838
  relatedIdField: "related_id_field",
@@ -7272,6 +7863,7 @@ function schemaToToml(schema) {
7272
7863
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
7273
7864
  lines.push(`type = ${tomlValue(field.type)}`);
7274
7865
  if (field.autoAssign) lines.push("auto_assign = true");
7866
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
7275
7867
  if (field.indexed) lines.push("indexed = true");
7276
7868
  if (field.unique) lines.push("unique = true");
7277
7869
  if (field.required) lines.push("required = true");
@@ -7318,10 +7910,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7318
7910
  "unique",
7319
7911
  "required",
7320
7912
  "auto_assign",
7913
+ "auto_stamp",
7321
7914
  "max_length",
7322
7915
  "max_count",
7323
- "default"
7916
+ "default",
7917
+ "enum"
7324
7918
  ]);
7919
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
7325
7920
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7326
7921
  "fields",
7327
7922
  "relationships",
@@ -7363,9 +7958,37 @@ function parseFieldOptions(raw, context, strict) {
7363
7958
  if (raw.unique === true) opts.unique = true;
7364
7959
  if (raw.required === true) opts.required = true;
7365
7960
  if (raw.auto_assign === true) opts.autoAssign = true;
7961
+ if (raw.auto_stamp !== void 0) {
7962
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7963
+ throw new Error(
7964
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7965
+ ...VALID_AUTO_STAMP_VALUES
7966
+ ].join(", ")}`
7967
+ );
7968
+ }
7969
+ opts.autoStamp = raw.auto_stamp;
7970
+ }
7366
7971
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7367
7972
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7368
7973
  if (raw.default !== void 0) opts.default = raw.default;
7974
+ if (raw.enum !== void 0) {
7975
+ if (raw.type !== "string") {
7976
+ throw new Error(
7977
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7978
+ );
7979
+ }
7980
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7981
+ throw new Error(
7982
+ `${context}: \`enum\` must be a non-empty array of strings.`
7983
+ );
7984
+ }
7985
+ if (!raw.enum.every((v) => typeof v === "string")) {
7986
+ throw new Error(
7987
+ `${context}: \`enum\` values must all be strings.`
7988
+ );
7989
+ }
7990
+ opts.enum = [...raw.enum];
7991
+ }
7369
7992
  return opts;
7370
7993
  }
7371
7994
  function requireField(raw, field, context) {
@@ -7494,18 +8117,19 @@ async function loadSchemaFromToml(filePath, options = {}) {
7494
8117
 
7495
8118
  // src/node.ts
7496
8119
  init_metaSync();
8120
+ init_writeLifecycle();
7497
8121
 
7498
8122
  // src/utils/yDocDump.ts
7499
- import * as Y4 from "yjs";
8123
+ import * as Y7 from "yjs";
7500
8124
  function toPlain(value) {
7501
- if (value instanceof Y4.Map) {
8125
+ if (value instanceof Y7.Map) {
7502
8126
  const obj = {};
7503
8127
  value.forEach((v, k) => {
7504
8128
  obj[k] = toPlain(v);
7505
8129
  });
7506
8130
  return obj;
7507
8131
  }
7508
- if (value instanceof Y4.Array) {
8132
+ if (value instanceof Y7.Array) {
7509
8133
  return value.toArray().map((item) => toPlain(item));
7510
8134
  }
7511
8135
  return value;
@@ -7514,7 +8138,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7514
8138
  const result = {};
7515
8139
  const includeIndexes = !!options.includeIndexes;
7516
8140
  yDoc.share.forEach((type, name) => {
7517
- if (!(type instanceof Y4.Map)) return;
8141
+ if (!(type instanceof Y7.Map)) return;
7518
8142
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7519
8143
  const records = {};
7520
8144
  type.forEach((recordValue, recordId) => {
@@ -7574,31 +8198,45 @@ export {
7574
8198
  SqljsEngine,
7575
8199
  StringSet,
7576
8200
  UniqueConstraintViolationError,
8201
+ applyAutoStampsFromSchema,
8202
+ applyDefaultsFromSchema,
8203
+ applyStringSetChange,
8204
+ applyStringSetDelta,
8205
+ assertNever,
7577
8206
  attachAndRegisterModel,
7578
8207
  attachSchemaToClass,
7579
8208
  autoRegisterModel,
7580
8209
  clearMetaSyncCache,
7581
8210
  constants,
7582
8211
  createModelClass,
8212
+ decodeFunctionDefault,
7583
8213
  defineModelSchema,
7584
8214
  detectEnvironment,
7585
8215
  discoverModelNames,
7586
8216
  discoverSchema,
7587
8217
  dumpYDocToPlain,
8218
+ ensureStringSetYMap,
7588
8219
  features,
8220
+ fieldWriteKind,
7589
8221
  generateULID,
7590
8222
  getRecommendedNodeEngine,
7591
8223
  inferFieldType,
7592
8224
  initJsBao,
7593
8225
  isBrowser,
8226
+ isKnownFieldType,
7594
8227
  isNode,
7595
8228
  isNodeModuleAvailable,
7596
8229
  loadSchemaFromToml,
7597
8230
  loadSchemaFromTomlString,
8231
+ parseStringSetOp,
7598
8232
  registerFunctionDefault,
8233
+ registerFunctionDefaultGenerator,
8234
+ replaceStringSet,
7599
8235
  resetJsBao,
8236
+ resolveDefaultValue,
7600
8237
  schemaToToml,
7601
8238
  summarizePlainYDoc,
7602
8239
  syncInferredMeta,
7603
- syncModelMeta
8240
+ syncModelMeta,
8241
+ writeField
7604
8242
  };