@peers-app/peers-device 0.0.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.
Files changed (52) hide show
  1. package/dist/chunk-download-manager.d.ts +14 -0
  2. package/dist/chunk-download-manager.js +132 -0
  3. package/dist/chunk-download-manager.js.map +1 -0
  4. package/dist/chunk-download-manager.test.d.ts +1 -0
  5. package/dist/chunk-download-manager.test.js +166 -0
  6. package/dist/chunk-download-manager.test.js.map +1 -0
  7. package/dist/chunk-download.types.d.ts +15 -0
  8. package/dist/chunk-download.types.js +3 -0
  9. package/dist/chunk-download.types.js.map +1 -0
  10. package/dist/connection-manager/connection-manager.d.ts +32 -0
  11. package/dist/connection-manager/connection-manager.js +235 -0
  12. package/dist/connection-manager/connection-manager.js.map +1 -0
  13. package/dist/connection-manager/least-preferred-connection.d.ts +3 -0
  14. package/dist/connection-manager/least-preferred-connection.js +45 -0
  15. package/dist/connection-manager/least-preferred-connection.js.map +1 -0
  16. package/dist/connection-manager/least-preferred-connection.test.d.ts +1 -0
  17. package/dist/connection-manager/least-preferred-connection.test.js +300 -0
  18. package/dist/connection-manager/least-preferred-connection.test.js.map +1 -0
  19. package/dist/device-sync.d.ts +78 -0
  20. package/dist/device-sync.js +541 -0
  21. package/dist/device-sync.js.map +1 -0
  22. package/dist/device-sync.test.d.ts +1 -0
  23. package/dist/device-sync.test.js +1618 -0
  24. package/dist/device-sync.test.js.map +1 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.js +22 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/json-diff.d.ts +9 -0
  29. package/dist/json-diff.js +137 -0
  30. package/dist/json-diff.js.map +1 -0
  31. package/dist/local.data-source.d.ts +19 -0
  32. package/dist/local.data-source.js +146 -0
  33. package/dist/local.data-source.js.map +1 -0
  34. package/dist/local.data-source.test.d.ts +1 -0
  35. package/dist/local.data-source.test.js +68 -0
  36. package/dist/local.data-source.test.js.map +1 -0
  37. package/dist/main.d.ts +4 -0
  38. package/dist/main.js +138 -0
  39. package/dist/main.js.map +1 -0
  40. package/dist/packages.tracked-data-source.d.ts +10 -0
  41. package/dist/packages.tracked-data-source.js +28 -0
  42. package/dist/packages.tracked-data-source.js.map +1 -0
  43. package/dist/pvars.tracked-data-source.d.ts +5 -0
  44. package/dist/pvars.tracked-data-source.js +54 -0
  45. package/dist/pvars.tracked-data-source.js.map +1 -0
  46. package/dist/tracked-data-source.d.ts +46 -0
  47. package/dist/tracked-data-source.js +387 -0
  48. package/dist/tracked-data-source.js.map +1 -0
  49. package/dist/tracked-data-source.test.d.ts +1 -0
  50. package/dist/tracked-data-source.test.js +825 -0
  51. package/dist/tracked-data-source.test.js.map +1 -0
  52. package/package.json +48 -0
