@peers-app/peers-sdk 0.8.2 → 0.8.4

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.
@@ -106,6 +106,40 @@ export declare class ChangeTrackingTable extends SQLDataSource<IChangeRecord> {
106
106
  * Override to serialize value field to JSON string
107
107
  */
108
108
  update(record: IChangeRecord): Promise<IChangeRecord>;
109
+ /**
110
+ * Synchronous insert for use within transactions.
111
+ * Table must be initialized before calling this method.
112
+ */
113
+ insertSync(record: IChangeRecord): IChangeRecord;
114
+ /**
115
+ * Synchronous update for use within transactions.
116
+ * Table must be initialized before calling this method.
117
+ */
118
+ updateSync(record: IChangeRecord): IChangeRecord;
119
+ /**
120
+ * Bulk insert multiple change records in a single transaction.
121
+ * Much faster than inserting records one at a time.
122
+ * All inserts succeed or all fail together.
123
+ */
124
+ bulkInsert(records: IChangeRecord[]): Promise<IChangeRecord[]>;
125
+ /**
126
+ * Synchronous batch mark superseded for use within transactions.
127
+ * Table must be initialized before calling this method.
128
+ */
129
+ batchMarkSupersededSync(updates: Array<{
130
+ changeId: string;
131
+ supersededAt: number;
132
+ }>): void;
133
+ /**
134
+ * Synchronous delete changes for use within transactions.
135
+ * Table must be initialized before calling this method.
136
+ */
137
+ deleteChangesSync(changeIds: string[]): void;
138
+ /**
139
+ * Synchronous delete superseded changes for use within transactions.
140
+ * Table must be initialized before calling this method.
141
+ */
142
+ deleteSupersededChangesOlderThanSync(tableName: string, beforeTimestamp: number): void;
109
143
  /**
110
144
  * Deserialize the value field if it's a JSON string
111
145
  * For path "/" (full object changes), use fromJSONString to handle special types
@@ -206,6 +206,92 @@ class ChangeTrackingTable extends orm_1.SQLDataSource {
206
206
  }
207
207
  return super.update(record);
208
208
  }
209
+ //================================================================================================
210
+ // Synchronous methods for use within transactions
211
+ //================================================================================================
212
+ /**
213
+ * Synchronous insert for use within transactions.
214
+ * Table must be initialized before calling this method.
215
+ */
216
+ insertSync(record) {
217
+ if (record.value !== undefined) {
218
+ record = { ...record, value: (0, serial_json_1.toJSONString)(record.value) };
219
+ }
220
+ return super.insertSync(record);
221
+ }
222
+ /**
223
+ * Synchronous update for use within transactions.
224
+ * Table must be initialized before calling this method.
225
+ */
226
+ updateSync(record) {
227
+ if (record.value !== undefined) {
228
+ record = { ...record, value: (0, serial_json_1.toJSONString)(record.value) };
229
+ }
230
+ return super.updateSync(record);
231
+ }
232
+ /**
233
+ * Bulk insert multiple change records in a single transaction.
234
+ * Much faster than inserting records one at a time.
235
+ * All inserts succeed or all fail together.
236
+ */
237
+ async bulkInsert(records) {
238
+ if (records.length === 0)
239
+ return [];
240
+ await this.initTable();
241
+ if (!this.db.runInTransaction) {
242
+ // Fallback for databases that don't support transactions
243
+ const results = [];
244
+ for (const record of records) {
245
+ results.push(await this.insert(record));
246
+ }
247
+ return results;
248
+ }
249
+ return this.db.runInTransaction(() => {
250
+ return records.map(record => this.insertSync(record));
251
+ });
252
+ }
253
+ /**
254
+ * Synchronous batch mark superseded for use within transactions.
255
+ * Table must be initialized before calling this method.
256
+ */
257
+ batchMarkSupersededSync(updates) {
258
+ if (updates.length === 0)
259
+ return;
260
+ // Build a CASE statement for bulk update
261
+ const changeIds = updates.map(u => u.changeId);
262
+ const caseStatement = updates.map(u => `WHEN '${u.changeId}' THEN ${u.supersededAt}`).join('\n ');
263
+ this.db.execSync(`
264
+ UPDATE "${this.tableName}"
265
+ SET supersededAt = CASE changeId
266
+ ${caseStatement}
267
+ END
268
+ WHERE changeId IN (${changeIds.map(() => '?').join(',')})
269
+ `, changeIds);
270
+ }
271
+ /**
272
+ * Synchronous delete changes for use within transactions.
273
+ * Table must be initialized before calling this method.
274
+ */
275
+ deleteChangesSync(changeIds) {
276
+ if (changeIds.length === 0)
277
+ return;
278
+ this.db.execSync(`
279
+ DELETE FROM "${this.tableName}"
280
+ WHERE changeId IN (${changeIds.map(c => "?").join()})
281
+ `, changeIds);
282
+ }
283
+ /**
284
+ * Synchronous delete superseded changes for use within transactions.
285
+ * Table must be initialized before calling this method.
286
+ */
287
+ deleteSupersededChangesOlderThanSync(tableName, beforeTimestamp) {
288
+ this.db.execSync(`
289
+ DELETE FROM "${this.tableName}"
290
+ WHERE tableName = ?
291
+ AND supersededAt IS NOT NULL
292
+ AND supersededAt < ?
293
+ `, [tableName, beforeTimestamp]);
294
+ }
209
295
  /**
210
296
  * Deserialize the value field if it's a JSON string
211
297
  * For path "/" (full object changes), use fromJSONString to handle special types
@@ -129,3 +129,27 @@ rpc_types_1.rpcServerCalls.getFileContents = async (fileId, encoding = 'utf8') =
129
129
  }
130
130
  return Buffer.from(data).toString(encoding);
131
131
  };
132
+ // Get file contents as base64 - useful for binary data like images
133
+ rpc_types_1.rpcServerCalls.getFileContentsBase64 = async (fileId) => {
134
+ const data = await Files().getFileContents(fileId);
135
+ if (data === null) {
136
+ throw new Error(`File not found: ${fileId}`);
137
+ }
138
+ return Buffer.from(data).toString('base64');
139
+ };
140
+ // Save a file from the UI - data should be base64 encoded for binary files
141
+ rpc_types_1.rpcServerCalls.saveFile = async (input) => {
142
+ const fileId = input.fileId || (0, utils_1.newid)();
143
+ const encoding = input.encoding || 'utf8';
144
+ // Convert data to Uint8Array based on encoding
145
+ const dataBuffer = encoding === 'base64'
146
+ ? Buffer.from(input.data, 'base64')
147
+ : Buffer.from(input.data, 'utf8');
148
+ const file = await Files().saveFile({
149
+ fileId,
150
+ name: input.name,
151
+ fileSize: dataBuffer.length,
152
+ mimeType: input.mimeType,
153
+ }, new Uint8Array(dataBuffer));
154
+ return file;
155
+ };
@@ -3,4 +3,19 @@ export interface ISqlDb {
3
3
  all: (sql: string, params?: any) => Promise<any[]>;
4
4
  exec: (sql: string, params?: any) => Promise<void>;
5
5
  close: () => Promise<void>;
6
+ execSync?: (sql: string, params?: any) => void;
7
+ getSync?: (sql: string, params?: any) => any;
8
+ allSync?: (sql: string, params?: any) => any[];
9
+ /**
10
+ * Execute multiple operations in a single SQLite transaction.
11
+ * All operations within the callback are committed atomically.
12
+ * If any operation throws, the entire transaction is rolled back.
13
+ *
14
+ * Note: The callback must be synchronous since better-sqlite3 transactions are synchronous.
15
+ * Use execSync/getSync/allSync within the transaction callback.
16
+ *
17
+ * @param fn - Synchronous function containing database operations
18
+ * @returns The return value of the callback function
19
+ */
20
+ runInTransaction?: <R>(fn: () => R) => R;
6
21
  }
