@peers-app/peers-sdk 0.27.2 → 0.27.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.
Files changed (260) hide show
  1. package/dist/contracts/contract-proxy.d.ts +7 -0
  2. package/dist/contracts/contract-proxy.js +19 -2
  3. package/dist/contracts/create-local-consumer.js +16 -1
  4. package/dist/contracts/index.d.ts +2 -0
  5. package/dist/contracts/index.js +2 -0
  6. package/dist/contracts/system/index.d.ts +2 -0
  7. package/dist/contracts/system/index.js +2 -0
  8. package/dist/contracts/system/invites-contract.js +2 -1
  9. package/dist/contracts/system/sync-check-contract.d.ts +97 -0
  10. package/dist/contracts/system/sync-check-contract.js +115 -0
  11. package/dist/contracts/tool-arguments.d.ts +15 -0
  12. package/dist/contracts/tool-arguments.js +41 -0
  13. package/dist/contracts/tool-catalog-service.js +2 -12
  14. package/dist/data/change-tracking.d.ts +40 -0
  15. package/dist/data/change-tracking.js +89 -0
  16. package/dist/data/orm/types.d.ts +24 -7
  17. package/dist/data/orm/types.js +97 -24
  18. package/dist/data/persistent-vars.d.ts +5 -0
  19. package/dist/data/persistent-vars.js +24 -6
  20. package/dist/define/manifest.js +6 -34
  21. package/dist/package-loader/package-loader.js +27 -22
  22. package/dist/system-ids.d.ts +5 -0
  23. package/dist/system-ids.js +6 -1
  24. package/package.json +4 -2
  25. package/dist/context/user-context-identity.test.d.ts +0 -1
  26. package/dist/context/user-context-identity.test.js +0 -220
  27. package/dist/contracts/__tests__/action-dispatch.test.d.ts +0 -1
  28. package/dist/contracts/__tests__/action-dispatch.test.js +0 -319
  29. package/dist/contracts/__tests__/also-implements.test.d.ts +0 -1
  30. package/dist/contracts/__tests__/also-implements.test.js +0 -110
  31. package/dist/contracts/__tests__/builder.test.d.ts +0 -1
  32. package/dist/contracts/__tests__/builder.test.js +0 -669
  33. package/dist/contracts/__tests__/contract-events.test.d.ts +0 -1
  34. package/dist/contracts/__tests__/contract-events.test.js +0 -199
  35. package/dist/contracts/__tests__/contract-observables.test.d.ts +0 -1
  36. package/dist/contracts/__tests__/contract-observables.test.js +0 -290
  37. package/dist/contracts/__tests__/contract-provider-router.test.d.ts +0 -1
  38. package/dist/contracts/__tests__/contract-provider-router.test.js +0 -315
  39. package/dist/contracts/__tests__/contract-proxy.test.d.ts +0 -1
  40. package/dist/contracts/__tests__/contract-proxy.test.js +0 -657
  41. package/dist/contracts/__tests__/contract-resolvers.test.d.ts +0 -1
  42. package/dist/contracts/__tests__/contract-resolvers.test.js +0 -758
  43. package/dist/contracts/__tests__/contract-subscription-lifecycle.test.d.ts +0 -1
  44. package/dist/contracts/__tests__/contract-subscription-lifecycle.test.js +0 -353
  45. package/dist/contracts/__tests__/contract-table-events.test.d.ts +0 -1
  46. package/dist/contracts/__tests__/contract-table-events.test.js +0 -209
  47. package/dist/contracts/__tests__/contract-transport.test.d.ts +0 -1
  48. package/dist/contracts/__tests__/contract-transport.test.js +0 -163
  49. package/dist/contracts/__tests__/extract.test.d.ts +0 -1
  50. package/dist/contracts/__tests__/extract.test.js +0 -145
  51. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +0 -1
  52. package/dist/contracts/__tests__/frozen-contracts.test.js +0 -191
  53. package/dist/contracts/__tests__/generic-tool-dispatch.test.d.ts +0 -1
  54. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +0 -130
  55. package/dist/contracts/__tests__/host-contract-client.test.d.ts +0 -1
  56. package/dist/contracts/__tests__/host-contract-client.test.js +0 -793
  57. package/dist/contracts/__tests__/host-contract-consumer.test.d.ts +0 -1
  58. package/dist/contracts/__tests__/host-contract-consumer.test.js +0 -163
  59. package/dist/contracts/__tests__/host-contract-transport.test.d.ts +0 -1
  60. package/dist/contracts/__tests__/host-contract-transport.test.js +0 -66
  61. package/dist/contracts/__tests__/integration.test.d.ts +0 -1
  62. package/dist/contracts/__tests__/integration.test.js +0 -348
  63. package/dist/contracts/__tests__/legacy-removal.test.d.ts +0 -1
  64. package/dist/contracts/__tests__/legacy-removal.test.js +0 -128
  65. package/dist/contracts/__tests__/persistent-registry.test.d.ts +0 -1
  66. package/dist/contracts/__tests__/persistent-registry.test.js +0 -109
  67. package/dist/contracts/__tests__/playground.test.d.ts +0 -1
  68. package/dist/contracts/__tests__/playground.test.js +0 -216
  69. package/dist/contracts/__tests__/registry.test.d.ts +0 -1
  70. package/dist/contracts/__tests__/registry.test.js +0 -238
  71. package/dist/contracts/__tests__/remote-contract-transport.test.d.ts +0 -1
  72. package/dist/contracts/__tests__/remote-contract-transport.test.js +0 -119
  73. package/dist/contracts/__tests__/system-contracts.test.d.ts +0 -1
  74. package/dist/contracts/__tests__/system-contracts.test.js +0 -247
  75. package/dist/contracts/__tests__/target-dispatch.d.ts +0 -11
  76. package/dist/contracts/__tests__/target-dispatch.js +0 -24
  77. package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +0 -1
  78. package/dist/contracts/__tests__/tool-catalog-inventory.test.js +0 -44
  79. package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +0 -1
  80. package/dist/contracts/__tests__/tool-catalog-service.test.js +0 -482
  81. package/dist/contracts/__tests__/tool-catalog.test.d.ts +0 -1
  82. package/dist/contracts/__tests__/tool-catalog.test.js +0 -1199
  83. package/dist/contracts/__tests__/validate.test.d.ts +0 -1
  84. package/dist/contracts/__tests__/validate.test.js +0 -991
  85. package/dist/contracts/__tests__/workflow-context.test.d.ts +0 -1
  86. package/dist/contracts/__tests__/workflow-context.test.js +0 -50
  87. package/dist/data/access-level.test.d.ts +0 -1
  88. package/dist/data/access-level.test.js +0 -53
  89. package/dist/data/change-tracking.test.d.ts +0 -1
  90. package/dist/data/change-tracking.test.js +0 -62
  91. package/dist/data/files/files.test.d.ts +0 -1
  92. package/dist/data/files/files.test.js +0 -770
  93. package/dist/data/group-members.test.d.ts +0 -1
  94. package/dist/data/group-members.test.js +0 -336
  95. package/dist/data/group-share.test.d.ts +0 -1
  96. package/dist/data/group-share.test.js +0 -110
  97. package/dist/data/groups.test.d.ts +0 -1
  98. package/dist/data/groups.test.js +0 -153
  99. package/dist/data/orm/cursor.test.d.ts +0 -1
  100. package/dist/data/orm/cursor.test.js +0 -314
  101. package/dist/data/orm/data-query.mongo.test.d.ts +0 -1
  102. package/dist/data/orm/data-query.mongo.test.js +0 -367
  103. package/dist/data/orm/data-query.sqlite.test.d.ts +0 -1
  104. package/dist/data/orm/data-query.sqlite.test.js +0 -365
  105. package/dist/data/orm/data-query.test.d.ts +0 -1
  106. package/dist/data/orm/data-query.test.js +0 -545
  107. package/dist/data/orm/dependency-injection.test.d.ts +0 -1
  108. package/dist/data/orm/dependency-injection.test.js +0 -171
  109. package/dist/data/orm/event-registry.test.d.ts +0 -1
  110. package/dist/data/orm/event-registry.test.js +0 -44
  111. package/dist/data/orm/multi-cursors.test.d.ts +0 -1
  112. package/dist/data/orm/multi-cursors.test.js +0 -463
  113. package/dist/data/orm/sql.data-source.test.d.ts +0 -1
  114. package/dist/data/orm/sql.data-source.test.js +0 -731
  115. package/dist/data/orm/table-container-events.test.d.ts +0 -1
  116. package/dist/data/orm/table-container-events.test.js +0 -101
  117. package/dist/data/orm/table-container.test.d.ts +0 -1
  118. package/dist/data/orm/table-container.test.js +0 -273
  119. package/dist/data/orm/table.event-source.test.d.ts +0 -1
  120. package/dist/data/orm/table.event-source.test.js +0 -331
  121. package/dist/data/orm/types.test.d.ts +0 -1
  122. package/dist/data/orm/types.test.js +0 -409
  123. package/dist/data/package-table-identity.test.d.ts +0 -1
  124. package/dist/data/package-table-identity.test.js +0 -46
  125. package/dist/data/package-version-permissions.test.d.ts +0 -1
  126. package/dist/data/package-version-permissions.test.js +0 -107
  127. package/dist/data/package-version-resolver.test.d.ts +0 -4
  128. package/dist/data/package-version-resolver.test.js +0 -543
  129. package/dist/data/package-versions.test.d.ts +0 -1
  130. package/dist/data/package-versions.test.js +0 -227
  131. package/dist/data/persistent-vars.conflict.test.d.ts +0 -1
  132. package/dist/data/persistent-vars.conflict.test.js +0 -126
  133. package/dist/data/persistent-vars.migration.test.d.ts +0 -1
  134. package/dist/data/persistent-vars.migration.test.js +0 -133
  135. package/dist/data/persistent-vars.package-scope.test.d.ts +0 -1
  136. package/dist/data/persistent-vars.package-scope.test.js +0 -27
  137. package/dist/data/sync-recovery-queue.test.d.ts +0 -1
  138. package/dist/data/sync-recovery-queue.test.js +0 -215
  139. package/dist/data/table-signing.test.d.ts +0 -1
  140. package/dist/data/table-signing.test.js +0 -118
  141. package/dist/data/tool-access.test.d.ts +0 -1
  142. package/dist/data/tool-access.test.js +0 -166
  143. package/dist/data/user-permissions.test.d.ts +0 -1
  144. package/dist/data/user-permissions.test.js +0 -305
  145. package/dist/data/user-trust-levels.test.d.ts +0 -1
  146. package/dist/data/user-trust-levels.test.js +0 -37
  147. package/dist/define/also-implements.test.d.ts +0 -1
  148. package/dist/define/also-implements.test.js +0 -123
  149. package/dist/define/declare-contract.test.d.ts +0 -1
  150. package/dist/define/declare-contract.test.js +0 -573
  151. package/dist/define/define.test.d.ts +0 -1
  152. package/dist/define/define.test.js +0 -1010
  153. package/dist/define/empty-fields.test.d.ts +0 -1
  154. package/dist/define/empty-fields.test.js +0 -55
  155. package/dist/define/events-observables.test.d.ts +0 -1
  156. package/dist/define/events-observables.test.js +0 -1013
  157. package/dist/define/serve-members.test.d.ts +0 -1
  158. package/dist/define/serve-members.test.js +0 -630
  159. package/dist/define/vars.test.d.ts +0 -1
  160. package/dist/define/vars.test.js +0 -39
  161. package/dist/device/binary-peer-connection-v2.test.d.ts +0 -1
  162. package/dist/device/binary-peer-connection-v2.test.js +0 -730
  163. package/dist/device/binary-peer-connection.test.d.ts +0 -1
  164. package/dist/device/binary-peer-connection.test.js +0 -184
  165. package/dist/device/connection.test.d.ts +0 -1
  166. package/dist/device/connection.test.js +0 -613
  167. package/dist/device/device-election.test.d.ts +0 -1
  168. package/dist/device/device-election.test.js +0 -516
  169. package/dist/device/device.test.d.ts +0 -1
  170. package/dist/device/device.test.js +0 -243
  171. package/dist/device/frame-cipher.test.d.ts +0 -1
  172. package/dist/device/frame-cipher.test.js +0 -219
  173. package/dist/device/get-trust-level-fn.test.d.ts +0 -1
  174. package/dist/device/get-trust-level-fn.test.js +0 -685
  175. package/dist/device/streamed-socket.test.d.ts +0 -1
  176. package/dist/device/streamed-socket.test.js +0 -216
  177. package/dist/device/tx-encoding.test.d.ts +0 -1
  178. package/dist/device/tx-encoding.test.js +0 -295
  179. package/dist/device-pairing/cpace.test.d.ts +0 -1
  180. package/dist/device-pairing/cpace.test.js +0 -116
  181. package/dist/device-pairing/device-pairing.test.d.ts +0 -1
  182. package/dist/device-pairing/device-pairing.test.js +0 -211
  183. package/dist/hlc.test.d.ts +0 -1
  184. package/dist/hlc.test.js +0 -45
  185. package/dist/host-credentials.test.d.ts +0 -1
  186. package/dist/host-credentials.test.js +0 -88
  187. package/dist/identity/account-email.test.d.ts +0 -1
  188. package/dist/identity/account-email.test.js +0 -59
  189. package/dist/identity/anchor-document.test.d.ts +0 -1
  190. package/dist/identity/anchor-document.test.js +0 -16
  191. package/dist/identity/anchor-resolution.test.d.ts +0 -1
  192. package/dist/identity/anchor-resolution.test.js +0 -130
  193. package/dist/identity/key-change.test.d.ts +0 -1
  194. package/dist/identity/key-change.test.js +0 -811
  195. package/dist/invites/group-approval.test.d.ts +0 -1
  196. package/dist/invites/group-approval.test.js +0 -223
  197. package/dist/invites/invite-token.test.d.ts +0 -1
  198. package/dist/invites/invite-token.test.js +0 -123
  199. package/dist/keys.test.d.ts +0 -1
  200. package/dist/keys.test.js +0 -336
  201. package/dist/logging/console-logger.test.d.ts +0 -1
  202. package/dist/logging/console-logger.test.js +0 -300
  203. package/dist/logging/console-logs.table.test.d.ts +0 -1
  204. package/dist/logging/console-logs.table.test.js +0 -156
  205. package/dist/observable.test.d.ts +0 -1
  206. package/dist/observable.test.js +0 -309
  207. package/dist/package-build/build-package.test.d.ts +0 -1
  208. package/dist/package-build/build-package.test.js +0 -204
  209. package/dist/package-build/contract-add.test.d.ts +0 -1
  210. package/dist/package-build/contract-add.test.js +0 -467
  211. package/dist/package-build/freeze-contracts.test.d.ts +0 -1
  212. package/dist/package-build/freeze-contracts.test.js +0 -355
  213. package/dist/package-installer/package-author-signing.test.d.ts +0 -1
  214. package/dist/package-installer/package-author-signing.test.js +0 -200
  215. package/dist/package-installer/package-cloner.test.d.ts +0 -1
  216. package/dist/package-installer/package-cloner.test.js +0 -278
  217. package/dist/package-installer/package-creator.test.d.ts +0 -1
  218. package/dist/package-installer/package-creator.test.js +0 -908
  219. package/dist/package-installer/package-dev-tools.test.d.ts +0 -1
  220. package/dist/package-installer/package-dev-tools.test.js +0 -64
  221. package/dist/package-installer/package-installer.test.d.ts +0 -1
  222. package/dist/package-installer/package-installer.test.js +0 -882
  223. package/dist/package-installer/package-propagation.test.d.ts +0 -1
  224. package/dist/package-installer/package-propagation.test.js +0 -1269
  225. package/dist/package-installer/package-publisher.test.d.ts +0 -1
  226. package/dist/package-installer/package-publisher.test.js +0 -153
  227. package/dist/package-installer/package-remote-checker.test.d.ts +0 -1
  228. package/dist/package-installer/package-remote-checker.test.js +0 -334
  229. package/dist/package-installer/package-seed-installer.test.d.ts +0 -1
  230. package/dist/package-installer/package-seed-installer.test.js +0 -123
  231. package/dist/package-installer/package-tarball.test.d.ts +0 -1
  232. package/dist/package-installer/package-tarball.test.js +0 -75
  233. package/dist/package-installer/peers-core-installer.test.d.ts +0 -1
  234. package/dist/package-installer/peers-core-installer.test.js +0 -49
  235. package/dist/package-installer/peers-core-trust-anchor.test.d.ts +0 -1
  236. package/dist/package-installer/peers-core-trust-anchor.test.js +0 -155
  237. package/dist/package-loader/contract-package-loader.test.d.ts +0 -1
  238. package/dist/package-loader/contract-package-loader.test.js +0 -274
  239. package/dist/package-loader/host-authority-loading.test.d.ts +0 -1
  240. package/dist/package-loader/host-authority-loading.test.js +0 -104
  241. package/dist/package-loader/host-dispatch-policy.test.d.ts +0 -1
  242. package/dist/package-loader/host-dispatch-policy.test.js +0 -56
  243. package/dist/package-loader/isolated-artifact.test.d.ts +0 -1
  244. package/dist/package-loader/isolated-artifact.test.js +0 -688
  245. package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +0 -1
  246. package/dist/package-loader/isolated-persistent-var-gateway.test.js +0 -56
  247. package/dist/package-loader/isolated-table-gateway.test.d.ts +0 -1
  248. package/dist/package-loader/isolated-table-gateway.test.js +0 -633
  249. package/dist/package-loader/package-loader.test.d.ts +0 -1
  250. package/dist/package-loader/package-loader.test.js +0 -1764
  251. package/dist/package-loader/resolve-consumed-contracts.test.d.ts +0 -1
  252. package/dist/package-loader/resolve-consumed-contracts.test.js +0 -166
  253. package/dist/serial-json.compat.test.d.ts +0 -1
  254. package/dist/serial-json.compat.test.js +0 -369
  255. package/dist/serial-json.test.d.ts +0 -1
  256. package/dist/serial-json.test.js +0 -401
  257. package/dist/testing/mock-host-contract.test.d.ts +0 -1
  258. package/dist/testing/mock-host-contract.test.js +0 -782
  259. package/dist/utils.test.d.ts +0 -1
  260. package/dist/utils.test.js +0 -232
