@peers-app/peers-device 0.15.0 → 0.15.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/chunk-download-manager.d.ts +2 -2
- package/dist/chunk-download-manager.js +1 -1
- package/dist/chunk-download-manager.js.map +1 -1
- package/dist/chunk-download-manager.test.js +57 -57
- package/dist/chunk-download-manager.test.js.map +1 -1
- package/dist/chunk-download.types.d.ts +1 -1
- package/dist/connection-manager/connection-manager-priorities.d.ts +1 -1
- package/dist/connection-manager/connection-manager-priorities.js +10 -6
- package/dist/connection-manager/connection-manager-priorities.js.map +1 -1
- package/dist/connection-manager/connection-manager-priorities.test.js +192 -194
- package/dist/connection-manager/connection-manager-priorities.test.js.map +1 -1
- package/dist/connection-manager/connection-manager.d.ts +2 -2
- package/dist/connection-manager/connection-manager.js +71 -55
- package/dist/connection-manager/connection-manager.js.map +1 -1
- package/dist/connection-manager/connection-manager.test.js +165 -147
- package/dist/connection-manager/connection-manager.test.js.map +1 -1
- package/dist/connection-manager/connection-state.type.d.ts +1 -1
- package/dist/connection-manager/device-message-handler.types.d.ts +6 -6
- package/dist/connection-manager/device-messages.d.ts +4 -4
- package/dist/connection-manager/device-messages.js +56 -40
- package/dist/connection-manager/device-messages.js.map +1 -1
- package/dist/connection-manager/group-invite-messages.d.ts +2 -2
- package/dist/connection-manager/group-invite-messages.js +36 -47
- package/dist/connection-manager/group-invite-messages.js.map +1 -1
- package/dist/connection-manager/hops-map.js +4 -4
- package/dist/connection-manager/hops-map.js.map +1 -1
- package/dist/connection-manager/hops-map.test.js +3 -3
- package/dist/connection-manager/hops-map.test.js.map +1 -1
- package/dist/connection-manager/network-manager.d.ts +2 -2
- package/dist/connection-manager/network-manager.js +81 -75
- package/dist/connection-manager/network-manager.js.map +1 -1
- package/dist/index.d.ts +12 -12
- package/dist/json-diff.d.ts +2 -2
- package/dist/json-diff.js +30 -27
- package/dist/json-diff.js.map +1 -1
- package/dist/local.data-source.d.ts +1 -1
- package/dist/local.data-source.js +23 -23
- package/dist/local.data-source.js.map +1 -1
- package/dist/local.data-source.test.js +17 -17
- package/dist/local.data-source.test.js.map +1 -1
- package/dist/machine-stats.js +57 -51
- package/dist/machine-stats.js.map +1 -1
- package/dist/machine-stats.test.js +42 -42
- package/dist/machine-stats.test.js.map +1 -1
- package/dist/main.d.ts +2 -2
- package/dist/main.js +10 -8
- package/dist/main.js.map +1 -1
- package/dist/packages.tracked-data-source.d.ts +1 -1
- package/dist/packages.tracked-data-source.js.map +1 -1
- package/dist/persistent-vars.test.js +148 -148
- package/dist/persistent-vars.test.js.map +1 -1
- package/dist/pvars.tracked-data-source.d.ts +1 -1
- package/dist/pvars.tracked-data-source.js +12 -10
- package/dist/pvars.tracked-data-source.js.map +1 -1
- package/dist/sync-group.d.ts +2 -2
- package/dist/sync-group.js +110 -88
- package/dist/sync-group.js.map +1 -1
- package/dist/sync-group.test.js +157 -120
- package/dist/sync-group.test.js.map +1 -1
- package/dist/tracked-data-source.d.ts +1 -1
- package/dist/tracked-data-source.js +61 -62
- package/dist/tracked-data-source.js.map +1 -1
- package/dist/tracked-data-source.test.js +507 -299
- package/dist/tracked-data-source.test.js.map +1 -1
- package/dist/websocket-client.d.ts +1 -1
- package/dist/websocket-client.js +50 -41
- package/dist/websocket-client.js.map +1 -1
- package/package.json +3 -3
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
7
7
|
const tracked_data_source_1 = require("./tracked-data-source");
|
|
8
8
|
const local_data_source_1 = require("./local.data-source");
|
|
9
|
-
describe(
|
|
9
|
+
describe("TrackedDataSource", () => {
|
|
10
10
|
let db;
|
|
11
11
|
let sqlDataSource;
|
|
12
12
|
// let Notes: Table<INote>;
|
|
@@ -21,13 +21,13 @@ describe('TrackedDataSource', () => {
|
|
|
21
21
|
links: peers_sdk_1.zodAnyObject.optional(),
|
|
22
22
|
});
|
|
23
23
|
const NotesMetaData = {
|
|
24
|
-
name:
|
|
25
|
-
description:
|
|
26
|
-
primaryKeyName:
|
|
24
|
+
name: "notes",
|
|
25
|
+
description: "",
|
|
26
|
+
primaryKeyName: "noteId",
|
|
27
27
|
fields: (0, peers_sdk_1.schemaToFields)(notesSchema),
|
|
28
28
|
};
|
|
29
29
|
beforeAll(async () => {
|
|
30
|
-
db = new local_data_source_1.DBLocal(
|
|
30
|
+
db = new local_data_source_1.DBLocal(":memory:");
|
|
31
31
|
});
|
|
32
32
|
afterEach(() => {
|
|
33
33
|
trackedTable.preserveHistory = false;
|
|
@@ -38,9 +38,9 @@ describe('TrackedDataSource', () => {
|
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
async function dataSourceFactory(db) {
|
|
41
|
-
|
|
41
|
+
const createDb = !db;
|
|
42
42
|
if (!db) {
|
|
43
|
-
db = new local_data_source_1.DBLocal(
|
|
43
|
+
db = new local_data_source_1.DBLocal(":memory:");
|
|
44
44
|
}
|
|
45
45
|
// Create fresh instances for each test
|
|
46
46
|
sqlDataSource = new peers_sdk_1.SQLDataSource(db, NotesMetaData, notesSchema);
|
|
@@ -71,19 +71,23 @@ describe('TrackedDataSource', () => {
|
|
|
71
71
|
beforeEach(async () => {
|
|
72
72
|
[changeTrackingTable, trackedTable] = await dataSourceFactory(db);
|
|
73
73
|
});
|
|
74
|
-
describe(
|
|
75
|
-
it(
|
|
76
|
-
const note = await trackedTable.insert({
|
|
74
|
+
describe("Insert Operations", () => {
|
|
75
|
+
it("should insert a record and create a single full-object change", async () => {
|
|
76
|
+
const note = await trackedTable.insert({
|
|
77
|
+
noteId: (0, peers_sdk_1.newid)(),
|
|
78
|
+
title: "test note",
|
|
79
|
+
completed: false,
|
|
80
|
+
});
|
|
77
81
|
expect(note.noteId).toBeDefined();
|
|
78
|
-
expect(note.title).toBe(
|
|
82
|
+
expect(note.title).toBe("test note");
|
|
79
83
|
// Check that changes were recorded
|
|
80
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
84
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId });
|
|
81
85
|
// Should have exactly one change at path "/"
|
|
82
86
|
expect(changes.length).toBe(1);
|
|
83
|
-
expect(changes[0].op).toBe(
|
|
84
|
-
expect(changes[0].path).toBe(
|
|
87
|
+
expect(changes[0].op).toBe("set");
|
|
88
|
+
expect(changes[0].path).toBe("/");
|
|
85
89
|
expect(changes[0].value).toBeNull(); // value is null in the changes table; resolved at query time via listChanges
|
|
86
|
-
expect(changes[0].tableName).toBe(
|
|
90
|
+
expect(changes[0].tableName).toBe("notes");
|
|
87
91
|
expect(changes[0].recordId).toBe(note.noteId);
|
|
88
92
|
expect(changes[0].createdAt).toBeDefined();
|
|
89
93
|
expect(changes[0].appliedAt).toBeDefined();
|
|
@@ -94,59 +98,78 @@ describe('TrackedDataSource', () => {
|
|
|
94
98
|
const resolvedChanges = await trackedTable.listChanges({ recordId: note.noteId });
|
|
95
99
|
expect(resolvedChanges[0].value).toEqual(note);
|
|
96
100
|
});
|
|
97
|
-
it(
|
|
98
|
-
const note = await trackedTable.insert({ title:
|
|
101
|
+
it("should auto-generate noteId if not provided", async () => {
|
|
102
|
+
const note = await trackedTable.insert({ title: "test note" });
|
|
99
103
|
expect(note.noteId).toBeDefined();
|
|
100
104
|
expect(note.noteId.length).toBeGreaterThan(0);
|
|
101
105
|
});
|
|
102
|
-
it(
|
|
103
|
-
const note = await trackedTable.insert({
|
|
104
|
-
|
|
106
|
+
it("should store operations in sequential timestamp order", async () => {
|
|
107
|
+
const note = await trackedTable.insert({
|
|
108
|
+
noteId: (0, peers_sdk_1.newid)(),
|
|
109
|
+
title: "test note",
|
|
110
|
+
completed: false,
|
|
111
|
+
});
|
|
112
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId }, { sortBy: ["createdAt"] });
|
|
105
113
|
// Verify timestamps are sequential
|
|
106
114
|
for (let i = 1; i < changes.length; i++) {
|
|
107
115
|
expect(changes[i].createdAt).toBeGreaterThan(changes[i - 1].createdAt);
|
|
108
116
|
}
|
|
109
117
|
});
|
|
110
118
|
});
|
|
111
|
-
describe(
|
|
112
|
-
it(
|
|
119
|
+
describe("Update Operations", () => {
|
|
120
|
+
it("should update a record and store granular change records", async () => {
|
|
113
121
|
// Insert
|
|
114
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
122
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "original" });
|
|
115
123
|
// Count initial changes
|
|
116
|
-
const initialChanges = await changeTrackingTable.list({
|
|
124
|
+
const initialChanges = await changeTrackingTable.list({
|
|
125
|
+
tableName: "notes",
|
|
126
|
+
recordId: note.noteId,
|
|
127
|
+
});
|
|
117
128
|
const initialCount = initialChanges.length;
|
|
118
129
|
// Update
|
|
119
|
-
note.title =
|
|
130
|
+
note.title = "updated";
|
|
120
131
|
note.completed = true;
|
|
121
132
|
await trackedTable.update(note);
|
|
122
133
|
// Check that new changes were added
|
|
123
|
-
const allChanges = await changeTrackingTable.list({
|
|
134
|
+
const allChanges = await changeTrackingTable.list({
|
|
135
|
+
tableName: "notes",
|
|
136
|
+
recordId: note.noteId,
|
|
137
|
+
});
|
|
124
138
|
expect(allChanges.length).toBeGreaterThan(initialCount);
|
|
125
139
|
// The new changes should be 'replace' or 'add' operations
|
|
126
140
|
const updateChanges = allChanges.slice(initialCount);
|
|
127
141
|
updateChanges.forEach((change) => {
|
|
128
|
-
expect([
|
|
142
|
+
expect(["set", "delete", "patch-text"]).toContain(change.op);
|
|
129
143
|
});
|
|
130
144
|
// Verify the record was actually updated
|
|
131
145
|
const retrieved = await trackedTable.get(note.noteId);
|
|
132
|
-
expect(retrieved?.title).toBe(
|
|
146
|
+
expect(retrieved?.title).toBe("updated");
|
|
133
147
|
expect(retrieved?.completed).toBe(true);
|
|
134
148
|
});
|
|
135
|
-
it(
|
|
136
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
137
|
-
const changesBeforeUpdate = await changeTrackingTable.list({
|
|
149
|
+
it("should handle no-op updates gracefully", async () => {
|
|
150
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
151
|
+
const changesBeforeUpdate = await changeTrackingTable.list({
|
|
152
|
+
tableName: "notes",
|
|
153
|
+
recordId: note.noteId,
|
|
154
|
+
});
|
|
138
155
|
// Update with same values
|
|
139
156
|
await trackedTable.update(note);
|
|
140
|
-
const changesAfterUpdate = await changeTrackingTable.list({
|
|
157
|
+
const changesAfterUpdate = await changeTrackingTable.list({
|
|
158
|
+
tableName: "notes",
|
|
159
|
+
recordId: note.noteId,
|
|
160
|
+
});
|
|
141
161
|
expect(changesAfterUpdate.length).toBe(changesBeforeUpdate.length); // No new changes
|
|
142
162
|
});
|
|
143
|
-
it(
|
|
144
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
163
|
+
it("should mark old changes as superseded when saveAsSnapshot is true", async () => {
|
|
164
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "original" });
|
|
145
165
|
// Get initial changes
|
|
146
|
-
const initialChanges = await changeTrackingTable.list({
|
|
166
|
+
const initialChanges = await changeTrackingTable.list({
|
|
167
|
+
tableName: "notes",
|
|
168
|
+
recordId: note.noteId,
|
|
169
|
+
});
|
|
147
170
|
expect(initialChanges.every((c) => !c.supersededAt)).toBe(true);
|
|
148
171
|
// Update with saveAsSnapshot
|
|
149
|
-
note.title =
|
|
172
|
+
note.title = "updated";
|
|
150
173
|
await trackedTable.save(note, { saveAsSnapshot: true });
|
|
151
174
|
// Check that old changes are marked as superseded
|
|
152
175
|
const oldChanges = await changeTrackingTable.list({
|
|
@@ -168,30 +191,30 @@ describe('TrackedDataSource', () => {
|
|
|
168
191
|
});
|
|
169
192
|
});
|
|
170
193
|
});
|
|
171
|
-
describe(
|
|
172
|
-
it(
|
|
173
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
194
|
+
describe("Delete Operations", () => {
|
|
195
|
+
it("should delete a record and create a remove operation", async () => {
|
|
196
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
174
197
|
await trackedTable.delete(note);
|
|
175
198
|
// Check for remove operation
|
|
176
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
177
|
-
const removeOp = changes.find((c) => c.op ===
|
|
199
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId });
|
|
200
|
+
const removeOp = changes.find((c) => c.op === "delete");
|
|
178
201
|
expect(removeOp).toBeDefined();
|
|
179
|
-
expect(removeOp?.op).toBe(
|
|
202
|
+
expect(removeOp?.op).toBe("delete");
|
|
180
203
|
// Verify record was deleted
|
|
181
204
|
const retrieved = await trackedTable.get(note.noteId);
|
|
182
205
|
expect(retrieved).toBeUndefined();
|
|
183
206
|
});
|
|
184
|
-
it(
|
|
185
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
207
|
+
it("should delete by string ID", async () => {
|
|
208
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
186
209
|
await trackedTable.delete(note.noteId);
|
|
187
210
|
const retrieved = await trackedTable.get(note.noteId);
|
|
188
211
|
expect(retrieved).toBeUndefined();
|
|
189
212
|
});
|
|
190
|
-
it(
|
|
191
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
213
|
+
it("should mark old changes as superseded after delete", async () => {
|
|
214
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
192
215
|
// Initial changes should not be superseded
|
|
193
216
|
const beforeDelete = await changeTrackingTable.list({
|
|
194
|
-
tableName:
|
|
217
|
+
tableName: "notes",
|
|
195
218
|
recordId: note.noteId,
|
|
196
219
|
supersededAt: { $exists: false },
|
|
197
220
|
});
|
|
@@ -199,16 +222,16 @@ describe('TrackedDataSource', () => {
|
|
|
199
222
|
await trackedTable.delete(note);
|
|
200
223
|
// Old changes should now be superseded (except the remove operation)
|
|
201
224
|
const superseded = await changeTrackingTable.list({
|
|
202
|
-
tableName:
|
|
225
|
+
tableName: "notes",
|
|
203
226
|
recordId: note.noteId,
|
|
204
227
|
supersededAt: { $exists: true },
|
|
205
228
|
});
|
|
206
229
|
expect(superseded.length).toBeGreaterThan(0);
|
|
207
230
|
});
|
|
208
231
|
});
|
|
209
|
-
describe(
|
|
210
|
-
it(
|
|
211
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
232
|
+
describe("Restore Operations", () => {
|
|
233
|
+
it("should restore a deleted record", async () => {
|
|
234
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
212
235
|
await trackedTable.delete(note);
|
|
213
236
|
// Restore
|
|
214
237
|
const restored = await trackedTable.restore(note);
|
|
@@ -218,52 +241,58 @@ describe('TrackedDataSource', () => {
|
|
|
218
241
|
const retrieved = await trackedTable.get(note.noteId);
|
|
219
242
|
expect(retrieved).toEqual(note);
|
|
220
243
|
});
|
|
221
|
-
it(
|
|
222
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
244
|
+
it("should create a single add operation for restored record", async () => {
|
|
245
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
223
246
|
const noteId = note.noteId;
|
|
224
247
|
await trackedTable.delete(note);
|
|
225
248
|
// Count changes before restore
|
|
226
|
-
const beforeRestore = await changeTrackingTable.list({
|
|
249
|
+
const beforeRestore = await changeTrackingTable.list({
|
|
250
|
+
tableName: "notes",
|
|
251
|
+
recordId: noteId,
|
|
252
|
+
});
|
|
227
253
|
await trackedTable.restore(note);
|
|
228
|
-
const afterRestore = await changeTrackingTable.list({ tableName:
|
|
254
|
+
const afterRestore = await changeTrackingTable.list({ tableName: "notes", recordId: noteId });
|
|
229
255
|
expect(afterRestore.length).toBe(beforeRestore.length + 1);
|
|
230
256
|
// Find the new 'add' operation from restore (should be exactly one at path "/")
|
|
231
257
|
const restoreOps = afterRestore.filter((c) => !c.supersededAt);
|
|
232
258
|
expect(restoreOps.length).toBe(1);
|
|
233
|
-
expect(restoreOps[0].op).toBe(
|
|
234
|
-
expect(restoreOps[0].path).toBe(
|
|
259
|
+
expect(restoreOps[0].op).toBe("set");
|
|
260
|
+
expect(restoreOps[0].path).toBe("/");
|
|
235
261
|
expect(restoreOps[0].value).toBeNull(); // value is null in the changes table; resolved at query time
|
|
236
262
|
});
|
|
237
|
-
it(
|
|
238
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
263
|
+
it("should mark old changes as superseded after restore", async () => {
|
|
264
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
239
265
|
await trackedTable.delete(note);
|
|
240
266
|
await trackedTable.restore(note);
|
|
241
267
|
const superseded = await changeTrackingTable.list({
|
|
242
|
-
tableName:
|
|
268
|
+
tableName: "notes",
|
|
243
269
|
recordId: note.noteId,
|
|
244
270
|
supersededAt: { $exists: true },
|
|
245
271
|
});
|
|
246
272
|
expect(superseded.length).toBeGreaterThan(0);
|
|
247
273
|
});
|
|
248
274
|
});
|
|
249
|
-
describe(
|
|
250
|
-
it(
|
|
251
|
-
const note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
275
|
+
describe("Save Operations", () => {
|
|
276
|
+
it("should route to insert for new records", async () => {
|
|
277
|
+
const note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
252
278
|
expect(note.noteId).toBeDefined();
|
|
253
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
279
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId });
|
|
254
280
|
expect(changes.length).toBeGreaterThan(0);
|
|
255
|
-
expect(changes.every((c) => c.op ===
|
|
281
|
+
expect(changes.every((c) => c.op === "set")).toBe(true);
|
|
256
282
|
});
|
|
257
|
-
it(
|
|
258
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
259
|
-
const initialCount = (await changeTrackingTable.list({ tableName:
|
|
260
|
-
note.title =
|
|
283
|
+
it("should route to update for existing records", async () => {
|
|
284
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "original" });
|
|
285
|
+
const initialCount = (await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId })).length;
|
|
286
|
+
note.title = "updated";
|
|
261
287
|
await trackedTable.save(note);
|
|
262
|
-
const allChanges = await changeTrackingTable.list({
|
|
288
|
+
const allChanges = await changeTrackingTable.list({
|
|
289
|
+
tableName: "notes",
|
|
290
|
+
recordId: note.noteId,
|
|
291
|
+
});
|
|
263
292
|
expect(allChanges.length).toBeGreaterThan(initialCount);
|
|
264
293
|
});
|
|
265
|
-
it(
|
|
266
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
294
|
+
it("should restore deleted record when restoreIfDeleted is true", async () => {
|
|
295
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
267
296
|
await trackedTable.delete(note);
|
|
268
297
|
// Try to save with restoreIfDeleted option
|
|
269
298
|
const restored = await trackedTable.save(note, { restoreIfDeleted: true });
|
|
@@ -271,15 +300,15 @@ describe('TrackedDataSource', () => {
|
|
|
271
300
|
const retrieved = await trackedTable.get(note.noteId);
|
|
272
301
|
expect(retrieved).toBeDefined();
|
|
273
302
|
});
|
|
274
|
-
it(
|
|
275
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
303
|
+
it("should throw error when saving deleted record without restoreIfDeleted", async () => {
|
|
304
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
276
305
|
await trackedTable.delete(note);
|
|
277
|
-
await expect(trackedTable.save(note)).rejects.toThrow(
|
|
306
|
+
await expect(trackedTable.save(note)).rejects.toThrow("has been deleted");
|
|
278
307
|
});
|
|
279
|
-
it(
|
|
280
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
281
|
-
const updatedNote = await trackedTable.save({ ...note, title:
|
|
282
|
-
const changes = await changeTrackingTable.list({ recordId: note.noteId }, { sortBy: [
|
|
308
|
+
it("should save snapshots as a single add at root path", async () => {
|
|
309
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "original", count: 1 });
|
|
310
|
+
const updatedNote = await trackedTable.save({ ...note, title: "updated", count: 2 }, { saveAsSnapshot: true });
|
|
311
|
+
const changes = await changeTrackingTable.list({ recordId: note.noteId }, { sortBy: ["createdAt"] });
|
|
283
312
|
expect(changes.length).toBe(2); // One for insert, one for snapshot save
|
|
284
313
|
// Both insert and snapshot changes store null value (resolved at query time)
|
|
285
314
|
expect(changes[0].value).toBeNull();
|
|
@@ -290,84 +319,84 @@ describe('TrackedDataSource', () => {
|
|
|
290
319
|
expect(resolvedChanges[0].value).toEqual(updatedNote);
|
|
291
320
|
expect(resolvedChanges[1].value).toEqual(updatedNote);
|
|
292
321
|
});
|
|
293
|
-
it(
|
|
294
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
295
|
-
await trackedTable.save({ ...note, title:
|
|
296
|
-
const changes = await changeTrackingTable.list({ recordId: note.noteId }, { sortBy: [
|
|
322
|
+
it("should save non-snapshot updates as individual changes", async () => {
|
|
323
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "original", count: 1 });
|
|
324
|
+
await trackedTable.save({ ...note, title: "updated", count: 2 });
|
|
325
|
+
const changes = await changeTrackingTable.list({ recordId: note.noteId }, { sortBy: ["createdAt"] });
|
|
297
326
|
expect(changes.length).toBe(3); // One for insert, two for field updates
|
|
298
327
|
expect(changes[0].value).toBeNull(); // Insert change stores null; resolved at query time
|
|
299
328
|
// Granular field updates still store their values inline
|
|
300
|
-
const titleChange = changes.find(c => c.path ===
|
|
301
|
-
const countChange = changes.find(c => c.path ===
|
|
302
|
-
expect(titleChange?.value).toBe(
|
|
329
|
+
const titleChange = changes.find((c) => c.path === "/title");
|
|
330
|
+
const countChange = changes.find((c) => c.path === "/count");
|
|
331
|
+
expect(titleChange?.value).toBe("updated");
|
|
303
332
|
expect(countChange?.value).toBe(2);
|
|
304
333
|
});
|
|
305
334
|
});
|
|
306
|
-
describe(
|
|
307
|
-
it(
|
|
308
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
335
|
+
describe("weakInsert Option", () => {
|
|
336
|
+
it("should use weak timestamp (1 + Math.random()) for insert with weakInsert: true", async () => {
|
|
337
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" }, { weakInsert: true });
|
|
309
338
|
const now = Date.now() - 2;
|
|
310
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
339
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId });
|
|
311
340
|
expect(changes.length).toBe(1);
|
|
312
341
|
const change = changes[0];
|
|
313
342
|
expect(change.createdAt).toBeGreaterThanOrEqual(1);
|
|
314
343
|
expect(change.createdAt).toBeLessThan(2); // 1 + Math.random() is between 1 and 2
|
|
315
|
-
expect(change.appliedAt).toBeGreaterThanOrEqual(now); // appliedAt should be the current datetime
|
|
344
|
+
expect(change.appliedAt).toBeGreaterThanOrEqual(now); // appliedAt should be the current datetime
|
|
316
345
|
});
|
|
317
|
-
it(
|
|
318
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
319
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
346
|
+
it("should use normal timestamp for insert without weakInsert", async () => {
|
|
347
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
348
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId });
|
|
320
349
|
expect(changes.length).toBe(1);
|
|
321
350
|
const change = changes[0];
|
|
322
351
|
// Normal timestamp should be much larger (performance.timeOrigin + performance.now() or Date.now())
|
|
323
352
|
expect(change.createdAt).toBeGreaterThan(1000);
|
|
324
353
|
});
|
|
325
|
-
it(
|
|
326
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
327
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
354
|
+
it("should use normal timestamp for insert with weakInsert: false", async () => {
|
|
355
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" }, { weakInsert: false });
|
|
356
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId });
|
|
328
357
|
expect(changes.length).toBe(1);
|
|
329
358
|
const change = changes[0];
|
|
330
359
|
// weakInsert: false should use normal timestamp
|
|
331
360
|
expect(change.createdAt).toBeGreaterThan(1000);
|
|
332
361
|
});
|
|
333
|
-
it(
|
|
334
|
-
const note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
335
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
362
|
+
it("should use weak timestamp for save (new record) with weakInsert: true", async () => {
|
|
363
|
+
const note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title: "test" }, { weakInsert: true });
|
|
364
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId });
|
|
336
365
|
expect(changes.length).toBe(1);
|
|
337
366
|
const change = changes[0];
|
|
338
367
|
expect(change.createdAt).toBeGreaterThanOrEqual(1);
|
|
339
368
|
expect(change.createdAt).toBeLessThan(2);
|
|
340
369
|
});
|
|
341
|
-
it(
|
|
370
|
+
it("should not affect update operations (weakInsert ignored for updates)", async () => {
|
|
342
371
|
// Insert with normal timestamp
|
|
343
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
344
|
-
const initialChange = (await changeTrackingTable.list({ tableName:
|
|
372
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "original" });
|
|
373
|
+
const initialChange = (await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId }))[0];
|
|
345
374
|
const initialTimestamp = initialChange.createdAt;
|
|
346
375
|
// Update with weakInsert - should not affect the update timestamp
|
|
347
|
-
note.title =
|
|
376
|
+
note.title = "updated";
|
|
348
377
|
await trackedTable.save(note, { weakInsert: true });
|
|
349
|
-
const allChanges = await changeTrackingTable.list({ tableName:
|
|
378
|
+
const allChanges = await changeTrackingTable.list({ tableName: "notes", recordId: note.noteId }, { sortBy: ["createdAt"] });
|
|
350
379
|
// Should have more changes (the update)
|
|
351
380
|
expect(allChanges.length).toBeGreaterThan(1);
|
|
352
381
|
// The update changes should have normal timestamps (not weak)
|
|
353
382
|
const updateChanges = allChanges.slice(1);
|
|
354
|
-
updateChanges.forEach(change => {
|
|
383
|
+
updateChanges.forEach((change) => {
|
|
355
384
|
expect(change.createdAt).toBeGreaterThan(initialTimestamp);
|
|
356
385
|
expect(change.createdAt).toBeGreaterThan(1000); // Normal timestamp
|
|
357
386
|
});
|
|
358
387
|
});
|
|
359
|
-
it(
|
|
388
|
+
it("should use weak timestamp when save routes to insert (existing ID but no record)", async () => {
|
|
360
389
|
const noteId = (0, peers_sdk_1.newid)();
|
|
361
390
|
// Save with an ID but no existing record - this routes to _insert
|
|
362
|
-
const note = await trackedTable.save({ noteId, title:
|
|
363
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
391
|
+
const note = await trackedTable.save({ noteId, title: "test" }, { weakInsert: true });
|
|
392
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: noteId });
|
|
364
393
|
expect(changes.length).toBe(1);
|
|
365
394
|
const change = changes[0];
|
|
366
395
|
expect(change.createdAt).toBeGreaterThanOrEqual(1);
|
|
367
396
|
expect(change.createdAt).toBeLessThan(2);
|
|
368
397
|
});
|
|
369
|
-
it(
|
|
370
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
398
|
+
it("should work with restoreIfDeleted option", async () => {
|
|
399
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
371
400
|
await trackedTable.delete(note);
|
|
372
401
|
// Restore with weakInsert - note: restore doesn't use weakInsert, but we can test the combination
|
|
373
402
|
const restored = await trackedTable.save(note, { restoreIfDeleted: true, weakInsert: true });
|
|
@@ -377,34 +406,34 @@ describe('TrackedDataSource', () => {
|
|
|
377
406
|
const retrieved = await trackedTable.get(note.noteId);
|
|
378
407
|
expect(retrieved).toBeDefined();
|
|
379
408
|
});
|
|
380
|
-
it(
|
|
381
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
409
|
+
it("should work with saveAsSnapshot option", async () => {
|
|
410
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "original" });
|
|
382
411
|
// Save as snapshot with weakInsert
|
|
383
|
-
const updated = await trackedTable.save({ ...note, title:
|
|
412
|
+
const updated = await trackedTable.save({ ...note, title: "updated" }, { saveAsSnapshot: true, weakInsert: true });
|
|
384
413
|
// The snapshot save is an update, so weakInsert won't affect it
|
|
385
414
|
// But we can verify the operation completed successfully
|
|
386
415
|
const retrieved = await trackedTable.get(note.noteId);
|
|
387
|
-
expect(retrieved?.title).toBe(
|
|
416
|
+
expect(retrieved?.title).toBe("updated");
|
|
388
417
|
});
|
|
389
|
-
it(
|
|
418
|
+
it("should allow weak writes to be superseded by normal writes", async () => {
|
|
390
419
|
const noteId = (0, peers_sdk_1.newid)();
|
|
391
420
|
// First, insert with weakInsert (simulating a new device creating the record)
|
|
392
|
-
const note1 = await trackedTable.insert({ noteId, title:
|
|
393
|
-
const change1 = (await changeTrackingTable.list({ tableName:
|
|
421
|
+
const note1 = await trackedTable.insert({ noteId, title: "weak write 1" }, { weakInsert: true });
|
|
422
|
+
const change1 = (await changeTrackingTable.list({ tableName: "notes", recordId: noteId }))[0];
|
|
394
423
|
expect(change1.createdAt).toBeLessThan(2);
|
|
395
424
|
expect(change1.createdAt).toBeGreaterThanOrEqual(1);
|
|
396
425
|
// Wait a bit to ensure normal timestamp is later
|
|
397
426
|
await (0, peers_sdk_1.sleep)(10);
|
|
398
427
|
// Then, update with normal write (simulating another device with authoritative data)
|
|
399
428
|
// This simulates the scenario where an existing device updates the record
|
|
400
|
-
note1.title =
|
|
429
|
+
note1.title = "normal write";
|
|
401
430
|
await trackedTable.save(note1); // Normal save without weakInsert
|
|
402
|
-
const allChanges = await changeTrackingTable.list({ tableName:
|
|
431
|
+
const allChanges = await changeTrackingTable.list({ tableName: "notes", recordId: noteId }, { sortBy: ["createdAt"] });
|
|
403
432
|
// Should have more than 1 change (initial insert + update changes)
|
|
404
433
|
expect(allChanges.length).toBeGreaterThan(1);
|
|
405
434
|
// The update changes should have normal timestamps
|
|
406
435
|
const updateChanges = allChanges.slice(1);
|
|
407
|
-
updateChanges.forEach(change => {
|
|
436
|
+
updateChanges.forEach((change) => {
|
|
408
437
|
expect(change.createdAt).toBeGreaterThan(1000); // Normal timestamp
|
|
409
438
|
});
|
|
410
439
|
// Verify that the initial weak write has a weak timestamp
|
|
@@ -412,45 +441,45 @@ describe('TrackedDataSource', () => {
|
|
|
412
441
|
expect(change1.createdAt).toBeGreaterThanOrEqual(1);
|
|
413
442
|
// The final record should reflect the normal write (update takes precedence)
|
|
414
443
|
const final = await trackedTable.get(noteId);
|
|
415
|
-
expect(final?.title).toBe(
|
|
444
|
+
expect(final?.title).toBe("normal write");
|
|
416
445
|
// Note: The initial change at path "/" may or may not be superseded depending on the update logic.
|
|
417
|
-
// The key point is that weak writes have very small timestamps (1 + Math.random()) and normal writes
|
|
418
|
-
// have large timestamps, so when syncing between devices, the normal writes will take precedence
|
|
446
|
+
// The key point is that weak writes have very small timestamps (1 + Math.random()) and normal writes
|
|
447
|
+
// have large timestamps, so when syncing between devices, the normal writes will take precedence
|
|
419
448
|
// due to their later timestamps.
|
|
420
449
|
});
|
|
421
|
-
it(
|
|
450
|
+
it("should use weak timestamp for insert called via save when recordId is provided but record does not exist", async () => {
|
|
422
451
|
const noteId = (0, peers_sdk_1.newid)();
|
|
423
452
|
// Save with an ID but no existing record - routes to _insert with skipDeletedCheck=true
|
|
424
|
-
const note = await trackedTable.save({ noteId, title:
|
|
425
|
-
const changes = await changeTrackingTable.list({ tableName:
|
|
453
|
+
const note = await trackedTable.save({ noteId, title: "test" }, { weakInsert: true });
|
|
454
|
+
const changes = await changeTrackingTable.list({ tableName: "notes", recordId: noteId });
|
|
426
455
|
expect(changes.length).toBe(1);
|
|
427
456
|
const change = changes[0];
|
|
428
457
|
expect(change.createdAt).toBeGreaterThanOrEqual(1);
|
|
429
458
|
expect(change.createdAt).toBeLessThan(2);
|
|
430
459
|
});
|
|
431
460
|
});
|
|
432
|
-
describe(
|
|
433
|
-
it(
|
|
434
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
461
|
+
describe("Read Operations", () => {
|
|
462
|
+
it("should get a record by id", async () => {
|
|
463
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
435
464
|
const retrieved = await trackedTable.get(note.noteId);
|
|
436
465
|
expect(retrieved).toEqual(note);
|
|
437
466
|
});
|
|
438
|
-
it(
|
|
439
|
-
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
440
|
-
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
467
|
+
it("should list records with filters", async () => {
|
|
468
|
+
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note1", completed: true });
|
|
469
|
+
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note2", completed: false });
|
|
441
470
|
const completed = await trackedTable.list({ completed: true });
|
|
442
471
|
expect(completed.length).toBe(1);
|
|
443
|
-
expect(completed[0].title).toBe(
|
|
472
|
+
expect(completed[0].title).toBe("note1");
|
|
444
473
|
});
|
|
445
|
-
it(
|
|
446
|
-
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
447
|
-
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
474
|
+
it("should count records", async () => {
|
|
475
|
+
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note1" });
|
|
476
|
+
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note2" });
|
|
448
477
|
const count = await trackedTable.count();
|
|
449
478
|
expect(count).toBe(2);
|
|
450
479
|
});
|
|
451
|
-
it(
|
|
452
|
-
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
453
|
-
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
480
|
+
it("should iterate records with cursor", async () => {
|
|
481
|
+
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note1" });
|
|
482
|
+
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note2" });
|
|
454
483
|
const cursor = trackedTable.cursor();
|
|
455
484
|
const notes = [];
|
|
456
485
|
for await (const note of cursor) {
|
|
@@ -459,25 +488,25 @@ describe('TrackedDataSource', () => {
|
|
|
459
488
|
expect(notes.length).toBe(2);
|
|
460
489
|
});
|
|
461
490
|
});
|
|
462
|
-
describe(
|
|
463
|
-
it(
|
|
464
|
-
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
491
|
+
describe("Change Tracking Methods", () => {
|
|
492
|
+
it("should list changes for this table", async () => {
|
|
493
|
+
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
465
494
|
const changes = await trackedTable.listChanges();
|
|
466
495
|
expect(changes.length).toBeGreaterThan(0);
|
|
467
496
|
changes.forEach((c) => {
|
|
468
|
-
expect(c.tableName).toBe(
|
|
497
|
+
expect(c.tableName).toBe("notes");
|
|
469
498
|
});
|
|
470
499
|
});
|
|
471
|
-
it(
|
|
472
|
-
const note1 = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
473
|
-
const note2 = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
500
|
+
it("should filter changes by recordId", async () => {
|
|
501
|
+
const note1 = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note1" });
|
|
502
|
+
const note2 = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note2" });
|
|
474
503
|
const changes1 = await trackedTable.listChanges({ recordId: note1.noteId });
|
|
475
504
|
expect(changes1.every((c) => c.recordId === note1.noteId)).toBe(true);
|
|
476
505
|
const changes2 = await trackedTable.listChanges({ recordId: note2.noteId });
|
|
477
506
|
expect(changes2.every((c) => c.recordId === note2.noteId)).toBe(true);
|
|
478
507
|
});
|
|
479
|
-
it(
|
|
480
|
-
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
508
|
+
it("should use cursor for changes", async () => {
|
|
509
|
+
await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
481
510
|
const cursor = await trackedTable.cursorChanges();
|
|
482
511
|
const changes = [];
|
|
483
512
|
for await (const change of cursor) {
|
|
@@ -486,36 +515,41 @@ describe('TrackedDataSource', () => {
|
|
|
486
515
|
expect(changes.length).toBeGreaterThan(0);
|
|
487
516
|
});
|
|
488
517
|
});
|
|
489
|
-
describe(
|
|
490
|
-
it(
|
|
491
|
-
const note1 = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
492
|
-
const note2 = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
518
|
+
describe("Helper Methods", () => {
|
|
519
|
+
it("should identify deleted records", async () => {
|
|
520
|
+
const note1 = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note1" });
|
|
521
|
+
const note2 = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note2" });
|
|
493
522
|
await trackedTable.delete(note1);
|
|
494
523
|
const deletedIds = await changeTrackingTable.filterToDeletedIds([note1.noteId, note2.noteId]);
|
|
495
524
|
expect(deletedIds).toContain(note1.noteId);
|
|
496
525
|
expect(deletedIds).not.toContain(note2.noteId);
|
|
497
526
|
});
|
|
498
|
-
it(
|
|
527
|
+
it("should handle empty array in filterToDeletedIds", async () => {
|
|
499
528
|
const deletedIds = await changeTrackingTable.filterToDeletedIds([]);
|
|
500
529
|
expect(deletedIds).toEqual([]);
|
|
501
530
|
});
|
|
502
531
|
});
|
|
503
|
-
describe(
|
|
504
|
-
it(
|
|
505
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
532
|
+
describe("Edge Cases", () => {
|
|
533
|
+
it("should handle insert with all optional fields", async () => {
|
|
534
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "minimal" });
|
|
506
535
|
expect(note.completed).toBeUndefined();
|
|
507
536
|
expect(note.count).toBeUndefined();
|
|
508
537
|
});
|
|
509
|
-
it(
|
|
510
|
-
const note = await trackedTable.insert({
|
|
538
|
+
it("should handle update removing optional fields", async () => {
|
|
539
|
+
const note = await trackedTable.insert({
|
|
540
|
+
noteId: (0, peers_sdk_1.newid)(),
|
|
541
|
+
title: "test",
|
|
542
|
+
completed: true,
|
|
543
|
+
count: 5,
|
|
544
|
+
});
|
|
511
545
|
// Update to remove optional fields
|
|
512
|
-
const updated = { noteId: note.noteId, title:
|
|
546
|
+
const updated = { noteId: note.noteId, title: "test" };
|
|
513
547
|
await trackedTable.update(updated);
|
|
514
548
|
const retrieved = await trackedTable.get(note.noteId);
|
|
515
|
-
expect(retrieved?.title).toBe(
|
|
549
|
+
expect(retrieved?.title).toBe("test");
|
|
516
550
|
// The optional fields behavior depends on the underlying implementation
|
|
517
551
|
});
|
|
518
|
-
it(
|
|
552
|
+
it("should handle concurrent operations on different records", async () => {
|
|
519
553
|
const promises = [];
|
|
520
554
|
for (let i = 0; i < 5; i++) {
|
|
521
555
|
promises.push(trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: `note${i}` }));
|
|
@@ -525,12 +559,12 @@ describe('TrackedDataSource', () => {
|
|
|
525
559
|
const count = await trackedTable.count();
|
|
526
560
|
expect(count).toBe(5);
|
|
527
561
|
});
|
|
528
|
-
it(
|
|
562
|
+
it("should handle distributed deletion scenario", async () => {
|
|
529
563
|
// Scenario: One peer deletes a record, another peer (disconnected) makes changes
|
|
530
564
|
const noteId = (0, peers_sdk_1.newid)();
|
|
531
565
|
trackedTable.preserveHistory = true;
|
|
532
566
|
// Peer 1: Create record
|
|
533
|
-
await trackedTable.insert({ noteId, title:
|
|
567
|
+
await trackedTable.insert({ noteId, title: "original", completed: false });
|
|
534
568
|
// Peer 1: Delete record at timestamp 2000
|
|
535
569
|
await trackedTable.delete(noteId);
|
|
536
570
|
// Peer 2: Makes changes AFTER deletion (while disconnected)
|
|
@@ -539,20 +573,20 @@ describe('TrackedDataSource', () => {
|
|
|
539
573
|
// Peer 2 writes to specific paths (doesn't know about deletion)
|
|
540
574
|
const laterChange1 = {
|
|
541
575
|
changeId: (0, peers_sdk_1.newid)(),
|
|
542
|
-
tableName:
|
|
576
|
+
tableName: "notes",
|
|
543
577
|
recordId: noteId,
|
|
544
|
-
op:
|
|
545
|
-
path:
|
|
546
|
-
value:
|
|
578
|
+
op: "set",
|
|
579
|
+
path: "/title",
|
|
580
|
+
value: "updated by peer 2",
|
|
547
581
|
createdAt: laterTimestamp,
|
|
548
582
|
appliedAt: laterTimestamp,
|
|
549
583
|
};
|
|
550
584
|
const laterChange2 = {
|
|
551
585
|
changeId: (0, peers_sdk_1.newid)(),
|
|
552
|
-
tableName:
|
|
586
|
+
tableName: "notes",
|
|
553
587
|
recordId: noteId,
|
|
554
|
-
op:
|
|
555
|
-
path:
|
|
588
|
+
op: "set",
|
|
589
|
+
path: "/completed",
|
|
556
590
|
value: true,
|
|
557
591
|
createdAt: laterTimestamp + 1,
|
|
558
592
|
appliedAt: laterTimestamp + 1,
|
|
@@ -563,7 +597,7 @@ describe('TrackedDataSource', () => {
|
|
|
563
597
|
const deletedIds = await changeTrackingTable.filterToDeletedIds([noteId]);
|
|
564
598
|
expect(deletedIds).toContain(noteId);
|
|
565
599
|
// Verify the later changes are marked as superseded
|
|
566
|
-
const allChanges = await changeTrackingTable.list({ tableName:
|
|
600
|
+
const allChanges = await changeTrackingTable.list({ tableName: "notes", recordId: noteId }, { sortBy: ["createdAt"] });
|
|
567
601
|
// Find the changes with the later timestamps (the ones we manually added)
|
|
568
602
|
const change1InDb = allChanges.find((c) => c.changeId === laterChange1.changeId);
|
|
569
603
|
const change2InDb = allChanges.find((c) => c.changeId === laterChange2.changeId);
|
|
@@ -576,49 +610,49 @@ describe('TrackedDataSource', () => {
|
|
|
576
610
|
const record = await trackedTable.get(noteId);
|
|
577
611
|
expect(record).toBeUndefined();
|
|
578
612
|
});
|
|
579
|
-
it(
|
|
613
|
+
it("should handle insert with existing deleted record", async () => {
|
|
580
614
|
const noteId = (0, peers_sdk_1.newid)();
|
|
581
615
|
// Insert and then delete a record
|
|
582
|
-
await trackedTable.insert({ noteId, title:
|
|
616
|
+
await trackedTable.insert({ noteId, title: "original note" });
|
|
583
617
|
await trackedTable.delete(noteId);
|
|
584
618
|
// Try to insert with same ID - should fail
|
|
585
|
-
await expect(trackedTable.insert({ noteId, title:
|
|
619
|
+
await expect(trackedTable.insert({ noteId, title: "new note" })).rejects.toThrow("because it has been deleted");
|
|
586
620
|
});
|
|
587
|
-
it(
|
|
621
|
+
it("should handle update with missing record", async () => {
|
|
588
622
|
const noteId = (0, peers_sdk_1.newid)();
|
|
589
623
|
// Try to update non-existent record
|
|
590
|
-
await expect(trackedTable.update({ noteId, title:
|
|
624
|
+
await expect(trackedTable.update({ noteId, title: "new note" })).rejects.toThrow("No record found to update");
|
|
591
625
|
});
|
|
592
|
-
it(
|
|
626
|
+
it("should handle delete with string ID", async () => {
|
|
593
627
|
const noteId = (0, peers_sdk_1.newid)();
|
|
594
628
|
// Insert a record first
|
|
595
|
-
await trackedTable.insert({ noteId, title:
|
|
629
|
+
await trackedTable.insert({ noteId, title: "test note" });
|
|
596
630
|
// Delete by string ID should work
|
|
597
631
|
await trackedTable.delete(noteId);
|
|
598
632
|
// Verify it was deleted
|
|
599
633
|
const note = await trackedTable.get(noteId);
|
|
600
634
|
expect(note).toBeUndefined();
|
|
601
635
|
});
|
|
602
|
-
it(
|
|
636
|
+
it("should find and track untracked records", async () => {
|
|
603
637
|
// Insert records directly into the underlying data source (bypassing change tracking)
|
|
604
|
-
const note1 = { noteId: (0, peers_sdk_1.newid)(), title:
|
|
605
|
-
const note2 = { noteId: (0, peers_sdk_1.newid)(), title:
|
|
606
|
-
const note3 = { noteId: (0, peers_sdk_1.newid)(), title:
|
|
638
|
+
const note1 = { noteId: (0, peers_sdk_1.newid)(), title: "untracked 1" };
|
|
639
|
+
const note2 = { noteId: (0, peers_sdk_1.newid)(), title: "untracked 2" };
|
|
640
|
+
const note3 = { noteId: (0, peers_sdk_1.newid)(), title: "untracked 3" };
|
|
607
641
|
await sqlDataSource.insert(note1);
|
|
608
642
|
await sqlDataSource.insert(note2);
|
|
609
643
|
await sqlDataSource.insert(note3);
|
|
610
644
|
// Verify no change records exist for these
|
|
611
|
-
let changes = await changeTrackingTable.list({ tableName:
|
|
645
|
+
let changes = await changeTrackingTable.list({ tableName: "notes" });
|
|
612
646
|
expect(changes.length).toBe(0);
|
|
613
647
|
// Run findAndTrackRecords
|
|
614
648
|
await trackedTable.findAndTrackRecords();
|
|
615
649
|
// Verify change records were created
|
|
616
|
-
changes = await changeTrackingTable.list({ tableName:
|
|
650
|
+
changes = await changeTrackingTable.list({ tableName: "notes" });
|
|
617
651
|
expect(changes.length).toBe(3);
|
|
618
652
|
// All should be 'set' operations at path "/"
|
|
619
|
-
changes.forEach(change => {
|
|
620
|
-
expect(change.op).toBe(
|
|
621
|
-
expect(change.path).toBe(
|
|
653
|
+
changes.forEach((change) => {
|
|
654
|
+
expect(change.op).toBe("set");
|
|
655
|
+
expect(change.path).toBe("/");
|
|
622
656
|
expect(change.createdAt).toBeLessThan(10); // Low timestamp
|
|
623
657
|
expect(change.appliedAt).toBeGreaterThan(Date.now() - 1000); // Recent appliedAt
|
|
624
658
|
});
|
|
@@ -626,21 +660,21 @@ describe('TrackedDataSource', () => {
|
|
|
626
660
|
const allNotes = await trackedTable.list();
|
|
627
661
|
expect(allNotes.length).toBe(3);
|
|
628
662
|
});
|
|
629
|
-
it(
|
|
630
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
663
|
+
it("should compact superseded changes older than given timestamp", async () => {
|
|
664
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
631
665
|
const noteId = note.noteId;
|
|
632
666
|
// Make several updates to create multiple changes
|
|
633
|
-
await trackedTable.update({ noteId, title:
|
|
667
|
+
await trackedTable.update({ noteId, title: "update 1" });
|
|
634
668
|
await (0, peers_sdk_1.sleep)(10);
|
|
635
|
-
await trackedTable.update({ noteId, title:
|
|
669
|
+
await trackedTable.update({ noteId, title: "update 2" });
|
|
636
670
|
await (0, peers_sdk_1.sleep)(10);
|
|
637
|
-
await trackedTable.update({ noteId, title:
|
|
671
|
+
await trackedTable.update({ noteId, title: "update 3" });
|
|
638
672
|
await (0, peers_sdk_1.sleep)(10);
|
|
639
673
|
// Get all changes before compacting
|
|
640
674
|
const changesBefore = await changeTrackingTable.list({ recordId: noteId });
|
|
641
675
|
expect(changesBefore.length).toBeGreaterThan(1);
|
|
642
676
|
// Count superseded changes
|
|
643
|
-
const supersededBefore = changesBefore.filter(c => c.supersededAt).length;
|
|
677
|
+
const supersededBefore = changesBefore.filter((c) => c.supersededAt).length;
|
|
644
678
|
expect(supersededBefore).toBeGreaterThan(0);
|
|
645
679
|
// Compact (remove all superseded changes by using future timestamp)
|
|
646
680
|
await trackedTable.compact(Date.now() + 1000);
|
|
@@ -649,25 +683,25 @@ describe('TrackedDataSource', () => {
|
|
|
649
683
|
// Should have fewer changes now
|
|
650
684
|
expect(changesAfter.length).toBeLessThan(changesBefore.length);
|
|
651
685
|
// Should have no superseded changes left
|
|
652
|
-
const supersededAfter = changesAfter.filter(c => c.supersededAt).length;
|
|
686
|
+
const supersededAfter = changesAfter.filter((c) => c.supersededAt).length;
|
|
653
687
|
expect(supersededAfter).toBe(0);
|
|
654
688
|
// Verify the record is still accessible and correct
|
|
655
689
|
const retrieved = await trackedTable.get(noteId);
|
|
656
|
-
expect(retrieved?.title).toBe(
|
|
690
|
+
expect(retrieved?.title).toBe("update 3");
|
|
657
691
|
});
|
|
658
|
-
it(
|
|
659
|
-
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
692
|
+
it("should compact only changes superseded before given timestamp", async () => {
|
|
693
|
+
const note = await trackedTable.insert({ noteId: (0, peers_sdk_1.newid)(), title: "test" });
|
|
660
694
|
const noteId = note.noteId;
|
|
661
695
|
// Make updates
|
|
662
|
-
await trackedTable.update({ noteId, title:
|
|
696
|
+
await trackedTable.update({ noteId, title: "update 1" });
|
|
663
697
|
await (0, peers_sdk_1.sleep)(10);
|
|
664
698
|
const midTimestamp = Date.now();
|
|
665
699
|
await (0, peers_sdk_1.sleep)(10);
|
|
666
|
-
await trackedTable.update({ noteId, title:
|
|
700
|
+
await trackedTable.update({ noteId, title: "update 2" });
|
|
667
701
|
await (0, peers_sdk_1.sleep)(10);
|
|
668
|
-
await trackedTable.update({ noteId, title:
|
|
669
|
-
const changesBefore = await changeTrackingTable.list({ recordId: noteId }, { sortBy: [
|
|
670
|
-
const supersededBefore = changesBefore.filter(c => c.supersededAt).length;
|
|
702
|
+
await trackedTable.update({ noteId, title: "update 3" });
|
|
703
|
+
const changesBefore = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ["createdAt"] });
|
|
704
|
+
const supersededBefore = changesBefore.filter((c) => c.supersededAt).length;
|
|
671
705
|
// Compact only changes superseded before midTimestamp
|
|
672
706
|
await trackedTable.compact(midTimestamp);
|
|
673
707
|
const changesAfter = await changeTrackingTable.list({ recordId: noteId });
|
|
@@ -677,138 +711,312 @@ describe('TrackedDataSource', () => {
|
|
|
677
711
|
// Therefore, no changes should be removed.
|
|
678
712
|
expect(changesAfter.length).toBe(changesBefore.length); // No changes should be removed
|
|
679
713
|
// There should still be superseded changes (the ones after midTimestamp)
|
|
680
|
-
const supersededAfter = changesAfter.filter(c => c.supersededAt).length;
|
|
714
|
+
const supersededAfter = changesAfter.filter((c) => c.supersededAt).length;
|
|
681
715
|
expect(supersededAfter).toBe(supersededBefore); // Same number of superseded changes
|
|
682
716
|
});
|
|
683
717
|
});
|
|
684
|
-
describe(
|
|
685
|
-
it(
|
|
718
|
+
describe("applyChanges", () => {
|
|
719
|
+
it("should save but supersede remote changes that are older", async () => {
|
|
686
720
|
const noteId = `000000000000000000note001`;
|
|
687
721
|
trackedTable.preserveHistory = true;
|
|
688
|
-
const note = await trackedTable.save({ noteId, title:
|
|
722
|
+
const note = await trackedTable.save({ noteId, title: "original" });
|
|
689
723
|
const timeUpdatedRemote = (0, peers_sdk_1.getTimestamp)();
|
|
690
|
-
note.title =
|
|
724
|
+
note.title = "updated1";
|
|
691
725
|
await trackedTable.save(note);
|
|
692
726
|
const changes = await changeTrackingTable.list({ recordId: note.noteId });
|
|
693
727
|
expect(changes.length).toBe(2);
|
|
694
728
|
// Create changes to apply
|
|
695
729
|
const remoteChange = {
|
|
696
730
|
changeId: (0, peers_sdk_1.newid)(),
|
|
697
|
-
tableName:
|
|
731
|
+
tableName: "notes",
|
|
698
732
|
recordId: note.noteId,
|
|
699
|
-
op:
|
|
700
|
-
path:
|
|
701
|
-
value:
|
|
733
|
+
op: "set",
|
|
734
|
+
path: "/title",
|
|
735
|
+
value: "updated2",
|
|
702
736
|
createdAt: timeUpdatedRemote,
|
|
703
737
|
appliedAt: timeUpdatedRemote,
|
|
704
738
|
};
|
|
705
739
|
await trackedTable.applyChanges([remoteChange]);
|
|
706
740
|
const finalRecord = await trackedTable.get(note.noteId);
|
|
707
741
|
expect(finalRecord).toBeDefined();
|
|
708
|
-
expect(finalRecord?.title).toBe(
|
|
709
|
-
const finalChanges = await changeTrackingTable.list({ recordId: note.noteId }, { sortBy: [
|
|
742
|
+
expect(finalRecord?.title).toBe("updated1"); // because remote change is superseded
|
|
743
|
+
const finalChanges = await changeTrackingTable.list({ recordId: note.noteId }, { sortBy: ["createdAt"] });
|
|
710
744
|
expect(finalChanges.length).toBe(3);
|
|
711
745
|
const remoteChangeInDb = finalChanges.find((c) => c.changeId === remoteChange.changeId);
|
|
712
746
|
expect(remoteChangeInDb).toBeDefined();
|
|
713
747
|
expect(remoteChangeInDb?.supersededAt).toBeDefined();
|
|
714
748
|
});
|
|
715
|
-
it(
|
|
716
|
-
const note = await trackedTable.save({
|
|
717
|
-
|
|
749
|
+
it("should replace the entire array when an item in the array is added or removed", async () => {
|
|
750
|
+
const note = await trackedTable.save({
|
|
751
|
+
noteId: (0, peers_sdk_1.newid)(),
|
|
752
|
+
title: "original",
|
|
753
|
+
tags: ["tag1", "tag2"],
|
|
754
|
+
});
|
|
755
|
+
note.tags = ["tag1", "tag2", "tag3"];
|
|
718
756
|
await trackedTable.save(note);
|
|
719
757
|
const changes = await changeTrackingTable.list({ recordId: note.noteId });
|
|
720
758
|
expect(changes.length).toBe(2);
|
|
721
|
-
const addTagChange = changes.find((c) => c.op ===
|
|
759
|
+
const addTagChange = changes.find((c) => c.op === "set" && c.path === "/tags");
|
|
722
760
|
expect(addTagChange).toBeDefined();
|
|
723
|
-
expect(addTagChange?.value).toEqual([
|
|
761
|
+
expect(addTagChange?.value).toEqual(["tag1", "tag2", "tag3"]);
|
|
724
762
|
});
|
|
725
|
-
it(
|
|
726
|
-
const note = await trackedTable.save({
|
|
763
|
+
it("should updating two similarly named fields", async () => {
|
|
764
|
+
const note = await trackedTable.save({
|
|
765
|
+
noteId: (0, peers_sdk_1.newid)(),
|
|
766
|
+
title: "original",
|
|
767
|
+
links: { a: 1, aa: 2 },
|
|
768
|
+
});
|
|
727
769
|
const ts = (0, peers_sdk_1.getTimestamp)();
|
|
728
770
|
await trackedTable.applyChanges([
|
|
729
|
-
{
|
|
730
|
-
|
|
771
|
+
{
|
|
772
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
773
|
+
tableName: "notes",
|
|
774
|
+
recordId: note.noteId,
|
|
775
|
+
op: "set",
|
|
776
|
+
path: "/links/aa",
|
|
777
|
+
value: 3,
|
|
778
|
+
createdAt: ts,
|
|
779
|
+
appliedAt: ts,
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
783
|
+
tableName: "notes",
|
|
784
|
+
recordId: note.noteId,
|
|
785
|
+
op: "set",
|
|
786
|
+
path: "/links/a",
|
|
787
|
+
value: 4,
|
|
788
|
+
createdAt: ts + 1,
|
|
789
|
+
appliedAt: ts + 1,
|
|
790
|
+
},
|
|
731
791
|
]);
|
|
732
|
-
const dbNote = await trackedTable.get(note.noteId);
|
|
792
|
+
const dbNote = (await trackedTable.get(note.noteId));
|
|
733
793
|
expect(dbNote).toBeDefined();
|
|
734
794
|
expect(dbNote?.links).toBeDefined();
|
|
735
795
|
expect(dbNote?.links?.aa).toBe(3);
|
|
736
796
|
expect(dbNote?.links?.a).toBe(4);
|
|
737
797
|
});
|
|
738
|
-
it(
|
|
798
|
+
it("should apply deep changes after higher changes", async () => {
|
|
739
799
|
const noteId = `000000000000000000note001`;
|
|
740
800
|
let note = {
|
|
741
801
|
noteId,
|
|
742
|
-
title:
|
|
802
|
+
title: "original",
|
|
743
803
|
};
|
|
744
804
|
const ts = (0, peers_sdk_1.getTimestamp)();
|
|
745
805
|
await trackedTable.applyChanges([
|
|
746
|
-
{
|
|
747
|
-
|
|
748
|
-
|
|
806
|
+
{
|
|
807
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
808
|
+
tableName: "notes",
|
|
809
|
+
recordId: noteId,
|
|
810
|
+
op: "set",
|
|
811
|
+
path: "/",
|
|
812
|
+
value: note,
|
|
813
|
+
createdAt: ts,
|
|
814
|
+
appliedAt: ts,
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
818
|
+
tableName: "notes",
|
|
819
|
+
recordId: noteId,
|
|
820
|
+
op: "set",
|
|
821
|
+
path: "/links",
|
|
822
|
+
value: { a: 1 },
|
|
823
|
+
createdAt: ts + 1,
|
|
824
|
+
appliedAt: ts + 1,
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
828
|
+
tableName: "notes",
|
|
829
|
+
recordId: noteId,
|
|
830
|
+
op: "set",
|
|
831
|
+
path: "/links/a",
|
|
832
|
+
value: "2",
|
|
833
|
+
createdAt: ts + 2,
|
|
834
|
+
appliedAt: ts + 2,
|
|
835
|
+
},
|
|
749
836
|
]);
|
|
750
|
-
note = await trackedTable.get(noteId);
|
|
837
|
+
note = (await trackedTable.get(noteId));
|
|
751
838
|
expect(note).toBeDefined();
|
|
752
839
|
expect(note?.links).toBeDefined();
|
|
753
|
-
expect(note?.links?.a).toBe(
|
|
840
|
+
expect(note?.links?.a).toBe("2");
|
|
754
841
|
});
|
|
755
|
-
it(
|
|
842
|
+
it("should correctly apply and supersede changes from a single batch", async () => {
|
|
756
843
|
const noteId = `000000000000000000note001`;
|
|
757
844
|
let note = {
|
|
758
845
|
noteId,
|
|
759
|
-
title:
|
|
846
|
+
title: "original",
|
|
760
847
|
};
|
|
761
848
|
const ts = (0, peers_sdk_1.getTimestamp)();
|
|
762
849
|
await trackedTable.applyChanges([
|
|
763
|
-
{
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
850
|
+
{
|
|
851
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
852
|
+
tableName: "notes",
|
|
853
|
+
recordId: noteId,
|
|
854
|
+
op: "set",
|
|
855
|
+
path: "/",
|
|
856
|
+
value: note,
|
|
857
|
+
createdAt: ts,
|
|
858
|
+
appliedAt: ts,
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
862
|
+
tableName: "notes",
|
|
863
|
+
recordId: noteId,
|
|
864
|
+
op: "set",
|
|
865
|
+
path: "/links",
|
|
866
|
+
value: { a: 1 },
|
|
867
|
+
createdAt: ts + 1,
|
|
868
|
+
appliedAt: ts + 1,
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
872
|
+
tableName: "notes",
|
|
873
|
+
recordId: noteId,
|
|
874
|
+
op: "set",
|
|
875
|
+
path: "/links/a",
|
|
876
|
+
value: "2",
|
|
877
|
+
createdAt: ts + 2,
|
|
878
|
+
appliedAt: ts + 2,
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
882
|
+
tableName: "notes",
|
|
883
|
+
recordId: noteId,
|
|
884
|
+
op: "delete",
|
|
885
|
+
path: "/links",
|
|
886
|
+
createdAt: ts + 3,
|
|
887
|
+
appliedAt: ts + 3,
|
|
888
|
+
},
|
|
767
889
|
]);
|
|
768
|
-
note = await trackedTable.get(noteId);
|
|
890
|
+
note = (await trackedTable.get(noteId));
|
|
769
891
|
expect(note).toBeDefined();
|
|
770
892
|
expect(note?.links).toBeUndefined();
|
|
771
893
|
});
|
|
772
|
-
it(
|
|
773
|
-
let note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
894
|
+
it("should delete records on a delete change on path /", async () => {
|
|
895
|
+
let note = await trackedTable.save({ noteId: (0, peers_sdk_1.newid)(), title: "to be deleted" });
|
|
774
896
|
const ts = (0, peers_sdk_1.getTimestamp)();
|
|
775
897
|
await trackedTable.applyChanges([
|
|
776
|
-
{
|
|
898
|
+
{
|
|
899
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
900
|
+
tableName: "notes",
|
|
901
|
+
recordId: note.noteId,
|
|
902
|
+
op: "delete",
|
|
903
|
+
path: "/",
|
|
904
|
+
createdAt: ts + 4,
|
|
905
|
+
appliedAt: ts + 4,
|
|
906
|
+
},
|
|
777
907
|
]);
|
|
778
|
-
note = await trackedTable.get(note.noteId);
|
|
908
|
+
note = (await trackedTable.get(note.noteId));
|
|
779
909
|
expect(note).toBeUndefined();
|
|
780
910
|
});
|
|
781
|
-
it(
|
|
911
|
+
it("should correctly apply and supersede changes from a single batch that includes both creating and deleting the object", async () => {
|
|
782
912
|
const noteId = `000000000000000000note001`;
|
|
783
913
|
let note = {
|
|
784
914
|
noteId,
|
|
785
|
-
title:
|
|
915
|
+
title: "original",
|
|
786
916
|
};
|
|
787
917
|
const ts = (0, peers_sdk_1.getTimestamp)();
|
|
788
918
|
await trackedTable.applyChanges([
|
|
789
|
-
{
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
919
|
+
{
|
|
920
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
921
|
+
tableName: "notes",
|
|
922
|
+
recordId: noteId,
|
|
923
|
+
op: "set",
|
|
924
|
+
path: "/",
|
|
925
|
+
value: note,
|
|
926
|
+
createdAt: ts,
|
|
927
|
+
appliedAt: ts,
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
931
|
+
tableName: "notes",
|
|
932
|
+
recordId: noteId,
|
|
933
|
+
op: "set",
|
|
934
|
+
path: "/links",
|
|
935
|
+
value: { a: 1 },
|
|
936
|
+
createdAt: ts + 1,
|
|
937
|
+
appliedAt: ts + 1,
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
941
|
+
tableName: "notes",
|
|
942
|
+
recordId: noteId,
|
|
943
|
+
op: "set",
|
|
944
|
+
path: "/links/a",
|
|
945
|
+
value: "2",
|
|
946
|
+
createdAt: ts + 2,
|
|
947
|
+
appliedAt: ts + 2,
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
951
|
+
tableName: "notes",
|
|
952
|
+
recordId: noteId,
|
|
953
|
+
op: "delete",
|
|
954
|
+
path: "/links",
|
|
955
|
+
createdAt: ts + 3,
|
|
956
|
+
appliedAt: ts + 3,
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
960
|
+
tableName: "notes",
|
|
961
|
+
recordId: noteId,
|
|
962
|
+
op: "delete",
|
|
963
|
+
path: "/",
|
|
964
|
+
createdAt: ts + 4,
|
|
965
|
+
appliedAt: ts + 4,
|
|
966
|
+
},
|
|
794
967
|
]);
|
|
795
|
-
note = await trackedTable.get(noteId);
|
|
968
|
+
note = (await trackedTable.get(noteId));
|
|
796
969
|
expect(note).toBeUndefined();
|
|
797
970
|
});
|
|
798
|
-
it(
|
|
971
|
+
it("should correctly apply and supersede changes from a single batch that includes both creating, deleting the object, and restoring the object", async () => {
|
|
799
972
|
const noteId = `000000000000000000note001`;
|
|
800
973
|
const note = {
|
|
801
974
|
noteId,
|
|
802
|
-
title:
|
|
975
|
+
title: "original",
|
|
803
976
|
};
|
|
804
977
|
const ts = (0, peers_sdk_1.getTimestamp)();
|
|
805
978
|
await trackedTable.applyChanges([
|
|
806
|
-
{
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
979
|
+
{
|
|
980
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
981
|
+
tableName: "notes",
|
|
982
|
+
recordId: noteId,
|
|
983
|
+
op: "set",
|
|
984
|
+
path: "/",
|
|
985
|
+
value: note,
|
|
986
|
+
createdAt: ts,
|
|
987
|
+
appliedAt: ts,
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
991
|
+
tableName: "notes",
|
|
992
|
+
recordId: noteId,
|
|
993
|
+
op: "set",
|
|
994
|
+
path: "/links",
|
|
995
|
+
value: { a: 1 },
|
|
996
|
+
createdAt: ts + 2,
|
|
997
|
+
appliedAt: ts + 2,
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
1001
|
+
tableName: "notes",
|
|
1002
|
+
recordId: noteId,
|
|
1003
|
+
op: "delete",
|
|
1004
|
+
path: "/",
|
|
1005
|
+
createdAt: ts + 4,
|
|
1006
|
+
appliedAt: ts + 4,
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
changeId: (0, peers_sdk_1.newid)(),
|
|
1010
|
+
tableName: "notes",
|
|
1011
|
+
recordId: noteId,
|
|
1012
|
+
op: "set",
|
|
1013
|
+
path: "/",
|
|
1014
|
+
value: note,
|
|
1015
|
+
createdAt: ts + 5,
|
|
1016
|
+
appliedAt: ts + 5,
|
|
1017
|
+
},
|
|
810
1018
|
]);
|
|
811
|
-
const dbNote = await trackedTable.get(noteId);
|
|
1019
|
+
const dbNote = (await trackedTable.get(noteId));
|
|
812
1020
|
expect(dbNote).toEqual(note);
|
|
813
1021
|
});
|
|
814
1022
|
// // not sure we want to support this behavior
|
|
@@ -826,61 +1034,61 @@ describe('TrackedDataSource', () => {
|
|
|
826
1034
|
// const dbNote = await trackedTable.get(noteId) as INote;
|
|
827
1035
|
// expect(dbNote?.links?.a).toEqual(1);
|
|
828
1036
|
// });
|
|
829
|
-
it(
|
|
1037
|
+
it("should handle two peers adding items to an array separately (last write wins)", async () => {
|
|
830
1038
|
const [changeTrackingTable, trackedTable] = await dataSourceFactory();
|
|
831
1039
|
const [changeTrackingTableRemote, trackedTableRemote] = await dataSourceFactory();
|
|
832
1040
|
const noteId = `000000000000000000note001`;
|
|
833
|
-
const note = await trackedTable.save({ noteId, title:
|
|
1041
|
+
const note = await trackedTable.save({ noteId, title: "original", tags: ["tag1", "tag2"] });
|
|
834
1042
|
// Use listChanges (resolves insert change values) instead of changeTrackingTable.list
|
|
835
1043
|
await trackedTableRemote.applyChanges(await trackedTable.listChanges({ recordId: note.noteId }));
|
|
836
1044
|
let noteLocal = await trackedTable.get(note.noteId);
|
|
837
1045
|
let noteRemote = await trackedTableRemote.get(note.noteId);
|
|
838
1046
|
expect(noteLocal).toEqual(noteRemote);
|
|
839
|
-
noteLocal.tags.push(
|
|
1047
|
+
noteLocal.tags.push("tag3");
|
|
840
1048
|
await trackedTable.save(noteLocal);
|
|
841
|
-
noteRemote.tags.push(
|
|
1049
|
+
noteRemote.tags.push("tag4");
|
|
842
1050
|
await trackedTableRemote.save(noteRemote);
|
|
843
1051
|
await trackedTableRemote.applyChanges(await trackedTable.listChanges({ recordId: note.noteId }));
|
|
844
1052
|
await trackedTable.applyChanges(await trackedTableRemote.listChanges({ recordId: note.noteId }));
|
|
845
1053
|
noteRemote = await trackedTableRemote.get(note.noteId);
|
|
846
1054
|
noteLocal = await trackedTable.get(note.noteId);
|
|
847
1055
|
// changes to arrays are treated as full replacements, so last write wins
|
|
848
|
-
expect(noteRemote?.tags).toEqual([
|
|
849
|
-
expect(noteLocal?.tags).toEqual([
|
|
1056
|
+
expect(noteRemote?.tags).toEqual(["tag1", "tag2", "tag4"]);
|
|
1057
|
+
expect(noteLocal?.tags).toEqual(["tag1", "tag2", "tag4"]);
|
|
850
1058
|
});
|
|
851
|
-
it(
|
|
1059
|
+
it("should treat changes to objects nested in arrays as a full replacement of the array", async () => {
|
|
852
1060
|
const noteId = `000000000000000000note001`;
|
|
853
|
-
const note = { noteId, title:
|
|
1061
|
+
const note = { noteId, title: "original", links: { a: [{ b: 1 }, { c: 2 }] } };
|
|
854
1062
|
const noteInserted = await trackedTable.save(note);
|
|
855
1063
|
noteInserted.links.a[0].b = 2;
|
|
856
1064
|
const noteUpdated = await trackedTable.save(noteInserted);
|
|
857
1065
|
expect(noteUpdated).toEqual(noteInserted);
|
|
858
|
-
const changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: [
|
|
1066
|
+
const changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ["createdAt"] });
|
|
859
1067
|
expect(changes.length).toBe(2);
|
|
860
1068
|
const arrayChange = changes[1];
|
|
861
|
-
expect(arrayChange.path).toBe(
|
|
1069
|
+
expect(arrayChange.path).toBe("/links/a");
|
|
862
1070
|
expect(arrayChange.value).toEqual([{ b: 2 }, { c: 2 }]);
|
|
863
1071
|
});
|
|
864
|
-
it(
|
|
1072
|
+
it("should treat changes to arrays nested in another array as a full replacement of the top array", async () => {
|
|
865
1073
|
const noteId = `000000000000000000note001`;
|
|
866
|
-
const note = { noteId, title:
|
|
1074
|
+
const note = { noteId, title: "original", links: { a: [[{ b: 1 }, { c: 2 }, 2], 3] } };
|
|
867
1075
|
const noteInserted = await trackedTable.save(note);
|
|
868
1076
|
noteInserted.links.a[0][0].b = 2;
|
|
869
1077
|
const noteUpdated = await trackedTable.save(noteInserted);
|
|
870
1078
|
expect(noteUpdated).toEqual(noteInserted);
|
|
871
|
-
const changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: [
|
|
1079
|
+
const changes = await changeTrackingTable.list({ recordId: noteId }, { sortBy: ["createdAt"] });
|
|
872
1080
|
expect(changes.length).toBe(2);
|
|
873
1081
|
const arrayChange = changes[1];
|
|
874
|
-
expect(arrayChange.path).toBe(
|
|
1082
|
+
expect(arrayChange.path).toBe("/links/a");
|
|
875
1083
|
expect(arrayChange.value).toEqual([[{ b: 2 }, { c: 2 }, 2], 3]);
|
|
876
1084
|
});
|
|
877
1085
|
});
|
|
878
|
-
describe(
|
|
879
|
-
it(
|
|
1086
|
+
describe("Transaction Support", () => {
|
|
1087
|
+
it("should support runInTransaction on DBLocal", async () => {
|
|
880
1088
|
// Verify that DBLocal supports transactions
|
|
881
|
-
expect(typeof db.runInTransaction).toBe(
|
|
1089
|
+
expect(typeof db.runInTransaction).toBe("function");
|
|
882
1090
|
});
|
|
883
|
-
it(
|
|
1091
|
+
it("should apply multiple changes in a single transaction via applyChanges", async () => {
|
|
884
1092
|
// Create a separate "remote" data source to generate changes
|
|
885
1093
|
const [changeTrackingTableRemote, trackedTableRemote] = await dataSourceFactory();
|
|
886
1094
|
// Insert multiple records on the remote
|
|
@@ -889,7 +1097,7 @@ describe('TrackedDataSource', () => {
|
|
|
889
1097
|
const note = await trackedTableRemote.insert({
|
|
890
1098
|
noteId: (0, peers_sdk_1.newid)(),
|
|
891
1099
|
title: `note ${i}`,
|
|
892
|
-
completed: false
|
|
1100
|
+
completed: false,
|
|
893
1101
|
});
|
|
894
1102
|
records.push(note);
|
|
895
1103
|
}
|
|
@@ -906,7 +1114,7 @@ describe('TrackedDataSource', () => {
|
|
|
906
1114
|
const localChanges = await changeTrackingTable.list({});
|
|
907
1115
|
expect(localChanges.length).toBe(remoteChanges.length);
|
|
908
1116
|
});
|
|
909
|
-
it(
|
|
1117
|
+
it("should handle large batches of changes efficiently", async () => {
|
|
910
1118
|
// Create a separate "remote" data source
|
|
911
1119
|
const [changeTrackingTableRemote, trackedTableRemote] = await dataSourceFactory();
|
|
912
1120
|
// Insert many records
|
|
@@ -940,16 +1148,16 @@ describe('TrackedDataSource', () => {
|
|
|
940
1148
|
// Log performance (informational)
|
|
941
1149
|
console.log(`Applied ${remoteChanges.length} changes in ${endTime - startTime}ms`);
|
|
942
1150
|
});
|
|
943
|
-
it(
|
|
1151
|
+
it("should bulk insert changes correctly", async () => {
|
|
944
1152
|
const changes = [];
|
|
945
1153
|
for (let i = 0; i < 5; i++) {
|
|
946
1154
|
const noteId = (0, peers_sdk_1.newid)();
|
|
947
1155
|
changes.push({
|
|
948
1156
|
changeId: (0, peers_sdk_1.newid)(),
|
|
949
|
-
tableName:
|
|
1157
|
+
tableName: "notes",
|
|
950
1158
|
recordId: noteId,
|
|
951
|
-
op:
|
|
952
|
-
path:
|
|
1159
|
+
op: "set",
|
|
1160
|
+
path: "/",
|
|
953
1161
|
value: { noteId, title: `bulk note ${i}`, completed: false },
|
|
954
1162
|
createdAt: (0, peers_sdk_1.getTimestamp)(),
|
|
955
1163
|
appliedAt: (0, peers_sdk_1.getTimestamp)(),
|
|
@@ -961,20 +1169,20 @@ describe('TrackedDataSource', () => {
|
|
|
961
1169
|
const insertedChanges = await changeTrackingTable.list({});
|
|
962
1170
|
expect(insertedChanges.length).toBeGreaterThanOrEqual(5);
|
|
963
1171
|
for (const change of changes) {
|
|
964
|
-
const found = insertedChanges.find(c => c.changeId === change.changeId);
|
|
1172
|
+
const found = insertedChanges.find((c) => c.changeId === change.changeId);
|
|
965
1173
|
expect(found).toBeDefined();
|
|
966
|
-
expect(found?.tableName).toBe(
|
|
1174
|
+
expect(found?.tableName).toBe("notes");
|
|
967
1175
|
}
|
|
968
1176
|
});
|
|
969
|
-
it(
|
|
1177
|
+
it("should handle mixed insert/update/delete in applyChanges transaction", async () => {
|
|
970
1178
|
// Create a separate "remote" data source
|
|
971
1179
|
const [changeTrackingTableRemote, trackedTableRemote] = await dataSourceFactory();
|
|
972
1180
|
// Insert some records
|
|
973
|
-
const note1 = await trackedTableRemote.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
974
|
-
const note2 = await trackedTableRemote.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
975
|
-
const note3 = await trackedTableRemote.insert({ noteId: (0, peers_sdk_1.newid)(), title:
|
|
1181
|
+
const note1 = await trackedTableRemote.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note 1" });
|
|
1182
|
+
const note2 = await trackedTableRemote.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note 2" });
|
|
1183
|
+
const note3 = await trackedTableRemote.insert({ noteId: (0, peers_sdk_1.newid)(), title: "note 3" });
|
|
976
1184
|
// Update one
|
|
977
|
-
note1.title =
|
|
1185
|
+
note1.title = "updated note 1";
|
|
978
1186
|
await trackedTableRemote.update(note1);
|
|
979
1187
|
// Delete one
|
|
980
1188
|
await trackedTableRemote.delete(note2.noteId);
|
|
@@ -984,19 +1192,19 @@ describe('TrackedDataSource', () => {
|
|
|
984
1192
|
await trackedTable.applyChanges(remoteChanges);
|
|
985
1193
|
// Verify results
|
|
986
1194
|
const localNote1 = await trackedTable.get(note1.noteId);
|
|
987
|
-
expect(localNote1?.title).toBe(
|
|
1195
|
+
expect(localNote1?.title).toBe("updated note 1");
|
|
988
1196
|
const localNote2 = await trackedTable.get(note2.noteId);
|
|
989
1197
|
expect(localNote2).toBeUndefined();
|
|
990
1198
|
const localNote3 = await trackedTable.get(note3.noteId);
|
|
991
|
-
expect(localNote3?.title).toBe(
|
|
1199
|
+
expect(localNote3?.title).toBe("note 3");
|
|
992
1200
|
});
|
|
993
|
-
it(
|
|
1201
|
+
it("should support sync methods on SQLDataSource", async () => {
|
|
994
1202
|
// Test sync methods directly on sqlDataSource
|
|
995
1203
|
await sqlDataSource.initTable();
|
|
996
1204
|
// Test insertSync via bulkInsert
|
|
997
1205
|
const records = [
|
|
998
|
-
{ noteId: (0, peers_sdk_1.newid)(), title:
|
|
999
|
-
{ noteId: (0, peers_sdk_1.newid)(), title:
|
|
1206
|
+
{ noteId: (0, peers_sdk_1.newid)(), title: "sync test 1" },
|
|
1207
|
+
{ noteId: (0, peers_sdk_1.newid)(), title: "sync test 2" },
|
|
1000
1208
|
];
|
|
1001
1209
|
const inserted = await sqlDataSource.bulkInsert(records);
|
|
1002
1210
|
expect(inserted.length).toBe(2);
|
|
@@ -1006,30 +1214,30 @@ describe('TrackedDataSource', () => {
|
|
|
1006
1214
|
expect(found?.title).toBe(record.title);
|
|
1007
1215
|
}
|
|
1008
1216
|
// Test bulkDelete
|
|
1009
|
-
await sqlDataSource.bulkDelete(inserted.map(r => r.noteId));
|
|
1217
|
+
await sqlDataSource.bulkDelete(inserted.map((r) => r.noteId));
|
|
1010
1218
|
// Verify records are deleted
|
|
1011
1219
|
for (const record of inserted) {
|
|
1012
1220
|
const found = await sqlDataSource.get(record.noteId);
|
|
1013
1221
|
expect(found).toBeUndefined();
|
|
1014
1222
|
}
|
|
1015
1223
|
});
|
|
1016
|
-
it(
|
|
1224
|
+
it("should support bulkSave on SQLDataSource", async () => {
|
|
1017
1225
|
await sqlDataSource.initTable();
|
|
1018
1226
|
// Insert some records
|
|
1019
1227
|
const records = [
|
|
1020
|
-
{ noteId: (0, peers_sdk_1.newid)(), title:
|
|
1021
|
-
{ noteId: (0, peers_sdk_1.newid)(), title:
|
|
1228
|
+
{ noteId: (0, peers_sdk_1.newid)(), title: "bulk save 1" },
|
|
1229
|
+
{ noteId: (0, peers_sdk_1.newid)(), title: "bulk save 2" },
|
|
1022
1230
|
];
|
|
1023
1231
|
// First bulkSave should insert
|
|
1024
1232
|
const inserted = await sqlDataSource.bulkSave(records);
|
|
1025
1233
|
expect(inserted.length).toBe(2);
|
|
1026
1234
|
// Modify and bulkSave again should update
|
|
1027
|
-
inserted[0].title =
|
|
1235
|
+
inserted[0].title = "updated bulk save 1";
|
|
1028
1236
|
inserted[1].completed = true;
|
|
1029
1237
|
const updated = await sqlDataSource.bulkSave(inserted);
|
|
1030
1238
|
// Verify updates
|
|
1031
1239
|
const found1 = await sqlDataSource.get(inserted[0].noteId);
|
|
1032
|
-
expect(found1?.title).toBe(
|
|
1240
|
+
expect(found1?.title).toBe("updated bulk save 1");
|
|
1033
1241
|
const found2 = await sqlDataSource.get(inserted[1].noteId);
|
|
1034
1242
|
expect(found2?.completed).toBe(true);
|
|
1035
1243
|
});
|