@peers-app/peers-sdk 0.26.5 → 0.27.0

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.
Files changed (194) hide show
  1. package/README.md +3 -1
  2. package/bin/peers-package.js +13 -0
  3. package/define/package.json +4 -0
  4. package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
  5. package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/also-implements.test.js +110 -0
  7. package/dist/contracts/__tests__/builder.test.js +45 -0
  8. package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
  9. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
  10. package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
  11. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
  12. package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
  14. package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
  15. package/dist/contracts/__tests__/registry.test.js +68 -0
  16. package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
  17. package/dist/contracts/__tests__/target-dispatch.js +24 -0
  18. package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
  19. package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
  20. package/dist/contracts/__tests__/validate.test.js +177 -7
  21. package/dist/contracts/__tests__/workflow-context.test.js +0 -11
  22. package/dist/contracts/builder.d.ts +16 -2
  23. package/dist/contracts/builder.js +44 -3
  24. package/dist/contracts/contract-definition-schema.d.ts +251 -0
  25. package/dist/contracts/contract-definition-schema.js +48 -0
  26. package/dist/contracts/contract-provider-router.d.ts +2 -0
  27. package/dist/contracts/contract-provider-router.js +5 -2
  28. package/dist/contracts/contract-proxy.d.ts +52 -18
  29. package/dist/contracts/contract-proxy.js +144 -203
  30. package/dist/contracts/contract-resolvers.d.ts +6 -4
  31. package/dist/contracts/contract-resolvers.js +32 -9
  32. package/dist/contracts/frozen-contracts.d.ts +59 -0
  33. package/dist/contracts/frozen-contracts.js +86 -0
  34. package/dist/contracts/host-contract-client.d.ts +134 -0
  35. package/dist/contracts/host-contract-client.js +396 -0
  36. package/dist/contracts/host-contract-transport.d.ts +2 -0
  37. package/dist/contracts/host-contract-transport.js +8 -0
  38. package/dist/contracts/index.d.ts +6 -4
  39. package/dist/contracts/index.js +11 -5
  40. package/dist/contracts/registry.d.ts +31 -1
  41. package/dist/contracts/registry.js +100 -6
  42. package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
  43. package/dist/contracts/system/contract-catalog-contract.js +17 -5
  44. package/dist/contracts/tool-catalog-service.js +26 -6
  45. package/dist/contracts/tool-catalog.d.ts +1 -1
  46. package/dist/contracts/tool-catalog.js +4 -1
  47. package/dist/contracts/types.d.ts +32 -2
  48. package/dist/contracts/validate.d.ts +16 -1
  49. package/dist/contracts/validate.js +139 -25
  50. package/dist/data/change-tracking.d.ts +17 -5
  51. package/dist/data/change-tracking.js +22 -6
  52. package/dist/data/change-tracking.test.d.ts +1 -0
  53. package/dist/data/change-tracking.test.js +62 -0
  54. package/dist/data/group-members.d.ts +6 -1
  55. package/dist/data/group-members.js +12 -4
  56. package/dist/data/groups.d.ts +6 -1
  57. package/dist/data/groups.js +12 -4
  58. package/dist/data/orm/sql.data-source.d.ts +9 -0
  59. package/dist/data/orm/sql.data-source.js +47 -2
  60. package/dist/data/orm/sql.data-source.test.js +142 -0
  61. package/dist/data/orm/table-container.js +9 -9
  62. package/dist/data/orm/table-container.test.js +44 -0
  63. package/dist/data/orm/types.d.ts +41 -1
  64. package/dist/data/orm/types.js +159 -28
  65. package/dist/data/orm/types.test.js +129 -0
  66. package/dist/data/package-versions.d.ts +5 -1
  67. package/dist/data/package-versions.js +11 -4
  68. package/dist/data/packages.d.ts +7 -1
  69. package/dist/data/packages.js +13 -4
  70. package/dist/data/peer-types.d.ts +8 -62
  71. package/dist/data/persistent-vars.d.ts +4 -1
  72. package/dist/data/persistent-vars.js +7 -2
  73. package/dist/data/table-signing.test.d.ts +1 -0
  74. package/dist/data/table-signing.test.js +118 -0
  75. package/dist/data/tools.d.ts +29 -221
  76. package/dist/data/users.d.ts +1 -2
  77. package/dist/data/users.js +8 -5
  78. package/dist/define/also-implements.test.d.ts +1 -0
  79. package/dist/define/also-implements.test.js +123 -0
  80. package/dist/define/context.d.ts +77 -0
  81. package/dist/define/context.js +2 -0
  82. package/dist/define/contract-ref.d.ts +177 -0
  83. package/dist/define/contract-ref.js +67 -0
  84. package/dist/define/declare-contract.d.ts +167 -0
  85. package/dist/define/declare-contract.js +220 -0
  86. package/dist/define/declare-contract.test.d.ts +1 -0
  87. package/dist/define/declare-contract.test.js +573 -0
  88. package/dist/define/define-package.d.ts +203 -0
  89. package/dist/define/define-package.js +289 -0
  90. package/dist/define/define.test.d.ts +1 -0
  91. package/dist/define/define.test.js +1010 -0
  92. package/dist/define/empty-fields.d.ts +15 -0
  93. package/dist/define/empty-fields.js +134 -0
  94. package/dist/define/empty-fields.test.d.ts +1 -0
  95. package/dist/define/empty-fields.test.js +55 -0
  96. package/dist/define/events-observables.test.d.ts +1 -0
  97. package/dist/define/events-observables.test.js +1013 -0
  98. package/dist/define/events.d.ts +81 -0
  99. package/dist/define/events.js +81 -0
  100. package/dist/define/generated-field.d.ts +20 -0
  101. package/dist/define/generated-field.js +25 -0
  102. package/dist/define/index.d.ts +28 -0
  103. package/dist/define/index.js +62 -0
  104. package/dist/define/lazy-members.d.ts +8 -0
  105. package/dist/define/lazy-members.js +38 -0
  106. package/dist/define/manifest.d.ts +63 -0
  107. package/dist/define/manifest.js +653 -0
  108. package/dist/define/observables.d.ts +146 -0
  109. package/dist/define/observables.js +187 -0
  110. package/dist/define/serve-members.d.ts +21 -0
  111. package/dist/define/serve-members.js +152 -0
  112. package/dist/define/serve-members.test.d.ts +1 -0
  113. package/dist/define/serve-members.test.js +630 -0
  114. package/dist/define/serve.d.ts +22 -0
  115. package/dist/define/serve.js +58 -0
  116. package/dist/define/system-contracts.d.ts +41 -0
  117. package/dist/define/system-contracts.js +21 -0
  118. package/dist/define/table-handle.d.ts +13 -0
  119. package/dist/define/table-handle.js +36 -0
  120. package/dist/define/table.d.ts +128 -0
  121. package/dist/define/table.js +122 -0
  122. package/dist/define/tool.d.ts +129 -0
  123. package/dist/define/tool.js +91 -0
  124. package/dist/define/vars.d.ts +82 -0
  125. package/dist/define/vars.js +256 -0
  126. package/dist/define/vars.test.d.ts +1 -0
  127. package/dist/define/vars.test.js +39 -0
  128. package/dist/define/wire-value.d.ts +12 -0
  129. package/dist/define/wire-value.js +76 -0
  130. package/dist/define/worker-state.d.ts +15 -0
  131. package/dist/define/worker-state.js +45 -0
  132. package/dist/device/tx-encoding.test.js +10 -0
  133. package/dist/index.d.ts +3 -0
  134. package/dist/index.js +5 -1
  135. package/dist/observable.d.ts +42 -2
  136. package/dist/observable.js +25 -7
  137. package/dist/package-build/build-package.d.ts +60 -0
  138. package/dist/package-build/build-package.js +312 -0
  139. package/dist/package-build/build-package.test.d.ts +1 -0
  140. package/dist/package-build/build-package.test.js +204 -0
  141. package/dist/package-build/cli.d.ts +7 -0
  142. package/dist/package-build/cli.js +218 -0
  143. package/dist/package-build/contract-add.d.ts +35 -0
  144. package/dist/package-build/contract-add.js +97 -0
  145. package/dist/package-build/contract-add.test.d.ts +1 -0
  146. package/dist/package-build/contract-add.test.js +467 -0
  147. package/dist/package-build/contract-source.d.ts +59 -0
  148. package/dist/package-build/contract-source.js +471 -0
  149. package/dist/package-build/freeze-contracts.d.ts +56 -0
  150. package/dist/package-build/freeze-contracts.js +198 -0
  151. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  152. package/dist/package-build/freeze-contracts.test.js +355 -0
  153. package/dist/package-installer/package-creator.d.ts +4 -0
  154. package/dist/package-installer/package-creator.js +57 -16
  155. package/dist/package-installer/package-creator.test.js +86 -0
  156. package/dist/package-loader/contract-members.d.ts +53 -0
  157. package/dist/package-loader/contract-members.js +31 -0
  158. package/dist/package-loader/contract-package-loader.js +5 -1
  159. package/dist/package-loader/contract-package-loader.test.js +11 -0
  160. package/dist/package-loader/host-authority-loading.test.js +9 -5
  161. package/dist/package-loader/index.d.ts +1 -0
  162. package/dist/package-loader/index.js +1 -0
  163. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  164. package/dist/package-loader/isolated-artifact.js +257 -51
  165. package/dist/package-loader/isolated-artifact.test.js +294 -21
  166. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  167. package/dist/package-loader/isolated-table-gateway.js +30 -11
  168. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  169. package/dist/package-loader/package-loader.d.ts +8 -1
  170. package/dist/package-loader/package-loader.js +39 -20
  171. package/dist/package-loader/package-loader.test.js +47 -0
  172. package/dist/peer-id.d.ts +25 -0
  173. package/dist/peer-id.js +99 -0
  174. package/dist/serial-json.compat.test.d.ts +1 -0
  175. package/dist/serial-json.compat.test.js +369 -0
  176. package/dist/serial-json.d.ts +41 -5
  177. package/dist/serial-json.js +216 -157
  178. package/dist/serial-json.oracle.d.ts +21 -0
  179. package/dist/serial-json.oracle.js +222 -0
  180. package/dist/serial-json.test.js +315 -0
  181. package/dist/testing/index.d.ts +4 -0
  182. package/dist/testing/index.js +8 -0
  183. package/dist/testing/mock-host-contract.d.ts +123 -0
  184. package/dist/testing/mock-host-contract.js +434 -0
  185. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  186. package/dist/testing/mock-host-contract.test.js +782 -0
  187. package/dist/types/field-type.d.ts +35 -18
  188. package/dist/types/field-type.js +4 -1
  189. package/dist/types/zod-types.d.ts +9 -0
  190. package/dist/types/zod-types.js +8 -2
  191. package/dist/utils.d.ts +1 -25
  192. package/dist/utils.js +10 -90
  193. package/package.json +9 -2
  194. package/testing/package.json +4 -0
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateProviderSatisfiesContract = validateProviderSatisfiesContract;
4
+ exports.contractShapeChanges = contractShapeChanges;
4
5
  exports.validateImmutability = validateImmutability;
