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