@xnetjs/data 0.1.0 → 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,8 @@ var DEFAULT_QUERY_VERIFICATION = {
|
|
|
3413
3413
|
logFailures: true
|
|
3414
3414
|
};
|
|
3415
3415
|
var QUERY_TELEMETRY_FLUSH_THRESHOLD = 50;
|
|
3416
|
+
var CHANGE_ENVELOPE_KEY = "__xnetChangeEnvelopeV1";
|
|
3417
|
+
var COMPILED_QUERY_DIAGNOSTICS_MEMO_LIMIT = 512;
|
|
3416
3418
|
var SQLITE_BIND_PARAMETER_BATCH_SIZE = 900;
|
|
3417
3419
|
var SQLITE_HYDRATE_NODE_BATCH_SIZE = Math.floor(SQLITE_BIND_PARAMETER_BATCH_SIZE / 2);
|
|
3418
3420
|
var SQL_HYDRATE_ARITY_BUCKETS = [1, 10, 50, 150, SQLITE_HYDRATE_NODE_BATCH_SIZE];
|
|
@@ -3474,6 +3476,14 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3474
3476
|
pendingQueryTelemetryHits = 0;
|
|
3475
3477
|
adaptiveIndexBudgetColumnsReady = false;
|
|
3476
3478
|
storageCapabilitiesPromise;
|
|
3479
|
+
/**
|
|
3480
|
+
* Plan diagnostics memoized per compiled SQL shape (the string EXPLAINed),
|
|
3481
|
+
* shared across executions AND concurrent callers. Cleared when this adapter
|
|
3482
|
+
* creates/drops an adaptive index, since that changes plans. Debug mode must
|
|
3483
|
+
* not distort what it measures: per-execution EXPLAIN + index-inventory
|
|
3484
|
+
* round-trips convoyed the single serial worker at boot.
|
|
3485
|
+
*/
|
|
3486
|
+
compiledQueryDiagnosticsMemo = /* @__PURE__ */ new Map();
|
|
3477
3487
|
spatialTablesState = "unknown";
|
|
3478
3488
|
fullTextSearchTablesState = "unknown";
|
|
3479
3489
|
writeQueue = Promise.resolve();
|
|
@@ -3572,7 +3582,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3572
3582
|
await this.enqueueWrite(() => this.appendChangeInternal(change));
|
|
3573
3583
|
}
|
|
3574
3584
|
async appendChangeInternal(change) {
|
|
3575
|
-
const payload = this.
|
|
3585
|
+
const payload = this.serializeChangeRecord(change);
|
|
3576
3586
|
await this.db.run(
|
|
3577
3587
|
`INSERT OR IGNORE INTO changes
|
|
3578
3588
|
(hash, node_id, payload, lamport_time, lamport_peer, wall_time, author, parent_hash, batch_id, signature)
|
|
@@ -3833,7 +3843,11 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3833
3843
|
lamport_time = excluded.lamport_time,
|
|
3834
3844
|
updated_by = excluded.updated_by,
|
|
3835
3845
|
updated_at = excluded.updated_at
|
|
3836
|
-
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)))`,
|
|
3837
3851
|
[
|
|
3838
3852
|
node.id,
|
|
3839
3853
|
key,
|
|
@@ -4326,7 +4340,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4326
4340
|
changes: input.changes.map((change) => ({
|
|
4327
4341
|
hash: change.hash,
|
|
4328
4342
|
nodeId: change.payload.nodeId,
|
|
4329
|
-
payload: this.
|
|
4343
|
+
payload: this.serializeChangeRecord(change),
|
|
4330
4344
|
lamportTime: change.lamport,
|
|
4331
4345
|
lamportPeer: change.authorDID,
|
|
4332
4346
|
wallTime: change.wallTime,
|
|
@@ -4479,6 +4493,8 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4479
4493
|
const timestamp = node.timestamps[key];
|
|
4480
4494
|
if (!timestamp) continue;
|
|
4481
4495
|
operations.push({
|
|
4496
|
+
// Full LWW ordering triple — keep in lockstep with the setNode
|
|
4497
|
+
// upsert above and `shouldReplace` in ./store.ts (0272).
|
|
4482
4498
|
sql: `INSERT INTO node_properties
|
|
4483
4499
|
(node_id, property_key, value, lamport_time, updated_by, updated_at)
|
|
4484
4500
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
@@ -4487,7 +4503,11 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4487
4503
|
lamport_time = excluded.lamport_time,
|
|
4488
4504
|
updated_by = excluded.updated_by,
|
|
4489
4505
|
updated_at = excluded.updated_at
|
|
4490
|
-
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)))`,
|
|
4491
4511
|
params: [
|
|
4492
4512
|
node.id,
|
|
4493
4513
|
key,
|
|
@@ -4520,7 +4540,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4520
4540
|
params: [
|
|
4521
4541
|
change.hash,
|
|
4522
4542
|
change.payload.nodeId,
|
|
4523
|
-
this.
|
|
4543
|
+
this.serializeChangeRecord(change),
|
|
4524
4544
|
change.lamport,
|
|
4525
4545
|
change.authorDID,
|
|
4526
4546
|
change.wallTime,
|
|
@@ -5741,7 +5761,34 @@ CREATE INDEX IF NOT EXISTS idx_node_spatial_ids_schema
|
|
|
5741
5761
|
isQueryDiagnosticsEnabled() {
|
|
5742
5762
|
return this.queryDiagnostics || this.isQueryDebugEnabled();
|
|
5743
5763
|
}
|
|
5744
|
-
|
|
5764
|
+
/**
|
|
5765
|
+
* Throttled: one collection per unique compiled SQL shape per session.
|
|
5766
|
+
* EXPLAIN plans depend on the statement, not the bound values, so keyset
|
|
5767
|
+
* pages of the same query share one entry. Concurrent callers share the
|
|
5768
|
+
* in-flight promise; failed collections are not memoized.
|
|
5769
|
+
*/
|
|
5770
|
+
collectCompiledQueryDiagnostics(compiled) {
|
|
5771
|
+
const memoKey = compiled.sql;
|
|
5772
|
+
const memoized = this.compiledQueryDiagnosticsMemo.get(memoKey);
|
|
5773
|
+
if (memoized) {
|
|
5774
|
+
return memoized;
|
|
5775
|
+
}
|
|
5776
|
+
const pending = this.computeCompiledQueryDiagnostics(compiled).then((diagnostics) => {
|
|
5777
|
+
if (diagnostics.diagnosticsError) {
|
|
5778
|
+
this.compiledQueryDiagnosticsMemo.delete(memoKey);
|
|
5779
|
+
}
|
|
5780
|
+
return diagnostics;
|
|
5781
|
+
});
|
|
5782
|
+
if (this.compiledQueryDiagnosticsMemo.size >= COMPILED_QUERY_DIAGNOSTICS_MEMO_LIMIT) {
|
|
5783
|
+
const oldest = this.compiledQueryDiagnosticsMemo.keys().next().value;
|
|
5784
|
+
if (oldest !== void 0) {
|
|
5785
|
+
this.compiledQueryDiagnosticsMemo.delete(oldest);
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
this.compiledQueryDiagnosticsMemo.set(memoKey, pending);
|
|
5789
|
+
return pending;
|
|
5790
|
+
}
|
|
5791
|
+
async computeCompiledQueryDiagnostics(compiled) {
|
|
5745
5792
|
try {
|
|
5746
5793
|
const [analysis, indexes, storageCapabilities] = await Promise.all([
|
|
5747
5794
|
analyzeQuery(this.db, compiled.sql, compiled.params),
|
|
@@ -5965,6 +6012,7 @@ CREATE INDEX IF NOT EXISTS idx_node_spatial_ids_schema
|
|
|
5965
6012
|
if (createdIndex) {
|
|
5966
6013
|
await runAnalyze(this.db, "node_property_scalars");
|
|
5967
6014
|
await this.db.exec("PRAGMA optimize");
|
|
6015
|
+
this.compiledQueryDiagnosticsMemo.clear();
|
|
5968
6016
|
}
|
|
5969
6017
|
return touchedIndexNames;
|
|
5970
6018
|
}
|
|
@@ -6113,6 +6161,7 @@ CREATE INDEX IF NOT EXISTS idx_node_spatial_ids_schema
|
|
|
6113
6161
|
async dropAdaptiveIndex(indexName, reason) {
|
|
6114
6162
|
await this.db.exec(`DROP INDEX IF EXISTS ${this.quoteSqlIdentifier(indexName)}`);
|
|
6115
6163
|
await this.db.run("DELETE FROM query_index_candidates WHERE index_name = ?", [indexName]);
|
|
6164
|
+
this.compiledQueryDiagnosticsMemo.clear();
|
|
6116
6165
|
this.debugAdaptiveIndex("drop", { indexName, reason });
|
|
6117
6166
|
}
|
|
6118
6167
|
debugAdaptiveIndex(action, details) {
|
|
@@ -6432,8 +6481,28 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
|
|
|
6432
6481
|
serializePayload(payload) {
|
|
6433
6482
|
return new TextEncoder().encode(JSON.stringify(payload));
|
|
6434
6483
|
}
|
|
6435
|
-
|
|
6436
|
-
|
|
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
|
+
);
|
|
6437
6506
|
}
|
|
6438
6507
|
serializeValue(value) {
|
|
6439
6508
|
return new TextEncoder().encode(JSON.stringify(value));
|
|
@@ -6443,13 +6512,16 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
|
|
|
6443
6512
|
return JSON.parse(new TextDecoder().decode(data));
|
|
6444
6513
|
}
|
|
6445
6514
|
deserializeChange(row) {
|
|
6446
|
-
const
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
//
|
|
6451
|
-
|
|
6452
|
-
//
|
|
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",
|
|
6453
6525
|
hash: row.hash,
|
|
6454
6526
|
payload,
|
|
6455
6527
|
lamport: row.lamport_time,
|
|
@@ -6459,6 +6531,10 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
|
|
|
6459
6531
|
batchId: row.batch_id ?? void 0,
|
|
6460
6532
|
signature: row.signature
|
|
6461
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;
|
|
6462
6538
|
}
|
|
6463
6539
|
};
|
|
6464
6540
|
function chunkItems(items, size) {
|
package/dist/index.js
CHANGED
package/dist/store/index.d.ts
CHANGED
|
@@ -120,9 +120,13 @@ interface SQLiteNodeStorageAdapterOptions {
|
|
|
120
120
|
adaptiveIndexing?: SQLiteAdaptiveIndexingOptions;
|
|
121
121
|
queryVerification?: SQLiteQueryVerificationOptions;
|
|
122
122
|
/**
|
|
123
|
-
* Collect EXPLAIN QUERY PLAN + index inventory
|
|
124
|
-
* round trips
|
|
125
|
-
* `xnet:query:debug` localStorage flag is set.
|
|
123
|
+
* Collect EXPLAIN QUERY PLAN + index inventory for queries. Costs extra
|
|
124
|
+
* round trips, so it is off unless explicitly enabled or the
|
|
125
|
+
* `xnet:query:debug` localStorage flag is set. Diagnostics are collected
|
|
126
|
+
* once per unique compiled SQL shape per session (invalidated when the
|
|
127
|
+
* adapter itself runs DDL) — per-execution collection convoyed the serial
|
|
128
|
+
* worker and delayed the very queries being measured by 18-20s at boot
|
|
129
|
+
* (2026-07-05 capture).
|
|
126
130
|
*/
|
|
127
131
|
queryDiagnostics?: boolean;
|
|
128
132
|
/**
|
|
@@ -175,6 +179,14 @@ declare class SQLiteNodeStorageAdapter implements NodeStorageAdapter {
|
|
|
175
179
|
private pendingQueryTelemetryHits;
|
|
176
180
|
private adaptiveIndexBudgetColumnsReady;
|
|
177
181
|
private storageCapabilitiesPromise?;
|
|
182
|
+
/**
|
|
183
|
+
* Plan diagnostics memoized per compiled SQL shape (the string EXPLAINed),
|
|
184
|
+
* shared across executions AND concurrent callers. Cleared when this adapter
|
|
185
|
+
* creates/drops an adaptive index, since that changes plans. Debug mode must
|
|
186
|
+
* not distort what it measures: per-execution EXPLAIN + index-inventory
|
|
187
|
+
* round-trips convoyed the single serial worker at boot.
|
|
188
|
+
*/
|
|
189
|
+
private compiledQueryDiagnosticsMemo;
|
|
178
190
|
private spatialTablesState;
|
|
179
191
|
private fullTextSearchTablesState;
|
|
180
192
|
private writeQueue;
|
|
@@ -449,7 +461,14 @@ declare class SQLiteNodeStorageAdapter implements NodeStorageAdapter {
|
|
|
449
461
|
flushQueryTelemetry(): Promise<void>;
|
|
450
462
|
private writeQueryTelemetryRow;
|
|
451
463
|
private isQueryDiagnosticsEnabled;
|
|
464
|
+
/**
|
|
465
|
+
* Throttled: one collection per unique compiled SQL shape per session.
|
|
466
|
+
* EXPLAIN plans depend on the statement, not the bound values, so keyset
|
|
467
|
+
* pages of the same query share one entry. Concurrent callers share the
|
|
468
|
+
* in-flight promise; failed collections are not memoized.
|
|
469
|
+
*/
|
|
452
470
|
private collectCompiledQueryDiagnostics;
|
|
471
|
+
private computeCompiledQueryDiagnostics;
|
|
453
472
|
private getStorageCapabilities;
|
|
454
473
|
private debugQueryPlan;
|
|
455
474
|
private isQueryDebugEnabled;
|
|
@@ -495,7 +514,21 @@ declare class SQLiteNodeStorageAdapter implements NodeStorageAdapter {
|
|
|
495
514
|
private buildSqlOrderBy;
|
|
496
515
|
private hasOnlySystemOrdering;
|
|
497
516
|
private serializePayload;
|
|
498
|
-
|
|
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;
|
|
499
532
|
private serializeValue;
|
|
500
533
|
private deserializeValue;
|
|
501
534
|
private deserializeChange;
|
package/dist/store/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xnetjs/data",
|
|
3
|
-
"version": "0.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.
|
|
61
|
-
"@xnetjs/crypto": "0.1.
|
|
62
|
-
"@xnetjs/identity": "0.1.
|
|
63
|
-
"@xnetjs/sqlite": "0.1.
|
|
64
|
-
"@xnetjs/storage": "0.1.
|
|
65
|
-
"@xnetjs/sync": "0.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",
|