5
6
  exports.validateAlsoImplements = validateAlsoImplements;
7
+ const lodash_1 = require("lodash");
6
8
  const types_1 = require("./types");
7
9
  function ok() {
8
10
  return { valid: true, errors: [] };
@@ -15,12 +17,13 @@ function merge(results) {
15
17
  return { valid: errors.length === 0, errors };
16
18
  }
17
19
  /**
18
- * Check whether `implField` satisfies the contract's `contractField`.
19
- * - Types must match exactly.
20
- * - If the contract field is required, the implementation must also be required.
21
- * - `isArray` must match.
20
+ * Check whether `implField` satisfies the contract's `contractField` for its callers.
21
+ * - Types must match exactly, and so must `isArray`.
22
+ * - What callers receive: a field the contract requires must be required.
23
+ * - What callers send: a field the contract makes optional must be optional, since callers may
24
+ * leave it out; the implementation may make a required one optional.
22
25
  */
23
- function isFieldCompatible(contractField, implField) {
26
+ function isFieldCompatible(contractField, implField, direction) {
24
27
  if (contractField.type !== implField.type) {
25
28
  return `type mismatch: contract requires '${contractField.type}', implementation has '${implField.type}'`;
26
29
  }
@@ -30,9 +33,12 @@ function isFieldCompatible(contractField, implField) {
30
33
  if (!contractField.isArray && implField.isArray) {
31
34
  return "contract requires scalar, implementation is an array";
32
35
  }
33
- if (!contractField.optional && implField.optional) {
36
+ if (direction !== "input" && !contractField.optional && implField.optional) {
34
37
  return "contract requires a required field, implementation marks it optional";
35
38
  }
39
+ if (direction !== "output" && contractField.optional && !implField.optional) {
40
+ return "contract marks the field optional, implementation requires it";
41
+ }
36
42
  return null;
37
43
  }
38
44
  function validateTableFields(contractTable, implTable) {
@@ -50,7 +56,7 @@ function validateTableFields(contractTable, implTable) {
50
56
  }
51
57
  continue;
52
58
  }
53
- const compat = isFieldCompatible(contractField, implField);
59
+ const compat = isFieldCompatible(contractField, implField, "both");
54
60
  if (compat) {
55
61
  errors.push({
56
62
  kind: "field",
@@ -89,7 +95,7 @@ function validateToolFields(contractTool, implTool, direction) {
89
95
  }
90
96
  continue;
91
97
  }
92
- const compat = isFieldCompatible(contractField, implField);
98
+ const compat = isFieldCompatible(contractField, implField, direction);
93
99
  if (compat) {
94
100
  errors.push({
95
101
  kind: "field",
@@ -129,7 +135,7 @@ function validateEventFields(contractEvent, implEvent) {
129
135
  }
130
136
  continue;
131
137
  }
132
- const compat = isFieldCompatible(contractField, implField);
138
+ const compat = isFieldCompatible(contractField, implField, "output");
133
139
  if (compat) {
134
140
  errors.push({
135
141
  kind: "field",
@@ -226,12 +232,132 @@ function validateProviderSatisfiesContract(impl, contract) {
226
232
  }
227
233
  return errors.length === 0 ? ok() : fail(errors);
228
234
  }
235
+ /**
236
+ * Keys the frozen comparison ignores: a stable version's descriptions may change, and so may a
237
+ * tool's policy (`remoteAccessLevel`, `requiresWorkflowRun`), which isn't part of its shape.
238
+ */
239
+ const IGNORED_KEYS = new Set([
240
+ "description",
241
+ "usageDescription",
242
+ "remoteAccessLevel",
243
+ "requiresWorkflowRun",
244
+ ]);
245
+ /**
246
+ * IR keys added after contracts were first frozen. A shape that has one matches a shape that
247
+ * doesn't (a provider built with an older SDK); where both have it, they must agree.
248
+ */
249
+ const LATER_IR_KEYS = new Set(["fields", "enumValues", "value"]);
250
+ function isRecord(value) {
251
+ return value !== null && typeof value === "object" && !Array.isArray(value);
252
+ }
253
+ function uniqueNames(items) {
254
+ const byName = new Map();
255
+ for (const item of items) {
256
+ if (!isRecord(item) || typeof item.name !== "string" || byName.has(item.name)) {
257
+ return undefined;
258
+ }
259
+ byName.set(item.name, item);
260
+ }
261
+ return byName;
262
+ }
263
+ /**
264
+ * Structural equality for the frozen comparison. Members and fields match by name in any order,
265
+ * `enumValues` as sets, and keys in any order; descriptions and tool policy are ignored, and so is
266
+ * a later IR key only one side has. A `defaultValue` must be equal as data.
267
+ */
268
+ function sameShape(a, b, key) {
269
+ if (key === "defaultValue") {
270
+ // Legacy schemaToFields retains Zod's default factories. Compare only their wire data,
271
+ // as the previous JSON comparison did, without executing or comparing factory identities.
272
+ return (0, lodash_1.isEqual)(JSON.parse(JSON.stringify({ value: a })), JSON.parse(JSON.stringify({ value: b })));
273
+ }
274
+ if (Array.isArray(a) && Array.isArray(b)) {
275
+ if (key === "enumValues") {
276
+ const values = new Set(b);
277
+ return new Set(a).size === values.size && a.every((value) => values.has(value));
278
+ }
279
+ if (a.length !== b.length) {
280
+ return false;
281
+ }
282
+ const aByName = uniqueNames(a);
283
+ const bByName = uniqueNames(b);
284
+ if (aByName && bByName) {
285
+ return [...aByName].every(([name, item]) => bByName.has(name) && sameShape(item, bByName.get(name)));
286
+ }
287
+ return a.every((item, index) => sameShape(item, b[index]));
288
+ }
289
+ if (isRecord(a) && isRecord(b)) {
290
+ for (const name of new Set([...Object.keys(a), ...Object.keys(b)])) {
291
+ const left = a[name];
292
+ const right = b[name];
293
+ if (IGNORED_KEYS.has(name) ||
294
+ (left === undefined && right === undefined) ||
295
+ (LATER_IR_KEYS.has(name) && (left === undefined || right === undefined))) {
296
+ continue;
297
+ }
298
+ if (!sameShape(left, right, name)) {
299
+ return false;
300
+ }
301
+ }
302
+ return true;
303
+ }
304
+ return a === b;
305
+ }
306
+ /** A contract's member lists, with what to call one member of each in a change. */
307
+ const MEMBER_KINDS = [
308
+ ["tables", "table"],
309
+ ["tools", "tool"],
310
+ ["observables", "observable"],
311
+ ["events", "event"],
312
+ ];
313
+ /**
314
+ * What changed between two shapes of one contract version, member by member: a table, tool,
315
+ * observable, or event that was added, removed, or changed. Empty when the shapes match.
316
+ * Descriptions and tool policy are ignored, as in {@link validateImmutability}.
317
+ *
318
+ * @param existing - The frozen shape.
319
+ * @param incoming - The shape to compare with it.
320
+ * @returns One line per changed member, such as `tool add-item changed`.
321
+ */
322
+ function contractShapeChanges(existing, incoming) {
323
+ const changes = [];
324
+ for (const [kind, label] of MEMBER_KINDS) {
325
+ const beforeMembers = existing[kind] ?? [];
326
+ const afterMembers = incoming[kind] ?? [];
327
+ // Do not collapse duplicate names: host-backed registrations can reach this comparison
328
+ // without isolated-artifact validation. Preserve the list comparison for such inputs.
329
+ if (!uniqueNames(beforeMembers) || !uniqueNames(afterMembers)) {
330
+ if (!sameShape(beforeMembers, afterMembers)) {
331
+ changes.push(`${label} list changed (duplicate member names)`);
332
+ }
333
+ continue;
334
+ }
335
+ const before = new Map(beforeMembers.map((member) => [member.name, member]));
336
+ const after = new Map(afterMembers.map((member) => [member.name, member]));
337
+ for (const [name, member] of before) {
338
+ if (!after.has(name))
339
+ changes.push(`${label} ${name} was removed`);
340
+ else if (!sameShape(member, after.get(name)))
341
+ changes.push(`${label} ${name} changed`);
342
+ }
343
+ for (const name of after.keys()) {
344
+ if (!before.has(name))
345
+ changes.push(`${label} ${name} was added`);
346
+ }
347
+ }
348
+ return changes;
349
+ }
229
350
  /**
230
351
  * Validate that a frozen (stable) contract definition has not been modified.
231
352
  * If the existing definition has no `devTag` it is frozen; the incoming
232
- * definition must be structurally identical and also stable.
353
+ * definition must have the same shape and also be stable.
233
354
  * Dev contracts (`devTag: "dev"`) may change freely but cannot be registered
234
355
  * once a stable version exists for that contract+version.
356
+ *
357
+ * Descriptions may change on a frozen version (to point callers at a newer one, say), and so
358
+ * may a tool's policy (`remoteAccessLevel`, `requiresWorkflowRun`); member, field, and key order
359
+ * don't matter. IR keys added later (`fields`, `enumValues`, and an
360
+ * observable's `value`) are compared only when both definitions have them.
235
361
  */
236
362
  function validateImmutability(existing, incoming) {
237
363
  if (existing.devTag === "dev") {
@@ -247,25 +373,13 @@ function validateImmutability(existing, incoming) {
247
373
  },
248
374
  ]);
249
375
  }
250
- // Deep-compare the shapes. We serialize to JSON for a simple structural comparison.
251
- const existingShape = JSON.stringify({
252
- tables: existing.tables,
253
- tools: existing.tools,
254
- observables: existing.observables,
255
- events: existing.events ?? [],
256
- });
257
- const incomingShape = JSON.stringify({
258
- tables: incoming.tables,
259
- tools: incoming.tools,
260
- observables: incoming.observables,
261
- events: incoming.events ?? [],
262
- });
263
- if (existingShape !== incomingShape) {
376
+ const changes = contractShapeChanges(existing, incoming);
377
+ if (changes.length > 0) {
264
378
  return fail([
265
379
  {
266
380
  kind: "contract",
267
381
  name: (0, types_1.contractKey)(existing.contractId, existing.version),
268
- message: `Stable contract ${existing.name} v${existing.version} is frozen and cannot be modified`,
382
+ message: `Stable contract ${existing.name} v${existing.version} is frozen and cannot be modified (${changes.join("; ")})`,
269
383
  },
270
384
  ]);
271
385
  }
@@ -59,7 +59,12 @@ export type IChangeRecord = z.infer<typeof changeRecordSchema>;
59
59
  * - `appliedClock` stamps `appliedAt` at insert time (per-device sync watermark)
60
60
  */
61
61
  export declare class ChangeTrackingTable extends SQLDataSource<IChangeRecord> {
62
+ /**
63
+ * Writers fire this after storing change rows, whether written on this device, applied from a
64
+ * peer, or backfilled. Sync listens to advertise its latest `appliedAt`.
65
+ */
62
66
  readonly dataChangedEmitter: Emitter<IChangeRecord>;
67
+ /** Subscribe side of {@link dataChangedEmitter}. */
63
68
  readonly dataChanged: Event<IChangeRecord>;
64
69
  /**
65
70
  * Hybrid logical clock for author write times (`createdAt`, `supersededAt`).
@@ -112,15 +117,16 @@ export declare class ChangeTrackingTable extends SQLDataSource<IChangeRecord> {
112
117
  */
113
118
  filterToDeletedIds(recordIds: string[]): Promise<string[]>;
114
119
  /**
115
- * Find recordIds that have no change records at all.
120
+ * Find recordIds that have no change records, in any table or only in `tableName`.
116
121
  *
117
122
  * This is useful for identifying records that were never synced
118
123
  * or have been completely cleaned up.
119
124
  *
120
125
  * @param recordIds - Array of recordIds to check
126
+ * @param tableName - Limit the lookup to this table; omitted checks across all tables
121
127
  * @returns Array of recordIds with no change records
122
128
  */
123
- filterToMissingIds(recordIds: string[]): Promise<string[]>;
129
+ filterToMissingIds(recordIds: string[], tableName?: string): Promise<string[]>;
124
130
  /**
125
131
  * Find changeIds that we do not have
126
132
  *
@@ -214,12 +220,18 @@ export declare class ChangeTrackingTable extends SQLDataSource<IChangeRecord> {
214
220
  */
215
221
  deleteSupersededChangesOlderThan(tableName: string, beforeTimestamp: number): Promise<void>;
216
222
  /**
217
- * Bulk delete changes
223
+ * Bulk delete changes by ID.
218
224
  *
219
- * @param tableName - Name of the table to compact
220
- * @param beforeTimestamp - Delete changes superseded before this timestamp
225
+ * @param changeIds - IDs of the change rows to delete
221
226
  */
222
227
  deleteChanges(changeIds: string[]): Promise<void>;
228
+ /**
229
+ * Delete every change row for one table, active or superseded. Used when a table is deleted or
230
+ * becomes local-only: its rows stop syncing until a later schema enables sync again.
231
+ *
232
+ * @param tableName - Full name of the tracked table, as stored in each row's `tableName`
233
+ */
234
+ deleteTableChanges(tableName: string): Promise<void>;
223
235
  /**
224
236
  * Clear inline values on insert changes (op='set', path='/') for a specific table.
225
237
  * These values are no longer needed since they are resolved from the source table at query time.
@@ -63,7 +63,12 @@ const changeTrackingV2TableMetaData = {
63
63
  * - `appliedClock` stamps `appliedAt` at insert time (per-device sync watermark)
64
64
  */
65
65
  class ChangeTrackingTable extends orm_1.SQLDataSource {
66
+ /**
67
+ * Writers fire this after storing change rows, whether written on this device, applied from a
68
+ * peer, or backfilled. Sync listens to advertise its latest `appliedAt`.
69
+ */
66
70
  dataChangedEmitter;
71
+ /** Subscribe side of {@link dataChangedEmitter}. */
67
72
  dataChanged;
68
73
  /**
69
74
  * Hybrid logical clock for author write times (`createdAt`, `supersededAt`).
@@ -171,15 +176,16 @@ class ChangeTrackingTable extends orm_1.SQLDataSource {
171
176
  return deletedRecordsResult.map((d) => d.recordId);
172
177
  }
173
178
  /**
174
- * Find recordIds that have no change records at all.
179
+ * Find recordIds that have no change records, in any table or only in `tableName`.
175
180
  *
176
181
  * This is useful for identifying records that were never synced
177
182
  * or have been completely cleaned up.
178
183
  *
179
184
  * @param recordIds - Array of recordIds to check
185
+ * @param tableName - Limit the lookup to this table; omitted checks across all tables
180
186
  * @returns Array of recordIds with no change records
181
187
  */
182
- async filterToMissingIds(recordIds) {
188
+ async filterToMissingIds(recordIds, tableName) {
183
189
  if (recordIds.length === 0)
184
190
  return [];
185
191
  await this.initTable();
@@ -193,8 +199,9 @@ class ChangeTrackingTable extends orm_1.SQLDataSource {
193
199
  FROM InputRecordIds AS i
194
200
  LEFT JOIN "${this.tableName}" AS existing
195
201
  ON i.recordId = existing.recordId
202
+ ${tableName === undefined ? "" : "AND existing.tableName = ?"}
196
203
  WHERE existing.recordId IS NULL
197
- `, recordIds);
204
+ `, tableName === undefined ? recordIds : [...recordIds, tableName]);
198
205
  return missingRecordsResult.map((d) => d.recordId);
199
206
  }
200
207
  /**
@@ -443,10 +450,9 @@ class ChangeTrackingTable extends orm_1.SQLDataSource {
443
450
  `, [tableName, beforeTimestamp]);
444
451
  }
445
452
  /**
446
- * Bulk delete changes
453
+ * Bulk delete changes by ID.
447
454
  *
448
- * @param tableName - Name of the table to compact
449
- * @param beforeTimestamp - Delete changes superseded before this timestamp
455
+ * @param changeIds - IDs of the change rows to delete
450
456
  */
451
457
  async deleteChanges(changeIds) {
452
458
  await this.initTable();
@@ -455,6 +461,16 @@ class ChangeTrackingTable extends orm_1.SQLDataSource {
455
461
  WHERE changeId IN (${changeIds.map((_c) => "?").join()})
456
462
  `, changeIds);
457
463
  }
464
+ /**
465
+ * Delete every change row for one table, active or superseded. Used when a table is deleted or
466
+ * becomes local-only: its rows stop syncing until a later schema enables sync again.
467
+ *
468
+ * @param tableName - Full name of the tracked table, as stored in each row's `tableName`
469
+ */
470
+ async deleteTableChanges(tableName) {
471
+ await this.initTable();
472
+ await this.db.exec(`DELETE FROM "${this.tableName}" WHERE tableName = ?`, [tableName]);
473
+ }
458
474
  /**
459
475
  * Clear inline values on insert changes (op='set', path='/') for a specific table.
460
476
  * These values are no longer needed since they are resolved from the source table at query time.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const SQLiteDB = require("better-sqlite3");
4
+ const utils_1 = require("../utils");
5
+ const change_tracking_1 = require("./change-tracking");
6
+ describe("ChangeTrackingTable table-scoped history", () => {
7
+ let sqlite;
8
+ let changes;
9
+ beforeEach(() => {
10
+ sqlite = new SQLiteDB(":memory:");
11
+ const db = {
12
+ async get(sql, params = []) {
13
+ return sqlite.prepare(sql).get(params);
14
+ },
15
+ async all(sql, params = []) {
16
+ return sqlite.prepare(sql).all(params);
17
+ },
18
+ async exec(sql, params = []) {
19
+ return { changes: sqlite.prepare(sql).run(params).changes };
20
+ },
21
+ async close() {
22
+ sqlite.close();
23
+ },
24
+ };
25
+ changes = new change_tracking_1.ChangeTrackingTable({ db });
26
+ });
27
+ afterEach(() => sqlite.close());
28
+ const change = (tableName, recordId) => ({
29
+ changeId: (0, utils_1.newid)(),
30
+ tableName,
31
+ recordId,
32
+ op: "set",
33
+ path: "/",
34
+ value: null,
35
+ createdAt: Date.now(),
36
+ appliedAt: 0,
37
+ });
38
+ it("checks missing IDs within the requested table, preserving unscoped lookups", async () => {
39
+ const sharedId = (0, utils_1.newid)();
40
+ const trackedId = (0, utils_1.newid)();
41
+ const missingId = (0, utils_1.newid)();
42
+ await changes.insert(change("Other", sharedId));
43
+ await changes.insert(change("State", trackedId));
44
+ await expect(changes.filterToMissingIds([sharedId, trackedId, missingId], "State")).resolves.toEqual([sharedId, missingId]);
45
+ await expect(changes.filterToMissingIds([sharedId, trackedId, missingId])).resolves.toEqual([
46
+ missingId,
47
+ ]);
48
+ await expect(changes.filterToMissingIds([], "State")).resolves.toEqual([]);
49
+ });
50
+ it("deletes active, superseded, and deleted history only for the named table", async () => {
51
+ const tableName = "State'with-quote";
52
+ const recordId = (0, utils_1.newid)();
53
+ const other = change("Other", recordId);
54
+ await changes.insert(change(tableName, recordId));
55
+ await changes.insert({ ...change(tableName, recordId), supersededAt: Date.now() });
56
+ await changes.insert({ ...change(tableName, recordId), op: "delete" });
57
+ await changes.insert(other);
58
+ await changes.deleteTableChanges(tableName);
59
+ await changes.deleteTableChanges(tableName);
60
+ expect(await changes.list()).toEqual([expect.objectContaining({ changeId: other.changeId })]);
61
+ });
62
+ });
@@ -3,6 +3,7 @@ import type { DataContext } from "../context/data-context";
3
3
  import { GroupMemberRole } from "./group-member-roles";
4
4
  import type { ISaveOptions } from "./orm/data-query";
5
5
  import { Table } from "./orm/table";
6
+ /** Schema for a signed group membership, including its role and soft deletion state. */
6
7
  export declare const groupMemberSchema: z.ZodObject<{
7
8
  groupMemberId: z.ZodEffects<z.ZodString, string, string>;
8
9
  groupId: z.ZodEffects<z.ZodString, string, string>;
@@ -25,18 +26,22 @@ export declare const groupMemberSchema: z.ZodObject<{
25
26
  groupMemberId: string;
26
27
  deleted?: boolean | undefined;
27
28
  }>;
29
+ /** A signed association between a user and a group with an assigned role. */
28
30
  export type IGroupMember = z.infer<typeof groupMemberSchema>;
31
+ /** System table name for group membership records. */
29
32
  export declare const groupMembersTableName = "GroupMembers";
33
+ /** Group membership table with signature checks and signed soft deletion. */
30
34
  export declare class GroupMembersTable extends Table<IGroupMember> {
31
35
  static isPassthrough: boolean;
32
36
  save(groupMember: IGroupMember, opts?: ISaveOptions): Promise<IGroupMember>;
33
37
  signAndSave(groupMember: IGroupMember, opts?: ISaveOptions): Promise<IGroupMember>;
34
38
  delete(groupMemberId: string): Promise<void>;
35
- private static addSignatureToGroupMember;
39
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
36
40
  static enableGroupMemberSigning(fn: (groupMember: IGroupMember) => IGroupMember): void;
37
41
  /** @deprecated Forbidden on GroupsTable; use save() */
38
42
  insert(..._args: Parameters<Table<IGroupMember>["insert"]>): never;
39
43
  /** @deprecated Forbidden on GroupsTable; use save() */
40
44
  update(..._args: Parameters<Table<IGroupMember>["update"]>): never;
41
45
  }
46
+ /** Returns the GroupMembers table for the given context or the current default context. */
42
47
  export declare function GroupMembers(dataContext?: DataContext): GroupMembersTable;
@@ -46,6 +46,7 @@ const table_1 = require("./orm/table");
46
46
  const table_definitions_system_1 = require("./orm/table-definitions.system");
47
47
  const types_1 = require("./orm/types");
48
48
  const users_1 = require("./users");
49
+ /** Schema for a signed group membership, including its role and soft deletion state. */
49
50
  exports.groupMemberSchema = zod_1.z.object({
50
51
  groupMemberId: zod_types_1.zodPeerId,
51
52
  groupId: zod_types_1.zodPeerId.describe("The id of the group"),
@@ -59,6 +60,7 @@ exports.groupMemberSchema = zod_1.z.object({
59
60
  .optional()
60
61
  .describe("Whether this membership has been deleted (soft delete)"),
61
62
  });
63
+ /** System table name for group membership records. */
62
64
  exports.groupMembersTableName = "GroupMembers";
63
65
  const metaData = {
64
66
  name: exports.groupMembersTableName,
@@ -67,6 +69,10 @@ const metaData = {
67
69
  fields: (0, types_1.schemaToFields)(exports.groupMemberSchema),
68
70
  indexes: [{ fields: ["groupId", "userId"], unique: true }],
69
71
  };
72
+ // Symbol.for keeps signer registration shared by separately installed SDK copies.
73
+ const signingKey = Symbol.for("@peers-app/peers-sdk/group-member-signing");
74
+ const signingGlobals = globalThis;
75
+ /** Group membership table with signature checks and signed soft deletion. */
70
76
  let GroupMembersTable = (() => {
71
77
  let _classSuper = table_1.Table;
72
78
  let _instanceExtraInitializers = [];
@@ -96,10 +102,11 @@ let GroupMembersTable = (() => {
96
102
  throw new Error("Group member signature is not valid");
97
103
  }
98
104
  async signAndSave(groupMember, opts) {
99
- if (!GroupMembersTable.addSignatureToGroupMember) {
105
+ const sign = signingGlobals[signingKey];
106
+ if (!sign) {
100
107
  throw new Error("Group member signing must be enabled to sign and save group members. Call GroupMembersTable.enableGroupMemberSigning(fn) to enable it.");
101
108
  }
102
- groupMember = GroupMembersTable.addSignatureToGroupMember(groupMember);
109
+ groupMember = sign.call(GroupMembersTable, groupMember);
103
110
  return this.save(groupMember, {
104
111
  saveAsSnapshot: true,
105
112
  restoreIfDeleted: true,
@@ -118,9 +125,9 @@ let GroupMembersTable = (() => {
118
125
  await this.signAndSave(groupMember);
119
126
  }
120
127
  }
121
- static addSignatureToGroupMember = undefined;
128
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
122
129
  static enableGroupMemberSigning(fn) {
123
- GroupMembersTable.addSignatureToGroupMember = fn;
130
+ signingGlobals[signingKey] = fn;
124
131
  }
125
132
  /** @deprecated Forbidden on GroupsTable; use save() */
126
133
  insert(..._args) {
@@ -138,6 +145,7 @@ let GroupMembersTable = (() => {
138
145
  })();
139
146
  exports.GroupMembersTable = GroupMembersTable;
140
147
  (0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.groupMemberSchema, GroupMembersTable);
148
+ /** Returns the GroupMembers table for the given context or the current default context. */
141
149
  function GroupMembers(dataContext) {
142
150
  return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.groupMemberSchema, GroupMembersTable);
143
151
  }
@@ -3,6 +3,7 @@ import type { DataContext } from "../context/data-context";
3
3
  import { GroupMemberRole } from "./group-member-roles";
4
4
  import type { ISaveOptions } from "./orm/data-query";
5
5
  import { Table } from "./orm/table";
6
+ /** Schema for a signed group record, including membership policy and group public keys. */
6
7
  export declare const groupSchema: z.ZodObject<{
7
8
  groupId: z.ZodEffects<z.ZodString, string, string>;
8
9
  name: z.ZodString;
@@ -37,18 +38,22 @@ export declare const groupSchema: z.ZodObject<{
37
38
  iconClassName?: string | undefined;
38
39
  publicRole?: GroupMemberRole | undefined;
39
40
  }>;
41
+ /** A signed group record shared between personal and group contexts. */
40
42
  export type IGroup = z.infer<typeof groupSchema>;
43
+ /** System table name for group records. */
41
44
  export declare const groupsTableName = "Groups";
45
+ /** Group table with signature enforcement outside the personal context. */
42
46
  export declare class GroupsTable extends Table<IGroup> {
43
47
  static isPassthrough: boolean;
44
48
  save(group: IGroup, opts?: ISaveOptions): Promise<IGroup>;
45
49
  signAndSave(group: IGroup, opts?: ISaveOptions): Promise<IGroup>;
46
50
  delete(groupId: string): Promise<void>;
47
- private static addSignatureToGroup;
51
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
48
52
  static enableGroupSigning(fn: (group: IGroup) => IGroup): void;
49
53
  /** @deprecated Direct inserts forbidden; use save() */
50
54
  insert(..._args: Parameters<Table<IGroup>["insert"]>): never;
51
55
  /** @deprecated Direct updates forbidden; use save() */
52
56
  update(..._args: Parameters<Table<IGroup>["update"]>): never;
53
57
  }
58
+ /** Returns the Groups table for the given context or the current default context. */
54
59
  export declare function Groups(dataContext?: DataContext): GroupsTable;
@@ -45,6 +45,7 @@ const decorators_1 = require("./orm/decorators");
45
45
  const table_1 = require("./orm/table");
46
46
  const table_definitions_system_1 = require("./orm/table-definitions.system");
47
47
  const types_1 = require("./orm/types");
48
+ /** Schema for a signed group record, including membership policy and group public keys. */
48
49
  exports.groupSchema = zod_1.z.object({
49
50
  groupId: zod_types_1.zodPeerId,
50
51
  name: zod_1.z.string(),
@@ -67,6 +68,7 @@ exports.groupSchema = zod_1.z.object({
67
68
  .string()
68
69
  .describe("The public key used to encrypt data that only group members with writer permissions or higher can decrypt"),
69
70
  });
71
+ /** System table name for group records. */
70
72
  exports.groupsTableName = "Groups";
71
73
  const metaData = {
72
74
  name: exports.groupsTableName,
@@ -76,6 +78,10 @@ const metaData = {
76
78
  iconClassName: "bi bi-people-fill",
77
79
  indexes: [{ fields: ["name"] }],
78
80
  };
81
+ // Symbol.for keeps signer registration shared by separately installed SDK copies.
82
+ const signingKey = Symbol.for("@peers-app/peers-sdk/group-signing");
83
+ const signingGlobals = globalThis;
84
+ /** Group table with signature enforcement outside the personal context. */
79
85
  let GroupsTable = (() => {
80
86
  let _classSuper = table_1.Table;
81
87
  let _instanceExtraInitializers = [];
@@ -103,10 +109,11 @@ let GroupsTable = (() => {
103
109
  return super.save(group, opts);
104
110
  }
105
111
  async signAndSave(group, opts) {
106
- if (!GroupsTable.addSignatureToGroup) {
112
+ const sign = signingGlobals[signingKey];
113
+ if (!sign) {
107
114
  throw new Error("Group signing must be enabled to sign and save groups. Call GroupsTable.enableGroupSigning(fn) to enable it.");
108
115
  }
109
- group = GroupsTable.addSignatureToGroup(group);
116
+ group = sign.call(GroupsTable, group);
110
117
  return this.save(group, opts);
111
118
  }
112
119
  async delete(groupId) {
@@ -117,9 +124,9 @@ let GroupsTable = (() => {
117
124
  }
118
125
  throw new Error("Only deleting groups from the user context Groups table is allowed");
119
126
  }
120
- static addSignatureToGroup = undefined;
127
+ /** Registers the signer for all SDK copies in this JavaScript realm. Replaces any prior signer. */
121
128
  static enableGroupSigning(fn) {
122
- GroupsTable.addSignatureToGroup = fn;
129
+ signingGlobals[signingKey] = fn;
123
130
  }
124
131
  /** @deprecated Direct inserts forbidden; use save() */
125
132
  insert(..._args) {
@@ -137,6 +144,7 @@ let GroupsTable = (() => {
137
144
  })();
138
145
  exports.GroupsTable = GroupsTable;
139
146
  (0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.groupSchema, GroupsTable);
147
+ /** Returns the Groups table for the given context or the current default context. */
140
148
  function Groups(dataContext) {
141
149
  return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.groupSchema, GroupsTable);
142
150
  }
@@ -42,6 +42,15 @@ export declare class SQLDataSource<T extends {
42
42
  * columns (for example `accessLevel`) are written on insert.
43
43
  */
44
44
  private applySchemaDefaults;
45
+ /**
46
+ * Remove `record`'s empty fields in place, so a row is written and returned as it's stored:
47
+ * an optional field that is `null` or `undefined` is left out, and so is an empty field with a
48
+ * default, which gets the default. Returns the defaulted fields' names. A required field
49
+ * without a default keeps its value for validation to refuse.
50
+ */
51
+ private removeEmptyFields;
52
+ /** Copy the parsed defaults requested by {@link removeEmptyFields} onto `record`. */
53
+ private applyDefaults;
45
54
  /**
46
55
  * Columns to SET on update. Required fields that are missing are omitted so
47
56
  * older peers without a new column do not overwrite a local value with NULL.