@peers-app/peers-sdk 0.7.24 → 0.7.26
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/context/user-context.js +3 -3
- package/dist/data/change-tracking.d.ts +116 -202
- package/dist/data/change-tracking.js +229 -79
- package/dist/data/data-locks.js +14 -3
- package/dist/data/data-locks.test.js +6 -6
- package/dist/data/persistent-vars.js +2 -0
- package/dist/data/users.js +2 -0
- package/dist/device/connection.js +2 -2
- package/dist/logging/console-logs.table.d.ts +1 -1
- package/dist/types/peer-device.d.ts +2 -2
- package/package.json +1 -1
|
@@ -189,10 +189,10 @@ class UserContext {
|
|
|
189
189
|
if (!this.personalUserSubscription) {
|
|
190
190
|
this.personalUserSubscription = this.subscribeToDataChangedAcrossAllGroups('Users', async (evt) => {
|
|
191
191
|
const changedUser = evt.data.dataObject;
|
|
192
|
-
if (changedUser.userId !== this.userId) {
|
|
192
|
+
if (changedUser.userId !== this.userId && evt.dataContext.dataContextId !== this.userDataContext.dataContextId) {
|
|
193
193
|
// sync to my personal db if this is a user that I have there
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
194
|
+
const personalContact = await (0, data_1.Users)(this.userDataContext).get(changedUser.userId);
|
|
195
|
+
if (personalContact && !(0, lodash_1.isEqual)(personalContact, changedUser)) {
|
|
196
196
|
try {
|
|
197
197
|
await (0, data_1.Users)(this.userDataContext).save(changedUser);
|
|
198
198
|
}
|
|
@@ -1,219 +1,133 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Change tracking table V2
|
|
3
|
+
*
|
|
4
|
+
* This table stores granular change records for data synchronization.
|
|
5
|
+
* Unlike V1, each JSON patch operation gets its own record, enabling
|
|
6
|
+
* path-based superseding and more efficient cleanup.
|
|
7
|
+
*/
|
|
8
|
+
import { Emitter, Event } from '../events';
|
|
9
|
+
import { DataFilter, IDataQueryParams, ISqlDb, ITableMetaData, SQLDataSource } from './orm';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
export declare const changeRecordSchema: z.ZodObject<{
|
|
5
12
|
changeId: z.ZodEffects<z.ZodString, string, string>;
|
|
6
|
-
|
|
7
|
-
timestamp: z.ZodNumber;
|
|
8
|
-
timestampApplied: z.ZodNumber;
|
|
13
|
+
transactionId: z.ZodOptional<z.ZodString>;
|
|
9
14
|
tableName: z.ZodString;
|
|
10
15
|
recordId: z.ZodString;
|
|
11
|
-
|
|
16
|
+
op: z.ZodEnum<["set", "delete", "patch-text"]>;
|
|
17
|
+
path: z.ZodString;
|
|
18
|
+
value: z.ZodOptional<z.ZodNullable<z.ZodAny>>;
|
|
19
|
+
createdAt: z.ZodNumber;
|
|
20
|
+
appliedAt: z.ZodNumber;
|
|
21
|
+
supersededAt: z.ZodOptional<z.ZodNumber>;
|
|
12
22
|
}, "strip", z.ZodTypeAny, {
|
|
23
|
+
path: string;
|
|
24
|
+
createdAt: number;
|
|
13
25
|
changeId: string;
|
|
14
|
-
changeType: "insert" | "snapshot" | "restore";
|
|
15
|
-
timestamp: number;
|
|
16
|
-
timestampApplied: number;
|
|
17
26
|
tableName: string;
|
|
18
27
|
recordId: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
op: "set" | "delete" | "patch-text";
|
|
29
|
+
appliedAt: number;
|
|
30
|
+
value?: any;
|
|
31
|
+
transactionId?: string | undefined;
|
|
32
|
+
supersededAt?: number | undefined;
|
|
22
33
|
}, {
|
|
34
|
+
path: string;
|
|
35
|
+
createdAt: number;
|
|
23
36
|
changeId: string;
|
|
24
|
-
changeType: "insert" | "snapshot" | "restore";
|
|
25
|
-
timestamp: number;
|
|
26
|
-
timestampApplied: number;
|
|
27
37
|
tableName: string;
|
|
28
38
|
recordId: string;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
op: "set" | "delete" | "patch-text";
|
|
40
|
+
appliedAt: number;
|
|
41
|
+
value?: any;
|
|
42
|
+
transactionId?: string | undefined;
|
|
43
|
+
supersededAt?: number | undefined;
|
|
32
44
|
}>;
|
|
33
|
-
export type
|
|
34
|
-
declare
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
timestampApplied: z.ZodNumber;
|
|
39
|
-
tableName: z.ZodString;
|
|
40
|
-
recordId: z.ZodString;
|
|
41
|
-
oldRecord: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>>;
|
|
42
|
-
}, "strip", z.ZodTypeAny, {
|
|
43
|
-
changeId: string;
|
|
44
|
-
changeType: "delete";
|
|
45
|
-
timestamp: number;
|
|
46
|
-
timestampApplied: number;
|
|
47
|
-
tableName: string;
|
|
48
|
-
recordId: string;
|
|
49
|
-
oldRecord?: z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
|
|
50
|
-
}, {
|
|
51
|
-
changeId: string;
|
|
52
|
-
changeType: "delete";
|
|
53
|
-
timestamp: number;
|
|
54
|
-
timestampApplied: number;
|
|
55
|
-
tableName: string;
|
|
56
|
-
recordId: string;
|
|
57
|
-
oldRecord?: z.objectInputType<{}, z.ZodAny, "strip"> | undefined;
|
|
58
|
-
}>;
|
|
59
|
-
export type IChangeDelete = z.infer<typeof deleteChange>;
|
|
60
|
-
declare const updateChange: z.ZodObject<{
|
|
61
|
-
changeId: z.ZodEffects<z.ZodString, string, string>;
|
|
62
|
-
changeType: z.ZodLiteral<"update">;
|
|
63
|
-
timestamp: z.ZodNumber;
|
|
64
|
-
timestampApplied: z.ZodNumber;
|
|
65
|
-
tableName: z.ZodString;
|
|
66
|
-
recordId: z.ZodString;
|
|
67
|
-
newRecord: z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>;
|
|
68
|
-
jsonDiff: z.ZodArray<z.ZodAny, "many">;
|
|
69
|
-
}, "strip", z.ZodTypeAny, {
|
|
70
|
-
changeId: string;
|
|
71
|
-
changeType: "update";
|
|
72
|
-
timestamp: number;
|
|
73
|
-
timestampApplied: number;
|
|
74
|
-
tableName: string;
|
|
75
|
-
recordId: string;
|
|
76
|
-
newRecord: {} & {
|
|
77
|
-
[k: string]: any;
|
|
78
|
-
};
|
|
79
|
-
jsonDiff: any[];
|
|
80
|
-
}, {
|
|
81
|
-
changeId: string;
|
|
82
|
-
changeType: "update";
|
|
83
|
-
timestamp: number;
|
|
84
|
-
timestampApplied: number;
|
|
85
|
-
tableName: string;
|
|
86
|
-
recordId: string;
|
|
87
|
-
newRecord: {} & {
|
|
88
|
-
[k: string]: any;
|
|
89
|
-
};
|
|
90
|
-
jsonDiff: any[];
|
|
91
|
-
}>;
|
|
92
|
-
export type IChangeUpdate = z.infer<typeof updateChange>;
|
|
93
|
-
declare const changeSchema: z.ZodUnion<[z.ZodObject<{
|
|
94
|
-
changeId: z.ZodEffects<z.ZodString, string, string>;
|
|
95
|
-
changeType: z.ZodUnion<[z.ZodUnion<[z.ZodLiteral<"insert">, z.ZodLiteral<"snapshot">]>, z.ZodLiteral<"restore">]>;
|
|
96
|
-
timestamp: z.ZodNumber;
|
|
97
|
-
timestampApplied: z.ZodNumber;
|
|
98
|
-
tableName: z.ZodString;
|
|
99
|
-
recordId: z.ZodString;
|
|
100
|
-
newRecord: z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>;
|
|
101
|
-
}, "strip", z.ZodTypeAny, {
|
|
102
|
-
changeId: string;
|
|
103
|
-
changeType: "insert" | "snapshot" | "restore";
|
|
104
|
-
timestamp: number;
|
|
105
|
-
timestampApplied: number;
|
|
106
|
-
tableName: string;
|
|
107
|
-
recordId: string;
|
|
108
|
-
newRecord: {} & {
|
|
109
|
-
[k: string]: any;
|
|
110
|
-
};
|
|
111
|
-
}, {
|
|
112
|
-
changeId: string;
|
|
113
|
-
changeType: "insert" | "snapshot" | "restore";
|
|
114
|
-
timestamp: number;
|
|
115
|
-
timestampApplied: number;
|
|
116
|
-
tableName: string;
|
|
117
|
-
recordId: string;
|
|
118
|
-
newRecord: {} & {
|
|
119
|
-
[k: string]: any;
|
|
120
|
-
};
|
|
121
|
-
}>, z.ZodObject<{
|
|
122
|
-
changeId: z.ZodEffects<z.ZodString, string, string>;
|
|
123
|
-
changeType: z.ZodLiteral<"delete">;
|
|
124
|
-
timestamp: z.ZodNumber;
|
|
125
|
-
timestampApplied: z.ZodNumber;
|
|
126
|
-
tableName: z.ZodString;
|
|
127
|
-
recordId: z.ZodString;
|
|
128
|
-
oldRecord: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>>;
|
|
129
|
-
}, "strip", z.ZodTypeAny, {
|
|
130
|
-
changeId: string;
|
|
131
|
-
changeType: "delete";
|
|
132
|
-
timestamp: number;
|
|
133
|
-
timestampApplied: number;
|
|
134
|
-
tableName: string;
|
|
135
|
-
recordId: string;
|
|
136
|
-
oldRecord?: z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
|
|
137
|
-
}, {
|
|
138
|
-
changeId: string;
|
|
139
|
-
changeType: "delete";
|
|
140
|
-
timestamp: number;
|
|
141
|
-
timestampApplied: number;
|
|
142
|
-
tableName: string;
|
|
143
|
-
recordId: string;
|
|
144
|
-
oldRecord?: z.objectInputType<{}, z.ZodAny, "strip"> | undefined;
|
|
145
|
-
}>, z.ZodObject<{
|
|
146
|
-
changeId: z.ZodEffects<z.ZodString, string, string>;
|
|
147
|
-
changeType: z.ZodLiteral<"update">;
|
|
148
|
-
timestamp: z.ZodNumber;
|
|
149
|
-
timestampApplied: z.ZodNumber;
|
|
150
|
-
tableName: z.ZodString;
|
|
151
|
-
recordId: z.ZodString;
|
|
152
|
-
newRecord: z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>;
|
|
153
|
-
jsonDiff: z.ZodArray<z.ZodAny, "many">;
|
|
154
|
-
}, "strip", z.ZodTypeAny, {
|
|
155
|
-
changeId: string;
|
|
156
|
-
changeType: "update";
|
|
157
|
-
timestamp: number;
|
|
158
|
-
timestampApplied: number;
|
|
159
|
-
tableName: string;
|
|
160
|
-
recordId: string;
|
|
161
|
-
newRecord: {} & {
|
|
162
|
-
[k: string]: any;
|
|
163
|
-
};
|
|
164
|
-
jsonDiff: any[];
|
|
165
|
-
}, {
|
|
166
|
-
changeId: string;
|
|
167
|
-
changeType: "update";
|
|
168
|
-
timestamp: number;
|
|
169
|
-
timestampApplied: number;
|
|
170
|
-
tableName: string;
|
|
171
|
-
recordId: string;
|
|
172
|
-
newRecord: {} & {
|
|
173
|
-
[k: string]: any;
|
|
174
|
-
};
|
|
175
|
-
jsonDiff: any[];
|
|
176
|
-
}>]>;
|
|
177
|
-
export type IChange = z.infer<typeof changeSchema>;
|
|
178
|
-
export declare const changeTrackingSchema: z.ZodObject<{
|
|
179
|
-
changeId: z.ZodEffects<z.ZodString, string, string>;
|
|
180
|
-
changeType: z.ZodEnum<["insert", "snapshot", "update", "delete", "restore"]>;
|
|
181
|
-
timestamp: z.ZodNumber;
|
|
182
|
-
timestampApplied: z.ZodNumber;
|
|
183
|
-
tableName: z.ZodString;
|
|
184
|
-
recordId: z.ZodString;
|
|
185
|
-
oldRecord: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>>;
|
|
186
|
-
newRecord: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>>;
|
|
187
|
-
jsonDiff: z.ZodOptional<z.ZodArray<z.ZodAny, "many">>;
|
|
188
|
-
}, "strip", z.ZodTypeAny, {
|
|
189
|
-
changeId: string;
|
|
190
|
-
changeType: "update" | "insert" | "delete" | "snapshot" | "restore";
|
|
191
|
-
timestamp: number;
|
|
192
|
-
timestampApplied: number;
|
|
193
|
-
tableName: string;
|
|
194
|
-
recordId: string;
|
|
195
|
-
newRecord?: z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
|
|
196
|
-
oldRecord?: z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
|
|
197
|
-
jsonDiff?: any[] | undefined;
|
|
198
|
-
}, {
|
|
199
|
-
changeId: string;
|
|
200
|
-
changeType: "update" | "insert" | "delete" | "snapshot" | "restore";
|
|
201
|
-
timestamp: number;
|
|
202
|
-
timestampApplied: number;
|
|
203
|
-
tableName: string;
|
|
204
|
-
recordId: string;
|
|
205
|
-
newRecord?: z.objectInputType<{}, z.ZodAny, "strip"> | undefined;
|
|
206
|
-
oldRecord?: z.objectInputType<{}, z.ZodAny, "strip"> | undefined;
|
|
207
|
-
jsonDiff?: any[] | undefined;
|
|
208
|
-
}>;
|
|
209
|
-
export type IChangeAny = z.infer<typeof changeTrackingSchema>;
|
|
210
|
-
export declare class ChangeTrackingTable extends SQLDataSource<IChange> {
|
|
211
|
-
constructor({ db, }: {
|
|
45
|
+
export type IChangeRecord = z.infer<typeof changeRecordSchema>;
|
|
46
|
+
export declare class ChangeTrackingTable extends SQLDataSource<IChangeRecord> {
|
|
47
|
+
readonly dataChangedEmitter: Emitter<IChangeRecord>;
|
|
48
|
+
readonly dataChanged: Event<IChangeRecord>;
|
|
49
|
+
constructor({ db }: {
|
|
212
50
|
db: ISqlDb;
|
|
213
51
|
});
|
|
214
|
-
|
|
215
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Efficiently find which recordIds have been deleted.
|
|
54
|
+
*
|
|
55
|
+
* A record is deleted if the most recent write to path "/" is a 'delete' operation.
|
|
56
|
+
* This handles distributed scenarios where peers may write to specific paths after
|
|
57
|
+
* deletion - those writes are valid but superseded.
|
|
58
|
+
*
|
|
59
|
+
* Uses optimized SQL with CTE for performance.
|
|
60
|
+
*
|
|
61
|
+
* @param recordIds - Array of recordIds to check
|
|
62
|
+
* @returns Array of recordIds that are deleted
|
|
63
|
+
*/
|
|
216
64
|
filterToDeletedIds(recordIds: string[]): Promise<string[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Find recordIds that have no change records at all.
|
|
67
|
+
*
|
|
68
|
+
* This is useful for identifying records that were never synced
|
|
69
|
+
* or have been completely cleaned up.
|
|
70
|
+
*
|
|
71
|
+
* @param recordIds - Array of recordIds to check
|
|
72
|
+
* @returns Array of recordIds with no change records
|
|
73
|
+
*/
|
|
217
74
|
filterToMissingIds(recordIds: string[]): Promise<string[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Find changeIds that we do not have
|
|
77
|
+
*
|
|
78
|
+
* @param changeIds - Array of changeIds to check
|
|
79
|
+
* @returns Array of changeIds with no change records
|
|
80
|
+
*/
|
|
81
|
+
filterToMissingChangeIds(changeIds: string[]): Promise<string[]>;
|
|
82
|
+
markAllPriorChangesSuperseded(tableName: string, recordId: string, supersededAt: number): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Batch update supersededAt timestamps for multiple changes.
|
|
85
|
+
* More efficient than updating one at a time.
|
|
86
|
+
*
|
|
87
|
+
* @param updates - Array of {changeId, supersededAt} pairs
|
|
88
|
+
*/
|
|
89
|
+
batchMarkSuperseded(updates: Array<{
|
|
90
|
+
changeId: string;
|
|
91
|
+
supersededAt: number;
|
|
92
|
+
}>): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Override list to deserialize value field from JSON string
|
|
95
|
+
*/
|
|
96
|
+
list(filter?: DataFilter<IChangeRecord>, opts?: IDataQueryParams<IChangeRecord>): Promise<IChangeRecord[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Override get to deserialize value field from JSON string
|
|
99
|
+
*/
|
|
100
|
+
get(id: string): Promise<IChangeRecord | undefined>;
|
|
101
|
+
/**
|
|
102
|
+
* Override to serialize value field to JSON string
|
|
103
|
+
*/
|
|
104
|
+
insert(record: IChangeRecord): Promise<IChangeRecord>;
|
|
105
|
+
/**
|
|
106
|
+
* Override to serialize value field to JSON string
|
|
107
|
+
*/
|
|
108
|
+
update(record: IChangeRecord): Promise<IChangeRecord>;
|
|
109
|
+
/**
|
|
110
|
+
* Deserialize the value field if it's a JSON string
|
|
111
|
+
* For path "/" (full object changes), use fromJSONString to handle special types
|
|
112
|
+
*/
|
|
113
|
+
private deserializeValue;
|
|
114
|
+
/**
|
|
115
|
+
* Delete all superseded changes for a specific table that were superseded before the given timestamp.
|
|
116
|
+
* This is used for compaction to prevent the change tracking table from growing indefinitely.
|
|
117
|
+
*
|
|
118
|
+
* @param tableName - Name of the table to compact
|
|
119
|
+
* @param beforeTimestamp - Delete changes superseded before this timestamp
|
|
120
|
+
*/
|
|
121
|
+
deleteSupersededChangesOlderThan(tableName: string, beforeTimestamp: number): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Bulk delete changes
|
|
124
|
+
*
|
|
125
|
+
* @param tableName - Name of the table to compact
|
|
126
|
+
* @param beforeTimestamp - Delete changes superseded before this timestamp
|
|
127
|
+
*/
|
|
128
|
+
deleteChanges(changeIds: string[]): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Get table metadata
|
|
131
|
+
*/
|
|
132
|
+
static getTableMetaData(): ITableMetaData;
|
|
218
133
|
}
|
|
219
|
-
export {};
|
|
@@ -1,119 +1,269 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Change tracking table V2
|
|
4
|
+
*
|
|
5
|
+
* This table stores granular change records for data synchronization.
|
|
6
|
+
* Unlike V1, each JSON patch operation gets its own record, enabling
|
|
7
|
+
* path-based superseding and more efficient cleanup.
|
|
8
|
+
*/
|
|
2
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ChangeTrackingTable = exports.
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
const zod_types_1 = require("../types/zod-types");
|
|
6
|
-
const orm_1 = require("./orm");
|
|
10
|
+
exports.ChangeTrackingTable = exports.changeRecordSchema = void 0;
|
|
7
11
|
const events_1 = require("../events");
|
|
12
|
+
const serial_json_1 = require("../serial-json");
|
|
8
13
|
const utils_1 = require("../utils");
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
timestamp: zod_1.z.number(),
|
|
14
|
-
timestampApplied: zod_1.z.number(),
|
|
15
|
-
tableName: zod_1.z.string(),
|
|
16
|
-
recordId: zod_1.z.string(),
|
|
17
|
-
newRecord: zod_types_1.zodAnyObject,
|
|
18
|
-
});
|
|
19
|
-
const deleteChange = zod_1.z.object({
|
|
20
|
-
changeId: zod_types_1.zodPeerId,
|
|
21
|
-
changeType: zod_1.z.literal('delete'),
|
|
22
|
-
timestamp: zod_1.z.number(),
|
|
23
|
-
timestampApplied: zod_1.z.number(),
|
|
24
|
-
tableName: zod_1.z.string(),
|
|
25
|
-
recordId: zod_1.z.string(),
|
|
26
|
-
oldRecord: zod_types_1.zodAnyObject.optional(),
|
|
27
|
-
});
|
|
28
|
-
const updateChange = zod_1.z.object({
|
|
29
|
-
changeId: zod_types_1.zodPeerId,
|
|
30
|
-
changeType: zod_1.z.literal('update'),
|
|
31
|
-
timestamp: zod_1.z.number(),
|
|
32
|
-
timestampApplied: zod_1.z.number(),
|
|
33
|
-
tableName: zod_1.z.string(),
|
|
34
|
-
recordId: zod_1.z.string(),
|
|
35
|
-
newRecord: zod_types_1.zodAnyObject,
|
|
36
|
-
jsonDiff: zod_1.z.array(zod_1.z.any()),
|
|
37
|
-
});
|
|
38
|
-
const changeSchema = zod_1.z.union([insertChange, deleteChange, updateChange]);
|
|
39
|
-
exports.changeTrackingSchema = zod_1.z.object({
|
|
14
|
+
const orm_1 = require("./orm");
|
|
15
|
+
const zod_1 = require("zod");
|
|
16
|
+
const zod_types_1 = require("../types/zod-types");
|
|
17
|
+
exports.changeRecordSchema = zod_1.z.object({
|
|
40
18
|
changeId: zod_types_1.zodPeerId,
|
|
41
|
-
|
|
42
|
-
timestamp: zod_1.z.number(),
|
|
43
|
-
timestampApplied: zod_1.z.number(),
|
|
19
|
+
transactionId: zod_1.z.string().optional(), // not used for now
|
|
44
20
|
tableName: zod_1.z.string(),
|
|
45
21
|
recordId: zod_1.z.string(),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
22
|
+
op: zod_1.z.enum(['set', 'delete', 'patch-text']),
|
|
23
|
+
path: zod_1.z.string().describe('JSON Patch path (e.g., "/title", "/age")'),
|
|
24
|
+
value: zod_1.z.any().nullish().describe('JSON value - can be any type (object, string, number, boolean, array, null, or undefined)'),
|
|
25
|
+
createdAt: zod_1.z.number(),
|
|
26
|
+
appliedAt: zod_1.z.number(),
|
|
27
|
+
supersededAt: zod_1.z.number().optional(),
|
|
49
28
|
});
|
|
50
|
-
const
|
|
51
|
-
name: '
|
|
29
|
+
const changeTrackingV2TableMetaData = {
|
|
30
|
+
name: 'ChangeTrackingV2',
|
|
52
31
|
primaryKeyName: 'changeId',
|
|
53
|
-
description: 'Change tracking table',
|
|
54
|
-
fields: (0, orm_1.schemaToFields)(exports.
|
|
32
|
+
description: 'Change tracking table V2 with superseding support',
|
|
33
|
+
fields: (0, orm_1.schemaToFields)(exports.changeRecordSchema),
|
|
55
34
|
indexes: [
|
|
56
|
-
|
|
57
|
-
{ fields: ['
|
|
58
|
-
{ fields: ['
|
|
59
|
-
|
|
60
|
-
{ fields: ['
|
|
35
|
+
// Single-column indexes for common sorting/filtering
|
|
36
|
+
{ fields: ['createdAt'] },
|
|
37
|
+
{ fields: ['appliedAt'] },
|
|
38
|
+
// Compound indexes optimized for actual query patterns
|
|
39
|
+
{ fields: ['tableName', 'recordId', 'createdAt'] }, // active changes by table + record
|
|
40
|
+
{ fields: ['tableName', 'recordId', 'supersededAt'] }, // active changes lookup without full sort
|
|
41
|
+
{ fields: ['recordId', 'path'] }, // path-based superseding and deleted record checks
|
|
42
|
+
{ fields: ['supersededAt', 'appliedAt', 'createdAt'] }, // main sync query (supersededAt filter + appliedAt range + createdAt sort)
|
|
43
|
+
{ fields: ['tableName', 'supersededAt'] }, // cleanup superseded changes by table
|
|
61
44
|
],
|
|
62
45
|
localOnly: true,
|
|
63
46
|
};
|
|
64
47
|
class ChangeTrackingTable extends orm_1.SQLDataSource {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
dataChangedEmitter;
|
|
49
|
+
dataChanged;
|
|
50
|
+
constructor({ db }) {
|
|
51
|
+
super(db, changeTrackingV2TableMetaData, exports.changeRecordSchema);
|
|
52
|
+
const tableName = changeTrackingV2TableMetaData.name;
|
|
53
|
+
this.dataChangedEmitter = new events_1.Emitter(tableName + '_DataChanged_' + (0, utils_1.newid)());
|
|
69
54
|
// @ts-ignore
|
|
70
55
|
this.dataChanged = this.dataChangedEmitter.event;
|
|
71
|
-
// TODO turn this back on to observe changes and make it performant
|
|
72
|
-
// this.dataChanged.subscribe((change) => {
|
|
73
|
-
// console.log("Data changed:", change);
|
|
74
|
-
// });
|
|
75
56
|
}
|
|
76
|
-
|
|
77
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Efficiently find which recordIds have been deleted.
|
|
59
|
+
*
|
|
60
|
+
* A record is deleted if the most recent write to path "/" is a 'delete' operation.
|
|
61
|
+
* This handles distributed scenarios where peers may write to specific paths after
|
|
62
|
+
* deletion - those writes are valid but superseded.
|
|
63
|
+
*
|
|
64
|
+
* Uses optimized SQL with CTE for performance.
|
|
65
|
+
*
|
|
66
|
+
* @param recordIds - Array of recordIds to check
|
|
67
|
+
* @returns Array of recordIds that are deleted
|
|
68
|
+
*/
|
|
78
69
|
async filterToDeletedIds(recordIds) {
|
|
70
|
+
if (recordIds.length === 0)
|
|
71
|
+
return [];
|
|
79
72
|
await this.initTable();
|
|
80
|
-
//
|
|
73
|
+
// Find the most recent change to path "/" for each record
|
|
74
|
+
// If it's a 'delete' operation, the record is deleted
|
|
81
75
|
const deletedRecordsResult = await this.db.all(`
|
|
82
|
-
WITH
|
|
83
|
-
SELECT
|
|
76
|
+
WITH RootPathChanges AS (
|
|
77
|
+
SELECT
|
|
84
78
|
recordId,
|
|
85
|
-
|
|
79
|
+
op,
|
|
80
|
+
MAX(createdAt) as last_timestamp
|
|
86
81
|
FROM "${this.tableName}"
|
|
87
82
|
WHERE
|
|
88
|
-
recordId IN (${recordIds.map(() => '?').join(',')})
|
|
89
|
-
|
|
83
|
+
recordId IN (${recordIds.map(() => '?').join(',')})
|
|
84
|
+
AND path = '/'
|
|
90
85
|
GROUP BY recordId
|
|
91
86
|
)
|
|
92
|
-
SELECT
|
|
93
|
-
|
|
94
|
-
FROM
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ct.changeType = 'delete'
|
|
98
|
-
`, recordIds);
|
|
87
|
+
SELECT
|
|
88
|
+
recordId
|
|
89
|
+
FROM RootPathChanges
|
|
90
|
+
WHERE op = 'delete'
|
|
91
|
+
`, recordIds);
|
|
99
92
|
return deletedRecordsResult.map(d => d.recordId);
|
|
100
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Find recordIds that have no change records at all.
|
|
96
|
+
*
|
|
97
|
+
* This is useful for identifying records that were never synced
|
|
98
|
+
* or have been completely cleaned up.
|
|
99
|
+
*
|
|
100
|
+
* @param recordIds - Array of recordIds to check
|
|
101
|
+
* @returns Array of recordIds with no change records
|
|
102
|
+
*/
|
|
101
103
|
async filterToMissingIds(recordIds) {
|
|
104
|
+
if (recordIds.length === 0)
|
|
105
|
+
return [];
|
|
102
106
|
await this.initTable();
|
|
103
107
|
const missingRecordsResult = await this.db.all(`
|
|
104
108
|
WITH InputRecordIds(recordId) AS (
|
|
105
109
|
VALUES
|
|
106
|
-
${recordIds.map(() => '(?)').join(',\n
|
|
110
|
+
${recordIds.map(() => '(?)').join(',\n ')}
|
|
107
111
|
)
|
|
108
|
-
SELECT
|
|
112
|
+
SELECT
|
|
109
113
|
i.recordId
|
|
110
114
|
FROM InputRecordIds AS i
|
|
111
115
|
LEFT JOIN "${this.tableName}" AS existing
|
|
112
116
|
ON i.recordId = existing.recordId
|
|
113
|
-
WHERE existing.recordId IS NULL
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
WHERE existing.recordId IS NULL
|
|
118
|
+
`, recordIds);
|
|
119
|
+
return missingRecordsResult.map(d => d.recordId);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Find changeIds that we do not have
|
|
123
|
+
*
|
|
124
|
+
* @param changeIds - Array of changeIds to check
|
|
125
|
+
* @returns Array of changeIds with no change records
|
|
126
|
+
*/
|
|
127
|
+
async filterToMissingChangeIds(changeIds) {
|
|
128
|
+
if (changeIds.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
await this.initTable();
|
|
131
|
+
const missingChangeIds = await this.db.all(`
|
|
132
|
+
WITH InputChangeIds(changeId) AS (
|
|
133
|
+
VALUES
|
|
134
|
+
${changeIds.map(() => '(?)').join(',\n ')}
|
|
135
|
+
)
|
|
136
|
+
SELECT
|
|
137
|
+
i.changeId
|
|
138
|
+
FROM InputChangeIds AS i
|
|
139
|
+
LEFT JOIN "${this.tableName}" AS existing
|
|
140
|
+
ON i.changeId = existing.changeId
|
|
141
|
+
WHERE existing.changeId IS NULL
|
|
142
|
+
`, changeIds);
|
|
143
|
+
return missingChangeIds.map(d => d.changeId);
|
|
144
|
+
}
|
|
145
|
+
async markAllPriorChangesSuperseded(tableName, recordId, supersededAt) {
|
|
146
|
+
await this.db.exec(`
|
|
147
|
+
UPDATE "${this.tableName}"
|
|
148
|
+
SET supersededAt = $supersededAt
|
|
149
|
+
WHERE
|
|
150
|
+
tableName = $tableName AND
|
|
151
|
+
recordId = $recordId AND
|
|
152
|
+
createdAt < $supersededAt AND
|
|
153
|
+
supersededAt IS NULL
|
|
154
|
+
`, { tableName, recordId, supersededAt });
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Batch update supersededAt timestamps for multiple changes.
|
|
158
|
+
* More efficient than updating one at a time.
|
|
159
|
+
*
|
|
160
|
+
* @param updates - Array of {changeId, supersededAt} pairs
|
|
161
|
+
*/
|
|
162
|
+
async batchMarkSuperseded(updates) {
|
|
163
|
+
if (updates.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
await this.initTable();
|
|
166
|
+
// Build a CASE statement for bulk update
|
|
167
|
+
const changeIds = updates.map(u => u.changeId);
|
|
168
|
+
const caseStatement = updates.map(u => `WHEN '${u.changeId}' THEN ${u.supersededAt}`).join('\n ');
|
|
169
|
+
await this.db.exec(`
|
|
170
|
+
UPDATE "${this.tableName}"
|
|
171
|
+
SET supersededAt = CASE changeId
|
|
172
|
+
${caseStatement}
|
|
173
|
+
END
|
|
174
|
+
WHERE changeId IN (${changeIds.map(() => '?').join(',')})
|
|
175
|
+
`, changeIds);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Override list to deserialize value field from JSON string
|
|
179
|
+
*/
|
|
180
|
+
async list(filter, opts) {
|
|
181
|
+
const records = await super.list(filter, opts);
|
|
182
|
+
return records.map(this.deserializeValue);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Override get to deserialize value field from JSON string
|
|
186
|
+
*/
|
|
187
|
+
async get(id) {
|
|
188
|
+
const record = await super.get(id);
|
|
189
|
+
return record ? this.deserializeValue(record) : undefined;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Override to serialize value field to JSON string
|
|
193
|
+
*/
|
|
194
|
+
async insert(record) {
|
|
195
|
+
if (record.value !== undefined) {
|
|
196
|
+
record.value = (0, serial_json_1.toJSONString)(record.value);
|
|
197
|
+
}
|
|
198
|
+
return super.insert(record);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Override to serialize value field to JSON string
|
|
202
|
+
*/
|
|
203
|
+
async update(record) {
|
|
204
|
+
if (record.value !== undefined) {
|
|
205
|
+
record.value = (0, serial_json_1.toJSONString)(record.value);
|
|
206
|
+
}
|
|
207
|
+
return super.update(record);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Deserialize the value field if it's a JSON string
|
|
211
|
+
* For path "/" (full object changes), use fromJSONString to handle special types
|
|
212
|
+
*/
|
|
213
|
+
deserializeValue(record) {
|
|
214
|
+
if (record.value !== undefined) {
|
|
215
|
+
record.value = (0, serial_json_1.fromJSONString)(record.value);
|
|
216
|
+
}
|
|
217
|
+
// if (typeof record.value === 'string') {
|
|
218
|
+
// try {
|
|
219
|
+
// if (record.path === '/') {
|
|
220
|
+
// // use metadata to convert record
|
|
221
|
+
// record.value = fromJSONString(record.value);
|
|
222
|
+
// } else {
|
|
223
|
+
// // Use plain JSON.parse for field values
|
|
224
|
+
// // record.value = JSON.parse(record.value);
|
|
225
|
+
// record.value = fromJSONString(record.value);
|
|
226
|
+
// }
|
|
227
|
+
// } catch (e) {
|
|
228
|
+
// // If it's not valid JSON, leave it as a string
|
|
229
|
+
// }
|
|
230
|
+
// }
|
|
231
|
+
return record;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Delete all superseded changes for a specific table that were superseded before the given timestamp.
|
|
235
|
+
* This is used for compaction to prevent the change tracking table from growing indefinitely.
|
|
236
|
+
*
|
|
237
|
+
* @param tableName - Name of the table to compact
|
|
238
|
+
* @param beforeTimestamp - Delete changes superseded before this timestamp
|
|
239
|
+
*/
|
|
240
|
+
async deleteSupersededChangesOlderThan(tableName, beforeTimestamp) {
|
|
241
|
+
await this.initTable();
|
|
242
|
+
await this.db.exec(`
|
|
243
|
+
DELETE FROM "${this.tableName}"
|
|
244
|
+
WHERE tableName = ?
|
|
245
|
+
AND supersededAt IS NOT NULL
|
|
246
|
+
AND supersededAt < ?
|
|
247
|
+
`, [tableName, beforeTimestamp]);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Bulk delete changes
|
|
251
|
+
*
|
|
252
|
+
* @param tableName - Name of the table to compact
|
|
253
|
+
* @param beforeTimestamp - Delete changes superseded before this timestamp
|
|
254
|
+
*/
|
|
255
|
+
async deleteChanges(changeIds) {
|
|
256
|
+
await this.initTable();
|
|
257
|
+
await this.db.exec(`
|
|
258
|
+
DELETE FROM "${this.tableName}"
|
|
259
|
+
WHERE changeId IN (${changeIds.map(c => "?").join()})
|
|
260
|
+
`, changeIds);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get table metadata
|
|
264
|
+
*/
|
|
265
|
+
static getTableMetaData() {
|
|
266
|
+
return changeTrackingV2TableMetaData;
|
|
117
267
|
}
|
|
118
268
|
}
|
|
119
269
|
exports.ChangeTrackingTable = ChangeTrackingTable;
|
package/dist/data/data-locks.js
CHANGED
|
@@ -115,12 +115,23 @@ class DataLocksTable extends orm_1.Table {
|
|
|
115
115
|
async countLockAcknowledgements(lock) {
|
|
116
116
|
if (!this.peerDevice)
|
|
117
117
|
throw new Error(`Peer device not set for DataLocksTable`);
|
|
118
|
-
const
|
|
118
|
+
const changes = await this.peerDevice.listChanges({
|
|
119
119
|
tableName: dataLocksTableMetaData.name,
|
|
120
120
|
recordId: lock.dataLockId,
|
|
121
|
-
|
|
121
|
+
op: 'set', // V2 uses 'set' for updates
|
|
122
122
|
});
|
|
123
|
-
|
|
123
|
+
// In V2, look for changes at path "/acknowledged" or full record changes at path "/"
|
|
124
|
+
const acknowledgedUpdates = changes
|
|
125
|
+
.map(change => {
|
|
126
|
+
if (change.path === '/acknowledged') {
|
|
127
|
+
return change.value;
|
|
128
|
+
}
|
|
129
|
+
else if (change.path === '/' && change.value && typeof change.value === 'object') {
|
|
130
|
+
return change.value.acknowledged;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
})
|
|
134
|
+
.filter(a => a !== undefined);
|
|
124
135
|
// NOTE this could potentially miscount if two peers acknowledge the lock with the same timestamp (very unlikely)
|
|
125
136
|
return (0, lodash_1.uniq)(acknowledgedUpdates).length;
|
|
126
137
|
}
|
|
@@ -60,7 +60,7 @@ class MockPeerDevice {
|
|
|
60
60
|
return this.changes;
|
|
61
61
|
return this.changes.filter(change => change.tableName === query.tableName &&
|
|
62
62
|
change.recordId === query.recordId &&
|
|
63
|
-
change.
|
|
63
|
+
change.op === query.op);
|
|
64
64
|
}
|
|
65
65
|
async notifyOfChanges(deviceId, timestampLastApplied) {
|
|
66
66
|
// Mock implementation
|
|
@@ -78,11 +78,11 @@ class MockPeerDevice {
|
|
|
78
78
|
changeId: (0, utils_1.newid)(),
|
|
79
79
|
tableName: 'DataLocks',
|
|
80
80
|
recordId,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
op: 'set',
|
|
82
|
+
path: '/acknowledged',
|
|
83
|
+
value: acknowledged,
|
|
84
|
+
createdAt: Date.now(),
|
|
85
|
+
appliedAt: Date.now(),
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
setConnectionCount(count) {
|
|
@@ -55,6 +55,8 @@ class PersistentVarsTable extends table_1.Table {
|
|
|
55
55
|
persistentVar.value.value = await rpc_types_1.rpcServerCalls.encryptData(persistentVar.value.value || '', this.groupId);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
opts ??= {};
|
|
59
|
+
opts = { saveAsSnapshot: true, ...opts };
|
|
58
60
|
return super.save(persistentVar, opts);
|
|
59
61
|
}
|
|
60
62
|
async getPersistentVarValue(name) {
|
package/dist/data/users.js
CHANGED
|
@@ -225,7 +225,7 @@ class Connection {
|
|
|
225
225
|
this.emit('reset');
|
|
226
226
|
throw new Error('Untrusted connection');
|
|
227
227
|
}
|
|
228
|
-
console.log(`Connection ${this.connectionId} verified on server side with trust level ${this.trustLevel}`);
|
|
228
|
+
// console.log(`Connection ${this.connectionId} verified on server side with trust level ${this.trustLevel}`);
|
|
229
229
|
this._verified = true;
|
|
230
230
|
if (this.onHandshakeComplete) {
|
|
231
231
|
setTimeout(() => {
|
|
@@ -264,7 +264,7 @@ class Connection {
|
|
|
264
264
|
this.emit('reset');
|
|
265
265
|
throw new Error('Untrusted connection');
|
|
266
266
|
}
|
|
267
|
-
console.log(`Connection ${this.connectionId} (${remoteAddress}) verified on client side with trust level ${this.trustLevel}`);
|
|
267
|
+
// console.log(`Connection ${this.connectionId} (${remoteAddress}) verified on client side with trust level ${this.trustLevel}`);
|
|
268
268
|
return handshakeResponse;
|
|
269
269
|
}
|
|
270
270
|
closeLocal() {
|
|
@@ -14,8 +14,8 @@ export declare const consoleLogSchema: z.ZodObject<{
|
|
|
14
14
|
}, "strip", z.ZodTypeAny, {
|
|
15
15
|
message: string;
|
|
16
16
|
level: "error" | "debug" | "info" | "log" | "warn";
|
|
17
|
-
timestamp: number;
|
|
18
17
|
logId: string;
|
|
18
|
+
timestamp: number;
|
|
19
19
|
process: string;
|
|
20
20
|
processInstanceId: string;
|
|
21
21
|
source?: string | undefined;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IChangeRecord, GroupMemberRole } from "../data";
|
|
2
2
|
import type { DataFilter, IDataQueryParams } from "../data/orm";
|
|
3
3
|
export interface IPeerDevice {
|
|
4
4
|
deviceId: string;
|
|
5
5
|
userId: string;
|
|
6
6
|
role: GroupMemberRole;
|
|
7
|
-
listChanges(filter?: DataFilter<
|
|
7
|
+
listChanges(filter?: DataFilter<IChangeRecord>, opts?: IDataQueryParams<IChangeRecord>): Promise<IChangeRecord[]>;
|
|
8
8
|
getNetworkInfo(): Promise<INetworkInfo>;
|
|
9
9
|
notifyOfChanges(deviceId: string, timestampLastApplied: number): Promise<void>;
|
|
10
10
|
sendDeviceMessage(message: IDeviceMessage): Promise<any>;
|