@@ -32,6 +32,44 @@ export declare class SQLDataSource<T extends {
32
32
  dropTableIfExists(): Promise<void>;
33
33
  clearTable(): Promise<void>;
34
34
  initRecord(data?: Partial<T>): T;
35
+ /**
36
+ * Synchronous insert for use within transactions.
37
+ * Table must be initialized before calling this method.
38
+ */
39
+ insertSync(record: T): T;
40
+ /**
41
+ * Synchronous update for use within transactions.
42
+ * Table must be initialized before calling this method.
43
+ */
44
+ updateSync(record: T): T;
45
+ /**
46
+ * Synchronous delete for use within transactions.
47
+ * Table must be initialized before calling this method.
48
+ */
49
+ deleteSync(idOrRecord: string | T): void;
50
+ /**
51
+ * Synchronous save (insert or update) for use within transactions.
52
+ * Table must be initialized before calling this method.
53
+ */
54
+ saveSync(record: T): T;
55
+ /**
56
+ * Insert multiple records in a single transaction.
57
+ * Much faster than inserting records one at a time.
58
+ * All inserts succeed or all fail together.
59
+ */
60
+ bulkInsert(records: T[]): Promise<T[]>;
61
+ /**
62
+ * Delete multiple records by ID in a single transaction.
63
+ * Much faster than deleting records one at a time.
64
+ * All deletes succeed or all fail together.
65
+ */
66
+ bulkDelete(ids: string[]): Promise<void>;
67
+ /**
68
+ * Save (insert or update) multiple records in a single transaction.
69
+ * Much faster than saving records one at a time.
70
+ * All saves succeed or all fail together.
71
+ */
72
+ bulkSave(records: T[]): Promise<T[]>;
35
73
  }
