@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.
@@ -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 personalUser = await (0, data_1.Users)(this.userDataContext).get(changedUser.userId);
195
- if (personalUser && !(0, lodash_1.isEqual)(personalUser, changedUser)) {
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
- 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,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.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'] },
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
- constructor({ db, }) {
66
- super(db, changeTrackingTableMetaData, changeSchema);
67
- const tableName = changeTrackingTableMetaData.name;
68
- 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)());
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
- dataChangedEmitter;
77
- 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
+ */
78
69
  async filterToDeletedIds(recordIds) {
70
+ if (recordIds.length === 0)
71
+ return [];
79
72
  await this.initTable();
80
- // 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
81
75
  const deletedRecordsResult = await this.db.all(`
82
- WITH InsertsAndDeletes AS (
83
- SELECT
76
+ WITH RootPathChanges AS (
77
+ SELECT
84
78
  recordId,
85
- MAX(timestamp) as last_timestamp
79
+ op,
80
+ MAX(createdAt) as last_timestamp
86
81
  FROM "${this.tableName}"
87
82
  WHERE
88
- recordId IN (${recordIds.map(() => '?').join(',')}) AND
89
- changeType IN ('restore', 'delete')
83
+ recordId IN (${recordIds.map(() => '?').join(',')})
84
+ AND path = '/'
90
85
  GROUP BY recordId
91
86
  )
92
- SELECT
93
- ct.recordId
94
- FROM "${this.tableName}" ct
95
- JOIN InsertsAndDeletes iad ON ct.recordId = iad.recordId AND ct.timestamp = iad.last_timestamp
96
- WHERE
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
- `, recordIds);
115
- let missingIds = missingRecordsResult.map(d => d.recordId);
116
- 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;
117
267
  }
118
268
  }
119
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) {
@@ -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) {
@@ -76,6 +76,8 @@ let UsersTable = (() => {
76
76
  }
77
77
  static isPassthrough = false;
78
78
  async save(user, opts) {
79
+ opts = opts || {};
80
+ opts = { saveAsSnapshot: true, ...opts };
79
81
  if (UsersTable.isPassthrough) {
80
82
  return super.save(user, opts);
81
83
  }
@@ -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 { 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.24",
3
+ "version": "0.7.26",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"