@peerbit/indexer-sqlite3 2.1.2 → 3.0.0-b712c6b

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/indexer-sqlite3",
3
- "version": "2.1.2",
3
+ "version": "3.0.0-b712c6b",
4
4
  "description": "SQLite index for document store",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -61,20 +61,20 @@
61
61
  "dependencies": {
62
62
  "better-sqlite3": "^12.5.0",
63
63
  "@dao-xyz/borsh": "^6.0.0",
64
+ "@peerbit/indexer-interface": "3.0.0-b712c6b",
65
+ "@peerbit/crypto": "3.0.0-b712c6b",
66
+ "@peerbit/time": "3.0.0-b712c6b",
64
67
  "@sqlite.org/sqlite-wasm": "^3.51.1-build2",
65
68
  "p-defer": "^4.0.0",
66
69
  "uint8arrays": "^5.1.0",
67
70
  "uuid": "^10.0.0",
68
- "libsodium-wrappers": "0.7.15",
69
- "@peerbit/indexer-interface": "2.1.1",
70
- "@peerbit/crypto": "2.4.1",
71
- "@peerbit/time": "2.3.0"
71
+ "libsodium-wrappers": "0.7.15"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/better-sqlite3": "^7.6.13",
75
- "esbuild": "0.27.0",
76
- "@peerbit/indexer-tests": "2.0.1",
77
- "@peerbit/build-assets": "1.1.0"
75
+ "@peerbit/indexer-tests": "3.0.0-b712c6b",
76
+ "@peerbit/build-assets": "1.1.0-b712c6b",
77
+ "esbuild": "0.27.0"
78
78
  },