@@ -0,0 +1,825 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const lodash_1 = require("lodash");
4
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
5
+ const zod_1 = require("zod");
6
+ const tracked_data_source_1 = require("./tracked-data-source");
7
+ const local_data_source_1 = require("./local.data-source");
8
+ describe('tracked-table', () => {
9
+ const db = new local_data_source_1.DBLocal(':memory:');
10
+ const notesSchema = zod_1.z.object({
11
+ noteId: zod_1.z.string(),
12
+ title: zod_1.z.string(),
13
+ completed: zod_1.z.boolean().optional(),
14
+ number: zod_1.z.number().optional(),
15
+ string: zod_1.z.string().optional(),
16
+ date: zod_1.z.date().optional(),
17
+ });
18
+ const NotesMetaData = {
19
+ name: 'notes',
20
+ description: '',
21
+ primaryKeyName: 'noteId',
22
+ fields: (0, peers_sdk_1.schemaToFields)(notesSchema),
23
+ };
24
+ const sqlDataSource = new peers_sdk_1.SQLDataSource(db, NotesMetaData, notesSchema);
25
+ const mockDataContext = {
26
+ dataSourceFactory: () => sqlDataSource,
27
+ eventRegistry: {
28
+ getEmitter: () => ({
29
+ event: { subscribe: () => { } },
30
+ emit: () => { }
31
+ })
32
+ }
33
+ };
34
+ const deps = {
35
+ dataSource: sqlDataSource,
36
+ eventRegistry: mockDataContext.eventRegistry
37
+ };
38
+ const Notes = new peers_sdk_1.Table(NotesMetaData, deps);
39
+ const changeTrackingTable = new peers_sdk_1.ChangeTrackingTable({ db });
40
+ const trackedTable = new tracked_data_source_1.TrackedDataSource(Notes, changeTrackingTable);
41
+ beforeEach(async () => {
42
+ // await Notes.dropTableIfExists();
43
+ await changeTrackingTable.dropTableIfExists();
44
+ await (0, peers_sdk_1.sleep)(200);
45
+ });
46
+ afterAll(async () => {
47
+ await (0, peers_sdk_1.sleep)(200);
48
+ });
49
+ it('should track changes when inserting data', async () => {
50
+ // Insert a new record
51
+ const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
52
+ expect(note.noteId).toBeDefined();
53
+ // Check that the change was recorded
54
+ const changes = await changeTrackingTable.list();
55
+ expect(changes.length).toBe(1);
56
+ expect(changes[0].tableName).toBe('notes');
57
+ const change = changes[0];
58
+ expect(change).toEqual({
59
+ changeId: expect.any(String),
60
+ changeType: 'insert',
61
+ timestamp: expect.any(Number),
62
+ timestampApplied: expect.any(Number),
63
+ tableName: 'notes',
64
+ recordId: note.noteId,
65
+ newRecord: note,
66
+ });
67
+ });
68
+ it('should track changes when updating data', async () => {
69
+ // Insert a new record
70
+ const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
71
+ // Update the record
72
+ note.title = 'updated note';
73
+ note.completed = true;
74
+ await trackedTable.update(note);
75
+ // Check that both changes were recorded
76
+ const changes = await changeTrackingTable.list({ tableName: 'notes' }, { sortBy: ['timestamp'] });
77
+ expect(changes.length).toBe(2);
78
+ // Verify insert change
79
+ expect(changes[0]).toEqual({
80
+ changeId: expect.any(String),
81
+ changeType: 'insert',
82
+ timestamp: expect.any(Number),
83
+ timestampApplied: expect.any(Number),
84
+ tableName: 'notes',
85
+ recordId: note.noteId,
86
+ newRecord: expect.objectContaining({
87
+ title: 'test note',
88
+ noteId: note.noteId
89
+ }),
90
+ });
91
+ // Verify update change
92
+ const updateChange = changes[1];
93
+ expect(updateChange).toEqual({
94
+ changeId: expect.any(String),
95
+ changeType: 'update',
96
+ timestamp: expect.any(Number),
97
+ timestampApplied: expect.any(Number),
98
+ tableName: 'notes',
99
+ recordId: note.noteId,
100
+ newRecord: expect.objectContaining({
101
+ title: 'updated note',
102
+ noteId: note.noteId,
103
+ completed: true
104
+ }),
105
+ jsonDiff: expect.any(Array),
106
+ });
107
+ // TypeScript needs this cast for type narrowing
108
+ expect(updateChange.jsonDiff.length).toBeGreaterThan(0);
109
+ });
110
+ it('should track changes when deleting data', async () => {
111
+ // Insert a new record
112
+ const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
113
+ // Delete the record
114
+ await trackedTable.delete(note);
115
+ // Check that both changes were recorded
116
+ const changes = await changeTrackingTable.list({ tableName: 'notes' }, { sortBy: ['timestamp'] });
117
+ expect(changes.length).toBe(2);
118
+ // Get the changes in the right order
119
+ const insertChange = changes.find(c => c.changeType === 'insert');
120
+ const deleteChange = changes.find(c => c.changeType === 'delete');
121
+ expect(insertChange).toEqual({
122
+ changeId: expect.any(String),
123
+ changeType: 'insert',
124
+ timestamp: expect.any(Number),
125
+ timestampApplied: expect.any(Number),
126
+ tableName: 'notes',
127
+ recordId: note.noteId,
128
+ newRecord: expect.objectContaining({
129
+ title: 'test note',
130
+ noteId: note.noteId
131
+ }),
132
+ });
133
+ // Verify delete change
134
+ expect(deleteChange).toEqual({
135
+ changeId: expect.any(String),
136
+ changeType: 'delete',
137
+ timestamp: expect.any(Number),
138
+ timestampApplied: expect.any(Number),
139
+ tableName: 'notes',
140
+ recordId: note.noteId,
141
+ oldRecord: expect.objectContaining({
142
+ title: 'test note',
143
+ noteId: note.noteId
144
+ }),
145
+ });
146
+ });
147
+ it('should track changes when using save method for new records', async () => {
148
+ // Save a new record
149
+ const note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
150
+ // Check that the change was recorded
151
+ const changes = await changeTrackingTable.list();
152
+ expect(changes.length).toBe(1);
153
+ expect(changes[0]).toEqual({
154
+ changeId: expect.any(String),
155
+ changeType: 'insert',
156
+ timestamp: expect.any(Number),
157
+ timestampApplied: expect.any(Number),
158
+ tableName: 'notes',
159
+ recordId: note.noteId,
160
+ newRecord: expect.objectContaining({
161
+ title: 'test note',
162
+ noteId: note.noteId
163
+ }),
164
+ });
165
+ });
166
+ it('should track changes when using save method for existing records', async () => {
167
+ // Save a new record
168
+ const note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
169
+ // Update using save
170
+ note.title = 'updated note';
171
+ await trackedTable.save(note);
172
+ // Check that both changes were recorded
173
+ const changes = await changeTrackingTable.list({ tableName: 'notes' }, { sortBy: ['timestamp'] });
174
+ expect(changes.length).toBe(2);
175
+ // Verify first change (insert)
176
+ expect(changes[0]).toEqual({
177
+ changeId: expect.any(String),
178
+ changeType: 'insert',
179
+ timestamp: expect.any(Number),
180
+ timestampApplied: expect.any(Number),
181
+ tableName: 'notes',
182
+ recordId: note.noteId,
183
+ newRecord: expect.objectContaining({
184
+ title: 'test note',
185
+ noteId: note.noteId
186
+ }),
187
+ });
188
+ // Verify second change (update)
189
+ const updateChange = changes[1];
190
+ expect(updateChange).toEqual({
191
+ changeId: expect.any(String),
192
+ changeType: 'update',
193
+ timestamp: expect.any(Number),
194
+ timestampApplied: expect.any(Number),
195
+ tableName: 'notes',
196
+ recordId: note.noteId,
197
+ newRecord: expect.objectContaining({
198
+ title: 'updated note',
199
+ noteId: note.noteId
200
+ }),
201
+ jsonDiff: expect.any(Array),
202
+ });
203
+ // TypeScript needs this cast for type narrowing
204
+ expect(updateChange.jsonDiff.length).toBeGreaterThan(0);
205
+ });
206
+ it('should not track read operations', async () => {
207
+ // Insert a record
208
+ const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
209
+ // Perform read operations
210
+ await trackedTable.get(note.noteId);
211
+ await trackedTable.list();
212
+ await trackedTable.cursor();
213
+ // Only the insert should be recorded
214
+ const changes = await changeTrackingTable.list();
215
+ expect(changes.length).toBe(1);
216
+ expect(changes[0].changeType).toBe('insert');
217
+ });
218
+ it('should handle snapshot changes', async () => {
219
+ const noteId = (0, peers_sdk_1.newid)();
220
+ // Create a snapshot change directly
221
+ const snapshotChange = {
222
+ changeId: (0, peers_sdk_1.newid)(),
223
+ changeType: 'snapshot',
224
+ timestamp: Date.now(),
225
+ timestampApplied: Date.now(),
226
+ tableName: 'notes',
227
+ recordId: noteId,
228
+ newRecord: { noteId, title: 'snapshot note', completed: false }
229
+ };
230
+ // Apply the snapshot change
231
+ await trackedTable.applyChanges([snapshotChange]);
232
+ // Verify the record was created
233
+ const note = await trackedTable.get(noteId);
234
+ expect(note).toEqual({ noteId, title: 'snapshot note', completed: false });
235
+ // Verify the snapshot change was recorded
236
+ const changes = await changeTrackingTable.list({ recordId: noteId });
237
+ expect(changes.length).toBe(1);
238
+ expect(changes[0]).toEqual({
239
+ changeId: snapshotChange.changeId,
240
+ changeType: 'snapshot',
241
+ timestamp: snapshotChange.timestamp,
242
+ timestampApplied: expect.any(Number),
243
+ tableName: 'notes',
244
+ recordId: noteId,
245
+ newRecord: expect.objectContaining({
246
+ noteId,
247
+ title: 'snapshot note',
248
+ completed: false
249
+ })
250
+ });
251
+ });
252
+ it('should list changes for a specific table', async () => {
253
+ // Insert a record
254
+ const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
255
+ // Update the record
256
+ note.title = 'updated note';
257
+ await trackedTable.update(note);
258
+ // Delete the record
259
+ await trackedTable.delete(note);
260
+ // Use listChanges to get changes
261
+ const changes = await trackedTable.listChanges({}, { sortBy: ['timestamp'] });
262
+ // Verify we have 3 changes (insert, update, delete)
263
+ expect(changes.length).toBe(3);
264
+ expect(changes[0].changeType).toBe('insert');
265
+ expect(changes[1].changeType).toBe('update');
266
+ expect(changes[2].changeType).toBe('delete');
267
+ // Test filtering by changeType
268
+ const insertChanges = await trackedTable.listChanges({ changeType: 'insert' });
269
+ expect(insertChanges.length).toBe(1);
270
+ expect(insertChanges[0].changeType).toBe('insert');
271
+ // Test filtering by recordId
272
+ const recordChanges = await trackedTable.listChanges({ recordId: note.noteId }, { sortBy: ['timestamp'] });
273
+ expect(recordChanges.length).toBe(3);
274
+ expect(recordChanges.map(c => c.changeType)).toEqual(['insert', 'update', 'delete']);
275
+ });
276
+ it('should apply new changes to a table and remove old, irrelevant changes', async () => {
277
+ // Create a change to insert a record
278
+ const noteId = (0, peers_sdk_1.newid)();
279
+ const insertChange = {
280
+ changeId: (0, peers_sdk_1.newid)(),
281
+ changeType: 'insert',
282
+ timestamp: Date.now(),
283
+ timestampApplied: Date.now(),
284
+ tableName: 'notes',
285
+ recordId: noteId,
286
+ newRecord: { noteId, title: 'test note' }
287
+ };
288
+ // Apply the insert change
289
+ await trackedTable.applyChanges([insertChange]);
290
+ // Verify the record was inserted
291
+ const note = await trackedTable.get(noteId);
292
+ expect(note).toEqual({ noteId, title: 'test note' });
293
+ // Create an update change
294
+ const updateChange = {
295
+ changeId: (0, peers_sdk_1.newid)(),
296
+ changeType: 'update',
297
+ timestamp: insertChange.timestamp + 1,
298
+ timestampApplied: insertChange.timestamp + 1,
299
+ tableName: 'notes',
300
+ recordId: noteId,
301
+ oldRecord: { noteId, title: 'test note' },
302
+ newRecord: { noteId, title: 'updated note', completed: true },
303
+ jsonDiff: [{ op: 'replace', path: '/title', value: 'updated note' },
304
+ { op: 'add', path: '/completed', value: true }]
305
+ };
306
+ // Apply the update change
307
+ await trackedTable.applyChanges([updateChange]);
308
+ // Verify the record was updated
309
+ const updatedNote = await trackedTable.get(noteId);
310
+ expect(updatedNote).toEqual({ noteId, title: 'updated note', completed: true });
311
+ // Verify the changes were recorded in the change tracking table
312
+ let changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
313
+ expect(changes.length).toBe(2);
314
+ expect(changes.map(c => c.changeType)).toEqual(['insert', 'update']);
315
+ // Create a delete change
316
+ const deleteChange = {
317
+ changeId: (0, peers_sdk_1.newid)(),
318
+ changeType: 'delete',
319
+ timestamp: insertChange.timestamp + 2,
320
+ timestampApplied: insertChange.timestamp + 2,
321
+ tableName: 'notes',
322
+ recordId: noteId,
323
+ oldRecord: { noteId, title: 'updated note', completed: true },
324
+ };
325
+ // Apply the delete change
326
+ await trackedTable.applyChanges([deleteChange]);
327
+ // Verify the record was deleted
328
+ const deletedNote = await trackedTable.get(noteId);
329
+ expect(deletedNote).toBeUndefined();
330
+ // Verify the delete change was recorded and removed older (and no longer applicable changes)
331
+ changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
332
+ expect(changes.length).toBe(1);
333
+ expect(changes.map(c => c.changeType)).toEqual(['delete']);
334
+ // reapply the insert change
335
+ // Create a change to insert a record
336
+ const insertChange2 = {
337
+ changeId: (0, peers_sdk_1.newid)(),
338
+ changeType: 'insert',
339
+ timestamp: insertChange.timestamp + 3,
340
+ timestampApplied: insertChange.timestamp + 3,
341
+ tableName: 'notes',
342
+ recordId: noteId,
343
+ newRecord: { noteId, title: 'reinsert note' }
344
+ };
345
+ // Apply the insert change
346
+ await trackedTable.applyChanges([insertChange2]);
347
+ // Verify the insert change was recorded and removed older (and no longer applicable delete change)
348
+ changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
349
+ expect(changes.length).toBe(1);
350
+ expect(changes.map(c => c.changeType)).toEqual(['insert']);
351
+ });
352
+ it('should not reapply changes that have already been applied', async () => {
353
+ // Create a test change
354
+ const noteId = (0, peers_sdk_1.newid)();
355
+ const changeId = (0, peers_sdk_1.newid)();
356
+ const insertChange = {
357
+ changeId,
358
+ changeType: 'insert',
359
+ timestamp: Date.now(),
360
+ timestampApplied: Date.now(),
361
+ tableName: 'notes',
362
+ recordId: noteId,
363
+ newRecord: { noteId, title: 'duplicate test' }
364
+ };
365
+ // Apply the change
366
+ await trackedTable.applyChanges([insertChange]);
367
+ // Verify the record was inserted
368
+ const note = await trackedTable.get(noteId);
369
+ expect(note).toEqual({ noteId, title: 'duplicate test' });
370
+ // Try to apply the same change again
371
+ await trackedTable.applyChanges([insertChange]);
372
+ // Verify the change was only recorded once
373
+ const changes = await changeTrackingTable.list({ changeId });
374
+ expect(changes.length).toBe(1);
375
+ });
376
+ it('should apply changes in timestamp order', async () => {
377
+ const noteId = (0, peers_sdk_1.newid)();
378
+ const now = Date.now();
379
+ // Create changes with out-of-order timestamps
380
+ const changes = [
381
+ {
382
+ changeId: (0, peers_sdk_1.newid)(),
383
+ changeType: 'update',
384
+ timestamp: now + 200,
385
+ timestampApplied: now + 200,
386
+ tableName: 'notes',
387
+ recordId: noteId,
388
+ oldRecord: { noteId, title: 'second title' },
389
+ newRecord: { noteId, title: 'final title' },
390
+ jsonDiff: [{ op: 'replace', path: '/title', value: 'final title' }]
391
+ },
392
+ {
393
+ changeId: (0, peers_sdk_1.newid)(),
394
+ changeType: 'insert',
395
+ timestamp: now,
396
+ timestampApplied: now,
397
+ tableName: 'notes',
398
+ recordId: noteId,
399
+ newRecord: { noteId, title: 'initial title' }
400
+ },
401
+ {
402
+ changeId: (0, peers_sdk_1.newid)(),
403
+ changeType: 'update',
404
+ timestamp: now + 100,
405
+ timestampApplied: now + 100,
406
+ tableName: 'notes',
407
+ recordId: noteId,
408
+ oldRecord: { noteId, title: 'initial title' },
409
+ newRecord: { noteId, title: 'second title' },
410
+ jsonDiff: [{ op: 'replace', path: '/title', value: 'second title' }]
411
+ }
412
+ ];
413
+ // Store the changeIds for verification later
414
+ const changeIds = {
415
+ insert: changes[1].changeId,
416
+ update1: changes[2].changeId,
417
+ update2: changes[0].changeId
418
+ };
419
+ // Apply the out-of-order changes
420
+ await trackedTable.applyChanges(changes);
421
+ // Verify the final state reflects the changes applied in timestamp order
422
+ const note = await trackedTable.get(noteId);
423
+ expect(note).toEqual({ noteId, title: 'final title' });
424
+ // Verify the changes were recorded in order
425
+ const recordedChanges = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
426
+ expect(recordedChanges.length).toBe(3);
427
+ expect(recordedChanges.map(c => c.changeId)).toEqual([
428
+ changeIds.insert,
429
+ changeIds.update1,
430
+ changeIds.update2
431
+ ]);
432
+ });
433
+ it('should require a restore operation to bring a record back after being deleted', async () => {
434
+ // Create a change to insert a record
435
+ const noteId = (0, peers_sdk_1.newid)();
436
+ const insertChange = {
437
+ changeId: (0, peers_sdk_1.newid)(),
438
+ changeType: 'insert',
439
+ timestamp: Date.now(),
440
+ timestampApplied: Date.now(),
441
+ tableName: 'notes',
442
+ recordId: noteId,
443
+ newRecord: { noteId, title: 'test note' }
444
+ };
445
+ // Apply the insert change
446
+ await trackedTable.applyChanges([insertChange]);
447
+ // Verify the record was inserted
448
+ const note = await trackedTable.get(noteId);
449
+ expect(note).toEqual({ noteId, title: 'test note' });
450
+ // Create a delete change
451
+ const deleteChange = {
452
+ changeId: (0, peers_sdk_1.newid)(),
453
+ changeType: 'delete',
454
+ timestamp: insertChange.timestamp + 1,
455
+ timestampApplied: insertChange.timestamp + 1,
456
+ tableName: 'notes',
457
+ recordId: noteId,
458
+ oldRecord: { noteId, title: 'test note' }
459
+ };
460
+ // Apply the delete change
461
+ await trackedTable.applyChanges([deleteChange]);
462
+ // Verify the record was deleted
463
+ const deletedNote = await trackedTable.get(noteId);
464
+ expect(deletedNote).toBeUndefined();
465
+ // Create an update change
466
+ const updateChange = {
467
+ changeId: (0, peers_sdk_1.newid)(),
468
+ changeType: 'update',
469
+ timestamp: insertChange.timestamp + 1,
470
+ timestampApplied: insertChange.timestamp + 1,
471
+ tableName: 'notes',
472
+ recordId: noteId,
473
+ oldRecord: { noteId, title: 'test note' },
474
+ newRecord: { noteId, title: 'updated note', completed: true },
475
+ jsonDiff: [{ op: 'replace', path: '/title', value: 'updated note' },
476
+ { op: 'add', path: '/completed', value: true }]
477
+ };
478
+ // Apply the update change
479
+ await trackedTable.applyChanges([updateChange]);
480
+ // Verify the record was not updated
481
+ const updatedNote = await trackedTable.get(noteId);
482
+ expect(updatedNote).toBeUndefined();
483
+ // Create a restore change
484
+ const restoreChange = {
485
+ changeId: (0, peers_sdk_1.newid)(),
486
+ changeType: 'restore',
487
+ timestamp: insertChange.timestamp + 2,
488
+ timestampApplied: insertChange.timestamp + 2,
489
+ tableName: 'notes',
490
+ recordId: noteId,
491
+ newRecord: { noteId, title: 'restored note' }
492
+ };
493
+ // Apply the restore change
494
+ await trackedTable.applyChanges([restoreChange]);
495
+ // Verify the record was restored
496
+ const restoredNote = await trackedTable.get(noteId);
497
+ expect(restoredNote).toEqual({ noteId, title: 'restored note' });
498
+ });
499
+ it('should correctly and efficiently apply new changes to records with a very large amount of existing writes', async () => {
500
+ const db2 = new local_data_source_1.DBLocal(':memory:');
501
+ const mockDataContext2 = {
502
+ dataSourceFactory: () => new peers_sdk_1.SQLDataSource(db2, NotesMetaData, notesSchema),
503
+ eventRegistry: {
504
+ getEmitter: () => ({
505
+ event: { subscribe: () => { } },
506
+ emit: () => { }
507
+ })
508
+ }
509
+ };
510
+ const deps2 = {
511
+ dataSource: new (require("@peers-app/peers-sdk")).SQLDataSource(db2, NotesMetaData, notesSchema),
512
+ eventRegistry: mockDataContext2.eventRegistry,
513
+ };
514
+ const Notes2 = new peers_sdk_1.Table(NotesMetaData, deps2);
515
+ const changeTrackingTable2 = new peers_sdk_1.ChangeTrackingTable({ db: db2 });
516
+ const trackedTable2 = new tracked_data_source_1.TrackedDataSource(Notes2, changeTrackingTable2);
517
+ // Create a record with a large number of changes
518
+ const noteId = (0, peers_sdk_1.newid)();
519
+ const note = { noteId, title: 'test note' };
520
+ const checkpointCount = 20;
521
+ const writesBetweenCheckpointsCount = 20;
522
+ let lastCheckpointTimestamp = 0;
523
+ let note1Deleted = false;
524
+ let note2Deleted = false;
525
+ async function save1() {
526
+ if (note1Deleted) {
527
+ await trackedTable.save(note, { restoreIfDeleted: true });
528
+ note1Deleted = false;
529
+ }
530
+ else {
531
+ await trackedTable.save(note);
532
+ }
533
+ }
534
+ async function save2() {
535
+ if (note2Deleted) {
536
+ await trackedTable2.save(note, { restoreIfDeleted: true });
537
+ note2Deleted = false;
538
+ }
539
+ else {
540
+ await trackedTable2.save(note);
541
+ }
542
+ }
543
+ for (let i = 0; i < checkpointCount; i++) {
544
+ for (let j = 0; j < writesBetweenCheckpointsCount; j++) {
545
+ note.title = `update ${i} ${j}`;
546
+ note.completed = (0, lodash_1.random)(0, 10) === 0;
547
+ if ((0, lodash_1.random)(0, 10) === 0) {
548
+ note.number = (0, lodash_1.random)(0, 1000);
549
+ }
550
+ if ((0, lodash_1.random)(0, 2) === 0) {
551
+ note.string = (note.string || '') + note.title;
552
+ note.string = (note.string || '') + note.title;
553
+ const replaceStartI = (0, lodash_1.random)(0, note.string.length - 1);
554
+ const replaceEndI = (0, lodash_1.random)(replaceStartI, note.string.length - 1);
555
+ note.string = note.string.replace(note.string?.substring(replaceStartI, replaceEndI), note.title);
556
+ }
557
+ if ((0, lodash_1.random)(0, 4) > 0) {
558
+ note.date = new Date();
559
+ }
560
+ if ((0, lodash_1.random)(0, 10) > 0 || j === writesBetweenCheckpointsCount - 1) {
561
+ await save1();
562
+ }
563
+ if ((0, lodash_1.random)(0, 10) > 0) {
564
+ await save2();
565
+ }
566
+ // random delete
567
+ if ((0, lodash_1.random)(0, 10) === 0) {
568
+ await trackedTable.delete(note);
569
+ note1Deleted = true;
570
+ }
571
+ if ((0, lodash_1.random)(0, 10) === 0) {
572
+ await trackedTable2.delete(note);
573
+ note2Deleted = true;
574
+ }
575
+ }
576
+ if (note1Deleted !== note2Deleted) {
577
+ note.title = `checkpoint ${i}`;
578
+ await save1();
579
+ await save2();
580
+ note1Deleted = false;
581
+ note2Deleted = false;
582
+ }
583
+ let changes = await trackedTable.listChanges({ timestamp: { $gte: lastCheckpointTimestamp } }, { sortBy: ['timestamp'] });
584
+ let changes2 = await trackedTable2.listChanges({ timestamp: { $gte: lastCheckpointTimestamp } }, { sortBy: ['timestamp'] });
585
+ await trackedTable.applyChanges(changes2);
586
+ await trackedTable2.applyChanges(changes);
587
+ changes = await trackedTable.listChanges({ timestamp: { $gte: lastCheckpointTimestamp } }, { sortBy: ['timestamp'] });
588
+ changes2 = await trackedTable2.listChanges({ timestamp: { $gte: lastCheckpointTimestamp } }, { sortBy: ['timestamp'] });
589
+ expect(changes.map(c => c.changeId)).toEqual(changes2.map(c => c.changeId));
590
+ lastCheckpointTimestamp = changes[changes.length - 1].timestamp;
591
+ }
592
+ const changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
593
+ const changes2 = await changeTrackingTable2.list({ recordId: noteId }, { sortBy: ['timestamp'] });
594
+ expect(changes.map(c => c.changeId)).toEqual(changes2.map(c => c.changeId));
595
+ const dbNote = await Notes.get(noteId);
596
+ expect(dbNote).toEqual(note);
597
+ const dbNote2 = await Notes2.get(noteId);
598
+ expect(dbNote2).toEqual(note);
599
+ });
600
+ it("should be able to compact changes to remain performant", async () => {
601
+ const db = new local_data_source_1.DBLocal(':memory:');
602
+ const mockDataContext3 = {
603
+ dataSourceFactory: () => new peers_sdk_1.SQLDataSource(db, NotesMetaData, notesSchema),
604
+ eventRegistry: {
605
+ getEmitter: () => ({
606
+ event: { subscribe: () => { } },
607
+ emit: () => { }
608
+ })
609
+ }
610
+ };
611
+ const deps3 = {
612
+ dataSource: new (require("@peers-app/peers-sdk")).SQLDataSource(db, NotesMetaData, notesSchema),
613
+ eventRegistry: mockDataContext3.eventRegistry,
614
+ };
615
+ const NotesTable = new peers_sdk_1.Table(NotesMetaData, deps3);
616
+ const changeTrackingTable = new peers_sdk_1.ChangeTrackingTable({ db: db });
617
+ const trackedTable = new tracked_data_source_1.TrackedDataSource(NotesTable, changeTrackingTable);
618
+ // Create a record with a large number of updates
619
+ const noteId = (0, peers_sdk_1.newid)();
620
+ const note = { noteId, title: 'test note' };
621
+ const writesCount = 200;
622
+ for (let i = 0; i < writesCount; i++) {
623
+ note.title = `update ${i}`;
624
+ note.completed = (0, lodash_1.random)(0, 10) === 0;
625
+ if ((0, lodash_1.random)(0, 10) === 0) {
626
+ note.number = (0, lodash_1.random)(0, 1000);
627
+ }
628
+ if ((0, lodash_1.random)(0, 2) === 0) {
629
+ note.string = (note.string || '') + note.title;
630
+ note.string = (note.string || '') + note.title;
631
+ const replaceStartI = (0, lodash_1.random)(0, note.string.length - 1);
632
+ const replaceEndI = (0, lodash_1.random)(replaceStartI, note.string.length - 1);
633
+ note.string = note.string.replace(note.string?.substring(replaceStartI, replaceEndI), note.title);
634
+ }
635
+ if ((0, lodash_1.random)(0, 4) > 0) {
636
+ note.date = new Date();
637
+ }
638
+ await trackedTable.save(note);
639
+ }
640
+ let dbNote = await NotesTable.get(noteId);
641
+ expect(dbNote).toEqual(note);
642
+ const changesBeforeCompact = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
643
+ expect(changesBeforeCompact.length).toBeGreaterThan(100);
644
+ const tenthLastChange = changesBeforeCompact[changesBeforeCompact.length - 10];
645
+ await trackedTable.compact(tenthLastChange.timestamp);
646
+ const changesAfterCompact = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ['timestamp'] });
647
+ expect(changesAfterCompact.length).toEqual(11);
648
+ dbNote = await NotesTable.get(noteId);
649
+ expect(dbNote).toEqual(note);
650
+ });
651
+ it('should handle restore operation correctly', async () => {
652
+ const noteId = (0, peers_sdk_1.newid)();
653
+ // Insert and then delete a record
654
+ await trackedTable.insert({ noteId, title: 'original note' });
655
+ await trackedTable.delete(noteId);
656
+ // Verify it's deleted
657
+ const deletedNote = await trackedTable.get(noteId);
658
+ expect(deletedNote).toBeUndefined();
659
+ // Restore the record
660
+ const restoredNote = await trackedTable.restore({ noteId, title: 'restored note' });
661
+ expect(restoredNote).toEqual({ noteId, title: 'restored note' });
662
+ // Verify it's restored
663
+ const retrievedNote = await trackedTable.get(noteId);
664
+ expect(retrievedNote).toEqual({ noteId, title: 'restored note' });
665
+ // Verify restore change was tracked
666
+ const changes = await trackedTable.listChanges({ recordId: noteId, changeType: 'restore' });
667
+ expect(changes.length).toBe(1);
668
+ expect(changes[0].changeType).toBe('restore');
669
+ });
670
+ it('should handle restore without recordId provided', async () => {
671
+ await expect(trackedTable.restore({ title: 'no id' })).rejects.toThrow('No recordId provided to restore');
672
+ });
673
+ it('should handle insert with existing deleted record', async () => {
674
+ const noteId = (0, peers_sdk_1.newid)();
675
+ // Insert and then delete a record
676
+ await trackedTable.insert({ noteId, title: 'original note' });
677
+ await trackedTable.delete(noteId);
678
+ // Try to insert with same ID - should fail
679
+ await expect(trackedTable.insert({ noteId, title: 'new note' })).rejects.toThrow('because it has been deleted. Use restore instead');
680
+ });
681
+ it('should handle save with deleted record without restore flag', async () => {
682
+ const noteId = (0, peers_sdk_1.newid)();
683
+ // Insert and then delete a record
684
+ await trackedTable.insert({ noteId, title: 'original note' });
685
+ await trackedTable.delete(noteId);
686
+ // Try to save without restore flag - should fail
687
+ await expect(trackedTable.save({ noteId, title: 'new note' })).rejects.toThrow('because it has been deleted. Use restore instead');
688
+ });
689
+ it('should handle update with missing record', async () => {
690
+ const noteId = (0, peers_sdk_1.newid)();
691
+ // Try to update non-existent record
692
+ await expect(trackedTable.update({ noteId, title: 'new note' })).rejects.toThrow('No record found to update');
693
+ });
694
+ it('should handle delete with string ID for non-existent record', async () => {
695
+ const noteId = (0, peers_sdk_1.newid)();
696
+ // Mock console.warn to capture the warning
697
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
698
+ // Try to delete non-existent record
699
+ await trackedTable.delete(noteId);
700
+ // Verify warning was logged
701
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Record not found'));
702
+ warnSpy.mockRestore();
703
+ });
704
+ it('should handle update with no changes', async () => {
705
+ const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: 'test note' });
706
+ // Update with same data - should return unchanged
707
+ const result = await trackedTable.update(note);
708
+ expect(result).toEqual(note);
709
+ // Should still only have one change (the insert)
710
+ const changes = await trackedTable.listChanges({ recordId: note.noteId });
711
+ expect(changes.length).toBe(1);
712
+ expect(changes[0].changeType).toBe('insert');
713
+ });
714
+ it('should handle compact with default timestamp', async () => {
715
+ const noteId = (0, peers_sdk_1.newid)();
716
+ const note = { noteId, title: 'test note' };
717
+ // Create some changes
718
+ await trackedTable.save(note);
719
+ note.title = 'updated';
720
+ await trackedTable.save(note);
721
+ // Test compact with default timestamp (should use 2 weeks ago)
722
+ await trackedTable.compact();
723
+ // Should still have changes since they're recent
724
+ const changes = await trackedTable.listChanges({ recordId: noteId });
725
+ expect(changes.length).toBeGreaterThan(0);
726
+ });
727
+ it('should handle findAndTrackRecords method', async () => {
728
+ // Use separate instance for isolation
729
+ const db = new local_data_source_1.DBLocal(':memory:');
730
+ const mockDataContext4 = {
731
+ dataSourceFactory: () => new peers_sdk_1.SQLDataSource(db, NotesMetaData, notesSchema),
732
+ eventRegistry: {
733
+ getEmitter: () => ({
734
+ event: { subscribe: () => { } },
735
+ emit: () => { }
736
+ })
737
+ }
738
+ };
739
+ const deps4 = {
740
+ dataSource: new (require("@peers-app/peers-sdk")).SQLDataSource(db, NotesMetaData, notesSchema),
741
+ eventRegistry: mockDataContext4.eventRegistry,
742
+ };
743
+ const NotesTable = new peers_sdk_1.Table(NotesMetaData, deps4);
744
+ const changeTrackingTable = new peers_sdk_1.ChangeTrackingTable({ db });
745
+ const isolatedTrackedTable = new tracked_data_source_1.TrackedDataSource(NotesTable, changeTrackingTable);
746
+ // Insert some records directly to data source (bypassing tracking)
747
+ const note1 = { noteId: (0, peers_sdk_1.newid)(), title: 'untracked 1' };
748
+ const note2 = { noteId: (0, peers_sdk_1.newid)(), title: 'untracked 2' };
749
+ await NotesTable.insert(note1);
750
+ await NotesTable.insert(note2);
751
+ // Mock console.log to capture output
752
+ const logSpy = jest.spyOn(console, 'log').mockImplementation();
753
+ // Find and track records
754
+ await isolatedTrackedTable.findAndTrackRecords(1);
755
+ // Verify records were tracked
756
+ const changes = await isolatedTrackedTable.listChanges({ changeType: 'snapshot' });
757
+ expect(changes.length).toBe(2);
758
+ // Verify console output
759
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Tracking recordId'));
760
+ logSpy.mockRestore();
761
+ });
762
+ it('should handle apply changes with restore after delete', async () => {
763
+ const noteId = (0, peers_sdk_1.newid)();
764
+ // Apply delete change
765
+ const deleteChange = {
766
+ changeId: (0, peers_sdk_1.newid)(),
767
+ changeType: 'delete',
768
+ timestamp: Date.now(),
769
+ timestampApplied: Date.now(),
770
+ tableName: 'notes',
771
+ recordId: noteId,
772
+ oldRecord: { noteId, title: 'deleted note' }
773
+ };
774
+ await trackedTable.applyChanges([deleteChange]);
775
+ // Apply restore change
776
+ const restoreChange = {
777
+ changeId: (0, peers_sdk_1.newid)(),
778
+ changeType: 'restore',
779
+ timestamp: Date.now() + 1,
780
+ timestampApplied: Date.now() + 1,
781
+ tableName: 'notes',
782
+ recordId: noteId,
783
+ newRecord: { noteId, title: 'restored note' }
784
+ };
785
+ await trackedTable.applyChanges([restoreChange]);
786
+ // Verify record was restored
787
+ const note = await trackedTable.get(noteId);
788
+ expect(note).toEqual({ noteId, title: 'restored note' });
789
+ });
790
+ it('should handle error cases in applyChanges', async () => {
791
+ const noteId = (0, peers_sdk_1.newid)();
792
+ // Create a change with unsupported type
793
+ const unsupportedChange = {
794
+ changeId: (0, peers_sdk_1.newid)(),
795
+ changeType: 'unsupported',
796
+ timestamp: Date.now(),
797
+ timestampApplied: Date.now(),
798
+ tableName: 'notes',
799
+ recordId: noteId,
800
+ };
801
+ await expect(trackedTable.applyChanges([unsupportedChange])).rejects.toThrow('Unsupported change type');
802
+ });
803
+ it('should handle insert without primaryKey', async () => {
804
+ // Test insert without providing primary key
805
+ const result = await trackedTable.insert({ title: 'no id' });
806
+ expect(result.noteId).toBeDefined();
807
+ expect(result.title).toBe('no id');
808
+ });
809
+ it('should handle save with record that needs insert', async () => {
810
+ const noteId = (0, peers_sdk_1.newid)();
811
+ // Test save with new record (should insert)
812
+ const result = await trackedTable.save({ noteId, title: 'new record' });
813
+ expect(result).toEqual({ noteId, title: 'new record' });
814
+ // Verify it was inserted
815
+ const retrieved = await trackedTable.get(noteId);
816
+ expect(retrieved).toEqual({ noteId, title: 'new record' });
817
+ });
818
+ it('should handle save without primaryKey', async () => {
819
+ // Test save without providing primary key (should insert)
820
+ const result = await trackedTable.save({ title: 'no id' });
821
+ expect(result.noteId).toBeDefined();
822
+ expect(result.title).toBe('no id');
823
+ });
824
+ });
825
+ //# sourceMappingURL=tracked-data-source.test.js.map