@breeztech/breez-sdk-spark 0.22.3 → 0.23.0
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/breez-sdk-spark.tgz +0 -0
- package/bundler/breez_sdk_spark_wasm.d.ts +65 -5
- package/bundler/breez_sdk_spark_wasm_bg.js +66 -27
- package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +9 -6
- package/bundler/index.js +15 -2
- package/bundler/package.json +5 -0
- package/bundler/storage/index.js +6 -0
- package/bundler/tree-store/index.js +1518 -0
- package/bundler/tree-store/package.json +12 -0
- package/deno/breez_sdk_spark_wasm.d.ts +65 -5
- package/deno/breez_sdk_spark_wasm.js +66 -27
- package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +9 -6
- package/nodejs/breez_sdk_spark_wasm.d.ts +65 -5
- package/nodejs/breez_sdk_spark_wasm.js +66 -27
- package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +9 -6
- package/nodejs/index.js +11 -0
- package/nodejs/mysql-storage/index.cjs +9 -1
- package/nodejs/mysql-storage/migrations.cjs +6 -0
- package/nodejs/mysql-tree-store/index.cjs +296 -29
- package/nodejs/mysql-tree-store/migrations.cjs +198 -34
- package/nodejs/package.json +1 -0
- package/nodejs/postgres-storage/index.cjs +9 -1
- package/nodejs/postgres-storage/migrations.cjs +6 -0
- package/nodejs/postgres-tree-store/index.cjs +301 -40
- package/nodejs/postgres-tree-store/migrations.cjs +42 -0
- package/nodejs/storage/index.cjs +14 -3
- package/nodejs/storage/migrations.cjs +6 -0
- package/nodejs/tree-store/errors.cjs +13 -0
- package/nodejs/tree-store/index.cjs +1185 -0
- package/nodejs/tree-store/migrations.cjs +185 -0
- package/nodejs/tree-store/package.json +9 -0
- package/package.json +1 -1
- package/web/breez_sdk_spark_wasm.d.ts +74 -11
- package/web/breez_sdk_spark_wasm.js +66 -27
- package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +9 -6
- package/web/index.js +15 -2
- package/web/package.json +5 -0
- package/web/storage/index.js +6 -0
- package/web/tree-store/index.js +1518 -0
- package/web/tree-store/package.json +12 -0
|
@@ -54,6 +54,17 @@ const SPENT_MARKER_CLEANUP_THRESHOLD_MS = 5 * 60 * 1000;
|
|
|
54
54
|
*/
|
|
55
55
|
const LEAF_UPSERT_CHUNK_SIZE = 1000;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Ancestor rows per INSERT when storing exit chains.
|
|
59
|
+
*
|
|
60
|
+
* A wallet-wide chain backfill carries a row per leaf per ancestor, each a JSON
|
|
61
|
+
* blob of up to five transactions. The insert goes through conn.query(), which
|
|
62
|
+
* interpolates its placeholders client-side, so the whole set would otherwise be
|
|
63
|
+
* built in memory as one statement and sent as one packet, against
|
|
64
|
+
* max_allowed_packet.
|
|
65
|
+
*/
|
|
66
|
+
const ANCESTOR_INSERT_CHUNK_SIZE = 4096;
|
|
67
|
+
|
|
57
68
|
/**
|
|
58
69
|
* Slim projection: only (id, value) for leaves the selection might use.
|
|
59
70
|
* Includes all leaves with value <= the max target (covers exact-match + the
|
|
@@ -114,6 +125,30 @@ function buildPlaceholders(n) {
|
|
|
114
125
|
return new Array(n).fill("?").join(", ");
|
|
115
126
|
}
|
|
116
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Pair a leaf with its ancestors (nearest first) by walking `parent_node_id`
|
|
130
|
+
* through `nodes`. Returns null if the leaf itself is absent; stops at a gap or
|
|
131
|
+
* cycle, returning a partial chain.
|
|
132
|
+
* @param {Map<string, object>} nodes
|
|
133
|
+
* @param {string} leafId
|
|
134
|
+
* @returns {{leaf: object, ancestors: Array<object>}|null}
|
|
135
|
+
*/
|
|
136
|
+
function assembleExitChain(nodes, leafId) {
|
|
137
|
+
const leaf = nodes.get(leafId);
|
|
138
|
+
if (!leaf) return null;
|
|
139
|
+
const ancestors = [];
|
|
140
|
+
const visited = new Set([leafId]);
|
|
141
|
+
let current = leaf.parent_node_id;
|
|
142
|
+
while (current != null && !visited.has(current)) {
|
|
143
|
+
visited.add(current);
|
|
144
|
+
const node = nodes.get(current);
|
|
145
|
+
if (!node) break;
|
|
146
|
+
ancestors.push(node);
|
|
147
|
+
current = node.parent_node_id;
|
|
148
|
+
}
|
|
149
|
+
return { leaf, ancestors };
|
|
150
|
+
}
|
|
151
|
+
|
|
117
152
|
class MysqlTreeStore {
|
|
118
153
|
/**
|
|
119
154
|
* @param {import('mysql2/promise').Pool} pool
|
|
@@ -172,8 +207,8 @@ class MysqlTreeStore {
|
|
|
172
207
|
|
|
173
208
|
/**
|
|
174
209
|
* Run a function inside a transaction, holding the named write lock for the
|
|
175
|
-
* duration.
|
|
176
|
-
*
|
|
210
|
+
* duration. Used by every operation that mutates the leaf set or its
|
|
211
|
+
* reservations.
|
|
177
212
|
* @param {function(import('mysql2/promise').PoolConnection): Promise<T>} fn
|
|
178
213
|
* @returns {Promise<T>}
|
|
179
214
|
* @template T
|
|
@@ -213,9 +248,8 @@ class MysqlTreeStore {
|
|
|
213
248
|
|
|
214
249
|
/**
|
|
215
250
|
* Run a function inside a transaction without the advisory lock. Used by
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* suffice and the global lock would only add contention.
|
|
251
|
+
* `addLeaves` and by read-only queries (`trySelectLeaves`), where row-level
|
|
252
|
+
* FK + InnoDB MVCC suffice and the global lock would only add contention.
|
|
219
253
|
* @param {function(import('mysql2/promise').PoolConnection): Promise<T>} fn
|
|
220
254
|
* @returns {Promise<T>}
|
|
221
255
|
* @template T
|
|
@@ -244,10 +278,11 @@ class MysqlTreeStore {
|
|
|
244
278
|
return;
|
|
245
279
|
}
|
|
246
280
|
|
|
281
|
+
const leafNodes = leaves;
|
|
247
282
|
await this._withTransaction(async (conn) => {
|
|
248
|
-
const leafIds =
|
|
283
|
+
const leafIds = leafNodes.map((l) => l.id);
|
|
249
284
|
await this._batchRemoveSpentLeaves(conn, leafIds);
|
|
250
|
-
await this._batchUpsertLeaves(conn,
|
|
285
|
+
await this._batchUpsertLeaves(conn, leafNodes, false, null);
|
|
251
286
|
});
|
|
252
287
|
} catch (error) {
|
|
253
288
|
if (error instanceof TreeStoreError) throw error;
|
|
@@ -258,6 +293,123 @@ class MysqlTreeStore {
|
|
|
258
293
|
}
|
|
259
294
|
}
|
|
260
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Store the ancestor chain of each pedigree, leaving the leaf pool and any
|
|
298
|
+
* spent marker untouched.
|
|
299
|
+
* @param {Array} pedigrees - Array of LeafPedigree { leaf, ancestors }
|
|
300
|
+
*/
|
|
301
|
+
async storeAncestors(pedigrees) {
|
|
302
|
+
try {
|
|
303
|
+
if (!pedigrees || pedigrees.length === 0) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
await this._withWriteTransaction(async (conn) => {
|
|
308
|
+
// A leaf can be spent between its chain being resolved and this write, and
|
|
309
|
+
// a chain is only ever removed with its leaf. Writing one for a leaf that is
|
|
310
|
+
// already gone would leave it behind for good.
|
|
311
|
+
const placeholders = buildPlaceholders(pedigrees.length);
|
|
312
|
+
const [storedRows] = await conn.query(
|
|
313
|
+
`SELECT id FROM brz_tree_leaves WHERE user_id = ? AND id IN (${placeholders})`,
|
|
314
|
+
[this.identity, ...pedigrees.map((p) => p.leaf.id)]
|
|
315
|
+
);
|
|
316
|
+
const storedLeafIds = new Set(storedRows.map((row) => row.id));
|
|
317
|
+
|
|
318
|
+
await this._batchUpsertAncestors(
|
|
319
|
+
conn,
|
|
320
|
+
pedigrees.filter((p) => storedLeafIds.has(p.leaf.id))
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (error instanceof TreeStoreError) throw error;
|
|
325
|
+
throw new TreeStoreError(
|
|
326
|
+
`Failed to store ancestors: ${error.message}`,
|
|
327
|
+
error
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Ids of the stored leaves whose chain cannot back an exit: the leaf has a
|
|
334
|
+
* parent, and no ancestor row of its own holds that parent.
|
|
335
|
+
* @returns {Promise<Array<string>>}
|
|
336
|
+
*/
|
|
337
|
+
async leavesMissingExitChains() {
|
|
338
|
+
try {
|
|
339
|
+
// A stored chain runs from its leaf's parent to a root, so a leaf whose
|
|
340
|
+
// chain holds the parent it has now is exitable. The join binds all three
|
|
341
|
+
// primary key columns, making it one index probe per leaf. A leaf that is
|
|
342
|
+
// itself a root needs no chain.
|
|
343
|
+
const [rows] = await this.pool.query(
|
|
344
|
+
`SELECT l.id
|
|
345
|
+
FROM brz_tree_leaves l
|
|
346
|
+
LEFT JOIN brz_tree_ancestors link
|
|
347
|
+
ON link.user_id = l.user_id AND link.leaf_id = l.id
|
|
348
|
+
AND link.id = l.parent_node_id
|
|
349
|
+
WHERE l.user_id = ?
|
|
350
|
+
AND l.parent_node_id IS NOT NULL
|
|
351
|
+
AND link.leaf_id IS NULL`,
|
|
352
|
+
[this.identity]
|
|
353
|
+
);
|
|
354
|
+
return rows.map((r) => r.id);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (error instanceof TreeStoreError) throw error;
|
|
357
|
+
throw new TreeStoreError(
|
|
358
|
+
`Failed to get leaves missing exit chains: ${error.message}`,
|
|
359
|
+
error
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Reconstruct the exit chains for many leaves in one query, each as
|
|
366
|
+
* { leaf, ancestors } with ancestors nearest first. A leaf absent from the store
|
|
367
|
+
* is skipped; a chain that hits a gap comes back partial.
|
|
368
|
+
* @param {Array<string>} leafIds
|
|
369
|
+
* @returns {Promise<Array<{leaf: object, ancestors: Array<object>}>>}
|
|
370
|
+
*/
|
|
371
|
+
async getExitChains(leafIds) {
|
|
372
|
+
try {
|
|
373
|
+
if (!leafIds || leafIds.length === 0) return [];
|
|
374
|
+
// One query loads each requested leaf's own row plus its ancestor rows,
|
|
375
|
+
// both tagged by the owning leaf id (a leaf's own row is tagged with its
|
|
376
|
+
// own id). Reading them in two queries could pair a leaf with ancestors
|
|
377
|
+
// from a different snapshot. Grouping by that tag keeps each leaf's node
|
|
378
|
+
// set separate, so a node id stored under another leaf can never
|
|
379
|
+
// cross-contaminate this one.
|
|
380
|
+
const placeholders = buildPlaceholders(leafIds.length);
|
|
381
|
+
const [rows] = await this.pool.query(
|
|
382
|
+
`SELECT leaf_id, data FROM brz_tree_ancestors WHERE user_id = ? AND leaf_id IN (${placeholders})
|
|
383
|
+
UNION ALL
|
|
384
|
+
SELECT id AS leaf_id, data FROM brz_tree_leaves WHERE user_id = ? AND id IN (${placeholders})`,
|
|
385
|
+
[this.identity, ...leafIds, this.identity, ...leafIds]
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const nodesByLeaf = new Map();
|
|
389
|
+
for (const r of rows) {
|
|
390
|
+
let nodes = nodesByLeaf.get(r.leaf_id);
|
|
391
|
+
if (!nodes) {
|
|
392
|
+
nodes = new Map();
|
|
393
|
+
nodesByLeaf.set(r.leaf_id, nodes);
|
|
394
|
+
}
|
|
395
|
+
const node = parseJson(r.data);
|
|
396
|
+
nodes.set(node.id, node);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = [];
|
|
400
|
+
for (const id of leafIds) {
|
|
401
|
+
const nodes = nodesByLeaf.get(id);
|
|
402
|
+
if (!nodes) continue;
|
|
403
|
+
const pedigree = assembleExitChain(nodes, id);
|
|
404
|
+
if (pedigree) result.push(pedigree);
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error instanceof TreeStoreError) throw error;
|
|
409
|
+
throw new TreeStoreError(`Failed to get exit chains: ${error.message}`, error);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
261
413
|
/**
|
|
262
414
|
* Returns the wallet's spendable balance (available + missing-from-operators
|
|
263
415
|
* + swap-reserved). Aggregated server-side so we don't fetch every leaf.
|
|
@@ -294,8 +446,8 @@ class MysqlTreeStore {
|
|
|
294
446
|
// one, and nothing non-Available and unreserved.
|
|
295
447
|
const [rows] = await this.pool.query(
|
|
296
448
|
`SELECT l.id AS id,
|
|
297
|
-
l.
|
|
298
|
-
l.
|
|
449
|
+
l.verifying_public_key AS verifying,
|
|
450
|
+
l.signing_public_key AS keyshare
|
|
299
451
|
FROM brz_tree_leaves l
|
|
300
452
|
LEFT JOIN brz_tree_reservations r
|
|
301
453
|
ON l.reservation_id = r.id AND l.user_id = r.user_id
|
|
@@ -332,6 +484,7 @@ class MysqlTreeStore {
|
|
|
332
484
|
|
|
333
485
|
for (const row of rows) {
|
|
334
486
|
const node = parseJson(row.data);
|
|
487
|
+
const spendable = node.status === "Available";
|
|
335
488
|
|
|
336
489
|
if (row.purpose) {
|
|
337
490
|
if (row.purpose === "Payment") {
|
|
@@ -339,14 +492,12 @@ class MysqlTreeStore {
|
|
|
339
492
|
} else if (row.purpose === "Swap") {
|
|
340
493
|
reservedForSwap.push(node);
|
|
341
494
|
}
|
|
495
|
+
} else if (!spendable) {
|
|
496
|
+
notAvailable.push(node);
|
|
342
497
|
} else if (toBool(row.is_missing_from_operators)) {
|
|
343
|
-
|
|
344
|
-
availableMissingFromOperators.push(node);
|
|
345
|
-
}
|
|
346
|
-
} else if (node.status === "Available") {
|
|
347
|
-
available.push(node);
|
|
498
|
+
availableMissingFromOperators.push(node);
|
|
348
499
|
} else {
|
|
349
|
-
|
|
500
|
+
available.push(node);
|
|
350
501
|
}
|
|
351
502
|
}
|
|
352
503
|
|
|
@@ -406,7 +557,13 @@ class MysqlTreeStore {
|
|
|
406
557
|
|
|
407
558
|
// Includes leaves released earlier in this transaction by
|
|
408
559
|
// _cleanupStaleReservations (which now NULLs reservation_id explicitly,
|
|
409
|
-
// since the composite FK uses NO ACTION).
|
|
560
|
+
// since the composite FK uses NO ACTION). MySQL has no DELETE ...
|
|
561
|
+
// RETURNING, so the ids are read first.
|
|
562
|
+
const [oldLeafRows] = await conn.query(
|
|
563
|
+
"SELECT id FROM brz_tree_leaves WHERE user_id = ? AND reservation_id IS NULL AND added_at < ?",
|
|
564
|
+
[this.identity, refreshTimestamp]
|
|
565
|
+
);
|
|
566
|
+
const deletedIds = oldLeafRows.map((r) => r.id);
|
|
410
567
|
await conn.query(
|
|
411
568
|
"DELETE FROM brz_tree_leaves WHERE user_id = ? AND reservation_id IS NULL AND added_at < ?",
|
|
412
569
|
[this.identity, refreshTimestamp]
|
|
@@ -414,6 +571,22 @@ class MysqlTreeStore {
|
|
|
414
571
|
|
|
415
572
|
await this._batchUpsertLeaves(conn, leaves, false, spentIds);
|
|
416
573
|
await this._batchUpsertLeaves(conn, missingLeaves, true, spentIds);
|
|
574
|
+
|
|
575
|
+
// A leaf reported again in this same refresh is re-inserted above, so its
|
|
576
|
+
// ancestor rows must survive: only ids that do NOT reappear (truly gone,
|
|
577
|
+
// e.g. spent) get their ancestor rows dropped alongside them.
|
|
578
|
+
const survivingIds = new Set();
|
|
579
|
+
for (const leaf of leaves.concat(missingLeaves || [])) {
|
|
580
|
+
if (!spentIds.has(leaf.id)) survivingIds.add(leaf.id);
|
|
581
|
+
}
|
|
582
|
+
const goneIds = deletedIds.filter((id) => !survivingIds.has(id));
|
|
583
|
+
if (goneIds.length > 0) {
|
|
584
|
+
const placeholders = buildPlaceholders(goneIds.length);
|
|
585
|
+
await conn.query(
|
|
586
|
+
`DELETE FROM brz_tree_ancestors WHERE user_id = ? AND leaf_id IN (${placeholders})`,
|
|
587
|
+
[this.identity, ...goneIds]
|
|
588
|
+
);
|
|
589
|
+
}
|
|
417
590
|
});
|
|
418
591
|
} catch (error) {
|
|
419
592
|
if (error instanceof TreeStoreError) throw error;
|
|
@@ -427,14 +600,19 @@ class MysqlTreeStore {
|
|
|
427
600
|
async cancelReservation(id, leavesToKeep) {
|
|
428
601
|
try {
|
|
429
602
|
await this._withTransaction(async (conn) => {
|
|
430
|
-
|
|
431
|
-
|
|
603
|
+
// Return leavesToKeep to the pool even when the reservation is already
|
|
604
|
+
// gone (e.g. released by stale cleanup): dropping them here would lose
|
|
605
|
+
// the leaves until the next refresh. The deletes no-op in that case.
|
|
606
|
+
// Only the leaves are re-inserted: a kept leaf's ancestor rows stay put
|
|
607
|
+
// (they are not touched below); a dropped leaf's are removed with it.
|
|
608
|
+
const keepIds = new Set((leavesToKeep || []).map((l) => l.id));
|
|
609
|
+
const [reservedRows] = await conn.query(
|
|
610
|
+
"SELECT id FROM brz_tree_leaves WHERE user_id = ? AND reservation_id = ?",
|
|
432
611
|
[this.identity, id]
|
|
433
612
|
);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
613
|
+
const droppedIds = reservedRows
|
|
614
|
+
.map((r) => r.id)
|
|
615
|
+
.filter((rid) => !keepIds.has(rid));
|
|
438
616
|
|
|
439
617
|
await conn.query(
|
|
440
618
|
"DELETE FROM brz_tree_leaves WHERE user_id = ? AND reservation_id = ?",
|
|
@@ -444,6 +622,13 @@ class MysqlTreeStore {
|
|
|
444
622
|
"DELETE FROM brz_tree_reservations WHERE user_id = ? AND id = ?",
|
|
445
623
|
[this.identity, id]
|
|
446
624
|
);
|
|
625
|
+
if (droppedIds.length > 0) {
|
|
626
|
+
const placeholders = buildPlaceholders(droppedIds.length);
|
|
627
|
+
await conn.query(
|
|
628
|
+
`DELETE FROM brz_tree_ancestors WHERE user_id = ? AND leaf_id IN (${placeholders})`,
|
|
629
|
+
[this.identity, ...droppedIds]
|
|
630
|
+
);
|
|
631
|
+
}
|
|
447
632
|
|
|
448
633
|
if (leavesToKeep && leavesToKeep.length > 0) {
|
|
449
634
|
await this._batchUpsertLeaves(conn, leavesToKeep, false, null);
|
|
@@ -471,13 +656,14 @@ class MysqlTreeStore {
|
|
|
471
656
|
);
|
|
472
657
|
|
|
473
658
|
let isSwap = false;
|
|
659
|
+
let reservedLeafIds = [];
|
|
474
660
|
if (resRows.length > 0) {
|
|
475
661
|
isSwap = resRows[0].purpose === "Swap";
|
|
476
662
|
const [leafRows] = await conn.query(
|
|
477
663
|
"SELECT id FROM brz_tree_leaves WHERE user_id = ? AND reservation_id = ?",
|
|
478
664
|
[this.identity, id]
|
|
479
665
|
);
|
|
480
|
-
|
|
666
|
+
reservedLeafIds = leafRows.map((r) => r.id);
|
|
481
667
|
await this._batchInsertSpentLeaves(conn, reservedLeafIds);
|
|
482
668
|
await conn.query(
|
|
483
669
|
"DELETE FROM brz_tree_leaves WHERE user_id = ? AND reservation_id = ?",
|
|
@@ -487,6 +673,15 @@ class MysqlTreeStore {
|
|
|
487
673
|
"DELETE FROM brz_tree_reservations WHERE user_id = ? AND id = ?",
|
|
488
674
|
[this.identity, id]
|
|
489
675
|
);
|
|
676
|
+
// The spent leaves own these ancestor rows; remove them in the same
|
|
677
|
+
// transaction rather than leaving them to a separate reclaim pass.
|
|
678
|
+
if (reservedLeafIds.length > 0) {
|
|
679
|
+
const placeholders = buildPlaceholders(reservedLeafIds.length);
|
|
680
|
+
await conn.query(
|
|
681
|
+
`DELETE FROM brz_tree_ancestors WHERE user_id = ? AND leaf_id IN (${placeholders})`,
|
|
682
|
+
[this.identity, ...reservedLeafIds]
|
|
683
|
+
);
|
|
684
|
+
}
|
|
490
685
|
}
|
|
491
686
|
|
|
492
687
|
if (newLeaves && newLeaves.length > 0) {
|
|
@@ -687,7 +882,8 @@ class MysqlTreeStore {
|
|
|
687
882
|
const [availableRows] = await conn.query(
|
|
688
883
|
`SELECT id FROM brz_tree_leaves
|
|
689
884
|
WHERE user_id = ? AND id IN (${placeholders})
|
|
690
|
-
AND status = 'Available'
|
|
885
|
+
AND status = 'Available'
|
|
886
|
+
AND is_missing_from_operators = 0
|
|
691
887
|
AND reservation_id IS NULL`,
|
|
692
888
|
[this.identity, ...leafIds]
|
|
693
889
|
);
|
|
@@ -746,6 +942,15 @@ class MysqlTreeStore {
|
|
|
746
942
|
"DELETE FROM brz_tree_leaves WHERE user_id = ? AND reservation_id = ?",
|
|
747
943
|
[this.identity, reservationId]
|
|
748
944
|
);
|
|
945
|
+
// The spent leaves own these ancestor rows; remove them now since there
|
|
946
|
+
// is no later reclaim pass.
|
|
947
|
+
if (oldLeafIds.length > 0) {
|
|
948
|
+
const placeholders = buildPlaceholders(oldLeafIds.length);
|
|
949
|
+
await conn.query(
|
|
950
|
+
`DELETE FROM brz_tree_ancestors WHERE user_id = ? AND leaf_id IN (${placeholders})`,
|
|
951
|
+
[this.identity, ...oldLeafIds]
|
|
952
|
+
);
|
|
953
|
+
}
|
|
749
954
|
|
|
750
955
|
await this._batchUpsertLeaves(conn, changeLeaves, false, null);
|
|
751
956
|
await this._batchUpsertLeaves(conn, reservedLeaves, false, null);
|
|
@@ -758,6 +963,8 @@ class MysqlTreeStore {
|
|
|
758
963
|
[this.identity, reservationId]
|
|
759
964
|
);
|
|
760
965
|
|
|
966
|
+
// Return value must be plain TreeNodes: the Rust side deserializes
|
|
967
|
+
// Vec<TreeNode>.
|
|
761
968
|
return { id: reservationId, leaves: reservedLeaves };
|
|
762
969
|
});
|
|
763
970
|
} catch (error) {
|
|
@@ -969,15 +1176,17 @@ class MysqlTreeStore {
|
|
|
969
1176
|
|
|
970
1177
|
if (filtered.length === 0) return;
|
|
971
1178
|
|
|
1179
|
+
const leafNodes = filtered;
|
|
1180
|
+
|
|
972
1181
|
// All chunks run inside the caller's transaction, so the full set still
|
|
973
1182
|
// lands atomically. UTC_TIMESTAMP(6) is re-evaluated per statement, so
|
|
974
1183
|
// added_at can differ by microseconds between chunks; every value is still
|
|
975
1184
|
// after the caller's refreshStartedAt, which is all the timestamp-based
|
|
976
1185
|
// deletion in setLeaves depends on.
|
|
977
|
-
for (let i = 0; i <
|
|
978
|
-
const chunk =
|
|
1186
|
+
for (let i = 0; i < leafNodes.length; i += LEAF_UPSERT_CHUNK_SIZE) {
|
|
1187
|
+
const chunk = leafNodes.slice(i, i + LEAF_UPSERT_CHUNK_SIZE);
|
|
979
1188
|
const valueClauses = new Array(chunk.length)
|
|
980
|
-
.fill("(?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(6))")
|
|
1189
|
+
.fill("(?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(6))")
|
|
981
1190
|
.join(", ");
|
|
982
1191
|
const params = [];
|
|
983
1192
|
for (const leaf of chunk) {
|
|
@@ -987,24 +1196,82 @@ class MysqlTreeStore {
|
|
|
987
1196
|
leaf.status,
|
|
988
1197
|
isMissingFromOperators ? 1 : 0,
|
|
989
1198
|
JSON.stringify(leaf),
|
|
990
|
-
leaf.value
|
|
1199
|
+
leaf.value,
|
|
1200
|
+
leaf.parent_node_id ?? null,
|
|
1201
|
+
leaf.verifying_public_key,
|
|
1202
|
+
leaf.signing_keyshare.public_key
|
|
991
1203
|
);
|
|
992
1204
|
}
|
|
993
1205
|
|
|
994
1206
|
await conn.query(
|
|
995
|
-
`INSERT INTO brz_tree_leaves
|
|
1207
|
+
`INSERT INTO brz_tree_leaves
|
|
1208
|
+
(user_id, id, status, is_missing_from_operators, data, value,
|
|
1209
|
+
parent_node_id, verifying_public_key, signing_public_key, added_at)
|
|
996
1210
|
VALUES ${valueClauses}
|
|
997
1211
|
ON DUPLICATE KEY UPDATE
|
|
998
1212
|
status = VALUES(status),
|
|
999
1213
|
is_missing_from_operators = VALUES(is_missing_from_operators),
|
|
1000
1214
|
data = VALUES(data),
|
|
1001
1215
|
value = VALUES(value),
|
|
1216
|
+
parent_node_id = VALUES(parent_node_id),
|
|
1217
|
+
verifying_public_key = VALUES(verifying_public_key),
|
|
1218
|
+
signing_public_key = VALUES(signing_public_key),
|
|
1002
1219
|
added_at = UTC_TIMESTAMP(6)`,
|
|
1003
1220
|
params
|
|
1004
1221
|
);
|
|
1005
1222
|
}
|
|
1006
1223
|
}
|
|
1007
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* Replaces the ancestor rows of every pedigree wholesale (delete then
|
|
1227
|
+
* insert), in one delete and an insert per ANCESTOR_INSERT_CHUNK_SIZE rows. A
|
|
1228
|
+
* pedigree carrying no ancestors is skipped: an empty list means the chain is
|
|
1229
|
+
* unknown, not that the leaf has none, so a stored chain must survive being
|
|
1230
|
+
* re-added without one.
|
|
1231
|
+
*/
|
|
1232
|
+
async _batchUpsertAncestors(conn, pedigrees) {
|
|
1233
|
+
const withAncestors = (pedigrees || []).filter(
|
|
1234
|
+
(p) => p.ancestors && p.ancestors.length > 0
|
|
1235
|
+
);
|
|
1236
|
+
if (withAncestors.length === 0) return;
|
|
1237
|
+
|
|
1238
|
+
const leafIds = withAncestors.map((p) => p.leaf.id);
|
|
1239
|
+
await conn.query(
|
|
1240
|
+
`DELETE FROM brz_tree_ancestors
|
|
1241
|
+
WHERE user_id = ? AND leaf_id IN (${buildPlaceholders(leafIds.length)})`,
|
|
1242
|
+
[this.identity, ...leafIds]
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
const rows = [];
|
|
1246
|
+
for (const pedigree of withAncestors) {
|
|
1247
|
+
for (const node of pedigree.ancestors) {
|
|
1248
|
+
rows.push([
|
|
1249
|
+
this.identity,
|
|
1250
|
+
pedigree.leaf.id,
|
|
1251
|
+
node.id,
|
|
1252
|
+
node.parent_node_id ?? null,
|
|
1253
|
+
node.status,
|
|
1254
|
+
JSON.stringify(node),
|
|
1255
|
+
node.value,
|
|
1256
|
+
node.verifying_public_key,
|
|
1257
|
+
]);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
for (let i = 0; i < rows.length; i += ANCESTOR_INSERT_CHUNK_SIZE) {
|
|
1262
|
+
const chunk = rows.slice(i, i + ANCESTOR_INSERT_CHUNK_SIZE);
|
|
1263
|
+
const valueClauses = new Array(chunk.length)
|
|
1264
|
+
.fill("(?, ?, ?, ?, ?, ?, ?, ?)")
|
|
1265
|
+
.join(", ");
|
|
1266
|
+
await conn.query(
|
|
1267
|
+
`INSERT INTO brz_tree_ancestors
|
|
1268
|
+
(user_id, leaf_id, id, parent_node_id, status, data, value, verifying_public_key)
|
|
1269
|
+
VALUES ${valueClauses}`,
|
|
1270
|
+
chunk.flat()
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1008
1275
|
async _batchSetReservationId(conn, reservationId, leafIds) {
|
|
1009
1276
|
if (leafIds.length === 0) return;
|
|
1010
1277
|
|