79
79
  "scripts": {
80
80
  "clean": "aegir clean",
package/src/engine.ts CHANGED
@@ -87,6 +87,38 @@ async function getIgnoreFK(stmt: Statement, values: any[]) {
87
87
  export class SQLLiteIndex<T extends Record<string, any>>
88
88
  implements Index<T, any>
89
89
  {
90
+ // SQLite writes are inherently serialized per connection.
91
+ // We still need an explicit async barrier because our API is async and
92
+ // awaits between statements (root insert -> many child inserts). Without
93
+ // a barrier, concurrent `put()` and `del()` can interleave mid-insert and
94
+ // create large volumes of FK constraint noise (and occasional timeouts in
95
+ // browser/webworker runners).
96
+ // TODO(perf): This is intentionally coarse-grained for correctness.
97
+ // Possible optimizations:
98
+ // 1) wrap nested writes in explicit transactions to reduce lock time;
99
+ // 2) use table/key-scoped write queues when overlap detection is available.
100
+ // Any relaxation must keep concurrent put/del stability across all runners.
101
+ private _writeBarrier: Promise<void> = Promise.resolve();
102
+
103
+ private async withWriteBarrier<R>(fn: () => Promise<R>): Promise<R> {
104
+ const prev = this._writeBarrier;
105
+ let release!: () => void;
106
+ const next = new Promise<void>((r) => (release = r));
107
+ // Keep the chain alive even if `prev` rejected.
108
+ this._writeBarrier = prev.then(
109
+ () => next,
110
+ () => next,
111
+ );
112
+
113
+ // Wait for previous writer without propagating its error.
114
+ await prev.catch(() => undefined);
115
+ try {
116
+ return await fn();
117
+ } finally {
118
+ release();
119
+ }
120
+ }
121
+
90
122
  primaryKeyArr!: string[];
91
123
  primaryKeyString!: string;
92
124
  planner: QueryPlanner;
@@ -397,52 +429,54 @@ export class SQLLiteIndex<T extends Record<string, any>>
397
429
  }
398
430
 
399
431
  async put(value: T, _id?: any): Promise<void> {
400
- const classOfValue = value.constructor as Constructor<T>;
401
- return insert(
402
- async (values, table) => {
403
- let preId = values[table.primaryIndex];
404
- let statement: Statement | undefined = undefined;
405
- try {
406
- if (preId != null) {
407
- statement = this.properties.db.statements.get(
408
- replaceStatementKey(table),
409
- )!;
410
- this.fkMode === "race-tolerant"
411
- ? await runIgnoreFK(statement, values)
412
- : await statement.run(values);
413
- return preId;
414
- } else {
415
- statement = this.properties.db.statements.get(
416
- putStatementKey(table),
417
- )!;
418
- const out =
432
+ return this.withWriteBarrier(async () => {
433
+ const classOfValue = value.constructor as Constructor<T>;
434
+ return insert(
435
+ async (values, table) => {
436
+ let preId = values[table.primaryIndex];
437
+ let statement: Statement | undefined = undefined;
438
+ try {
439
+ if (preId != null) {
440
+ statement = this.properties.db.statements.get(
441
+ replaceStatementKey(table),
442
+ )!;
419
443
  this.fkMode === "race-tolerant"
420
- ? await getIgnoreFK(statement, values)
421
- : await statement.get(values);
422
-
423
- // TODO types
424
- if (out == null) {
425
- return undefined;
444
+ ? await runIgnoreFK(statement, values)
445
+ : await statement.run(values);
446
+ return preId;
447
+ } else {
448
+ statement = this.properties.db.statements.get(
449
+ putStatementKey(table),
450
+ )!;
451
+ const out =
452
+ this.fkMode === "race-tolerant"
453
+ ? await getIgnoreFK(statement, values)
454
+ : await statement.get(values);
455
+
456
+ // TODO types
457
+ if (out == null) {
458
+ return undefined;
459
+ }
460
+ return out[table.primary as string];
426
461
  }
427
- return out[table.primary as string];
462
+ } finally {
463
+ await statement?.reset?.();
428
464
  }
429
- } finally {
430
- await statement?.reset?.();
431
- }
432
- },
433
- value,
434
- this.tables,
435
- resolveTable(
436
- this.scopeString ? [this.scopeString] : [],
465
+ },
466
+ value,
437
467
  this.tables,
438
- classOfValue,
439
- true,
440
- ),
441
- getSchema(classOfValue).fields,
442
- (_fn) => {
443
- throw new Error("Unexpected");
444
- },
445
- );
468
+ resolveTable(
469
+ this.scopeString ? [this.scopeString] : [],
470
+ this.tables,
471
+ classOfValue,
472
+ true,
473
+ ),
474
+ getSchema(classOfValue).fields,
475
+ (_fn) => {
476
+ throw new Error("Unexpected");
477
+ },
478
+ );
479
+ });
446
480
  }
447
481
 
448
482
  iterate<S extends Shape | undefined>(
@@ -629,46 +663,48 @@ export class SQLLiteIndex<T extends Record<string, any>>
629
663
  }
630
664
 
631
665
  async del(query: types.DeleteOptions): Promise<types.IdKey[]> {
632
- let ret: types.IdKey[] = [];
633
- let once = false;
634
- let lastError: Error | undefined = undefined;
635
- for (const table of this._rootTables) {
636
- try {
637
- const { sql, bindable } = convertDeleteRequestToQuery(
638
- query,
639
- this.tables,
640
- table,
641
- );
642
- const stmt = await this.properties.db.prepare(sql, sql);
643
- const results: any[] = await stmt.all(bindable);
644
-
645
- // TODO types
646
- for (const result of results) {
647
- ret.push(
648
- types.toId(
649
- convertFromSQLType(
650
- result[table.primary as string],
651
- table.primaryField!.from!.type,
652
- ),
653
- ),
666
+ return this.withWriteBarrier(async () => {
667
+ let ret: types.IdKey[] = [];
668
+ let once = false;
669
+ let lastError: Error | undefined = undefined;
670
+ for (const table of this._rootTables) {
671
+ try {
672
+ const { sql, bindable } = convertDeleteRequestToQuery(
673
+ query,
674
+ this.tables,
675
+ table,
654
676
  );
655
- }
656
- once = true;
657
- } catch (error) {
658
- if (error instanceof MissingFieldError) {
659
- lastError = error;
660
- continue;
661
- }
677
+ const stmt = await this.properties.db.prepare(sql, sql);
678
+ const results: any[] = await stmt.all(bindable);
662
679
 
663
- throw error;
680
+ // TODO types
681
+ for (const result of results) {
682
+ ret.push(
683
+ types.toId(
684
+ convertFromSQLType(
685
+ result[table.primary as string],
686
+ table.primaryField!.from!.type,
687
+ ),
688
+ ),
689
+ );
690
+ }
691
+ once = true;
692
+ } catch (error) {
693
+ if (error instanceof MissingFieldError) {
694
+ lastError = error;
695
+ continue;
696
+ }
697
+
698
+ throw error;
699
+ }
664
700
  }
665
- }
666
701
 
667
- if (!once) {
668
- throw lastError!;
669
- }
702
+ if (!once) {
703
+ throw lastError!;
704
+ }
670
705
 
671
- return ret;
706
+ return ret;
707
+ });
672
708
  }
673
709
 
674
710
  async sum(query: types.SumOptions): Promise<number | bigint> {
package/src/schema.ts CHANGED
@@ -222,7 +222,13 @@ const resolvePrimaryFieldInfoFromSchema = (
222
222
  }
223
223
 
224
224
  if (typeof fieldType === "string" || isUint8ArrayType(fieldType)) {
225
- const sqlField = createScalarSQLField(path, field, fieldType, primary, true);
225
+ const sqlField = createScalarSQLField(
226
+ path,
227
+ field,
228
+ fieldType,
229
+ primary,
230
+ true,
231
+ );
226
232
  if (sqlField.isPrimary) {
227
233
  return {
228
234
  name: sqlField.name,
@@ -545,7 +551,13 @@ export const getSQLFields = (
545
551
  type: FieldType,
546
552
  isOptional: boolean,
547
553
  ) => {
548
- const sqlField = createScalarSQLField(path, field, type, primary, isOptional);
554
+ const sqlField = createScalarSQLField(
555
+ path,
556
+ field,
557
+ type,
558
+ primary,
559
+ isOptional,
560
+ );
549
561
  foundPrimary = foundPrimary || sqlField.isPrimary;
550
562
  sqlFields.push(sqlField);
551
563
  };