36
74
  export declare function jsValueToSqlValue(value: any, escapeQuote?: boolean): any;
37
75
  export declare function sqlValueToJsValue(value: any, fieldType: FieldType, isArray: boolean): any;
@@ -312,6 +312,152 @@ class SQLDataSource {
312
312
  }
313
313
  return { ...defaults, ...data };
314
314
  }
315
+ //================================================================================================
316
+ // Synchronous methods for use within transactions
317
+ //================================================================================================
318
+ /**
319
+ * Synchronous insert for use within transactions.
320
+ * Table must be initialized before calling this method.
321
+ */
322
+ insertSync(record) {
323
+ if (!record[this.metaData.primaryKeyName]) {
324
+ // @ts-expect-error - need to figure out how to get typescript to understand what's going on here
325
+ record[this.metaData.primaryKeyName] = (0, utils_1.newid)();
326
+ }
327
+ // validate data
328
+ try {
329
+ this.schema.parse(record);
330
+ }
331
+ catch (e) {
332
+ throw new Error(`Validation on insert failed for ${this.tableName}: ${e}`);
333
+ }
334
+ const fields = this.metaData.fields.map(field => `"${field.name}"`).join(', ');
335
+ const values = this.metaData.fields.map(field => jsValueToSqlValue(record[field.name]));
336
+ const placeholders = values.map(v => '?').join(', ');
337
+ const sql = `INSERT INTO "${this.tableName}"(${fields}) VALUES(${placeholders})`;
338
+ this.db.execSync(sql, values);
339
+ return record;
340
+ }
341
+ /**
342
+ * Synchronous update for use within transactions.
343
+ * Table must be initialized before calling this method.
344
+ */
345
+ updateSync(record) {
346
+ if (!record[this.metaData.primaryKeyName]) {
347
+ // @ts-expect-error - need to figure out how to get typescript to understand what's going on here
348
+ record[this.metaData.primaryKeyName] = (0, utils_1.newid)();
349
+ }
350
+ // validate data
351
+ this.schema.parse(record);
352
+ const primaryKey = record[this.metaData.primaryKeyName];
353
+ const fields = this.metaData.fields.map(field => {
354
+ if (field.name === this.metaData.primaryKeyName)
355
+ return;
356
+ return `"${field.name}" = ?`;
357
+ }).filter(v => v !== undefined).join(',\n');
358
+ const values = this.metaData.fields.map(field => {
359
+ if (field.name === this.metaData.primaryKeyName)
360
+ return;
361
+ return jsValueToSqlValue(record[field.name]);
362
+ }).filter(v => v !== undefined);
363
+ const sql = [
364
+ `UPDATE "${this.tableName}"`,
365
+ `SET ${fields}`,
366
+ `WHERE "${this.metaData.primaryKeyName}" = ?`,
367
+ ].join('\n');
368
+ this.db.execSync(sql, [...values, primaryKey]);
369
+ return record;
370
+ }
371
+ /**
372
+ * Synchronous delete for use within transactions.
373
+ * Table must be initialized before calling this method.
374
+ */
375
+ deleteSync(idOrRecord) {
376
+ const primaryKey = typeof idOrRecord === 'string' ? idOrRecord : idOrRecord[this.metaData.primaryKeyName];
377
+ const sql = `DELETE FROM "${this.tableName}" WHERE "${this.metaData.primaryKeyName}" = ?`;
378
+ this.db.execSync(sql, [primaryKey]);
379
+ }
380
+ /**
381
+ * Synchronous save (insert or update) for use within transactions.
382
+ * Table must be initialized before calling this method.
383
+ */
384
+ saveSync(record) {
385
+ const primaryKey = record[this.metaData.primaryKeyName];
386
+ if (primaryKey) {
387
+ const existingRecord = this.db.getSync(`SELECT "${this.metaData.primaryKeyName}" FROM "${this.tableName}" WHERE ${this.metaData.primaryKeyName} = ?`, [primaryKey]);
388
+ if (existingRecord) {
389
+ return this.updateSync(record);
390
+ }
391
+ }
392
+ return this.insertSync(record);
393
+ }
394
+ //================================================================================================
395
+ // Bulk operations (use transactions for atomicity and performance)
396
+ //================================================================================================
397
+ /**
398
+ * Insert multiple records in a single transaction.
399
+ * Much faster than inserting records one at a time.
400
+ * All inserts succeed or all fail together.
401
+ */
402
+ async bulkInsert(records) {
403
+ if (records.length === 0)
404
+ return [];
405
+ await this.initTable();
406
+ if (!this.db.runInTransaction) {
407
+ // Fallback for databases that don't support transactions
408
+ const results = [];
409
+ for (const record of records) {
410
+ results.push(await this.insert(record));
411
+ }
412
+ return results;
413
+ }
414
+ return this.db.runInTransaction(() => {
415
+ return records.map(record => this.insertSync(record));
416
+ });
417
+ }
418
+ /**
419
+ * Delete multiple records by ID in a single transaction.
420
+ * Much faster than deleting records one at a time.
421
+ * All deletes succeed or all fail together.
422
+ */
423
+ async bulkDelete(ids) {
424
+ if (ids.length === 0)
425
+ return;
426
+ await this.initTable();
427
+ if (!this.db.runInTransaction) {
428
+ // Fallback for databases that don't support transactions
429
+ for (const id of ids) {
430
+ await this.delete(id);
431
+ }
432
+ return;
433
+ }
434
+ this.db.runInTransaction(() => {
435
+ for (const id of ids) {
436
+ this.deleteSync(id);
437
+ }
438
+ });
439
+ }
440
+ /**
441
+ * Save (insert or update) multiple records in a single transaction.
442
+ * Much faster than saving records one at a time.
443
+ * All saves succeed or all fail together.
444
+ */
445
+ async bulkSave(records) {
446
+ if (records.length === 0)
447
+ return [];
448
+ await this.initTable();
449
+ if (!this.db.runInTransaction) {
450
+ // Fallback for databases that don't support transactions
451
+ const results = [];
452
+ for (const record of records) {
453
+ results.push(await this.save(record));
454
+ }
455
+ return results;
456
+ }
457
+ return this.db.runInTransaction(() => {
458
+ return records.map(record => this.saveSync(record));
459
+ });
460
+ }
315
461
  }
