@bitcobblers/wod-wiki-storage 0.35.2116 → 0.35.2119

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/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ var wodWikiCore = require('@bitcobblers/wod-wiki-core');
4
+
3
5
  // src/schema.ts
4
- var DB_VERSION = 28;
6
+ var DB_VERSION = 30;
5
7
  var COLUMN_TYPES = {
6
8
  notes: { date: "number", createdAt: "number" },
7
9
  page: { createdAt: "number" },
@@ -10,16 +12,9 @@ var COLUMN_TYPES = {
10
12
  tags: { createdAt: "number" },
11
13
  tag_types: { createdAt: "number" },
12
14
  note_tags: {},
13
- segments: {
14
- version: "number",
15
- position: "number",
16
- createdAt: "number",
17
- updatedAt: "number",
18
- isHistory: "boolean"
19
- },
15
+ block_revisions: { version: "number", savedAt: "number" },
20
16
  sessions: {
21
- segmentVersion: "number",
22
- version: "number",
17
+ blockVersion: "number",
23
18
  startTime: "number",
24
19
  endTime: "number",
25
20
  duration: "number",
@@ -32,7 +27,7 @@ var COLUMN_TYPES = {
32
27
  attachments: { createdAt: "number" },
33
28
  events: {
34
29
  timestamp: "number",
35
- segmentVersion: "number",
30
+ blockVersion: "number",
36
31
  line: "number",
37
32
  sourceStatementId: "number",
38
33
  stackLevel: "number",
@@ -45,7 +40,10 @@ var COLUMN_TYPES = {
45
40
  field_catalog_meta: {},
46
41
  efforts: { "baseAttributes.met": "number" },
47
42
  block_index: {
48
- segmentVersion: "number",
43
+ blockVersion: "number",
44
+ sourceRevision: "number",
45
+ from: "number",
46
+ to: "number",
49
47
  position: "number",
50
48
  createdAt: "number",
51
49
  isStatic: "boolean"
@@ -92,17 +90,12 @@ var STORE_DEFS = [
92
90
  { name: "by-note-tag", keyPath: ["noteId", "tagId"], unique: true },
93
91
  { name: "by-user", keyPath: ["userId"] }
94
92
  ] },
95
- // 5. segments
96
- { name: "segments", keyPath: ["id", "version"], indexes: [
93
+ { name: "block_revisions", keyPath: ["noteId", "blockId", "version"], indexes: [
97
94
  { name: "by-note", keyPath: ["noteId"] },
98
- { name: "by-type", keyPath: ["dataType"] },
99
- { name: "by-page", keyPath: ["pageId"] },
100
- { name: "by-history", keyPath: ["isHistory"] },
101
- { name: "by-user", keyPath: ["userId"] }
95
+ { name: "by-block", keyPath: ["noteId", "blockId"] }
102
96
  ] },
103
97
  // 6. sessions
104
98
  { name: "sessions", keyPath: ["id"], indexes: [
105
- { name: "by-segment", keyPath: ["segmentId"] },
106
99
  { name: "by-note", keyPath: ["noteId"] },
107
100
  { name: "by-completed", keyPath: ["createdAt"] },
108
101
  { name: "by-content", keyPath: ["blockContentId"] },
@@ -111,9 +104,6 @@ var STORE_DEFS = [
111
104
  { name: "by-origin", keyPath: ["origin"] },
112
105
  { name: "by-user", keyPath: ["userId"] }
113
106
  ] },
114
- // 7. results — removed in V26: legacy alias of sessions. The V26 upgrade
115
- // migrates any residual rows into `sessions` (projecting pre-V21 data.logs
116
- // into event rows) before dropping the store; no compatibility alias.
117
107
  // 8. attachments
118
108
  { name: "attachments", keyPath: ["id"], indexes: [
119
109
  { name: "by-note", keyPath: ["noteId"] },
@@ -182,7 +172,6 @@ var USER_OWNED_STORES = [
182
172
  "tags",
183
173
  "tag_types",
184
174
  "note_tags",
185
- "segments",
186
175
  "sessions",
187
176
  "attachments",
188
177
  "events",
@@ -243,7 +232,7 @@ function rangeFromIDB(query) {
243
232
  wire.lowerOpen = query.lowerOpen === true;
244
233
  }
245
234
  if (query.upper !== void 0 && query.upper !== null) {
246
- wire.upper = query.upper;
235
+ wire.upper = Array.isArray(query.upper) ? query.upper.map((p) => Array.isArray(p) && p.length === 0 ? "\uFFFF" : p) : query.upper;
247
236
  wire.upperOpen = query.upperOpen === true;
248
237
  }
249
238
  return wire.lower !== void 0 || wire.upper !== void 0 ? wire : void 0;
@@ -275,9 +264,7 @@ function keyInRange(key, range) {
275
264
  }
276
265
  return true;
277
266
  }
278
-
279
- // src/domain.ts
280
- var DOMAIN_PROJECTION_VERSION = 1;
267
+ var DOMAIN_PROJECTION_VERSION = 2;
281
268
  function isObject(value) {
282
269
  return typeof value === "object" && value !== null && !Array.isArray(value);
283
270
  }
@@ -288,13 +275,16 @@ function finiteNumbers(row, fields) {
288
275
  return fields.every((field) => typeof row[field] === "number" && Number.isFinite(row[field]));
289
276
  }
290
277
  function isNote(value) {
291
- return isObject(value) && typeof value.id === "string" && typeof value.title === "string" && finiteNumbers(value, ["createdAt"]) && (value.date === void 0 || finiteNumbers(value, ["date"])) && optionalStrings(value, ["type", "sourceId", "catalog", "sourcePath", "seedOrigin", "seedChunkId"]) && (value.tags === void 0 || Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string"));
278
+ if (!isObject(value) || typeof value.id !== "string" || typeof value.title !== "string" || typeof value.rawContent !== "string" || !Number.isSafeInteger(value.sourceRevision) || typeof value.sourceRevision !== "number" || value.sourceRevision < 1 || !isObject(value.blocks) || !finiteNumbers(value, ["createdAt"]) || value.date !== void 0 && !finiteNumbers(value, ["date"]) || !optionalStrings(value, ["type", "sourceId", "catalog", "sourcePath", "seedOrigin", "seedChunkId"]) || value.tags !== void 0 && (!Array.isArray(value.tags) || !value.tags.every((tag) => typeof tag === "string"))) return false;
279
+ try {
280
+ wodWikiCore.validateNoteBlocks(value.rawContent, value.blocks, value.sourceRevision);
281
+ return true;
282
+ } catch {
283
+ return false;
284
+ }
292
285
  }
293
286
  function isBlock(value) {
294
- return isObject(value) && typeof value.id === "string" && typeof value.noteId === "string" && typeof value.segmentId === "string" && finiteNumbers(value, ["segmentVersion", "createdAt"]) && typeof value.dataType === "string" && typeof value.rawContent === "string" && typeof value.noteTitle === "string" && optionalStrings(value, ["blockContentId", "sourceId", "sourcePath", "routeId"]) && (value.position === void 0 || finiteNumbers(value, ["position"])) && (value.isStatic === void 0 || typeof value.isStatic === "boolean");
295
- }
296
- function isSegment(value) {
297
- return isObject(value) && typeof value.id === "string" && typeof value.noteId === "string" && finiteNumbers(value, ["version", "createdAt"]) && typeof value.dataType === "string" && typeof value.rawContent === "string" && (value.data === null || isObject(value.data)) && optionalStrings(value, ["sourceContent", "pageId"]) && (value.position === void 0 || finiteNumbers(value, ["position"])) && (value.isHistory === void 0 || typeof value.isHistory === "boolean");
287
+ return isObject(value) && typeof value.id === "string" && typeof value.noteId === "string" && typeof value.blockId === "string" && finiteNumbers(value, ["blockVersion", "sourceRevision", "from", "to", "createdAt"]) && value.id === `${value.noteId}:${value.blockId}` && typeof value.blockVersion === "number" && Number.isSafeInteger(value.blockVersion) && value.blockVersion > 0 && typeof value.sourceRevision === "number" && Number.isSafeInteger(value.sourceRevision) && value.sourceRevision > 0 && typeof value.from === "number" && Number.isSafeInteger(value.from) && value.from >= 0 && typeof value.to === "number" && Number.isSafeInteger(value.to) && value.to > value.from && ["wod", "query", "widget", "embed"].includes(String(value.dataType)) && typeof value.rawContent === "string" && typeof value.noteTitle === "string" && optionalStrings(value, ["blockContentId", "sourceId", "sourcePath", "routeId"]) && (value.position === void 0 || finiteNumbers(value, ["position"])) && (value.isStatic === void 0 || typeof value.isStatic === "boolean");
298
288
  }
299
289
  function isTag(value) {
300
290
  return isObject(value) && typeof value.id === "string" && typeof value.label === "string" && finiteNumbers(value, ["createdAt"]) && optionalStrings(value, ["type"]);
@@ -306,7 +296,7 @@ function isLink(value) {
306
296
  return isObject(value) && typeof value.id === "string" && typeof value.noteId === "string" && typeof value.pageId === "string" && finiteNumbers(value, ["createdAt"]) && (value.position === void 0 || finiteNumbers(value, ["position"]));
307
297
  }
308
298
  function isEntry(value) {
309
- return isObject(value) && isNote(value.note) && Array.isArray(value.segments) && value.segments.every(isSegment) && Array.isArray(value.tags) && value.tags.every(isTag) && Array.isArray(value.links) && value.links.every(isLink) && Array.isArray(value.pages) && value.pages.every(isPage);
299
+ return isObject(value) && isNote(value.note) && Array.isArray(value.tags) && value.tags.every(isTag) && Array.isArray(value.links) && value.links.every(isLink) && Array.isArray(value.pages) && value.pages.every(isPage);
310
300
  }
311
301
  function parseDomainQueryResult(value, plan) {
312
302
  if (!isObject(value) || value.projectionVersion !== DOMAIN_PROJECTION_VERSION || value.plan !== plan || !Array.isArray(value.rows) || typeof value.selectedCount !== "number" || !Number.isSafeInteger(value.selectedCount) || value.selectedCount < 0 || typeof value.matchedCount !== "number" || !Number.isSafeInteger(value.matchedCount) || value.matchedCount < 0 || value.matchedCount > value.selectedCount || value.rows.length > value.matchedCount) {
@@ -402,6 +392,100 @@ function bufferToOps(storeNames, buffer, cleared) {
402
392
  return ops;
403
393
  }
404
394
 
395
+ // src/sql/errors.ts
396
+ var StorageError = class extends Error {
397
+ constructor(status, message, code) {
398
+ super(message);
399
+ this.status = status;
400
+ this.code = code;
401
+ this.name = "StorageError";
402
+ }
403
+ status;
404
+ code;
405
+ };
406
+
407
+ // src/validation.ts
408
+ function rowsEqual(left, right) {
409
+ if (Object.is(left, right)) return true;
410
+ if (left instanceof ArrayBuffer && right instanceof ArrayBuffer) {
411
+ const a2 = new Uint8Array(left), b2 = new Uint8Array(right);
412
+ return a2.length === b2.length && a2.every((value, i) => value === b2[i]);
413
+ }
414
+ if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) return false;
415
+ if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, i) => rowsEqual(value, right[i]));
416
+ const a = Object.keys(left).filter((key) => Reflect.get(left, key) !== void 0);
417
+ const b = Object.keys(right).filter((key) => Reflect.get(right, key) !== void 0);
418
+ return a.length === b.length && a.every((key) => Object.hasOwn(right, key) && rowsEqual(Reflect.get(left, key), Reflect.get(right, key)));
419
+ }
420
+ function record(value) {
421
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new StorageError(400, "storage row must be an object");
422
+ return value;
423
+ }
424
+ function validateStorageRow(store, value) {
425
+ if (store !== "notes" && store !== "block_revisions" && store !== "sessions" && store !== "block_index") return;
426
+ const row = record(value);
427
+ const positive = (field) => typeof row[field] === "number" && Number.isSafeInteger(row[field]) && row[field] > 0;
428
+ const required = (field) => typeof row[field] === "string" && row[field].length > 0;
429
+ if (store === "notes") {
430
+ if (!required("id") || typeof row.title !== "string" || typeof row.rawContent !== "string" || !positive("sourceRevision") || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || "segments" in row || "segmentIds" in row || row.date !== void 0 && (typeof row.date !== "number" || !Number.isFinite(row.date)) || row.seedOrigin !== void 0 && row.seedOrigin !== "seed" && row.seedOrigin !== "user" || row.seedVersion !== void 0 && !positive("seedVersion") || row.tags !== void 0 && (!Array.isArray(row.tags) || !row.tags.every((tag) => typeof tag === "string")) || ["type", "sourceId", "sourcePath", "catalog", "seedChunkId"].some((field) => row[field] !== void 0 && typeof row[field] !== "string")) {
431
+ throw new StorageError(400, "invalid whole Note", "invalid_note_blocks");
432
+ }
433
+ try {
434
+ wodWikiCore.validateNoteBlocks(row.rawContent, record(row.blocks), row.sourceRevision);
435
+ } catch (error) {
436
+ throw new StorageError(400, error instanceof Error ? error.message : "invalid block map", "invalid_note_blocks");
437
+ }
438
+ } else if (store === "block_revisions") {
439
+ if (!required("noteId") || !required("blockId") || !positive("version") || typeof row.rawContent !== "string" || typeof row.savedAt !== "number" || !Number.isFinite(row.savedAt) || !["wod", "query", "widget", "embed"].includes(String(row.kind)) || (row.kind === "wod" ? row.dialect !== "time" && row.dialect !== "log" : row.dialect !== void 0 || row.sport !== void 0) || row.sport !== void 0 && typeof row.sport !== "string") throw new StorageError(400, "invalid block revision", "invalid_note_blocks");
440
+ try {
441
+ wodWikiCore.validateNoteBlocks(row.rawContent, { [String(row.blockId)]: {
442
+ id: String(row.blockId),
443
+ kind: row.kind,
444
+ version: row.version,
445
+ sourceRevision: 1,
446
+ from: 0,
447
+ to: row.rawContent.length,
448
+ ...row.dialect === void 0 ? {} : { dialect: row.dialect },
449
+ ...row.sport === void 0 ? {} : { sport: row.sport }
450
+ } }, 1);
451
+ } catch (error) {
452
+ throw new StorageError(400, error instanceof Error ? error.message : "invalid revision source", "invalid_note_blocks");
453
+ }
454
+ } else if (store === "sessions") {
455
+ if (!required("noteId") || !required("blockId") || !positive("blockVersion") || ["segmentId", "segmentVersion", "version", "rawContent", "script", "serializedScript", "source", "data", "logs"].some((field) => field in row)) {
456
+ throw new StorageError(400, "Session requires immutable block pin", "invalid_session_pin");
457
+ }
458
+ } else if (!required("noteId") || !required("blockId") || !positive("blockVersion") || !positive("sourceRevision") || row.id !== `${row.noteId}:${row.blockId}` || !Number.isSafeInteger(row.from) || !Number.isSafeInteger(row.to) || "segmentId" in row || "segmentVersion" in row || !["wod", "query", "widget", "embed"].includes(String(row.dataType)) || typeof row.rawContent !== "string" || typeof row.noteTitle !== "string" || typeof row.from !== "number" || typeof row.to !== "number" || row.from < 0 || row.to <= row.from) {
459
+ throw new StorageError(400, "invalid actionable block projection", "invalid_note_blocks");
460
+ }
461
+ }
462
+ function validateNoteWrite(previous, value) {
463
+ if (previous === void 0 || rowsEqual(previous, value)) return;
464
+ const before = record(previous), after = record(value);
465
+ if (typeof before.sourceRevision !== "number" || typeof after.sourceRevision !== "number" || after.sourceRevision < before.sourceRevision || (!rowsEqual(before.blocks, after.blocks) || before.rawContent !== after.rawContent) && after.sourceRevision <= before.sourceRevision) throw new StorageError(409, "stale Note source revision", "transaction_conflict");
466
+ }
467
+ function validateSessionWrite(previous, value) {
468
+ if (previous === void 0) return;
469
+ const before = record(previous), after = record(value);
470
+ if (before.noteId !== after.noteId || before.blockId !== after.blockId || before.blockVersion !== after.blockVersion)
471
+ throw new StorageError(409, "Session pin is immutable", "invalid_session_pin");
472
+ }
473
+ async function validateStoredPins(notes, sessions, getRevisions) {
474
+ const keys = [];
475
+ for (const note of notes) for (const block of Object.values(note.blocks)) keys.push([note.id, block.id, block.version]);
476
+ for (const session of sessions) keys.push([session.noteId, session.blockId, session.blockVersion]);
477
+ const revisions = await getRevisions(keys);
478
+ let i = 0;
479
+ for (const note of notes) for (const block of Object.values(note.blocks)) {
480
+ const revision = revisions[i++];
481
+ if (!revision || revision.kind !== block.kind || revision.rawContent !== note.rawContent.slice(block.from, block.to) || revision.dialect !== block.dialect || revision.sport !== block.sport) throw new StorageError(409, "Note block has no matching immutable revision", "invalid_note_blocks");
482
+ }
483
+ for (let sessionIndex = 0; sessionIndex < sessions.length; sessionIndex++) {
484
+ const revision = revisions[i++];
485
+ if (!revision || revision.kind !== "wod") throw new StorageError(409, "Session pin must reference a saved workout revision", "invalid_session_pin");
486
+ }
487
+ }
488
+
405
489
  // src/api.ts
406
490
  async function request(http, method, path, init) {
407
491
  const qs = init?.params?.toString();
@@ -451,14 +535,14 @@ var ApiStorage = class {
451
535
  if (!def) throw new Error(`ApiStorage: unknown store '${store}'`);
452
536
  return def;
453
537
  }
454
- makeView(store, buffer, cleared) {
455
- return new ApiStore(this.http, this.def(store), store, buffer, cleared, this.getUserId);
538
+ makeView(store, buffer, cleared, checks, mutable = false) {
539
+ return new ApiStore(this.http, this.def(store), store, buffer, cleared, this.getUserId, checks, mutable);
456
540
  }
457
541
  readonly(store) {
458
542
  return this.makeView(store);
459
543
  }
460
544
  readwrite(store) {
461
- return this.makeView(store);
545
+ return this.makeView(store, void 0, void 0, void 0, true);
462
546
  }
463
547
  async queryDomain(query) {
464
548
  const res = await this.http.fetchImpl(`${this.http.baseUrl}/v1/query`, {
@@ -473,21 +557,28 @@ var ApiStorage = class {
473
557
  if (!res.ok) throw new Error(`ApiStorage: POST /v1/query \u2192 ${res.status}`);
474
558
  return parseDomainQueryResult(await res.json(), query.plan);
475
559
  }
476
- async transaction(_stores, _mode, fn) {
560
+ async transaction(stores, mode, fn) {
477
561
  const buffer = /* @__PURE__ */ new Map();
478
562
  const cleared = /* @__PURE__ */ new Set();
563
+ const checks = /* @__PURE__ */ new Map();
564
+ const scoped = new Set(stores);
565
+ const view = (store, mutable) => {
566
+ if (!scoped.has(store)) throw new Error("store outside transaction scope");
567
+ if (mutable && mode !== "readwrite") throw new Error("read-only transaction");
568
+ return this.makeView(store, buffer, cleared, checks, mutable);
569
+ };
479
570
  const tx = {
480
- readonly: (store) => this.makeView(store, buffer, cleared),
481
- readwrite: (store) => this.makeView(store, buffer, cleared)
571
+ readonly: (store) => view(store, false),
572
+ readwrite: (store) => view(store, true)
482
573
  };
483
574
  const result = await fn(tx);
484
- await this.flush(buffer, cleared);
575
+ await this.flush(buffer, cleared, checks);
485
576
  return result;
486
577
  }
487
578
  /** Sends buffered ops as one server-side SQL transaction. */
488
- async flush(buffer, cleared) {
579
+ async flush(buffer, cleared, checks) {
489
580
  const ops = bufferToOps(STORE_DEFS.map((d) => d.name), buffer, cleared);
490
- if (ops.length > 0) await request(this.http, "POST", "/tx", { body: { ops } });
581
+ if (ops.length > 0) await request(this.http, "POST", "/tx", { body: { ops: [...checks.values(), ...ops] } });
491
582
  }
492
583
  async wipe() {
493
584
  await request(this.http, "POST", "/wipe");
@@ -496,13 +587,15 @@ var ApiStorage = class {
496
587
  }
497
588
  };
498
589
  var ApiStore = class {
499
- constructor(http, def, store, buffer, cleared, getUserId) {
590
+ constructor(http, def, store, buffer, cleared, getUserId, checks, mutable = false) {
500
591
  this.http = http;
501
592
  this.def = def;
502
593
  this.store = store;
503
594
  this.buffer = buffer;
504
595
  this.cleared = cleared;
505
596
  this.getUserId = getUserId;
597
+ this.checks = checks;
598
+ this.mutable = mutable;
506
599
  }
507
600
  http;
508
601
  def;
@@ -510,6 +603,8 @@ var ApiStore = class {
510
603
  buffer;
511
604
  cleared;
512
605
  getUserId;
606
+ checks;
607
+ mutable;
513
608
  /** True when this tx has buffered writes for this store. */
514
609
  dirty() {
515
610
  return isDirty(this.store, this.buffer, this.cleared);
@@ -528,7 +623,16 @@ var ApiStore = class {
528
623
  const state = bufferedGet(this.store, k, this.buffer);
529
624
  if (state) return state.hit ? state.value : void 0;
530
625
  if (this.cleared?.has(this.store)) return void 0;
531
- return remoteGet(this.http, this.store, k);
626
+ const value = await remoteGet(this.http, this.store, k);
627
+ const slot = bufferSlot(this.store, k);
628
+ if (!this.checks?.has(slot)) this.checks?.set(slot, { op: "check", store: this.store, key: k, value: structuredClone(value ?? null) });
629
+ return value;
630
+ }
631
+ async getMany(keys) {
632
+ return Promise.all(keys.map((key) => this.get(key)));
633
+ }
634
+ async getAllFromIndexMany(indexName, keys) {
635
+ return Promise.all(keys.map((key) => this.getAllFromIndex(indexName, key)));
532
636
  }
533
637
  async getAll(query, count) {
534
638
  const range = rangeFromIDB(query);
@@ -549,18 +653,28 @@ var ApiStore = class {
549
653
  return this.overlay(await this.serverRows(`index/${encodeURIComponent(indexName)}`, range), void 0, range, indexName).length;
550
654
  }
551
655
  /** Server rows for a list path, or none when this tx cleared the store. */
552
- serverRows(path, range, count) {
553
- if (this.cleared?.has(this.store)) return Promise.resolve([]);
554
- return remoteList(this.http, this.store, path, range, count);
656
+ async serverRows(path, range, count) {
657
+ if (this.cleared?.has(this.store)) return [];
658
+ const rows = await remoteList(this.http, this.store, path, range, count);
659
+ for (const row of rows) {
660
+ const key = keyFromValue(this.def, row), slot = bufferSlot(this.store, key);
661
+ if (!this.checks?.has(slot)) this.checks?.set(slot, { op: "check", store: this.store, key, value: structuredClone(row) });
662
+ }
663
+ return rows;
555
664
  }
556
665
  /** Applies buffered puts and deletes before range filtering, ordering and paging. */
557
666
  overlay(rows, count, range, indexName) {
558
667
  return overlayRows(this.def, this.store, this.buffer, this.cleared, rows, count, range, indexName);
559
668
  }
560
669
  async put(value, _key) {
670
+ if (!this.mutable) throw new Error("read-only store");
561
671
  const userId = this.getUserId?.();
562
672
  const stored = userId ? stampUserOwned(this.store, value, userId) : value;
563
673
  const key = keyFromValue(this.def, stored);
674
+ if (this.store === "block_revisions") {
675
+ const previous = this.buffer?.get(bufferSlot(this.store, key));
676
+ if (previous?.kind === "put" && !rowsEqual(previous.value, stored)) throw new Error("immutable block revision collision");
677
+ }
564
678
  if (this.buffer) {
565
679
  this.buffer.set(bufferSlot(this.store, key), { kind: "put", value: stored });
566
680
  return key;
@@ -568,7 +682,21 @@ var ApiStore = class {
568
682
  await request(this.http, "POST", "/tx", { body: { ops: [{ op: "put", store: this.store, value: stored }] } });
569
683
  return key;
570
684
  }
685
+ async putMany(values, _keys) {
686
+ if (!this.mutable) throw new Error("read-only store");
687
+ if (this.buffer) {
688
+ const keys2 = [];
689
+ for (const value of values) keys2.push(await this.put(value));
690
+ return keys2;
691
+ }
692
+ const stored = values.map((value) => this.getUserId ? stampUserOwned(this.store, value, this.getUserId()) : value);
693
+ const keys = stored.map((value) => keyFromValue(this.def, value));
694
+ await request(this.http, "POST", "/tx", { body: { ops: stored.map((value) => ({ op: "put", store: this.store, value })) } });
695
+ return keys;
696
+ }
571
697
  async delete(key) {
698
+ if (!this.mutable) throw new Error("read-only store");
699
+ if (this.store === "block_revisions") throw new Error("block history requires explicit wipe");
572
700
  const range = isRange(key) ? key : void 0;
573
701
  if (range) {
574
702
  if (this.buffer) {
@@ -590,6 +718,8 @@ var ApiStore = class {
590
718
  await remoteDelete(this.http, this.store, k);
591
719
  }
592
720
  async clear() {
721
+ if (!this.mutable) throw new Error("read-only store");
722
+ if (this.store === "block_revisions") throw new Error("block history requires explicit wipe");
593
723
  if (this.buffer) {
594
724
  this.cleared?.add(this.store);
595
725
  const prefix = `${this.store}\0`;
@@ -618,6 +748,11 @@ exports.keyFromValue = keyFromValue;
618
748
  exports.keyInRange = keyInRange;
619
749
  exports.parseDomainQueryResult = parseDomainQueryResult;
620
750
  exports.rangeFromIDB = rangeFromIDB;
751
+ exports.rowsEqual = rowsEqual;
621
752
  exports.stampUserOwned = stampUserOwned;
753
+ exports.validateNoteWrite = validateNoteWrite;
754
+ exports.validateSessionWrite = validateSessionWrite;
755
+ exports.validateStorageRow = validateStorageRow;
756
+ exports.validateStoredPins = validateStoredPins;
622
757
  //# sourceMappingURL=index.cjs.map
623
758
  //# sourceMappingURL=index.cjs.map