@peers-app/peers-sdk 0.7.25 → 0.7.27

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.
@@ -158,13 +158,13 @@ class UserContext {
158
158
  // sync my user to personal db
159
159
  if (!(0, lodash_1.isEqual)(me, meSigned)) {
160
160
  (0, keys_1.verifyObjectSignature)(meSigned);
161
- await (0, data_1.Users)(userContext.userDataContext).save(meSigned);
161
+ await (0, data_1.Users)(userContext.userDataContext).save(meSigned, { weakInsert: true });
162
162
  }
163
163
  // sync my user to all my groups
164
164
  for (const [, dataContext] of userContext.groupDataContexts) {
165
165
  let groupMe = await (0, data_1.Users)(dataContext).get(me.userId);
166
166
  if (!(0, lodash_1.isEqual)(groupMe, meSigned)) {
167
- await (0, data_1.Users)(dataContext).save(meSigned);
167
+ await (0, data_1.Users)(dataContext).save(meSigned, { weakInsert: true });
168
168
  }
169
169
  }
170
170
  // sync group objects to my personal db
@@ -1,219 +1,133 @@
1
- import { z } from "zod";
2
- import { ISqlDb, SQLDataSource } from "./orm";
3
- import { Emitter, Event } from "../events";
4
- declare const insertChange: z.ZodObject<{
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
- changeType: z.ZodUnion<[z.ZodUnion<[z.ZodLiteral<"insert">, z.ZodLiteral<"snapshot">]>, z.ZodLiteral<"restore">]>;
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
- newRecord: z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>;
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
- newRecord: {} & {
20
- [k: string]: any;
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
- newRecord: {} & {
30
- [k: string]: any;
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 IChangeInsert = z.infer<typeof insertChange>;
34
- declare const deleteChange: z.ZodObject<{
35
- changeId: z.ZodEffects<z.ZodString, string, string>;
36
- changeType: z.ZodLiteral<"delete">;
37
- timestamp: z.ZodNumber;
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
- readonly dataChangedEmitter: Emitter<IChange>;
215
- readonly dataChanged: Event<IChange>;
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,122 +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.changeTrackingSchema = void 0;
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 changeType = zod_1.z.enum(['insert', 'snapshot', 'update', 'delete', 'restore']);
10
- const insertChange = zod_1.z.object({
11
- changeId: zod_types_1.zodPeerId,
12
- changeType: zod_1.z.literal('insert').or(zod_1.z.literal('snapshot')).or(zod_1.z.literal('restore')),
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
- changeType: changeType,
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
- oldRecord: zod_types_1.zodAnyObject.optional(),
47
- newRecord: zod_types_1.zodAnyObject.optional(),
48
- jsonDiff: zod_1.z.array(zod_1.z.any()).optional(),
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 changeTrackingTableMetaData = {
51
- name: 'ChangeTracking',
29
+ const changeTrackingV2TableMetaData = {
30
+ name: 'ChangeTrackingV2',
52
31
  primaryKeyName: 'changeId',
53
- description: 'Change tracking table',
54
- fields: (0, orm_1.schemaToFields)(exports.changeTrackingSchema),
32
+ description: 'Change tracking table V2 with superseding support',
33
+ fields: (0, orm_1.schemaToFields)(exports.changeRecordSchema),
55
34
  indexes: [
56
- { fields: ['tableName'] },
57
- { fields: ['recordId'] },
58
- { fields: ['changeType'] },
59
- { fields: ['timestamp'] },
60
- { fields: ['timestampApplied'] },
61
- // compound indexes for common queries
62
- { fields: ['tableName', 'recordId', 'timestamp'] },
63
- { fields: ['recordId', 'changeType'] },
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
64
44
  ],
65
45
  localOnly: true,
66
46
  };
67
47
  class ChangeTrackingTable extends orm_1.SQLDataSource {
68
- constructor({ db, }) {
69
- super(db, changeTrackingTableMetaData, changeSchema);
70
- const tableName = changeTrackingTableMetaData.name;
71
- this.dataChangedEmitter = new events_1.Emitter(tableName + "_DataChanged_" + (0, utils_1.newid)());
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)());
72
54
  // @ts-ignore
73
55
  this.dataChanged = this.dataChangedEmitter.event;
74
- // TODO turn this back on to observe changes and make it performant
75
- // this.dataChanged.subscribe((change) => {
76
- // console.log("Data changed:", change);
77
- // });
78
56
  }
79
- dataChangedEmitter;
80
- dataChanged;
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
+ */
81
69
  async filterToDeletedIds(recordIds) {
70
+ if (recordIds.length === 0)
71
+ return [];
82
72
  await this.initTable();
83
- // find all changes who have been deleted
73
+ // Find the most recent change to path "/" for each record
74
+ // If it's a 'delete' operation, the record is deleted
84
75
  const deletedRecordsResult = await this.db.all(`
85
- WITH InsertsAndDeletes AS (
86
- SELECT
76
+ WITH RootPathChanges AS (
77
+ SELECT
87
78
  recordId,
88
- MAX(timestamp) as last_timestamp
79
+ op,
80
+ MAX(createdAt) as last_timestamp
89
81
  FROM "${this.tableName}"
90
82
  WHERE
91
- recordId IN (${recordIds.map(() => '?').join(',')}) AND
92
- changeType IN ('restore', 'delete')
83
+ recordId IN (${recordIds.map(() => '?').join(',')})
84
+ AND path = '/'
93
85
  GROUP BY recordId
94
86
  )
95
- SELECT
96
- ct.recordId
97
- FROM "${this.tableName}" ct
98
- JOIN InsertsAndDeletes iad ON ct.recordId = iad.recordId AND ct.timestamp = iad.last_timestamp
99
- WHERE
100
- ct.changeType = 'delete'
101
- `, recordIds);
87
+ SELECT
88
+ recordId
89
+ FROM RootPathChanges
90
+ WHERE op = 'delete'
91
+ `, recordIds);
102
92
  return deletedRecordsResult.map(d => d.recordId);
103
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
+ */
104
103
  async filterToMissingIds(recordIds) {
104
+ if (recordIds.length === 0)
105
+ return [];
105
106
  await this.initTable();
106
107
  const missingRecordsResult = await this.db.all(`
107
108
  WITH InputRecordIds(recordId) AS (
108
109
  VALUES
109
- ${recordIds.map(() => '(?)').join(',\n ')}
110
+ ${recordIds.map(() => '(?)').join(',\n ')}
110
111
  )
111
- SELECT
112
+ SELECT
112
113
  i.recordId
113
114
  FROM InputRecordIds AS i
114
115
  LEFT JOIN "${this.tableName}" AS existing
115
116
  ON i.recordId = existing.recordId
116
- WHERE existing.recordId IS NULL;
117
- `, recordIds);
118
- let missingIds = missingRecordsResult.map(d => d.recordId);
119
- return missingIds;
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;
120
267
  }
121
268
  }
122
269
  exports.ChangeTrackingTable = ChangeTrackingTable;
@@ -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 updates = await this.peerDevice.listChanges({
118
+ const changes = await this.peerDevice.listChanges({
119
119
  tableName: dataLocksTableMetaData.name,
120
120
  recordId: lock.dataLockId,
121
- changeType: 'update',
121
+ op: 'set', // V2 uses 'set' for updates
122
122
  });
123
- const acknowledgedUpdates = updates.map(u => u.newRecord.acknowledged).filter(a => a !== undefined);
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.changeType === query.changeType);
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
- changeType: 'update',
82
- newRecord: { acknowledged },
83
- timestamp: Date.now(),
84
- timestampApplied: Date.now(),
85
- jsonDiff: []
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) {
@@ -1,6 +1,7 @@
1
1
  export interface ISaveOptions {
2
2
  restoreIfDeleted?: boolean;
3
3
  saveAsSnapshot?: boolean;
4
+ weakInsert?: boolean;
4
5
  }
5
6
  export interface IDataSource<T extends {
6
7
  [key: string]: any;
@@ -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() {
@@ -13,19 +13,24 @@ function getTrustLevelFn(me, serverUrl) {
13
13
  // if (deviceInfo.deviceId === thisDeviceId()) {
14
14
  // return TrustLevel.Untrusted;
15
15
  // }
16
- const device = await (0, data_1.Devices)(userDataContext).get(deviceInfo.deviceId) || {
16
+ const existingDevice = await (0, data_1.Devices)(userDataContext).get(deviceInfo.deviceId);
17
+ const device = existingDevice || {
17
18
  deviceId: deviceInfo.deviceId,
18
19
  userId: deviceInfo.userId,
19
20
  firstSeen: new Date(),
20
21
  lastSeen: new Date(),
21
- trustLevel: socket_type_1.TrustLevel.Trusted,
22
+ trustLevel: socket_type_1.TrustLevel.NewDevice,
22
23
  serverUrl,
23
24
  };
24
25
  device.lastSeen = new Date();
25
- device.trustLevel = socket_type_1.TrustLevel.Trusted;
26
+ const seenMoreThan2DaysAgo = Date.now() - device.firstSeen.getTime() > 1000 * 60 * 60 * 24 * 2;
27
+ device.trustLevel = seenMoreThan2DaysAgo ? socket_type_1.TrustLevel.Trusted : socket_type_1.TrustLevel.NewDevice;
26
28
  console.log(`Updating my own device: ${deviceInfo.deviceId}`);
27
- await (0, data_1.Devices)(userDataContext).save(device, { restoreIfDeleted: true });
28
- return socket_type_1.TrustLevel.Trusted;
29
+ await (0, data_1.Devices)(userDataContext).save(device, {
30
+ restoreIfDeleted: true,
31
+ weakInsert: true,
32
+ });
33
+ return device.trustLevel;
29
34
  }
30
35
  let [user, device, userTrustLevel] = await Promise.all([
31
36
  (0, data_1.Users)(userDataContext).get(deviceInfo.userId),
@@ -113,10 +118,16 @@ function getTrustLevelFn(me, serverUrl) {
113
118
  // }
114
119
  if (newUser) {
115
120
  // TODO: I'm not sure we immediately want to save this user to the my personal db...
116
- await (0, data_1.Users)(userDataContext).save(user, { restoreIfDeleted: true });
121
+ await (0, data_1.Users)(userDataContext).save(user, {
122
+ restoreIfDeleted: true,
123
+ weakInsert: true,
124
+ });
117
125
  }
118
126
  // device.trustLevel = remoteTrustLevel || trustLevel;
119
- await (0, data_1.Devices)(userDataContext).save(device, { restoreIfDeleted: true });
127
+ await (0, data_1.Devices)(userDataContext).save(device, {
128
+ restoreIfDeleted: true,
129
+ weakInsert: true,
130
+ });
120
131
  return device.trustLevel;
121
132
  };
122
133
  }
@@ -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 { IChange, GroupMemberRole } from "../data";
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<IChange>, opts?: IDataQueryParams<IChange>): Promise<IChange[]>;
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.7.25",
3
+ "version": "0.7.27",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"