sqlite-zod-orm 3.6.1 → 3.6.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/dist/index.js CHANGED
@@ -4799,23 +4799,33 @@ class _Database {
4799
4799
  stopped = true;
4800
4800
  };
4801
4801
  }
4802
- _createChangeStream(entityName, eventType, callback, intervalOverride) {
4803
- const interval = intervalOverride ?? this.pollInterval;
4802
+ _watchers = new Map;
4803
+ _getWatcher(entityName) {
4804
+ if (this._watchers.has(entityName))
4805
+ return this._watchers.get(entityName);
4804
4806
  const allRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all();
4805
- let snapshot = new Map;
4806
- let snapshotEntities = new Map;
4807
+ const snapshot = new Map;
4808
+ const snapshotEntities = new Map;
4807
4809
  for (const row of allRows) {
4808
4810
  snapshot.set(row.id, JSON.stringify(row));
4809
4811
  snapshotEntities.set(row.id, row);
4810
4812
  }
4811
- let lastRevision = this._getRevision(entityName);
4812
- let stopped = false;
4813
+ const watcher = {
4814
+ updateListeners: new Set,
4815
+ deleteListeners: new Set,
4816
+ snapshot,
4817
+ snapshotEntities,
4818
+ lastRevision: this._getRevision(entityName),
4819
+ stopped: false
4820
+ };
4821
+ this._watchers.set(entityName, watcher);
4822
+ const interval = this.pollInterval;
4813
4823
  const poll = async () => {
4814
- if (stopped)
4824
+ if (watcher.stopped)
4815
4825
  return;
4816
4826
  const currentRevision = this._getRevision(entityName);
4817
- if (currentRevision !== lastRevision) {
4818
- lastRevision = currentRevision;
4827
+ if (currentRevision !== watcher.lastRevision) {
4828
+ watcher.lastRevision = currentRevision;
4819
4829
  const currentRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all();
4820
4830
  const currentMap = new Map;
4821
4831
  const currentEntities = new Map;
@@ -4823,35 +4833,57 @@ class _Database {
4823
4833
  currentMap.set(row.id, JSON.stringify(row));
4824
4834
  currentEntities.set(row.id, row);
4825
4835
  }
4826
- if (eventType === "update") {
4836
+ if (watcher.updateListeners.size > 0) {
4827
4837
  for (const [id, json] of currentMap) {
4828
- if (stopped)
4838
+ if (watcher.stopped)
4829
4839
  return;
4830
- if (snapshot.has(id) && snapshot.get(id) !== json) {
4840
+ if (watcher.snapshot.has(id) && watcher.snapshot.get(id) !== json) {
4831
4841
  const entity = this._attachMethods(entityName, transformFromStorage(currentEntities.get(id), this.schemas[entityName]));
4832
- const oldEntity = this._attachMethods(entityName, transformFromStorage(snapshotEntities.get(id), this.schemas[entityName]));
4833
- await callback(entity, oldEntity);
4842
+ const oldEntity = this._attachMethods(entityName, transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]));
4843
+ for (const cb of watcher.updateListeners) {
4844
+ await cb(entity, oldEntity);
4845
+ }
4834
4846
  }
4835
4847
  }
4836
- } else if (eventType === "delete") {
4837
- for (const [id] of snapshot) {
4838
- if (stopped)
4848
+ }
4849
+ if (watcher.deleteListeners.size > 0) {
4850
+ for (const [id] of watcher.snapshot) {
4851
+ if (watcher.stopped)
4839
4852
  return;
4840
4853
  if (!currentMap.has(id)) {
4841
- const oldEntity = this._attachMethods(entityName, transformFromStorage(snapshotEntities.get(id), this.schemas[entityName]));
4842
- await callback(oldEntity);
4854
+ const oldEntity = this._attachMethods(entityName, transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]));
4855
+ for (const cb of watcher.deleteListeners) {
4856
+ await cb(oldEntity);
4857
+ }
4843
4858
  }
4844
4859
  }
4845
4860
  }
4846
- snapshot = currentMap;
4847
- snapshotEntities = currentEntities;
4861
+ watcher.snapshot = currentMap;
4862
+ watcher.snapshotEntities = currentEntities;
4848
4863
  }
4849
- if (!stopped)
4864
+ if (!watcher.stopped)
4850
4865
  setTimeout(poll, interval);
4851
4866
  };
4852
4867
  setTimeout(poll, interval);
4868
+ return watcher;
4869
+ }
4870
+ _createChangeStream(entityName, eventType, callback, _intervalOverride) {
4871
+ const watcher = this._getWatcher(entityName);
4872
+ if (eventType === "update") {
4873
+ watcher.updateListeners.add(callback);
4874
+ } else {
4875
+ watcher.deleteListeners.add(callback);
4876
+ }
4853
4877
  return () => {
4854
- stopped = true;
4878
+ if (eventType === "update") {
4879
+ watcher.updateListeners.delete(callback);
4880
+ } else {
4881
+ watcher.deleteListeners.delete(callback);
4882
+ }
4883
+ if (watcher.updateListeners.size === 0 && watcher.deleteListeners.size === 0) {
4884
+ watcher.stopped = true;
4885
+ this._watchers.delete(entityName);
4886
+ }
4855
4887
  };
4856
4888
  }
4857
4889
  insert(entityName, data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.6.1",
3
+ "version": "3.6.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
@@ -204,41 +204,54 @@ class _Database<Schemas extends SchemaMap> {
204
204
  return () => { stopped = true; };
205
205
  }
206
206
 
207
+ // ===========================================================================
208
+ // Table Watcher — shared snapshot-diff engine for update/delete events
209
+ // ===========================================================================
210
+
211
+ /** One watcher per table — shared by all update/delete listeners. */
212
+ private _watchers = new Map<string, {
213
+ updateListeners: Set<(row: any, oldRow: any) => void | Promise<void>>;
214
+ deleteListeners: Set<(row: any) => void | Promise<void>>;
215
+ snapshot: Map<number, string>;
216
+ snapshotEntities: Map<number, any>;
217
+ lastRevision: string;
218
+ stopped: boolean;
219
+ }>();
220
+
207
221
  /**
208
- * Stream specific mutations (update or delete) with natural callback signatures.
209
- *
210
- * Maintains a full snapshot and diffs on each poll.
211
- * Only fires for the subscribed event type.
212
- *
213
- * - 'update': callback(row, oldRow)
214
- * - 'delete': callback(row)
222
+ * Get or create a shared table watcher. One SELECT * per change event,
223
+ * no matter how many update/delete listeners are registered.
215
224
  */
216
- public _createChangeStream(
217
- entityName: string,
218
- eventType: 'update' | 'delete',
219
- callback: (...args: any[]) => void | Promise<void>,
220
- intervalOverride?: number,
221
- ): () => void {
222
- const interval = intervalOverride ?? this.pollInterval;
225
+ private _getWatcher(entityName: string) {
226
+ if (this._watchers.has(entityName)) return this._watchers.get(entityName)!;
223
227
 
224
- // Build initial snapshot: Map<id, serialized row>
228
+ // Build initial snapshot
225
229
  const allRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all() as any[];
226
- let snapshot = new Map<number, string>();
227
- let snapshotEntities = new Map<number, any>();
230
+ const snapshot = new Map<number, string>();
231
+ const snapshotEntities = new Map<number, any>();
228
232
  for (const row of allRows) {
229
233
  snapshot.set(row.id, JSON.stringify(row));
230
234
  snapshotEntities.set(row.id, row);
231
235
  }
232
236
 
233
- let lastRevision: string = this._getRevision(entityName);
234
- let stopped = false;
237
+ const watcher = {
238
+ updateListeners: new Set<(row: any, oldRow: any) => void | Promise<void>>(),
239
+ deleteListeners: new Set<(row: any) => void | Promise<void>>(),
240
+ snapshot,
241
+ snapshotEntities,
242
+ lastRevision: this._getRevision(entityName),
243
+ stopped: false,
244
+ };
245
+ this._watchers.set(entityName, watcher);
235
246
 
247
+ // Single poll loop for all listeners on this table
248
+ const interval = this.pollInterval;
236
249
  const poll = async () => {
237
- if (stopped) return;
250
+ if (watcher.stopped) return;
238
251
 
239
252
  const currentRevision = this._getRevision(entityName);
240
- if (currentRevision !== lastRevision) {
241
- lastRevision = currentRevision;
253
+ if (currentRevision !== watcher.lastRevision) {
254
+ watcher.lastRevision = currentRevision;
242
255
 
243
256
  const currentRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all() as any[];
244
257
  const currentMap = new Map<number, string>();
@@ -248,46 +261,83 @@ class _Database<Schemas extends SchemaMap> {
248
261
  currentEntities.set(row.id, row);
249
262
  }
250
263
 
251
- if (eventType === 'update') {
252
- // Detect updates: existing rows whose JSON changed
264
+ // Dispatch updates
265
+ if (watcher.updateListeners.size > 0) {
253
266
  for (const [id, json] of currentMap) {
254
- if (stopped) return;
255
- if (snapshot.has(id) && snapshot.get(id) !== json) {
267
+ if (watcher.stopped) return;
268
+ if (watcher.snapshot.has(id) && watcher.snapshot.get(id) !== json) {
256
269
  const entity = this._attachMethods(
257
270
  entityName,
258
271
  transformFromStorage(currentEntities.get(id), this.schemas[entityName]!)
259
272
  );
260
273
  const oldEntity = this._attachMethods(
261
274
  entityName,
262
- transformFromStorage(snapshotEntities.get(id), this.schemas[entityName]!)
275
+ transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]!)
263
276
  );
264
- await callback(entity, oldEntity);
277
+ for (const cb of watcher.updateListeners) {
278
+ await cb(entity, oldEntity);
279
+ }
265
280
  }
266
281
  }
267
- } else if (eventType === 'delete') {
268
- // Detect deletes: rows in snapshot but not in current
269
- for (const [id] of snapshot) {
270
- if (stopped) return;
282
+ }
283
+
284
+ // Dispatch deletes
285
+ if (watcher.deleteListeners.size > 0) {
286
+ for (const [id] of watcher.snapshot) {
287
+ if (watcher.stopped) return;
271
288
  if (!currentMap.has(id)) {
272
289
  const oldEntity = this._attachMethods(
273
290
  entityName,
274
- transformFromStorage(snapshotEntities.get(id), this.schemas[entityName]!)
291
+ transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]!)
275
292
  );
276
- await callback(oldEntity);
293
+ for (const cb of watcher.deleteListeners) {
294
+ await cb(oldEntity);
295
+ }
277
296
  }
278
297
  }
279
298
  }
280
299
 
281
- snapshot = currentMap;
282
- snapshotEntities = currentEntities;
300
+ watcher.snapshot = currentMap;
301
+ watcher.snapshotEntities = currentEntities;
283
302
  }
284
303
 
285
- if (!stopped) setTimeout(poll, interval);
304
+ if (!watcher.stopped) setTimeout(poll, interval);
286
305
  };
287
306
 
288
307
  setTimeout(poll, interval);
308
+ return watcher;
309
+ }
289
310
 
290
- return () => { stopped = true; };
311
+ /**
312
+ * Register an update or delete listener on the shared table watcher.
313
+ * Returns an unsubscribe function that auto-stops the watcher when empty.
314
+ */
315
+ public _createChangeStream(
316
+ entityName: string,
317
+ eventType: 'update' | 'delete',
318
+ callback: (...args: any[]) => void | Promise<void>,
319
+ _intervalOverride?: number, // reserved, uses global pollInterval
320
+ ): () => void {
321
+ const watcher = this._getWatcher(entityName);
322
+
323
+ if (eventType === 'update') {
324
+ watcher.updateListeners.add(callback as any);
325
+ } else {
326
+ watcher.deleteListeners.add(callback as any);
327
+ }
328
+
329
+ return () => {
330
+ if (eventType === 'update') {
331
+ watcher.updateListeners.delete(callback as any);
332
+ } else {
333
+ watcher.deleteListeners.delete(callback as any);
334
+ }
335
+ // Auto-stop watcher when all listeners removed
336
+ if (watcher.updateListeners.size === 0 && watcher.deleteListeners.size === 0) {
337
+ watcher.stopped = true;
338
+ this._watchers.delete(entityName);
339
+ }
340
+ };
291
341
  }
292
342
 
293
343
  // ===========================================================================