@xnetjs/data 0.1.1 → 0.1.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.
@@ -3413,6 +3413,7 @@ var DEFAULT_QUERY_VERIFICATION = {
3413
3413
  logFailures: true
3414
3414
  };
3415
3415
  var QUERY_TELEMETRY_FLUSH_THRESHOLD = 50;
3416
+ var CHANGE_ENVELOPE_KEY = "__xnetChangeEnvelopeV1";
3416
3417
  var COMPILED_QUERY_DIAGNOSTICS_MEMO_LIMIT = 512;
3417
3418
  var SQLITE_BIND_PARAMETER_BATCH_SIZE = 900;
3418
3419
  var SQLITE_HYDRATE_NODE_BATCH_SIZE = Math.floor(SQLITE_BIND_PARAMETER_BATCH_SIZE / 2);
@@ -3581,7 +3582,7 @@ var SQLiteNodeStorageAdapter = class {
3581
3582
  await this.enqueueWrite(() => this.appendChangeInternal(change));
3582
3583
  }
3583
3584
  async appendChangeInternal(change) {
3584
- const payload = this.serializePayload(change.payload);
3585
+ const payload = this.serializeChangeRecord(change);
3585
3586
  await this.db.run(
3586
3587
  `INSERT OR IGNORE INTO changes
3587
3588
  (hash, node_id, payload, lamport_time, lamport_peer, wall_time, author, parent_hash, batch_id, signature)
@@ -3842,7 +3843,11 @@ var SQLiteNodeStorageAdapter = class {
3842
3843
  lamport_time = excluded.lamport_time,
3843
3844
  updated_by = excluded.updated_by,
3844
3845
  updated_at = excluded.updated_at
3845
- WHERE excluded.lamport_time > node_properties.lamport_time`,
3846
+ WHERE excluded.lamport_time > node_properties.lamport_time
3847
+ OR (excluded.lamport_time = node_properties.lamport_time
3848
+ AND (excluded.updated_at > node_properties.updated_at
3849
+ OR (excluded.updated_at = node_properties.updated_at
3850
+ AND excluded.updated_by > node_properties.updated_by)))`,
3846
3851
  [
3847
3852
  node.id,
3848
3853
  key,
@@ -4335,7 +4340,7 @@ var SQLiteNodeStorageAdapter = class {
4335
4340
  changes: input.changes.map((change) => ({
4336
4341
  hash: change.hash,
4337
4342
  nodeId: change.payload.nodeId,
4338
- payload: this.serializePayload(change.payload),
4343
+ payload: this.serializeChangeRecord(change),
4339
4344
  lamportTime: change.lamport,
4340
4345
  lamportPeer: change.authorDID,
4341
4346
  wallTime: change.wallTime,
@@ -4488,6 +4493,8 @@ var SQLiteNodeStorageAdapter = class {
4488
4493
  const timestamp = node.timestamps[key];
4489
4494
  if (!timestamp) continue;
4490
4495
  operations.push({
4496
+ // Full LWW ordering triple — keep in lockstep with the setNode
4497
+ // upsert above and `shouldReplace` in ./store.ts (0272).
4491
4498
  sql: `INSERT INTO node_properties
4492
4499
  (node_id, property_key, value, lamport_time, updated_by, updated_at)
4493
4500
  VALUES (?, ?, ?, ?, ?, ?)
@@ -4496,7 +4503,11 @@ var SQLiteNodeStorageAdapter = class {
4496
4503
  lamport_time = excluded.lamport_time,
4497
4504
  updated_by = excluded.updated_by,
4498
4505
  updated_at = excluded.updated_at
4499
- WHERE excluded.lamport_time > node_properties.lamport_time`,
4506
+ WHERE excluded.lamport_time > node_properties.lamport_time
4507
+ OR (excluded.lamport_time = node_properties.lamport_time
4508
+ AND (excluded.updated_at > node_properties.updated_at
4509
+ OR (excluded.updated_at = node_properties.updated_at
4510
+ AND excluded.updated_by > node_properties.updated_by)))`,
4500
4511
  params: [
4501
4512
  node.id,
4502
4513
  key,
@@ -4529,7 +4540,7 @@ var SQLiteNodeStorageAdapter = class {
4529
4540
  params: [
4530
4541
  change.hash,
4531
4542
  change.payload.nodeId,
4532
- this.serializePayload(change.payload),
4543
+ this.serializeChangeRecord(change),
4533
4544
  change.lamport,
4534
4545
  change.authorDID,
4535
4546
  change.wallTime,
@@ -6470,8 +6481,28 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
6470
6481
  serializePayload(payload) {
6471
6482
  return new TextEncoder().encode(JSON.stringify(payload));
6472
6483
  }
6473
- deserializePayload(data) {
6474
- return JSON.parse(new TextDecoder().decode(data));
6484
+ /**
6485
+ * Serialize a change for the `changes` table (exploration 0272).
6486
+ *
6487
+ * The table has no columns for `id`, `type`, `protocolVersion`,
6488
+ * `batchIndex`, or `batchSize`, yet all of them are part of the signed
6489
+ * content hash — a change re-read without them can never pass
6490
+ * `verifyChangeHash`, so the reload-resync push (getChangesSince → hub)
6491
+ * was structurally rejected as INVALID_HASH and tripped the 0224 breaker,
6492
+ * stranding offline edits. The hub's own `node_changes` table persists
6493
+ * every one of these fields; the client log now does too, by wrapping the
6494
+ * payload BLOB in an envelope instead of a schema migration (applySchema
6495
+ * re-runs idempotent DDL and never executes ALTERs, so new columns would
6496
+ * not reach existing databases).
6497
+ */
6498
+ serializeChangeRecord(change) {
6499
+ const meta = { id: change.id, type: change.type };
6500
+ if (change.protocolVersion !== void 0) meta.protocolVersion = change.protocolVersion;
6501
+ if (change.batchIndex !== void 0) meta.batchIndex = change.batchIndex;
6502
+ if (change.batchSize !== void 0) meta.batchSize = change.batchSize;
6503
+ return new TextEncoder().encode(
6504
+ JSON.stringify({ [CHANGE_ENVELOPE_KEY]: meta, payload: change.payload })
6505
+ );
6475
6506
  }
6476
6507
  serializeValue(value) {
6477
6508
  return new TextEncoder().encode(JSON.stringify(value));
@@ -6481,13 +6512,16 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
6481
6512
  return JSON.parse(new TextDecoder().decode(data));
6482
6513
  }
6483
6514
  deserializeChange(row) {
6484
- const payload = this.deserializePayload(row.payload);
6485
- return {
6486
- // Required Change<T> fields
6487
- id: row.hash,
6488
- // Use hash as ID since we don't store separate id
6489
- type: "node",
6490
- // All node changes have type 'node'
6515
+ const parsed = JSON.parse(new TextDecoder().decode(row.payload));
6516
+ const envelope = parsed !== null && typeof parsed === "object" && CHANGE_ENVELOPE_KEY in parsed ? parsed[CHANGE_ENVELOPE_KEY] : null;
6517
+ const payload = envelope ? parsed.payload : parsed;
6518
+ const change = {
6519
+ // Envelope rows (exploration 0272) round-trip the hashed identity
6520
+ // fields exactly, so verifyChangeHash holds after a re-read. Legacy
6521
+ // rows predate the envelope: their id/type/protocolVersion are gone
6522
+ // for good, so we keep the historical fabricated values.
6523
+ id: envelope?.id ?? row.hash,
6524
+ type: envelope?.type ?? "node",
6491
6525
  hash: row.hash,
6492
6526
  payload,
6493
6527
  lamport: row.lamport_time,
@@ -6497,6 +6531,10 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
6497
6531
  batchId: row.batch_id ?? void 0,
6498
6532
  signature: row.signature
6499
6533
  };
6534
+ if (envelope?.protocolVersion !== void 0) change.protocolVersion = envelope.protocolVersion;
6535
+ if (envelope?.batchIndex !== void 0) change.batchIndex = envelope.batchIndex;
6536
+ if (envelope?.batchSize !== void 0) change.batchSize = envelope.batchSize;
6537
+ return change;
6500
6538
  }
6501
6539
  };
6502
6540
  function chunkItems(items, size) {
package/dist/index.js CHANGED
@@ -605,7 +605,7 @@ import {
605
605
  sum,
606
606
  validateQueryAST,
607
607
  validateSavedViewDescriptor
608
- } from "./chunk-EI5CY2FZ.js";
608
+ } from "./chunk-2UPMDBWD.js";
609
609
  import {
610
610
  applyNodeQueryDescriptor,
611
611
  createNodeQueryDescriptor,
@@ -514,7 +514,21 @@ declare class SQLiteNodeStorageAdapter implements NodeStorageAdapter {
514
514
  private buildSqlOrderBy;
515
515
  private hasOnlySystemOrdering;
516
516
  private serializePayload;
517
- private deserializePayload;
517
+ /**
518
+ * Serialize a change for the `changes` table (exploration 0272).
519
+ *
520
+ * The table has no columns for `id`, `type`, `protocolVersion`,
521
+ * `batchIndex`, or `batchSize`, yet all of them are part of the signed
522
+ * content hash — a change re-read without them can never pass
523
+ * `verifyChangeHash`, so the reload-resync push (getChangesSince → hub)
524
+ * was structurally rejected as INVALID_HASH and tripped the 0224 breaker,
525
+ * stranding offline edits. The hub's own `node_changes` table persists
526
+ * every one of these fields; the client log now does too, by wrapping the
527
+ * payload BLOB in an envelope instead of a schema migration (applySchema
528
+ * re-runs idempotent DDL and never executes ALTERs, so new columns would
529
+ * not reach existing databases).
530
+ */
531
+ private serializeChangeRecord;
518
532
  private serializeValue;
519
533
  private deserializeValue;
520
534
  private deserializeChange;
@@ -49,7 +49,7 @@ import {
49
49
  sum,
50
50
  validateQueryAST,
51
51
  validateSavedViewDescriptor
52
- } from "../chunk-EI5CY2FZ.js";
52
+ } from "../chunk-2UPMDBWD.js";
53
53
  import {
54
54
  applyNodeQueryDescriptor,
55
55
  createNodeQueryDescriptor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xnetjs/data",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -57,12 +57,12 @@
57
57
  "nanoid": "^5.1.6",
58
58
  "y-protocols": "^1.0.6",
59
59
  "yjs": "^13.6.24",
60
- "@xnetjs/core": "0.1.1",
61
- "@xnetjs/crypto": "0.1.1",
62
- "@xnetjs/identity": "0.1.1",
63
- "@xnetjs/sqlite": "0.1.1",
64
- "@xnetjs/storage": "0.1.1",
65
- "@xnetjs/sync": "0.1.1"
60
+ "@xnetjs/core": "0.1.2",
61
+ "@xnetjs/crypto": "0.1.2",
62
+ "@xnetjs/identity": "0.1.2",
63
+ "@xnetjs/sqlite": "0.1.2",
64
+ "@xnetjs/storage": "0.1.2",
65
+ "@xnetjs/sync": "0.1.2"
66
66
  },
67
67
  "devDependencies": {
68
68
  "tsup": "^8.0.0",