sqlite-zod-orm 3.5.0 → 3.5.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.
package/README.md CHANGED
@@ -41,6 +41,7 @@ const db = new Database(':memory:', {
41
41
  relations: {
42
42
  books: { author_id: 'authors' },
43
43
  },
44
+ pollInterval: 300, // global default for .on() and .subscribe() (default: 500ms)
44
45
  });
45
46
  ```
46
47
 
package/dist/index.js CHANGED
@@ -177,7 +177,8 @@ class QueryBuilder {
177
177
  conditionResolver;
178
178
  revisionGetter;
179
179
  eagerLoader;
180
- constructor(tableName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader) {
180
+ defaultPollInterval;
181
+ constructor(tableName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader, pollInterval) {
181
182
  this.tableName = tableName;
182
183
  this.executor = executor;
183
184
  this.singleExecutor = singleExecutor;
@@ -185,6 +186,7 @@ class QueryBuilder {
185
186
  this.conditionResolver = conditionResolver ?? null;
186
187
  this.revisionGetter = revisionGetter ?? null;
187
188
  this.eagerLoader = eagerLoader ?? null;
189
+ this.defaultPollInterval = pollInterval ?? 500;
188
190
  this.iqo = {
189
191
  selects: [],
190
192
  wheres: [],
@@ -357,7 +359,7 @@ class QueryBuilder {
357
359
  return results[0]?.count ?? 0;
358
360
  }
359
361
  subscribe(callback, options = {}) {
360
- const { interval = 500, immediate = true } = options;
362
+ const { interval = this.defaultPollInterval, immediate = true } = options;
361
363
  const fingerprintSQL = this.buildFingerprintSQL();
362
364
  let lastFingerprint = null;
363
365
  const poll = () => {
@@ -4686,6 +4688,7 @@ class _Database {
4686
4688
  schemas;
4687
4689
  relationships;
4688
4690
  options;
4691
+ pollInterval;
4689
4692
  _revisions = {};
4690
4693
  constructor(dbFile, schemas, options = {}) {
4691
4694
  this.db = new SqliteDatabase(dbFile);
@@ -4693,6 +4696,7 @@ class _Database {
4693
4696
  this.db.run("PRAGMA foreign_keys = ON");
4694
4697
  this.schemas = schemas;
4695
4698
  this.options = options;
4699
+ this.pollInterval = options.pollInterval ?? 500;
4696
4700
  this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
4697
4701
  this.initializeTables();
4698
4702
  this.runMigrations();
@@ -4710,7 +4714,7 @@ class _Database {
4710
4714
  upsert: (conditions, data) => this.upsert(entityName, data, conditions),
4711
4715
  delete: (id) => this.delete(entityName, id),
4712
4716
  select: (...cols) => this._createQueryBuilder(entityName, cols),
4713
- on: (callback, options2) => this._createOnStream(entityName, callback, options2),
4717
+ on: (callback, options2) => this._createOnStream(entityName, callback, options2?.interval),
4714
4718
  _tableName: entityName
4715
4719
  };
4716
4720
  this[key] = accessor;
@@ -4760,24 +4764,34 @@ class _Database {
4760
4764
  const dataVersion = this.db.query("PRAGMA data_version").get()?.data_version ?? 0;
4761
4765
  return `${rev}:${dataVersion}`;
4762
4766
  }
4763
- _createOnStream(entityName, callback, options) {
4764
- const { interval = 500 } = options ?? {};
4767
+ _createOnStream(entityName, callback, intervalOverride) {
4768
+ const interval = intervalOverride ?? this.pollInterval;
4765
4769
  const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get();
4766
4770
  let lastMaxId = maxRow?._max ?? 0;
4767
4771
  let lastRevision = this._getRevision(entityName);
4768
- const timer = setInterval(() => {
4769
- const currentRevision = this._getRevision(entityName);
4770
- if (currentRevision === lastRevision)
4772
+ let stopped = false;
4773
+ const poll = async () => {
4774
+ if (stopped)
4771
4775
  return;
4772
- lastRevision = currentRevision;
4773
- const newRows = this.db.query(`SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`).all(lastMaxId);
4774
- for (const rawRow of newRows) {
4775
- const entity = this._attachMethods(entityName, transformFromStorage(rawRow, this.schemas[entityName]));
4776
- callback(entity);
4777
- lastMaxId = rawRow.id;
4776
+ const currentRevision = this._getRevision(entityName);
4777
+ if (currentRevision !== lastRevision) {
4778
+ lastRevision = currentRevision;
4779
+ const newRows = this.db.query(`SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`).all(lastMaxId);
4780
+ for (const rawRow of newRows) {
4781
+ if (stopped)
4782
+ return;
4783
+ const entity = this._attachMethods(entityName, transformFromStorage(rawRow, this.schemas[entityName]));
4784
+ await callback(entity);
4785
+ lastMaxId = rawRow.id;
4786
+ }
4778
4787
  }
4779
- }, interval);
4780
- return () => clearInterval(timer);
4788
+ if (!stopped)
4789
+ setTimeout(poll, interval);
4790
+ };
4791
+ setTimeout(poll, interval);
4792
+ return () => {
4793
+ stopped = true;
4794
+ };
4781
4795
  }
4782
4796
  insert(entityName, data) {
4783
4797
  const schema = this.schemas[entityName];
@@ -5034,7 +5048,7 @@ class _Database {
5034
5048
  }
5035
5049
  return null;
5036
5050
  };
5037
- const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader);
5051
+ const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader, this.pollInterval);
5038
5052
  if (initialCols.length > 0)
5039
5053
  builder.select(...initialCols);
5040
5054
  return builder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/database.ts CHANGED
@@ -29,6 +29,7 @@ class _Database<Schemas extends SchemaMap> {
29
29
  private schemas: Schemas;
30
30
  private relationships: Relationship[];
31
31
  private options: DatabaseOptions;
32
+ private pollInterval: number;
32
33
 
33
34
  /** In-memory revision counter per table — bumps on every write (insert/update/delete).
34
35
  * Used by QueryBuilder.subscribe() fingerprint to detect ALL changes with zero overhead. */
@@ -40,6 +41,7 @@ class _Database<Schemas extends SchemaMap> {
40
41
  this.db.run('PRAGMA foreign_keys = ON');
41
42
  this.schemas = schemas;
42
43
  this.options = options;
44
+ this.pollInterval = options.pollInterval ?? 500;
43
45
  this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
44
46
  this.initializeTables();
45
47
  this.runMigrations();
@@ -57,8 +59,8 @@ class _Database<Schemas extends SchemaMap> {
57
59
  upsert: (conditions, data) => this.upsert(entityName, data, conditions),
58
60
  delete: (id) => this.delete(entityName, id),
59
61
  select: (...cols: string[]) => this._createQueryBuilder(entityName, cols),
60
- on: (callback: (row: any) => void, options?: { interval?: number }) =>
61
- this._createOnStream(entityName, callback, options),
62
+ on: (callback: (row: any) => void | Promise<void>, options?: { interval?: number }) =>
63
+ this._createOnStream(entityName, callback, options?.interval),
62
64
  _tableName: entityName,
63
65
  };
64
66
  (this as any)[key] = accessor;
@@ -151,39 +153,52 @@ class _Database<Schemas extends SchemaMap> {
151
153
  */
152
154
  public _createOnStream(
153
155
  entityName: string,
154
- callback: (row: any) => void,
155
- options?: { interval?: number },
156
+ callback: (row: any) => void | Promise<void>,
157
+ intervalOverride?: number,
156
158
  ): () => void {
157
- const { interval = 500 } = options ?? {};
159
+ const interval = intervalOverride ?? this.pollInterval;
158
160
 
159
161
  // Initialize watermark to current max id (only emit NEW rows)
160
162
  const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get() as any;
161
163
  let lastMaxId: number = maxRow?._max ?? 0;
162
164
  let lastRevision: string = this._getRevision(entityName);
165
+ let stopped = false;
166
+
167
+ // Self-scheduling async loop: guarantees strict ordering
168
+ // - Each callback (sync or async) completes before the next row is emitted
169
+ // - Next poll only starts after the current batch is fully processed
170
+ const poll = async () => {
171
+ if (stopped) return;
163
172
 
164
- const timer = setInterval(() => {
165
173
  // Fast check: did anything change?
166
174
  const currentRevision = this._getRevision(entityName);
167
- if (currentRevision === lastRevision) return;
168
- lastRevision = currentRevision;
169
-
170
- // Fetch new rows since watermark
171
- const newRows = this.db.query(
172
- `SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`
173
- ).all(lastMaxId) as any[];
174
-
175
- for (const rawRow of newRows) {
176
- // Hydrate with entity methods and schema transforms
177
- const entity = this._attachMethods(
178
- entityName,
179
- transformFromStorage(rawRow, this.schemas[entityName]!)
180
- );
181
- callback(entity);
182
- lastMaxId = rawRow.id;
175
+ if (currentRevision !== lastRevision) {
176
+ lastRevision = currentRevision;
177
+
178
+ // Fetch new rows since watermark
179
+ const newRows = this.db.query(
180
+ `SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`
181
+ ).all(lastMaxId) as any[];
182
+
183
+ for (const rawRow of newRows) {
184
+ if (stopped) return; // bail if unsubscribed mid-batch
185
+ const entity = this._attachMethods(
186
+ entityName,
187
+ transformFromStorage(rawRow, this.schemas[entityName]!)
188
+ );
189
+ await callback(entity); // await async callbacks
190
+ lastMaxId = rawRow.id;
191
+ }
183
192
  }
184
- }, interval);
185
193
 
186
- return () => clearInterval(timer);
194
+ // Schedule next poll only after this one is done
195
+ if (!stopped) setTimeout(poll, interval);
196
+ };
197
+
198
+ // Start the loop
199
+ setTimeout(poll, interval);
200
+
201
+ return () => { stopped = true; };
187
202
  }
188
203
 
189
204
  // ===========================================================================
@@ -534,7 +549,7 @@ class _Database<Schemas extends SchemaMap> {
534
549
  return null;
535
550
  };
536
551
 
537
- const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader);
552
+ const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader, this.pollInterval);
538
553
  if (initialCols.length > 0) builder.select(...initialCols);
539
554
  return builder;
540
555
  }
@@ -174,6 +174,7 @@ export class QueryBuilder<T extends Record<string, any>> {
174
174
  private conditionResolver: ((conditions: Record<string, any>) => Record<string, any>) | null;
175
175
  private revisionGetter: (() => string) | null;
176
176
  private eagerLoader: ((parentTable: string, relation: string, parentIds: number[]) => { key: string; groups: Map<number, any[]> } | null) | null;
177
+ private defaultPollInterval: number;
177
178
 
178
179
  constructor(
179
180
  tableName: string,
@@ -183,6 +184,7 @@ export class QueryBuilder<T extends Record<string, any>> {
183
184
  conditionResolver?: ((conditions: Record<string, any>) => Record<string, any>) | null,
184
185
  revisionGetter?: (() => string) | null,
185
186
  eagerLoader?: ((parentTable: string, relation: string, parentIds: number[]) => { key: string; groups: Map<number, any[]> } | null) | null,
187
+ pollInterval?: number,
186
188
  ) {
187
189
  this.tableName = tableName;
188
190
  this.executor = executor;
@@ -191,6 +193,7 @@ export class QueryBuilder<T extends Record<string, any>> {
191
193
  this.conditionResolver = conditionResolver ?? null;
192
194
  this.revisionGetter = revisionGetter ?? null;
193
195
  this.eagerLoader = eagerLoader ?? null;
196
+ this.defaultPollInterval = pollInterval ?? 500;
194
197
  this.iqo = {
195
198
  selects: [],
196
199
  wheres: [],
@@ -497,7 +500,7 @@ export class QueryBuilder<T extends Record<string, any>> {
497
500
  callback: (rows: T[]) => void,
498
501
  options: { interval?: number; immediate?: boolean } = {},
499
502
  ): () => void {
500
- const { interval = 500, immediate = true } = options;
503
+ const { interval = this.defaultPollInterval, immediate = true } = options;
501
504
 
502
505
  // Build the fingerprint SQL (COUNT + MAX(id)) using the same WHERE
503
506
  const fingerprintSQL = this.buildFingerprintSQL();
package/src/types.ts CHANGED
@@ -30,6 +30,11 @@ export type DatabaseOptions<R extends RelationsConfig = RelationsConfig> = {
30
30
  * `books: { author_id: 'authors' }` → FOREIGN KEY, lazy nav, fluent join.
31
31
  */
32
32
  relations?: R;
33
+ /**
34
+ * Global polling interval (ms) for `.on()` and `.subscribe()`.
35
+ * Can be overridden per-call. Default: 500ms.
36
+ */
37
+ pollInterval?: number;
33
38
  };
34
39
 
35
40
  export type Relationship = {
@@ -146,9 +151,10 @@ export type NavEntityAccessor<
146
151
  /**
147
152
  * Stream new rows one at a time, in insertion order.
148
153
  * Only emits rows inserted AFTER subscription starts.
154
+ * Callbacks are awaited — strict ordering guaranteed even with async handlers.
149
155
  * @returns Unsubscribe function.
150
156
  */
151
- on: (callback: (row: NavEntity<S, R, Table>) => void, options?: { interval?: number }) => () => void;
157
+ on: (callback: (row: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }) => () => void;
152
158
  _tableName: string;
153
159
  readonly _schema?: S[Table & keyof S];
154
160
  };
@@ -177,9 +183,10 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
177
183
  /**
178
184
  * Stream new rows one at a time, in insertion order.
179
185
  * Only emits rows inserted AFTER subscription starts.
186
+ * Callbacks are awaited — strict ordering guaranteed even with async handlers.
180
187
  * @returns Unsubscribe function.
181
188
  */
182
- on: (callback: (row: AugmentedEntity<S>) => void, options?: { interval?: number }) => () => void;
189
+ on: (callback: (row: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }) => () => void;
183
190
  _tableName: string;
184
191
  readonly _schema?: S;
185
192
  };