@@ -375,6 +375,13 @@ export interface IContractResolution {
375
375
  version: number;
376
376
  tables?: Record<string, Table<any>>;
377
377
  tools?: Record<string, IContractToolImplementation>;
378
+ /**
379
+ * Trusted runtime invocation after access checks, with host context already appended.
380
+ * It validates the requested version's input and checks endpoint liveness immediately
381
+ * before running provider code. Without it, dispatch validates the system definition or
382
+ * the implementation's declared input fields and calls the tool function directly.
383
+ */
384
+ invokeTool?: (name: string, args: any[]) => any;
378
385
  /**
379
386
  * Host-only capabilities appended as the final argument to provider tool functions.
380
387
  * Omit this for remote or otherwise untrusted provider routes.
@@ -15,6 +15,8 @@ const observable_1 = require("../observable");
15
15
  const utils_1 = require("../utils");
16
16
  const action_dispatch_1 = require("./action-dispatch");
17
17
  const contract_transport_1 = require("./contract-transport");
18
+ const system_contracts_1 = require("./system-contracts");
19
+ const tool_arguments_1 = require("./tool-arguments");
18
20
  const STANDARD_REMOTE_TABLE_METHODS = new Set([
19
21
  "get",
20
22
  "list",
@@ -594,6 +596,21 @@ function createContractProvider(resolution, opts = {}) {
594
596
  }
595
597
  const { workflow, ...caller } = context;
596
598
  const authority = await assertContractToolAccess(tool, call, caller);
599
+ const invoke = (...args) => {
600
+ if (resolution.invokeTool)
601
+ return resolution.invokeTool(call.name, args);
602
+ const definition = (0, system_contracts_1.listSystemContracts)()
603
+ .find((entry) => (entry.contractId ?? entry.getDefinition().contractId) === call.contractId &&
604
+ (entry.version ?? entry.getDefinition().version) === call.version)
605
+ ?.getDefinition();
606
+ const member = definition?.tools.find((entry) => entry.name === call.name);
607
+ if (definition && !member)
608
+ throw new Error(`Contract ${call.contractId} does not declare tool '${call.name}'`);
609
+ const fields = member?.inputFields ?? tool.tool.inputSchema?.fields;
610
+ if (fields?.length)
611
+ (0, tool_arguments_1.checkToolArguments)(fields, call.args[0], call.name);
612
+ return tool.toolFn(...args);
613
+ };
597
614
  if (resolution.toolContext ||
598
615
  resolution.forwardCallContext ||
599
616
  authority.strictActionScope) {
@@ -621,7 +638,7 @@ function createContractProvider(resolution, opts = {}) {
621
638
  authority: (0, action_dispatch_1.narrowActionPrincipal)(frame, true),
622
639
  });
623
640
  };
624
- return tool.toolFn(call.args[0], {
641
+ return invoke(call.args[0], {
625
642
  ...base,
626
643
  workflow: caller.callerDeviceId || caller.callerPackageId ? undefined : workflow,
627
644
  callerPackageId: caller.callerPackageId,
@@ -632,7 +649,7 @@ function createContractProvider(resolution, opts = {}) {
632
649
  authority,
633
650
  });
634
651
  }
635
- return tool.toolFn(...call.args);
652
+ return invoke(...call.args);
636
653
  }
637
654
  case "observable": {
638
655
  const obs = resolution.observables?.[call.name];
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createLocalContractConsumer = createLocalContractConsumer;
4
4
  const contract_proxy_1 = require("./contract-proxy");
5
5
  const contract_transport_1 = require("./contract-transport");
6
+ const tool_arguments_1 = require("./tool-arguments");
6
7
  /**
7
8
  * Build a same-process contract consumer wired to a live provider resolution.
8
9
  *
@@ -12,7 +13,21 @@ const contract_transport_1 = require("./contract-transport");
12
13
  */
13
14
  function createLocalContractConsumer(definition, resolution, opts) {
14
15
  const [consumerTransport, providerTransport] = (0, contract_transport_1.inProcessTransportPair)();
15
- (0, contract_proxy_1.createContractProviderSession)(resolution, providerTransport, { context: opts?.context });
16
+ (0, contract_proxy_1.createContractProviderSession)({
17
+ ...resolution,
18
+ invokeTool: resolution.invokeTool ??
19
+ ((name, args) => {
20
+ const tool = resolution.tools?.[name];
21
+ if (!tool)
22
+ throw new Error(`Contract ${definition.contractId} does not provide tool '${name}'`);
23
+ const member = definition.tools.find((tool) => tool.name === name);
24
+ if (!member)
25
+ throw new Error(`Contract ${definition.contractId} does not declare tool '${name}'`);
26
+ if (member.inputFields.length)
27
+ (0, tool_arguments_1.checkToolArguments)(member.inputFields, args[0], name);
28
+ return tool.toolFn(...args);
29
+ }),
30
+ }, providerTransport, { context: opts?.context });
16
31
  return (0, contract_proxy_1.createContractConsumer)(definition, consumerTransport, {
17
32
  dataContextId: opts?.dataContextId,
18
33
  });
@@ -29,9 +29,11 @@ export { getLogsContractDefinition, type ILogsConsumer, LOGS_CONTRACT_VERSION, r
29
29
  export * from "./system/network-contracts";
30
30
  export * from "./system/packages-contract";
31
31
  export { getSchedulerContractDefinition, type IScheduleInfo, type IScheduleInput, type ISchedulerConsumer, SCHEDULER_CONTRACT_VERSION, SCHEDULER_MAX_DELAY_MS, schedulerProviderPackageId, } from "./system/scheduler-contract";
32
+ export * from "./system/sync-check-contract";
32
33
  export { getToolAccessControlContractDefinition, type IToolAccessControlConsumer, TOOL_ACCESS_CONTROL_CONTRACT_VERSION, } from "./system/tool-access-control-contract";
33
34
  export * from "./system/tool-catalog-contract";
34
35
  export { type ISystemContractRegistration, installSystemContracts, isSystemContractId, listSystemContracts, registerSystemContract, } from "./system-contracts";
36
+ export * from "./tool-arguments";
35
37
  export * from "./tool-catalog";
36
38
  export * from "./tool-catalog-identity";
37
39
  export * from "./tool-catalog-migrations";
@@ -143,6 +143,7 @@ Object.defineProperty(exports, "getSchedulerContractDefinition", { enumerable: t
143
143
  Object.defineProperty(exports, "SCHEDULER_CONTRACT_VERSION", { enumerable: true, get: function () { return scheduler_contract_1.SCHEDULER_CONTRACT_VERSION; } });
144
144
  Object.defineProperty(exports, "SCHEDULER_MAX_DELAY_MS", { enumerable: true, get: function () { return scheduler_contract_1.SCHEDULER_MAX_DELAY_MS; } });
145
145
  Object.defineProperty(exports, "schedulerProviderPackageId", { enumerable: true, get: function () { return scheduler_contract_1.schedulerProviderPackageId; } });
146
+ __exportStar(require("./system/sync-check-contract"), exports);
146
147
  var tool_access_control_contract_1 = require("./system/tool-access-control-contract");
147
148
  Object.defineProperty(exports, "getToolAccessControlContractDefinition", { enumerable: true, get: function () { return tool_access_control_contract_1.getToolAccessControlContractDefinition; } });
148
149
  Object.defineProperty(exports, "TOOL_ACCESS_CONTROL_CONTRACT_VERSION", { enumerable: true, get: function () { return tool_access_control_contract_1.TOOL_ACCESS_CONTROL_CONTRACT_VERSION; } });
@@ -152,6 +153,7 @@ Object.defineProperty(exports, "installSystemContracts", { enumerable: true, get
152
153
  Object.defineProperty(exports, "isSystemContractId", { enumerable: true, get: function () { return system_contracts_1.isSystemContractId; } });
153
154
  Object.defineProperty(exports, "listSystemContracts", { enumerable: true, get: function () { return system_contracts_1.listSystemContracts; } });
154
155
  Object.defineProperty(exports, "registerSystemContract", { enumerable: true, get: function () { return system_contracts_1.registerSystemContract; } });
156
+ __exportStar(require("./tool-arguments"), exports);
155
157
  __exportStar(require("./tool-catalog"), exports);
156
158
  __exportStar(require("./tool-catalog-identity"), exports);
157
159
  __exportStar(require("./tool-catalog-migrations"), exports);
@@ -13,6 +13,7 @@ import "./logs-contract";
13
13
  import "./network-contracts";
14
14
  import "./packages-contract";
15
15
  import "./scheduler-contract";
16
+ import "./sync-check-contract";
16
17
  import "./tool-access-control-contract";
17
18
  export * from "./app-updates-contract";
18
19
  export { DEVICE_OPERATIONS_CONTRACT_VERSION, DEVICE_OPERATIONS_MAX_QUERY_LENGTH, DEVICE_OPERATIONS_MAX_RESULT_BYTES, DEVICE_OPERATIONS_MAX_ROWS, DEVICE_OPERATIONS_MAX_TOOLS, type DeviceOperationCapability, deviceOperationsProviderPackageId, deviceOperationsToolIds, getDeviceOperationsContractDefinition, type IDeviceDatabaseQueryInput, type IDeviceDatabaseQueryResult, type IDeviceOperationStatus, type IDeviceToolListInput, type IDeviceToolLookupInput, type IDeviceToolLookupResult, type IDeviceToolRunInput, } from "./device-operations-contract";
@@ -22,6 +23,7 @@ export { getInvitesContractDefinition, type IAcceptInviteResult, type ICreatedIn
22
23
  export { getLogsContractDefinition, type ILogsConsumer, LOGS_CONTRACT_VERSION, resolveLogsContract, } from "./logs-contract";
23
24
  export * from "./network-contracts";
24
25
  export * from "./packages-contract";
26
+ export * from "./sync-check-contract";
25
27
  export { getToolAccessControlContractDefinition, type IToolAccessControlConsumer, TOOL_ACCESS_CONTROL_CONTRACT_VERSION, } from "./tool-access-control-contract";
26
28
  import "./tool-catalog-contract";
27
29
  export * from "./desktop-tools-contract";
@@ -30,6 +30,7 @@ require("./logs-contract");
30
30
  require("./network-contracts");
31
31
  require("./packages-contract");
32
32
  require("./scheduler-contract");
33
+ require("./sync-check-contract");
33
34
  require("./tool-access-control-contract");
34
35
  __exportStar(require("./app-updates-contract"), exports);
35
36
  var device_operations_contract_1 = require("./device-operations-contract");
@@ -54,6 +55,7 @@ Object.defineProperty(exports, "LOGS_CONTRACT_VERSION", { enumerable: true, get:
54
55
  Object.defineProperty(exports, "resolveLogsContract", { enumerable: true, get: function () { return logs_contract_1.resolveLogsContract; } });
55
56
  __exportStar(require("./network-contracts"), exports);
56
57
  __exportStar(require("./packages-contract"), exports);
58
+ __exportStar(require("./sync-check-contract"), exports);
57
59
  var tool_access_control_contract_1 = require("./tool-access-control-contract");
58
60
  Object.defineProperty(exports, "getToolAccessControlContractDefinition", { enumerable: true, get: function () { return tool_access_control_contract_1.getToolAccessControlContractDefinition; } });
59
61
  Object.defineProperty(exports, "TOOL_ACCESS_CONTROL_CONTRACT_VERSION", { enumerable: true, get: function () { return tool_access_control_contract_1.TOOL_ACCESS_CONTROL_CONTRACT_VERSION; } });
@@ -47,8 +47,9 @@ function getInvitesContractDefinition() {
47
47
  field("expiresInMs", field_type_1.FieldType.number, "Lifetime override in milliseconds.", {
48
48
  optional: true,
49
49
  }),
50
- field("uses", field_type_1.FieldType.string, "1 or unlimited (default unlimited).", {
50
+ field("uses", field_type_1.FieldType.any, "1 or unlimited (default unlimited).", {
51
51
  optional: true,
52
+ enumValues: [1, "unlimited"],
52
53
  }),
53
54
  ],
54
55
  outputFields: [field("invite", field_type_1.FieldType.object, "The issued invite and encodings.")],
@@ -0,0 +1,97 @@
1
+ import type { IChangeCount, ICountChangesRequest } from "../../data/change-tracking";
2
+ import type { Observable } from "../../observable";
3
+ import type { IContractConsumerBase } from "../contract-proxy";
4
+ import type { IContractDefinition } from "../types";
5
+ /** Version of the built-in System Sync Check contract. */
6
+ export declare const SYNC_CHECK_CONTRACT_VERSION = 1;
7
+ /** A device's answer to `countChanges`. */
8
+ export interface ICountChangesResult {
9
+ /** Tables this device never syncs for the context; the asking device leaves them out too. */
10
+ excludedTableNames: string[];
11
+ counts: IChangeCount[];
12
+ }
13
+ /** One change another device has that this device is missing. */
14
+ export interface IMissingChange {
15
+ changeId: string;
16
+ tableName: string;
17
+ recordId: string;
18
+ op: string;
19
+ path: string;
20
+ /** Author time. Hybrid-logical-clock milliseconds. */
21
+ createdAt: number;
22
+ }
23
+ /** What the check found when it compared this device with one other device in one context. */
24
+ export interface ISyncGap {
25
+ dataContextId: string;
26
+ dataContextName: string;
27
+ /** The device that has the changes. */
28
+ deviceId: string;
29
+ deviceName?: string;
30
+ checkedAt: number;
31
+ /** Changes the other device has that this device is missing. */
32
+ missingCount: number;
33
+ /** Up to 100 of the missing changes, oldest first. */
34
+ missing: IMissingChange[];
35
+ /** True when the check stopped before comparing everything; there may be more. */
36
+ truncated: boolean;
37
+ }
38
+ /** Live summary of the latest checks on this device. */
39
+ export interface ISyncCheckStatus {
40
+ /** Missing changes found, each counted once across contexts and devices. */
41
+ missingChangeCount: number;
42
+ /** When the latest check finished. */
43
+ lastCheckedAt?: number;
44
+ }
45
+ /** Result of `checkSyncGaps`. */
46
+ export interface ISyncCheckResult {
47
+ /** Device comparisons that completed. Devices without the check are skipped. */
48
+ compared: number;
49
+ /** Missing changes found. */
50
+ missingCount: number;
51
+ /** At least one comparison stopped at its bound; another check continues it. */
52
+ truncated?: boolean;
53
+ message: string;
54
+ }
55
+ /** Result of `repairSyncGaps`. */
56
+ export interface ISyncRepairResult {
57
+ /** Missing changes this device now holds. */
58
+ repaired: number;
59
+ /** Missing changes still found after the repair. */
60
+ remaining: number;
61
+ message: string;
62
+ }
63
+ /** Typed consumer proxy for System Sync Check v1. */
64
+ export interface ISyncCheckConsumer extends IContractConsumerBase {
65
+ tools: {
66
+ /** Count this device's active changes in the call's context. Group Readers may call it. */
67
+ countChanges(input: ICountChangesRequest): Promise<ICountChangesResult>;
68
+ /** What the latest checks found, for one context or every context. Local callers only. */
69
+ getSyncGaps(input?: {
70
+ dataContextId?: string;
71
+ }): Promise<{
72
+ gaps: ISyncGap[];
73
+ }>;
74
+ /** Check now against each connected device. Local callers only. */
75
+ checkSyncGaps(input?: {
76
+ dataContextId?: string;
77
+ }): Promise<ISyncCheckResult>;
78
+ /** Fetch and apply the missing changes found in one context. Local callers only. */
79
+ repairSyncGaps(input: {
80
+ dataContextId: string;
81
+ deviceId?: string;
82
+ }): Promise<ISyncRepairResult>;
83
+ };
84
+ observables: {
85
+ /** Live summary; available over the local host transport only. */
86
+ status: Observable<ISyncCheckStatus>;
87
+ };
88
+ }
89
+ /**
90
+ * Return the System Sync Check contract shape.
91
+ *
92
+ * Once a day each device compares its change history for each personal and group context with
93
+ * each connected device's, and reports changes the other device has that this one is missing.
94
+ * `countChanges` is how devices compare: other devices in a group may call it with Reader
95
+ * access. The other tools serve this device's UI and CLI and refuse remote callers.
96
+ */
97
+ export declare function getSyncCheckContractDefinition(): IContractDefinition;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SYNC_CHECK_CONTRACT_VERSION = void 0;
4
+ exports.getSyncCheckContractDefinition = getSyncCheckContractDefinition;
5
+ const access_level_1 = require("../../data/access-level");
6
+ const system_ids_1 = require("../../system-ids");
7
+ const field_type_1 = require("../../types/field-type");
8
+ const system_contracts_1 = require("../system-contracts");
9
+ /** Version of the built-in System Sync Check contract. */
10
+ exports.SYNC_CHECK_CONTRACT_VERSION = 1;
11
+ const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
12
+ const contextInput = field("dataContextId", field_type_1.FieldType.id, "Personal or group context ID.", {
13
+ optional: true,
14
+ });
15
+ let definition;
16
+ /**
17
+ * Return the System Sync Check contract shape.
18
+ *
19
+ * Once a day each device compares its change history for each personal and group context with
20
+ * each connected device's, and reports changes the other device has that this one is missing.
21
+ * `countChanges` is how devices compare: other devices in a group may call it with Reader
22
+ * access. The other tools serve this device's UI and CLI and refuse remote callers.
23
+ */
24
+ function getSyncCheckContractDefinition() {
25
+ definition ??= {
26
+ contractId: system_ids_1.syncCheckContractId,
27
+ version: exports.SYNC_CHECK_CONTRACT_VERSION,
28
+ devTag: "dev",
29
+ name: "System Sync Check",
30
+ description: "Find changes another device has that this device is missing, and repair them.",
31
+ tables: [],
32
+ tools: [
33
+ {
34
+ name: "countChanges",
35
+ usageDescription: "Count this device's active changes per table, or per table and change-id prefix, with a digest of their ids.",
36
+ inputFields: [
37
+ field("before", field_type_1.FieldType.string, "Count only change ids below this one."),
38
+ field("excludedTableNames", field_type_1.FieldType.string, "Tables to leave out.", {
39
+ optional: true,
40
+ isArray: true,
41
+ }),
42
+ field("tableName", field_type_1.FieldType.string, "Count only this table.", { optional: true }),
43
+ field("prefix", field_type_1.FieldType.string, "Count only ids that start with this.", {
44
+ optional: true,
45
+ }),
46
+ field("prefixLength", field_type_1.FieldType.number, "Also group by this many id characters.", {
47
+ optional: true,
48
+ }),
49
+ field("afterPrefix", field_type_1.FieldType.string, "Resume after this prefix; grouped pages contain at most 500 rows.", {
50
+ optional: true,
51
+ }),
52
+ ],
53
+ outputFields: [
54
+ field("excludedTableNames", field_type_1.FieldType.string, "Tables this device never syncs.", {
55
+ isArray: true,
56
+ }),
57
+ field("counts", field_type_1.FieldType.object, "Counts and digests.", { isArray: true }),
58
+ ],
59
+ remoteAccessLevel: access_level_1.AccessLevel.Reader,
60
+ },
61
+ {
62
+ name: "getSyncGaps",
63
+ usageDescription: "List changes the latest checks found on another device but not here.",
64
+ inputFields: [contextInput],
65
+ outputFields: [
66
+ field("gaps", field_type_1.FieldType.object, "Findings, one per context and device.", {
67
+ isArray: true,
68
+ }),
69
+ ],
70
+ },
71
+ {
72
+ name: "checkSyncGaps",
73
+ usageDescription: "Compare one context, or every context, with each connected device now. Only reports.",
74
+ inputFields: [contextInput],
75
+ outputFields: [
76
+ field("compared", field_type_1.FieldType.number, "Device comparisons that completed."),
77
+ field("missingCount", field_type_1.FieldType.number, "Missing changes found."),
78
+ field("truncated", field_type_1.FieldType.boolean, "Another check is needed to finish a bounded comparison.", { optional: true }),
79
+ field("message", field_type_1.FieldType.string, "Human-readable result."),
80
+ ],
81
+ },
82
+ {
83
+ name: "repairSyncGaps",
84
+ usageDescription: "Fetch the missing changes found in one context from the devices that have them and apply them as sync would.",
85
+ inputFields: [
86
+ field("dataContextId", field_type_1.FieldType.id, "Personal or group context ID."),
87
+ field("deviceId", field_type_1.FieldType.id, "Repair only what this device has.", {
88
+ optional: true,
89
+ }),
90
+ ],
91
+ outputFields: [
92
+ field("repaired", field_type_1.FieldType.number, "Missing changes this device now holds."),
93
+ field("remaining", field_type_1.FieldType.number, "Missing changes still found."),
94
+ field("message", field_type_1.FieldType.string, "Human-readable result."),
95
+ ],
96
+ },
97
+ ],
98
+ observables: [
99
+ {
100
+ name: "status",
101
+ description: "Live summary of the latest checks on this device.",
102
+ valueType: field_type_1.FieldType.object,
103
+ writable: false,
104
+ },
105
+ ],
106
+ events: [],
107
+ };
108
+ return definition;
109
+ }
110
+ (0, system_contracts_1.registerSystemContract)({
111
+ providerPackageId: system_ids_1.peersSystemPackageId,
112
+ contractId: system_ids_1.syncCheckContractId,
113
+ version: exports.SYNC_CHECK_CONTRACT_VERSION,
114
+ getDefinition: getSyncCheckContractDefinition,
115
+ });
@@ -0,0 +1,15 @@
1
+ import { type IField } from "../types/field-type";
2
+ /**
3
+ * Check a contract tool's arguments against its declared input fields. The Runtime does this
4
+ * before every tool run, whoever calls: the UI, the CLI, an assistant, a package, or another
5
+ * device. A missing or `null` input counts as `{}`. Optional fields that are `null` are left
6
+ * out first, and an ISO string in a `Date` field is read as a date.
7
+ *
8
+ * @param fields - The tool's input fields, from its contract definition.
9
+ * @param input - The tool's argument as the caller sent it.
10
+ * @param toolName - The tool's name, for the error message.
11
+ * @returns The arguments without empty optional fields and with `Date` fields as dates.
12
+ * @throws If the input isn't an object, or a field is missing, of the wrong type, or not one
13
+ * of its allowed values.
14
+ */
15
+ export declare function checkToolArguments(fields: IField[], input: unknown, toolName: string): Record<string, unknown>;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkToolArguments = checkToolArguments;
4
+ const types_1 = require("../data/orm/types");
5
+ const field_type_1 = require("../types/field-type");
6
+ /**
7
+ * Check a contract tool's arguments against its declared input fields. The Runtime does this
8
+ * before every tool run, whoever calls: the UI, the CLI, an assistant, a package, or another
9
+ * device. A missing or `null` input counts as `{}`. Optional fields that are `null` are left
10
+ * out first, and an ISO string in a `Date` field is read as a date.
11
+ *
12
+ * @param fields - The tool's input fields, from its contract definition.
13
+ * @param input - The tool's argument as the caller sent it.
14
+ * @param toolName - The tool's name, for the error message.
15
+ * @returns The arguments without empty optional fields and with `Date` fields as dates.
16
+ * @throws If the input isn't an object, or a field is missing, of the wrong type, or not one
17
+ * of its allowed values.
18
+ */
19
+ function checkToolArguments(fields, input, toolName) {
20
+ const source = input ?? {};
21
+ if (typeof source !== "object" ||
22
+ Array.isArray(source) ||
23
+ (Object.getPrototypeOf(source) !== Object.prototype && Object.getPrototypeOf(source) !== null)) {
24
+ throw new Error(`Tool argument validation failed for ${toolName}: input must be an object`);
25
+ }
26
+ // An optional argument that is null (as models often send) or undefined is left out.
27
+ const args = (0, types_1.withoutEmptyFields)(fields, { ...source });
28
+ for (const field of fields) {
29
+ if (field.type === field_type_1.FieldType.Date && !field.isArray && typeof args[field.name] === "string") {
30
+ args[field.name] = new Date(args[field.name]);
31
+ }
32
+ }
33
+ // Zod reads named properties, including inherited ones. Object.prototype must not supply
34
+ // a missing argument such as `constructor`, or invalidate an omitted optional one.
35
+ const result = (0, types_1.fieldsToArgumentsSchema)(fields).safeParse(Object.assign(Object.create(null), args));
36
+ if (!result.success) {
37
+ const problems = result.error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`);
38
+ throw new Error(`Tool argument validation failed for ${toolName}: ${problems.join("; ")}`);
39
+ }
40
+ return args;
41
+ }
@@ -8,15 +8,14 @@ exports.findToolCatalogEntry = findToolCatalogEntry;
8
8
  exports.projectToolCatalogAccess = projectToolCatalogAccess;
9
9
  exports.invokeToolCatalogTool = invokeToolCatalogTool;
10
10
  const group_permissions_1 = require("../data/group-permissions");
11
- const types_1 = require("../data/orm/types");
12
11
  const tool_access_1 = require("../data/tool-access");
13
12
  const tools_1 = require("../data/tools");
14
13
  const system_ids_1 = require("../system-ids");
15
- const field_type_1 = require("../types/field-type");
16
14
  const utils_1 = require("../utils");
17
15
  const contract_provider_router_1 = require("./contract-provider-router");
18
16
  const contract_resolvers_1 = require("./contract-resolvers");
19
17
  const system_contracts_1 = require("./system-contracts");
18
+ const tool_arguments_1 = require("./tool-arguments");
20
19
  const tool_catalog_1 = require("./tool-catalog");
21
20
  const tool_catalog_identity_1 = require("./tool-catalog-identity");
22
21
  const tool_catalog_migrations_1 = require("./tool-catalog-migrations");
@@ -258,16 +257,7 @@ async function invokeToolCatalogTool(dataContext, input, context, opts = {}) {
258
257
  if (liveLevel === undefined || liveLevel > opts.maxAccessLevel)
259
258
  throw new Error("Tool provider access level exceeds the authorized workflow call");
260
259
  }
261
- const source = input.args === undefined ? {} : input.args;
262
- requirePlainObject(source, "Tool args");
263
- // An optional argument that is null (as models often send) or undefined is left out.
264
- const args = (0, types_1.withoutEmptyFields)(tool.inputFields, { ...source });
265
- for (const field of tool.inputFields)
266
- if (field.type === field_type_1.FieldType.Date && !field.isArray && typeof args[field.name] === "string")
267
- args[field.name] = new Date(args[field.name]);
268
- const validated = (0, types_1.fieldsToSchema)(tool.inputFields).passthrough().safeParse(args);
269
- if (!validated.success)
270
- throw new Error(`Tool argument validation failed: ${validated.error.message}`);
260
+ const args = (0, tool_arguments_1.checkToolArguments)(tool.inputFields, input.args, target.toolName);
271
261
  const dataContextId = dataContext.dataContextId;
272
262
  const { workflow: _workflow, ...caller } = context;
273
263
  const endpointContext = loader.isolatedRuntime?.isInstalled(providerPackageId)
@@ -51,6 +51,37 @@ export declare const changeRecordSchema: z.ZodObject<{
51
51
  * One JSON-patch operation (or root snapshot / delete) stored for synchronization.
52
52
  */
53
53
  export type IChangeRecord = z.infer<typeof changeRecordSchema>;
54
+ /** Which active changes {@link ChangeTrackingTable.countChanges} counts, and how it groups them. */
55
+ export interface ICountChangesRequest {
56
+ /**
57
+ * Count only change ids below this one. The first 10 characters of a change id are its author
58
+ * time in base36 milliseconds, so a 10-character prefix counts changes written before then.
59
+ */
60
+ before: string;
61
+ /** Tables to leave out. */
62
+ excludedTableNames?: string[];
63
+ /** Count only this table. */
64
+ tableName?: string;
65
+ /** Count only change ids that start with this. */
66
+ prefix?: string;
67
+ /** Group by the first this-many characters of the change id as well as by table. */
68
+ prefixLength?: number;
69
+ /** Count ids after this entire prefix; also resumes grouped pages (at most 500 rows each). */
70
+ afterPrefix?: string;
71
+ }
72
+ /** Number of active changes in one table (and one id prefix), with a digest of their ids. */
73
+ export interface IChangeCount {
74
+ tableName: string;
75
+ /** The id prefix counted, when the request set `prefixLength`. */
76
+ prefix?: string;
77
+ count: number;
78
+ /**
79
+ * Two sums of numbers taken from the last 10 characters of each change id, as decimal text.
80
+ * Equal counts with different sums mean different changes, such as one device missing an edit
81
+ * that replaced an earlier change.
82
+ */
83
+ sums: string;
84
+ }
54
85
  /**
55
86
  * Local-only change log used for peer sync.
56
87
  *
@@ -134,6 +165,15 @@ export declare class ChangeTrackingTable extends SQLDataSource<IChangeRecord> {
134
165
  * @returns Array of changeIds with no change records
135
166
  */
136
167
  filterToMissingChangeIds(changeIds: string[]): Promise<string[]>;
168
+ /**
169
+ * Count active changes per table, or per table and change-id prefix, with a digest of their ids.
170
+ * Two devices holding the same active changes get the same results. One scan of the
171
+ * `(tableName, supersededAt)` index, which holds every change id.
172
+ *
173
+ * @param request - Which changes to count and how to group them
174
+ * @returns One row per table (and prefix) that has any active changes
175
+ */
176
+ countChanges(request: ICountChangesRequest): Promise<IChangeCount[]>;
137
177
  /**
138
178
  * Mark every still-active change for `recordId` whose `createdAt` is older
139
179
  * than `supersededAt`. Used after a root snapshot, delete, or restore.
@@ -37,6 +37,12 @@ exports.changeRecordSchema = zod_1.z.object({
37
37
  .describe("Local apply / sync-visibility time; assigned at insert, incoming values replaced"),
38
38
  supersededAt: zod_1.z.number().optional().describe("When this change was superseded; unset if active"),
39
39
  });
40
+ /** Five id characters starting at `start`, read as one base-128 number. */
41
+ function idTailNumber(start) {
42
+ return [0, 1, 2, 3, 4]
43
+ .map((i) => `${128 ** (4 - i)} * unicode(substr(changeId, ${start + i}, 1))`)
44
+ .join(" + ");
45
+ }
40
46
  const changeTrackingV2TableMetaData = {
41
47
  name: "ChangeTrackingV2",
42
48
  primaryKeyName: "changeId",
@@ -228,6 +234,89 @@ class ChangeTrackingTable extends orm_1.SQLDataSource {
228
234
  `, changeIds);
229
235
  return missingChangeIds.map((d) => d.changeId);
230
236
  }
237
+ /**
238
+ * Count active changes per table, or per table and change-id prefix, with a digest of their ids.
239
+ * Two devices holding the same active changes get the same results. One scan of the
240
+ * `(tableName, supersededAt)` index, which holds every change id.
241
+ *
242
+ * @param request - Which changes to count and how to group them
243
+ * @returns One row per table (and prefix) that has any active changes
244
+ */
245
+ async countChanges(request) {
246
+ if (!request ||
247
+ typeof request.before !== "string" ||
248
+ !/^[0-9a-z]{1,25}$/i.test(request.before) ||
249
+ (request.tableName !== undefined &&
250
+ (typeof request.tableName !== "string" ||
251
+ !request.tableName ||
252
+ request.tableName.length > 256)) ||
253
+ (request.prefix !== undefined &&
254
+ (typeof request.prefix !== "string" || !/^[0-9a-z]{0,25}$/i.test(request.prefix))) ||
255
+ (request.prefixLength !== undefined &&
256
+ (!Number.isInteger(request.prefixLength) ||
257
+ request.prefixLength < 1 ||
258
+ request.prefixLength > 25 ||
259
+ !request.tableName ||
260
+ request.prefixLength <= (request.prefix?.length ?? 0))) ||
261
+ (request.afterPrefix !== undefined &&
262
+ (typeof request.afterPrefix !== "string" ||
263
+ !request.tableName ||
264
+ !/^[0-9a-z]{1,25}$/i.test(request.afterPrefix))) ||
265
+ (request.excludedTableNames !== undefined &&
266
+ (!Array.isArray(request.excludedTableNames) ||
267
+ request.excludedTableNames.some((name) => typeof name !== "string" || name.length > 256)))) {
268
+ throw new Error("Invalid countChanges request");
269
+ }
270
+ await this.initTable();
271
+ const { before, excludedTableNames = [], tableName, prefix, prefixLength, afterPrefix, } = request;
272
+ const conditions = ["supersededAt IS NULL", "changeId < ?"];
273
+ const params = [before];
274
+ // Without one table, list the table names with a seek per name. Without index statistics,
275
+ // SQLite would otherwise read every row through the (supersededAt, ...) index, which lacks
276
+ // tableName, instead of scanning the covering one.
277
+ let tableNames = "";
278
+ if (tableName !== undefined) {
279
+ conditions.push("tableName = ?");
280
+ params.push(tableName);
281
+ }
282
+ else {
283
+ tableNames = `WITH RECURSIVE names(name) AS (
284
+ SELECT MIN(tableName) FROM "${this.tableName}"
285
+ UNION ALL
286
+ SELECT (SELECT MIN(tableName) FROM "${this.tableName}" WHERE tableName > names.name)
287
+ FROM names WHERE names.name IS NOT NULL
288
+ )`;
289
+ conditions.push("tableName IN (SELECT name FROM names)");
290
+ }
291
+ if (excludedTableNames.length) {
292
+ conditions.push(`tableName NOT IN (${excludedTableNames.map(() => "?").join(", ")})`);
293
+ params.push(...excludedTableNames);
294
+ }
295
+ if (prefix) {
296
+ // Change ids are lowercase base36, so every id with this prefix sorts below `prefix~`.
297
+ conditions.push("changeId >= ?", "changeId < ?");
298
+ params.push(prefix, `${prefix}~`);
299
+ }
300
+ if (afterPrefix) {
301
+ conditions.push("changeId > ?");
302
+ params.push(`${afterPrefix}~`);
303
+ }
304
+ const length = prefixLength ?? 0;
305
+ const prefixColumn = length ? `, substr(changeId, 1, ${length}) AS prefix` : "";
306
+ const rows = await this.db.all(`${tableNames}
307
+ SELECT tableName${prefixColumn}, COUNT(*) AS count,
308
+ CAST(SUM(${idTailNumber(16)}) AS TEXT) || ':' || CAST(SUM(${idTailNumber(21)}) AS TEXT) AS sums
309
+ FROM "${this.tableName}"
310
+ WHERE ${conditions.join(" AND ")}
311
+ GROUP BY tableName${length ? ", prefix" : ""}
312
+ ORDER BY tableName${length ? ", prefix LIMIT 500" : ""}`, params);
313
+ return rows.map((row) => ({
314
+ tableName: row.tableName,
315
+ ...(length ? { prefix: row.prefix } : {}),
316
+ count: Number(row.count),
317
+ sums: String(row.sums),
318
+ }));
319
+ }
231
320
  /**
232
321
  * Mark every still-active change for `recordId` whose `createdAt` is older
233
322
  * than `supersededAt`. Used after a root snapshot, delete, or restore.