316
462
  exports.SQLDataSource = SQLDataSource;
317
463
  function jsValueToSqlValue(value, escapeQuote = false) {
@@ -280,7 +280,7 @@ function handleRPCMessage(bytes, handlers, callbacks, sendRPCData) {
280
280
  if (message.type === 'call') {
281
281
  const handler = handlers[message.eventName];
282
282
  if (!handler) {
283
- console.error(`No handler for event: ${message.eventName}`);
283
+ console.warn(`No handler for event: ${message.eventName}`);
284
284
  return;
285
285
  }
286
286
  try {
@@ -18,6 +18,20 @@ export declare const rpcServerCalls: {
18
18
  encryptData: ((value: string, groupId?: string) => Promise<string>);
19
19
  tableMethodCall: ((dataContextId: string, tableName: string, methodName: string, ...args: any[]) => Promise<any>);
20
20
  getFileContents: ((fileId: string, encoding?: BufferEncoding) => Promise<string>);
21
+ getFileContentsBase64: ((fileId: string) => Promise<string>);
22
+ saveFile: ((input: {
23
+ name: string;
24
+ mimeType?: string;
25
+ data: string;
26
+ encoding?: "utf8" | "base64";
27
+ fileId?: string;
28
+ }) => Promise<{
29
+ fileId: string;
30
+ name: string;
31
+ fileSize: number;
32
+ fileHash: string;
33
+ mimeType?: string;
34
+ }>);
21
35
  injectUIBundle: ((uiBundleFileId: string) => Promise<void>);
22
36
  resetAllDeviceSyncInfo: (() => Promise<void>);
23
37
  importGroupShare: ((groupShareJson: string) => Promise<string>);
package/dist/rpc-types.js CHANGED
@@ -19,6 +19,10 @@ exports.rpcServerCalls = {
19
19
  tableMethodCall: rpcStub('tableMethodCall'),
20
20
  // TODO lock this down so not all code can get any file contents
21
21
  getFileContents: rpcStub('getFileContents'),
22
+ // Get file contents as base64 - useful for binary data like images
23
+ getFileContentsBase64: rpcStub('getFileContentsBase64'),
24
+ // Save a file from the UI - data should be base64 encoded for binary files
25
+ saveFile: rpcStub('saveFile'),
22
26
  // Inject UI bundle directly into WebView (bypasses slow postMessage for large strings)
23
27
  injectUIBundle: rpcStub('injectUIBundle'),
24
28
  resetAllDeviceSyncInfo: rpcStub('resetAllDeviceSyncInfo'),
@@ -6,6 +6,7 @@ exports.PeerDeviceConsts = Object.freeze({
6
6
  MAX_CONNECTIONS: 30,
7
7
  RESYNC_INTERVAL: 300_000, // play with this to find the right balance
8
8
  NOTIFY_CHANGE_DELAY: 100,
9
+ // CHANGES_PAGE_SIZE: 2_000,
9
10
  CHANGES_PAGE_SIZE: 100,
10
11
  RETRY_COUNT: 2,
11
12
  TIMEOUT_MIN: 3_000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"