sqlite-zod-orm 3.6.2 → 3.7.1
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 +28 -123
- package/package.json +1 -1
- package/src/database.ts +0 -201
- package/src/query-builder.ts +59 -0
- package/src/types.ts +0 -30
package/dist/index.js
CHANGED
|
@@ -383,6 +383,34 @@ class QueryBuilder {
|
|
|
383
383
|
clearInterval(timer);
|
|
384
384
|
};
|
|
385
385
|
}
|
|
386
|
+
each(callback, options = {}) {
|
|
387
|
+
const { interval = this.defaultPollInterval } = options;
|
|
388
|
+
const maxRows = this.executor(`SELECT MAX(id) as _max FROM ${this.tableName}`, [], true);
|
|
389
|
+
let lastMaxId = maxRows[0]?._max ?? 0;
|
|
390
|
+
let lastRevision = this.revisionGetter?.() ?? "0";
|
|
391
|
+
let stopped = false;
|
|
392
|
+
const poll = async () => {
|
|
393
|
+
if (stopped)
|
|
394
|
+
return;
|
|
395
|
+
const rev = this.revisionGetter?.() ?? "0";
|
|
396
|
+
if (rev !== lastRevision) {
|
|
397
|
+
lastRevision = rev;
|
|
398
|
+
const newRows = this.executor(`SELECT * FROM ${this.tableName} WHERE id > ? ORDER BY id ASC`, [lastMaxId], true);
|
|
399
|
+
for (const rawRow of newRows) {
|
|
400
|
+
if (stopped)
|
|
401
|
+
return;
|
|
402
|
+
await callback(rawRow);
|
|
403
|
+
lastMaxId = rawRow.id;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!stopped)
|
|
407
|
+
setTimeout(poll, interval);
|
|
408
|
+
};
|
|
409
|
+
setTimeout(poll, interval);
|
|
410
|
+
return () => {
|
|
411
|
+
stopped = true;
|
|
412
|
+
};
|
|
413
|
+
}
|
|
386
414
|
buildFingerprintSQL() {
|
|
387
415
|
const params = [];
|
|
388
416
|
let sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}`;
|
|
@@ -4714,13 +4742,6 @@ class _Database {
|
|
|
4714
4742
|
upsert: (conditions, data) => this.upsert(entityName, data, conditions),
|
|
4715
4743
|
delete: (id) => this.delete(entityName, id),
|
|
4716
4744
|
select: (...cols) => this._createQueryBuilder(entityName, cols),
|
|
4717
|
-
on: (event, callback, options2) => {
|
|
4718
|
-
if (event === "insert")
|
|
4719
|
-
return this._createOnStream(entityName, callback, options2?.interval);
|
|
4720
|
-
if (event === "update" || event === "delete")
|
|
4721
|
-
return this._createChangeStream(entityName, event, callback, options2?.interval);
|
|
4722
|
-
throw new Error(`Unknown event type: '${event}'. Supported: 'insert', 'update', 'delete'`);
|
|
4723
|
-
},
|
|
4724
4745
|
_tableName: entityName
|
|
4725
4746
|
};
|
|
4726
4747
|
this[key] = accessor;
|
|
@@ -4770,122 +4791,6 @@ class _Database {
|
|
|
4770
4791
|
const dataVersion = this.db.query("PRAGMA data_version").get()?.data_version ?? 0;
|
|
4771
4792
|
return `${rev}:${dataVersion}`;
|
|
4772
4793
|
}
|
|
4773
|
-
_createOnStream(entityName, callback, intervalOverride) {
|
|
4774
|
-
const interval = intervalOverride ?? this.pollInterval;
|
|
4775
|
-
const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get();
|
|
4776
|
-
let lastMaxId = maxRow?._max ?? 0;
|
|
4777
|
-
let lastRevision = this._getRevision(entityName);
|
|
4778
|
-
let stopped = false;
|
|
4779
|
-
const poll = async () => {
|
|
4780
|
-
if (stopped)
|
|
4781
|
-
return;
|
|
4782
|
-
const currentRevision = this._getRevision(entityName);
|
|
4783
|
-
if (currentRevision !== lastRevision) {
|
|
4784
|
-
lastRevision = currentRevision;
|
|
4785
|
-
const newRows = this.db.query(`SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`).all(lastMaxId);
|
|
4786
|
-
for (const rawRow of newRows) {
|
|
4787
|
-
if (stopped)
|
|
4788
|
-
return;
|
|
4789
|
-
const entity = this._attachMethods(entityName, transformFromStorage(rawRow, this.schemas[entityName]));
|
|
4790
|
-
await callback(entity);
|
|
4791
|
-
lastMaxId = rawRow.id;
|
|
4792
|
-
}
|
|
4793
|
-
}
|
|
4794
|
-
if (!stopped)
|
|
4795
|
-
setTimeout(poll, interval);
|
|
4796
|
-
};
|
|
4797
|
-
setTimeout(poll, interval);
|
|
4798
|
-
return () => {
|
|
4799
|
-
stopped = true;
|
|
4800
|
-
};
|
|
4801
|
-
}
|
|
4802
|
-
_watchers = new Map;
|
|
4803
|
-
_getWatcher(entityName) {
|
|
4804
|
-
if (this._watchers.has(entityName))
|
|
4805
|
-
return this._watchers.get(entityName);
|
|
4806
|
-
const allRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all();
|
|
4807
|
-
const snapshot = new Map;
|
|
4808
|
-
const snapshotEntities = new Map;
|
|
4809
|
-
for (const row of allRows) {
|
|
4810
|
-
snapshot.set(row.id, JSON.stringify(row));
|
|
4811
|
-
snapshotEntities.set(row.id, row);
|
|
4812
|
-
}
|
|
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;
|
|
4823
|
-
const poll = async () => {
|
|
4824
|
-
if (watcher.stopped)
|
|
4825
|
-
return;
|
|
4826
|
-
const currentRevision = this._getRevision(entityName);
|
|
4827
|
-
if (currentRevision !== watcher.lastRevision) {
|
|
4828
|
-
watcher.lastRevision = currentRevision;
|
|
4829
|
-
const currentRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all();
|
|
4830
|
-
const currentMap = new Map;
|
|
4831
|
-
const currentEntities = new Map;
|
|
4832
|
-
for (const row of currentRows) {
|
|
4833
|
-
currentMap.set(row.id, JSON.stringify(row));
|
|
4834
|
-
currentEntities.set(row.id, row);
|
|
4835
|
-
}
|
|
4836
|
-
if (watcher.updateListeners.size > 0) {
|
|
4837
|
-
for (const [id, json] of currentMap) {
|
|
4838
|
-
if (watcher.stopped)
|
|
4839
|
-
return;
|
|
4840
|
-
if (watcher.snapshot.has(id) && watcher.snapshot.get(id) !== json) {
|
|
4841
|
-
const entity = this._attachMethods(entityName, transformFromStorage(currentEntities.get(id), this.schemas[entityName]));
|
|
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
|
-
}
|
|
4846
|
-
}
|
|
4847
|
-
}
|
|
4848
|
-
}
|
|
4849
|
-
if (watcher.deleteListeners.size > 0) {
|
|
4850
|
-
for (const [id] of watcher.snapshot) {
|
|
4851
|
-
if (watcher.stopped)
|
|
4852
|
-
return;
|
|
4853
|
-
if (!currentMap.has(id)) {
|
|
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
|
-
}
|
|
4858
|
-
}
|
|
4859
|
-
}
|
|
4860
|
-
}
|
|
4861
|
-
watcher.snapshot = currentMap;
|
|
4862
|
-
watcher.snapshotEntities = currentEntities;
|
|
4863
|
-
}
|
|
4864
|
-
if (!watcher.stopped)
|
|
4865
|
-
setTimeout(poll, interval);
|
|
4866
|
-
};
|
|
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
|
-
}
|
|
4877
|
-
return () => {
|
|
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
|
-
}
|
|
4887
|
-
};
|
|
4888
|
-
}
|
|
4889
4794
|
insert(entityName, data) {
|
|
4890
4795
|
const schema = this.schemas[entityName];
|
|
4891
4796
|
const validatedData = asZodObject(schema).passthrough().parse(data);
|
package/package.json
CHANGED
package/src/database.ts
CHANGED
|
@@ -59,11 +59,6 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
59
59
|
upsert: (conditions, data) => this.upsert(entityName, data, conditions),
|
|
60
60
|
delete: (id) => this.delete(entityName, id),
|
|
61
61
|
select: (...cols: string[]) => this._createQueryBuilder(entityName, cols),
|
|
62
|
-
on: (event: string, callback: (...args: any[]) => void | Promise<void>, options?: { interval?: number }) => {
|
|
63
|
-
if (event === 'insert') return this._createOnStream(entityName, callback, options?.interval);
|
|
64
|
-
if (event === 'update' || event === 'delete') return this._createChangeStream(entityName, event, callback, options?.interval);
|
|
65
|
-
throw new Error(`Unknown event type: '${event}'. Supported: 'insert', 'update', 'delete'`);
|
|
66
|
-
},
|
|
67
62
|
_tableName: entityName,
|
|
68
63
|
};
|
|
69
64
|
(this as any)[key] = accessor;
|
|
@@ -144,202 +139,6 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
144
139
|
return `${rev}:${dataVersion}`;
|
|
145
140
|
}
|
|
146
141
|
|
|
147
|
-
// ===========================================================================
|
|
148
|
-
// Row Stream — .on(callback)
|
|
149
|
-
// ===========================================================================
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Stream new rows one at a time, in insertion order.
|
|
153
|
-
*
|
|
154
|
-
* Uses a watermark (last seen id) to query only `WHERE id > ?`.
|
|
155
|
-
* Checks revision + data_version first to avoid unnecessary queries.
|
|
156
|
-
*/
|
|
157
|
-
public _createOnStream(
|
|
158
|
-
entityName: string,
|
|
159
|
-
callback: (row: any) => void | Promise<void>,
|
|
160
|
-
intervalOverride?: number,
|
|
161
|
-
): () => void {
|
|
162
|
-
const interval = intervalOverride ?? this.pollInterval;
|
|
163
|
-
|
|
164
|
-
// Initialize watermark to current max id (only emit NEW rows)
|
|
165
|
-
const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get() as any;
|
|
166
|
-
let lastMaxId: number = maxRow?._max ?? 0;
|
|
167
|
-
let lastRevision: string = this._getRevision(entityName);
|
|
168
|
-
let stopped = false;
|
|
169
|
-
|
|
170
|
-
// Self-scheduling async loop: guarantees strict ordering
|
|
171
|
-
// - Each callback (sync or async) completes before the next row is emitted
|
|
172
|
-
// - Next poll only starts after the current batch is fully processed
|
|
173
|
-
const poll = async () => {
|
|
174
|
-
if (stopped) return;
|
|
175
|
-
|
|
176
|
-
// Fast check: did anything change?
|
|
177
|
-
const currentRevision = this._getRevision(entityName);
|
|
178
|
-
if (currentRevision !== lastRevision) {
|
|
179
|
-
lastRevision = currentRevision;
|
|
180
|
-
|
|
181
|
-
// Fetch new rows since watermark
|
|
182
|
-
const newRows = this.db.query(
|
|
183
|
-
`SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`
|
|
184
|
-
).all(lastMaxId) as any[];
|
|
185
|
-
|
|
186
|
-
for (const rawRow of newRows) {
|
|
187
|
-
if (stopped) return; // bail if unsubscribed mid-batch
|
|
188
|
-
const entity = this._attachMethods(
|
|
189
|
-
entityName,
|
|
190
|
-
transformFromStorage(rawRow, this.schemas[entityName]!)
|
|
191
|
-
);
|
|
192
|
-
await callback(entity); // await async callbacks
|
|
193
|
-
lastMaxId = rawRow.id;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Schedule next poll only after this one is done
|
|
198
|
-
if (!stopped) setTimeout(poll, interval);
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
// Start the loop
|
|
202
|
-
setTimeout(poll, interval);
|
|
203
|
-
|
|
204
|
-
return () => { stopped = true; };
|
|
205
|
-
}
|
|
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
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Get or create a shared table watcher. One SELECT * per change event,
|
|
223
|
-
* no matter how many update/delete listeners are registered.
|
|
224
|
-
*/
|
|
225
|
-
private _getWatcher(entityName: string) {
|
|
226
|
-
if (this._watchers.has(entityName)) return this._watchers.get(entityName)!;
|
|
227
|
-
|
|
228
|
-
// Build initial snapshot
|
|
229
|
-
const allRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all() as any[];
|
|
230
|
-
const snapshot = new Map<number, string>();
|
|
231
|
-
const snapshotEntities = new Map<number, any>();
|
|
232
|
-
for (const row of allRows) {
|
|
233
|
-
snapshot.set(row.id, JSON.stringify(row));
|
|
234
|
-
snapshotEntities.set(row.id, row);
|
|
235
|
-
}
|
|
236
|
-
|
|
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);
|
|
246
|
-
|
|
247
|
-
// Single poll loop for all listeners on this table
|
|
248
|
-
const interval = this.pollInterval;
|
|
249
|
-
const poll = async () => {
|
|
250
|
-
if (watcher.stopped) return;
|
|
251
|
-
|
|
252
|
-
const currentRevision = this._getRevision(entityName);
|
|
253
|
-
if (currentRevision !== watcher.lastRevision) {
|
|
254
|
-
watcher.lastRevision = currentRevision;
|
|
255
|
-
|
|
256
|
-
const currentRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all() as any[];
|
|
257
|
-
const currentMap = new Map<number, string>();
|
|
258
|
-
const currentEntities = new Map<number, any>();
|
|
259
|
-
for (const row of currentRows) {
|
|
260
|
-
currentMap.set(row.id, JSON.stringify(row));
|
|
261
|
-
currentEntities.set(row.id, row);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Dispatch updates
|
|
265
|
-
if (watcher.updateListeners.size > 0) {
|
|
266
|
-
for (const [id, json] of currentMap) {
|
|
267
|
-
if (watcher.stopped) return;
|
|
268
|
-
if (watcher.snapshot.has(id) && watcher.snapshot.get(id) !== json) {
|
|
269
|
-
const entity = this._attachMethods(
|
|
270
|
-
entityName,
|
|
271
|
-
transformFromStorage(currentEntities.get(id), this.schemas[entityName]!)
|
|
272
|
-
);
|
|
273
|
-
const oldEntity = this._attachMethods(
|
|
274
|
-
entityName,
|
|
275
|
-
transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]!)
|
|
276
|
-
);
|
|
277
|
-
for (const cb of watcher.updateListeners) {
|
|
278
|
-
await cb(entity, oldEntity);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Dispatch deletes
|
|
285
|
-
if (watcher.deleteListeners.size > 0) {
|
|
286
|
-
for (const [id] of watcher.snapshot) {
|
|
287
|
-
if (watcher.stopped) return;
|
|
288
|
-
if (!currentMap.has(id)) {
|
|
289
|
-
const oldEntity = this._attachMethods(
|
|
290
|
-
entityName,
|
|
291
|
-
transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]!)
|
|
292
|
-
);
|
|
293
|
-
for (const cb of watcher.deleteListeners) {
|
|
294
|
-
await cb(oldEntity);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
watcher.snapshot = currentMap;
|
|
301
|
-
watcher.snapshotEntities = currentEntities;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (!watcher.stopped) setTimeout(poll, interval);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
setTimeout(poll, interval);
|
|
308
|
-
return watcher;
|
|
309
|
-
}
|
|
310
|
-
|
|
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
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
142
|
// ===========================================================================
|
|
344
143
|
// CRUD
|
|
345
144
|
// ===========================================================================
|
package/src/query-builder.ts
CHANGED
|
@@ -540,6 +540,65 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
540
540
|
};
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Stream new rows one at a time via a watermark (last seen id).
|
|
545
|
+
*
|
|
546
|
+
* Unlike `.subscribe()` (which gives you an array snapshot), `.each()`
|
|
547
|
+
* calls your callback once per new row, in insertion order. The SQL
|
|
548
|
+
* `WHERE id > watermark` is rebuilt each poll with the latest value,
|
|
549
|
+
* so it's always O(new_rows) — not O(table_size).
|
|
550
|
+
*
|
|
551
|
+
* ```ts
|
|
552
|
+
* const unsub = db.messages.select().each((msg) => {
|
|
553
|
+
* console.log('New:', msg.text);
|
|
554
|
+
* });
|
|
555
|
+
* ```
|
|
556
|
+
*
|
|
557
|
+
* @param callback Called once per new row. Async callbacks are awaited.
|
|
558
|
+
* @param options `interval` in ms (default: pollInterval).
|
|
559
|
+
* @returns Unsubscribe function.
|
|
560
|
+
*/
|
|
561
|
+
each(
|
|
562
|
+
callback: (row: T) => void | Promise<void>,
|
|
563
|
+
options: { interval?: number } = {},
|
|
564
|
+
): () => void {
|
|
565
|
+
const { interval = this.defaultPollInterval } = options;
|
|
566
|
+
|
|
567
|
+
// Initialize watermark to current max id
|
|
568
|
+
const maxRows = this.executor(
|
|
569
|
+
`SELECT MAX(id) as _max FROM ${this.tableName}`, [], true
|
|
570
|
+
);
|
|
571
|
+
let lastMaxId: number = (maxRows[0] as any)?._max ?? 0;
|
|
572
|
+
let lastRevision = this.revisionGetter?.() ?? '0';
|
|
573
|
+
let stopped = false;
|
|
574
|
+
|
|
575
|
+
const poll = async () => {
|
|
576
|
+
if (stopped) return;
|
|
577
|
+
|
|
578
|
+
const rev = this.revisionGetter?.() ?? '0';
|
|
579
|
+
if (rev !== lastRevision) {
|
|
580
|
+
lastRevision = rev;
|
|
581
|
+
|
|
582
|
+
// Fetch only new rows since watermark — O(new_rows)
|
|
583
|
+
const newRows = this.executor(
|
|
584
|
+
`SELECT * FROM ${this.tableName} WHERE id > ? ORDER BY id ASC`,
|
|
585
|
+
[lastMaxId], true
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
for (const rawRow of newRows) {
|
|
589
|
+
if (stopped) return;
|
|
590
|
+
await callback(rawRow as unknown as T);
|
|
591
|
+
lastMaxId = (rawRow as any).id;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!stopped) setTimeout(poll, interval);
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
setTimeout(poll, interval);
|
|
599
|
+
return () => { stopped = true; };
|
|
600
|
+
}
|
|
601
|
+
|
|
543
602
|
/** Build a lightweight fingerprint query (COUNT + MAX(id)) that shares the same WHERE clause. */
|
|
544
603
|
private buildFingerprintSQL(): { sql: string; params: any[] } {
|
|
545
604
|
const params: any[] = [];
|
package/src/types.ts
CHANGED
|
@@ -149,21 +149,6 @@ export type NavEntityAccessor<
|
|
|
149
149
|
upsert: (conditions?: Partial<z.infer<S[Table & keyof S]>>, data?: Partial<z.infer<S[Table & keyof S]>>) => NavEntity<S, R, Table>;
|
|
150
150
|
delete: (id: number) => void;
|
|
151
151
|
select: (...cols: (keyof z.infer<S[Table & keyof S]> & string)[]) => QueryBuilder<NavEntity<S, R, Table>>;
|
|
152
|
-
/**
|
|
153
|
-
* Listen for table events.
|
|
154
|
-
*
|
|
155
|
-
* `'insert'` — streams new rows, one at a time.
|
|
156
|
-
* `'update'` — fires on row changes with (newRow, oldRow).
|
|
157
|
-
* `'delete'` — fires when a row is removed.
|
|
158
|
-
*
|
|
159
|
-
* Callbacks are awaited — strict ordering guaranteed even with async handlers.
|
|
160
|
-
* @returns Unsubscribe function.
|
|
161
|
-
*/
|
|
162
|
-
on: {
|
|
163
|
-
(event: 'insert', callback: (row: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }): () => void;
|
|
164
|
-
(event: 'update', callback: (row: NavEntity<S, R, Table>, oldRow: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }): () => void;
|
|
165
|
-
(event: 'delete', callback: (row: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }): () => void;
|
|
166
|
-
};
|
|
167
152
|
_tableName: string;
|
|
168
153
|
readonly _schema?: S[Table & keyof S];
|
|
169
154
|
};
|
|
@@ -189,21 +174,6 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
|
|
|
189
174
|
upsert: (conditions?: Partial<InferSchema<S>>, data?: Partial<InferSchema<S>>) => AugmentedEntity<S>;
|
|
190
175
|
delete: (id: number) => void;
|
|
191
176
|
select: (...cols: (keyof InferSchema<S> & string)[]) => QueryBuilder<AugmentedEntity<S>>;
|
|
192
|
-
/**
|
|
193
|
-
* Listen for table events.
|
|
194
|
-
*
|
|
195
|
-
* `'insert'` — streams new rows, one at a time.
|
|
196
|
-
* `'update'` — fires on row changes with (newRow, oldRow).
|
|
197
|
-
* `'delete'` — fires when a row is removed.
|
|
198
|
-
*
|
|
199
|
-
* Callbacks are awaited — strict ordering guaranteed even with async handlers.
|
|
200
|
-
* @returns Unsubscribe function.
|
|
201
|
-
*/
|
|
202
|
-
on: {
|
|
203
|
-
(event: 'insert', callback: (row: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }): () => void;
|
|
204
|
-
(event: 'update', callback: (row: AugmentedEntity<S>, oldRow: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }): () => void;
|
|
205
|
-
(event: 'delete', callback: (row: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }): () => void;
|
|
206
|
-
};
|
|
207
177
|
_tableName: string;
|
|
208
178
|
readonly _schema?: S;
|
|
209
179
|
};
|