@dxos/client-services 0.8.4-main.fffef41 → 0.9.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 (253) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/{chunk-I2RGLVJF.mjs → chunk-37BKOM5O.mjs} +2870 -3767
  4. package/dist/lib/browser/chunk-37BKOM5O.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-QCWEHHJW.mjs +24 -0
  6. package/dist/lib/browser/chunk-QCWEHHJW.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-XJRPB3GA.mjs +22 -0
  8. package/dist/lib/browser/chunk-XJRPB3GA.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +576 -139
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs +88 -0
  13. package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +7 -0
  14. package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs +11 -0
  15. package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs.map +7 -0
  16. package/dist/lib/browser/packlets/locks/browser.mjs +86 -0
  17. package/dist/lib/browser/packlets/locks/browser.mjs.map +7 -0
  18. package/dist/lib/browser/packlets/locks/node.mjs +48 -0
  19. package/dist/lib/browser/packlets/locks/node.mjs.map +7 -0
  20. package/dist/lib/browser/testing/index.mjs +58 -53
  21. package/dist/lib/browser/testing/index.mjs.map +3 -3
  22. package/dist/lib/node-esm/chunk-2DT3MZRL.mjs +22 -0
  23. package/dist/lib/node-esm/chunk-2DT3MZRL.mjs.map +7 -0
  24. package/dist/lib/node-esm/{chunk-QTUURCR4.mjs → chunk-2ONS4DLO.mjs} +2810 -3576
  25. package/dist/lib/node-esm/chunk-2ONS4DLO.mjs.map +7 -0
  26. package/dist/lib/node-esm/chunk-2SZHAWBN.mjs +24 -0
  27. package/dist/lib/node-esm/chunk-2SZHAWBN.mjs.map +7 -0
  28. package/dist/lib/node-esm/index.mjs +576 -139
  29. package/dist/lib/node-esm/index.mjs.map +4 -4
  30. package/dist/lib/node-esm/meta.json +1 -1
  31. package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs +88 -0
  32. package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +7 -0
  33. package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs +11 -0
  34. package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs.map +7 -0
  35. package/dist/lib/node-esm/packlets/locks/browser.mjs +86 -0
  36. package/dist/lib/node-esm/packlets/locks/browser.mjs.map +7 -0
  37. package/dist/lib/node-esm/packlets/locks/node.mjs +48 -0
  38. package/dist/lib/node-esm/packlets/locks/node.mjs.map +7 -0
  39. package/dist/lib/node-esm/testing/index.mjs +58 -53
  40. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  41. package/dist/types/src/index.d.ts +1 -0
  42. package/dist/types/src/index.d.ts.map +1 -1
  43. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +3 -2
  44. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -1
  45. package/dist/types/src/packlets/agents/edge-agent-service.d.ts +2 -1
  46. package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -1
  47. package/dist/types/src/packlets/devices/devices-service.d.ts.map +1 -1
  48. package/dist/types/src/packlets/devtools/devtools.d.ts +7 -3
  49. package/dist/types/src/packlets/devtools/devtools.d.ts.map +1 -1
  50. package/dist/types/src/packlets/devtools/feeds.d.ts +1 -1
  51. package/dist/types/src/packlets/devtools/feeds.d.ts.map +1 -1
  52. package/dist/types/src/packlets/devtools/keys.d.ts +2 -2
  53. package/dist/types/src/packlets/devtools/keys.d.ts.map +1 -1
  54. package/dist/types/src/packlets/devtools/metadata.d.ts.map +1 -1
  55. package/dist/types/src/packlets/devtools/network.d.ts.map +1 -1
  56. package/dist/types/src/packlets/devtools/spaces.d.ts.map +1 -1
  57. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts.map +1 -1
  58. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  59. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -1
  60. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts +2 -3
  61. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -1
  62. package/dist/types/src/packlets/diagnostics/index.d.ts +1 -1
  63. package/dist/types/src/packlets/diagnostics/index.d.ts.map +1 -1
  64. package/dist/types/src/packlets/identity/authenticator.d.ts +3 -3
  65. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  66. package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
  67. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  68. package/dist/types/src/packlets/identity/identity-manager.d.ts +10 -10
  69. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  70. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +14 -9
  71. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -1
  72. package/dist/types/src/packlets/identity/identity-service.d.ts +7 -11
  73. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  74. package/dist/types/src/packlets/identity/identity.d.ts +10 -13
  75. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  76. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +7 -6
  77. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  78. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +1 -1
  79. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -1
  80. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  81. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  82. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +7 -4
  83. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  84. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -1
  85. package/dist/types/src/packlets/invitations/invitation-topology.d.ts.map +1 -1
  86. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +4 -4
  87. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  88. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +5 -5
  89. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
  90. package/dist/types/src/packlets/invitations/invitations-service.d.ts +3 -3
  91. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  92. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +6 -5
  93. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  94. package/dist/types/src/packlets/invitations/utils.d.ts.map +1 -1
  95. package/dist/types/src/packlets/locks/browser.d.ts.map +1 -1
  96. package/dist/types/src/packlets/locks/index.d.ts +1 -1
  97. package/dist/types/src/packlets/locks/index.d.ts.map +1 -1
  98. package/dist/types/src/packlets/locks/node.d.ts.map +1 -1
  99. package/dist/types/src/packlets/logging/logging-service.d.ts +4 -0
  100. package/dist/types/src/packlets/logging/logging-service.d.ts.map +1 -1
  101. package/dist/types/src/packlets/network/network-service.d.ts +5 -4
  102. package/dist/types/src/packlets/network/network-service.d.ts.map +1 -1
  103. package/dist/types/src/packlets/services/client-rpc-server.d.ts +5 -5
  104. package/dist/types/src/packlets/services/client-rpc-server.d.ts.map +1 -1
  105. package/dist/types/src/packlets/services/feed-syncer.d.ts +75 -0
  106. package/dist/types/src/packlets/services/feed-syncer.d.ts.map +1 -0
  107. package/dist/types/src/packlets/services/feed-syncer.test.d.ts +2 -0
  108. package/dist/types/src/packlets/services/feed-syncer.test.d.ts.map +1 -0
  109. package/dist/types/src/packlets/services/index.d.ts +1 -0
  110. package/dist/types/src/packlets/services/index.d.ts.map +1 -1
  111. package/dist/types/src/packlets/services/platform.d.ts.map +1 -1
  112. package/dist/types/src/packlets/services/service-context.d.ts +22 -19
  113. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  114. package/dist/types/src/packlets/services/service-host.d.ts +20 -13
  115. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  116. package/dist/types/src/packlets/services/service-registry.d.ts.map +1 -1
  117. package/dist/types/src/packlets/services/sqlite-storage.d.ts +27 -0
  118. package/dist/types/src/packlets/services/sqlite-storage.d.ts.map +1 -0
  119. package/dist/types/src/packlets/services/util.d.ts.map +1 -1
  120. package/dist/types/src/packlets/space-export/archive-format.d.ts +9 -0
  121. package/dist/types/src/packlets/space-export/archive-format.d.ts.map +1 -0
  122. package/dist/types/src/packlets/space-export/index.d.ts +4 -1
  123. package/dist/types/src/packlets/space-export/index.d.ts.map +1 -1
  124. package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts +23 -0
  125. package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts.map +1 -0
  126. package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts +36 -0
  127. package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts.map +1 -0
  128. package/dist/types/src/packlets/space-export/space-archive-reader.d.ts +9 -1
  129. package/dist/types/src/packlets/space-export/space-archive-reader.d.ts.map +1 -1
  130. package/dist/types/src/packlets/space-export/space-archive-writer.d.ts +7 -1
  131. package/dist/types/src/packlets/space-export/space-archive-writer.d.ts.map +1 -1
  132. package/dist/types/src/packlets/space-export/space-archive.test.d.ts +2 -0
  133. package/dist/types/src/packlets/space-export/space-archive.test.d.ts.map +1 -0
  134. package/dist/types/src/packlets/spaces/automerge-space-state.d.ts.map +1 -1
  135. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +49 -22
  136. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  137. package/dist/types/src/packlets/spaces/data-space.d.ts +38 -13
  138. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  139. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +2 -2
  140. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  141. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
  142. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  143. package/dist/types/src/packlets/spaces/genesis.d.ts +4 -3
  144. package/dist/types/src/packlets/spaces/genesis.d.ts.map +1 -1
  145. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +6 -9
  146. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  147. package/dist/types/src/packlets/spaces/spaces-service.d.ts +10 -7
  148. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  149. package/dist/types/src/packlets/storage/index.d.ts +1 -0
  150. package/dist/types/src/packlets/storage/index.d.ts.map +1 -1
  151. package/dist/types/src/packlets/storage/level.d.ts.map +1 -1
  152. package/dist/types/src/packlets/storage/profile-archive-sqlite.d.ts +24 -0
  153. package/dist/types/src/packlets/storage/profile-archive-sqlite.d.ts.map +1 -0
  154. package/dist/types/src/packlets/storage/profile-archive-sqlite.test.d.ts +2 -0
  155. package/dist/types/src/packlets/storage/profile-archive-sqlite.test.d.ts.map +1 -0
  156. package/dist/types/src/packlets/storage/profile-archive.d.ts.map +1 -1
  157. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  158. package/dist/types/src/packlets/storage/util.d.ts.map +1 -1
  159. package/dist/types/src/packlets/system/system-service.d.ts +1 -1
  160. package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
  161. package/dist/types/src/packlets/testing/credential-utils.d.ts.map +1 -1
  162. package/dist/types/src/packlets/testing/invitation-utils.d.ts +6 -3
  163. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  164. package/dist/types/src/packlets/testing/test-builder.d.ts +20 -22
  165. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  166. package/dist/types/src/packlets/worker/worker-runtime.d.ts +41 -4
  167. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  168. package/dist/types/src/packlets/worker/worker-session.d.ts +2 -4
  169. package/dist/types/src/packlets/worker/worker-session.d.ts.map +1 -1
  170. package/dist/types/src/testing/setup.d.ts.map +1 -1
  171. package/dist/types/src/version.d.ts +1 -1
  172. package/dist/types/src/version.d.ts.map +1 -1
  173. package/dist/types/tsconfig.tsbuildinfo +1 -1
  174. package/package.json +71 -57
  175. package/src/index.ts +1 -0
  176. package/src/packlets/agents/edge-agent-manager.ts +8 -5
  177. package/src/packlets/agents/edge-agent-service.ts +4 -2
  178. package/src/packlets/devices/devices-service.test.ts +0 -1
  179. package/src/packlets/devtools/devtools.ts +28 -7
  180. package/src/packlets/devtools/feeds.ts +1 -1
  181. package/src/packlets/devtools/keys.ts +2 -2
  182. package/src/packlets/devtools/spaces.ts +1 -1
  183. package/src/packlets/diagnostics/diagnostics.ts +1 -2
  184. package/src/packlets/diagnostics/index.ts +1 -1
  185. package/src/packlets/identity/authenticator.ts +3 -3
  186. package/src/packlets/identity/contacts-service.ts +1 -2
  187. package/src/packlets/identity/identity-manager.test.ts +6 -6
  188. package/src/packlets/identity/identity-manager.ts +29 -28
  189. package/src/packlets/identity/identity-recovery-manager.ts +31 -22
  190. package/src/packlets/identity/identity-service.test.ts +6 -27
  191. package/src/packlets/identity/identity-service.ts +17 -83
  192. package/src/packlets/identity/identity.test.ts +3 -3
  193. package/src/packlets/identity/identity.ts +12 -35
  194. package/src/packlets/invitations/device-invitation-protocol.ts +10 -9
  195. package/src/packlets/invitations/edge-invitation-handler.ts +9 -5
  196. package/src/packlets/invitations/invitation-guest-extenstion.ts +6 -4
  197. package/src/packlets/invitations/invitation-host-extension.ts +13 -14
  198. package/src/packlets/invitations/invitation-protocol.ts +7 -4
  199. package/src/packlets/invitations/invitation-state.ts +1 -15
  200. package/src/packlets/invitations/invitations-handler.test.ts +4 -5
  201. package/src/packlets/invitations/invitations-handler.ts +74 -22
  202. package/src/packlets/invitations/invitations-manager.ts +42 -17
  203. package/src/packlets/invitations/invitations-service.ts +9 -9
  204. package/src/packlets/invitations/space-invitation-protocol.test.ts +17 -16
  205. package/src/packlets/invitations/space-invitation-protocol.ts +13 -18
  206. package/src/packlets/locks/index.ts +1 -1
  207. package/src/packlets/logging/logging-service.ts +19 -15
  208. package/src/packlets/network/network-service.test.ts +0 -1
  209. package/src/packlets/network/network-service.ts +10 -8
  210. package/src/packlets/services/client-rpc-server.ts +19 -16
  211. package/src/packlets/services/feed-syncer.test.ts +376 -0
  212. package/src/packlets/services/feed-syncer.ts +536 -0
  213. package/src/packlets/services/index.ts +1 -0
  214. package/src/packlets/services/platform.ts +7 -1
  215. package/src/packlets/services/service-context.test.ts +3 -2
  216. package/src/packlets/services/service-context.ts +215 -78
  217. package/src/packlets/services/service-host.test.ts +8 -10
  218. package/src/packlets/services/service-host.ts +102 -70
  219. package/src/packlets/services/service-registry.test.ts +0 -1
  220. package/src/packlets/services/sqlite-storage.ts +390 -0
  221. package/src/packlets/space-export/archive-format.ts +42 -0
  222. package/src/packlets/space-export/index.ts +4 -1
  223. package/src/packlets/space-export/serialized-space-reader.ts +129 -0
  224. package/src/packlets/space-export/serialized-space-writer.ts +260 -0
  225. package/src/packlets/space-export/space-archive-reader.ts +64 -3
  226. package/src/packlets/space-export/space-archive-writer.ts +41 -4
  227. package/src/packlets/space-export/space-archive.test.ts +482 -0
  228. package/src/packlets/spaces/data-space-manager.test.ts +169 -14
  229. package/src/packlets/spaces/data-space-manager.ts +192 -127
  230. package/src/packlets/spaces/data-space.ts +89 -43
  231. package/src/packlets/spaces/edge-feed-replicator.test.ts +2 -2
  232. package/src/packlets/spaces/edge-feed-replicator.ts +11 -9
  233. package/src/packlets/spaces/epoch-migrations.ts +7 -6
  234. package/src/packlets/spaces/genesis.ts +9 -4
  235. package/src/packlets/spaces/notarization-plugin.test.ts +2 -2
  236. package/src/packlets/spaces/notarization-plugin.ts +10 -9
  237. package/src/packlets/spaces/spaces-service.test.ts +18 -11
  238. package/src/packlets/spaces/spaces-service.ts +130 -24
  239. package/src/packlets/storage/index.ts +1 -0
  240. package/src/packlets/storage/profile-archive-sqlite.test.ts +79 -0
  241. package/src/packlets/storage/profile-archive-sqlite.ts +100 -0
  242. package/src/packlets/storage/profile-archive.ts +3 -0
  243. package/src/packlets/storage/storage.ts +4 -4
  244. package/src/packlets/testing/invitation-utils.ts +10 -6
  245. package/src/packlets/testing/test-builder.ts +59 -40
  246. package/src/packlets/worker/worker-runtime.ts +173 -17
  247. package/src/packlets/worker/worker-session.ts +12 -18
  248. package/src/version.ts +5 -1
  249. package/dist/lib/browser/chunk-I2RGLVJF.mjs.map +0 -7
  250. package/dist/lib/node-esm/chunk-QTUURCR4.mjs.map +0 -7
  251. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts +0 -19
  252. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts.map +0 -1
  253. package/src/packlets/identity/default-space-state-machine.ts +0 -44
@@ -0,0 +1,536 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import type * as SqlClient from '@effect/sql/SqlClient';
6
+ import { Encoder, decode as cborXdecode } from 'cbor-x';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Schema from 'effect/Schema';
9
+
10
+ import { AsyncTask, Mutex, scheduleTask } from '@dxos/async';
11
+ import { Context, Resource } from '@dxos/context';
12
+ import { type EdgeConnection, MessageSchema } from '@dxos/edge-client';
13
+ import { RuntimeProvider } from '@dxos/effect';
14
+ import { type FeedStore, SyncClient } from '@dxos/feed';
15
+ import { invariant } from '@dxos/invariant';
16
+ import { SpaceId } from '@dxos/keys';
17
+ import { log } from '@dxos/log';
18
+ import { FeedProtocol } from '@dxos/protocols';
19
+ import { EdgeService } from '@dxos/protocols';
20
+ import { createBuf } from '@dxos/protocols/buf';
21
+ import { type Message as RouterMessage } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
22
+ import type { GetSyncStateRequest, GetSyncStateResponse } from '@dxos/protocols/proto/dxos/client/services';
23
+ import type { SqlTransaction } from '@dxos/sql-sqlite';
24
+ import { bufferToArray } from '@dxos/util';
25
+
26
+ const encoder = new Encoder({ tagUint8Array: false, useRecords: false });
27
+
28
+ const DEFAULT_MESSAGE_BLOCKS_LIMIT = 50;
29
+ const DEFAULT_SYNC_CONCURRENCY = 5;
30
+ const DEFAULT_POLLING_INTERVAL = 5_000;
31
+ const DEFAULT_POLL_REQUEST_THROTTLE_MS = 250;
32
+ const DEFAULT_PUSH_FAILURE_BACKOFF_MS = 250;
33
+ const MAX_PUSH_FAILURE_BACKOFF_MS = 30_000;
34
+ const MAX_BLOCKING_SYNC_ITERATIONS = 100;
35
+
36
+ export type FeedSyncerOptions = {
37
+ runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
38
+ feedStore: FeedStore;
39
+ edgeClient: EdgeConnection;
40
+ peerId: string;
41
+ getSpaceIds: () => SpaceId[];
42
+
43
+ /**
44
+ * Namespaces to sync.
45
+ */
46
+ syncNamespaces: string[];
47
+
48
+ /**
49
+ * Maximum number of blocks to sync in a single message.
50
+ * @default 50
51
+ */
52
+ messageBlocksLimit?: number;
53
+
54
+ /**
55
+ * Maximum number of spaces to sync concurrently.
56
+ * @default 5
57
+ */
58
+ syncConcurrency?: number;
59
+
60
+ /**
61
+ * Interval between full polls.
62
+ * @default 10 seconds
63
+ */
64
+ pollingInterval?: number;
65
+
66
+ /**
67
+ * Minimum delay between externally requested best-effort polls.
68
+ * @default 250 ms
69
+ */
70
+ pollRequestThrottleMs?: number;
71
+
72
+ /**
73
+ * When false, only wires the edge message handler; poll/push background tasks and
74
+ * `feedStore.onNewBlocks` auto-push are disabled. Use for tests that call `syncBlocking` explicitly.
75
+ * @default true
76
+ */
77
+ backgroundSync?: boolean;
78
+
79
+ /**
80
+ * Max time to wait for a feed sync RPC response from edge, in milliseconds.
81
+ * @default 30000 (see `DEFAULT_SYNC_RPC_TIMEOUT_MS` in `@dxos/feed`).
82
+ */
83
+ syncRpcTimeoutMs?: number;
84
+ };
85
+
86
+ export class FeedSyncer extends Resource {
87
+ readonly #syncNamespaces: string[];
88
+ readonly #messageBlocksLimit: number;
89
+ readonly #syncConcurrency: number;
90
+ readonly #pollingInterval: number;
91
+ readonly #pollRequestThrottleMs: number;
92
+ readonly #backgroundSync: boolean;
93
+
94
+ readonly #runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
95
+ readonly #feedStore: FeedStore;
96
+ readonly #edgeClient: EdgeConnection;
97
+ readonly #syncClient: SyncClient;
98
+ readonly #getSpaceIds: () => SpaceId[];
99
+
100
+ #spacesToPoll = new Set<SpaceId>();
101
+ /** Last time full poll was completed. */
102
+ #lastFullPoll: number | null = null;
103
+ #throttledPollScheduled = false;
104
+ #lastRequestedPollAt: number | null = null;
105
+ readonly #feedStoreMutex = new Mutex();
106
+ #pushFailureBackoffMs = DEFAULT_PUSH_FAILURE_BACKOFF_MS;
107
+
108
+ constructor(options: FeedSyncerOptions) {
109
+ super();
110
+ this.#runtime = options.runtime;
111
+ this.#feedStore = options.feedStore;
112
+ this.#edgeClient = options.edgeClient;
113
+ this.#syncClient = new SyncClient({
114
+ peerId: options.peerId,
115
+ feedStore: options.feedStore,
116
+ sendMessage: this.#sendMessage.bind(this),
117
+ rpcTimeoutMs: options.syncRpcTimeoutMs,
118
+ });
119
+ this.#getSpaceIds = options.getSpaceIds;
120
+ this.#syncNamespaces = options.syncNamespaces;
121
+ this.#messageBlocksLimit = options.messageBlocksLimit ?? DEFAULT_MESSAGE_BLOCKS_LIMIT;
122
+ this.#syncConcurrency = options.syncConcurrency ?? DEFAULT_SYNC_CONCURRENCY;
123
+ this.#pollingInterval = options.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
124
+ this.#pollRequestThrottleMs = options.pollRequestThrottleMs ?? DEFAULT_POLL_REQUEST_THROTTLE_MS;
125
+ this.#backgroundSync = options.backgroundSync ?? true;
126
+ }
127
+
128
+ protected override async _open(): Promise<void> {
129
+ this._ctx.onDispose(
130
+ this.#edgeClient.onMessage((msg: RouterMessage) => {
131
+ if (!msg.serviceId) {
132
+ return;
133
+ }
134
+ const service = msg.serviceId.split(':')[0];
135
+ if (service !== EdgeService.QUEUE_REPLICATOR) {
136
+ return;
137
+ }
138
+ log('feed sync edge ingress', {
139
+ serviceId: msg.serviceId,
140
+ payloadByteLength: msg.payload?.value?.byteLength,
141
+ });
142
+ const handleMessageEffect = Effect.gen(this, function* () {
143
+ const decoded = yield* Effect.try({
144
+ try: () => cborXdecode(msg.payload!.value),
145
+ catch: (error) => new Error(`Failed to decode feed sync message: ${error}`),
146
+ });
147
+ const payload = yield* Schema.validate(FeedProtocol.ProtocolMessage)(decoded);
148
+ yield* this.#syncClient.handleMessage(payload);
149
+ }).pipe(
150
+ Effect.tapError((cause) =>
151
+ Effect.sync(() =>
152
+ log('feed sync edge message handling failed', {
153
+ serviceId: msg.serviceId,
154
+ payloadByteLength: msg.payload?.value?.byteLength,
155
+ cause: cause instanceof Error ? cause.message : String(cause),
156
+ }),
157
+ ),
158
+ ),
159
+ );
160
+
161
+ void this.#runSerialized(() => RuntimeProvider.runPromise(this.#runtime)(handleMessageEffect));
162
+ }),
163
+ );
164
+
165
+ if (this.#backgroundSync) {
166
+ // Tasks must be opened before registering listeners that call `schedule()`:
167
+ // * `onNewBlocks` can fire from any `feedStore.append` happening on a separate
168
+ // microtask while `_open()` is still awaiting.
169
+ // * The edge client invokes `onReconnected` as a microtask when already connected.
170
+ // `AsyncTask.schedule()` throws if the task is not yet open.
171
+ await this.#pollTask.open();
172
+ await this.#pushTask.open();
173
+
174
+ this.#feedStore.onNewBlocks.on(this._ctx, () => {
175
+ this.#pushTask.schedule();
176
+ });
177
+ }
178
+
179
+ this._ctx.onDispose(
180
+ // NOTE: Fires immediately (as a microtask) if the connection is already open, and again
181
+ // on every subsequent reconnect.
182
+ this.#edgeClient.onReconnected(async () => {
183
+ log('feed sync edge reconnected', {
184
+ peerKey: this.#edgeClient.peerKey,
185
+ identityKey: this.#edgeClient.identityKey,
186
+ });
187
+ if (this.#backgroundSync) {
188
+ this.#resetSpacesToPoll();
189
+ this.#pollTask.schedule();
190
+ this.#pushTask.schedule();
191
+ }
192
+ }),
193
+ );
194
+
195
+ if (this.#backgroundSync) {
196
+ this.#resetSpacesToPoll();
197
+ this.#pollTask.schedule();
198
+ // Flush blocks written before the syncer opened: `onNewBlocks` only fires on append,
199
+ // so existing unpositioned blocks would otherwise never be pushed.
200
+ this.#pushTask.schedule();
201
+ }
202
+ }
203
+
204
+ protected override async _close(): Promise<void> {
205
+ if (this.#backgroundSync) {
206
+ await this.#pollTask.close();
207
+ await this.#pushTask.close();
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Schedules a best-effort pull without blocking the caller.
213
+ */
214
+ schedulePoll(): void {
215
+ if (!this.#backgroundSync) {
216
+ return;
217
+ }
218
+ this.#resetSpacesToPoll();
219
+ if (this.#throttledPollScheduled) {
220
+ return;
221
+ }
222
+
223
+ const now = Date.now();
224
+ const delay =
225
+ this.#lastRequestedPollAt == null
226
+ ? 0
227
+ : Math.max(this.#pollRequestThrottleMs - (now - this.#lastRequestedPollAt), 0);
228
+ this.#throttledPollScheduled = true;
229
+ scheduleTask(
230
+ this._ctx,
231
+ () => {
232
+ this.#throttledPollScheduled = false;
233
+ this.#lastRequestedPollAt = Date.now();
234
+ this.#pollTask.schedule();
235
+ },
236
+ delay,
237
+ );
238
+ }
239
+
240
+ /**
241
+ * Returns per-namespace queue sync backlog for a space.
242
+ * `blocksToPull` and `blocksToPush` of 0 mean caught up for that namespace.
243
+ */
244
+ async getSyncState(ctx: Context, request: GetSyncStateRequest): Promise<GetSyncStateResponse> {
245
+ const spaceId = request.spaceId as SpaceId;
246
+ invariant(SpaceId.isValid(spaceId));
247
+ const namespaces =
248
+ request.namespaces != null && request.namespaces.length > 0 ? request.namespaces : this.#syncNamespaces;
249
+ for (const feedNamespace of namespaces) {
250
+ invariant(FeedProtocol.isWellKnownNamespace(feedNamespace));
251
+ }
252
+
253
+ return this.#runSerialized(() =>
254
+ RuntimeProvider.runPromise(this.#runtime)(
255
+ Effect.gen(this, function* () {
256
+ const namespaceStates = yield* Effect.forEach(
257
+ namespaces,
258
+ (feedNamespace) =>
259
+ Effect.gen(this, function* () {
260
+ const blocksToPush = yield* this.#feedStore.countUnpositionedBlocks({
261
+ spaceId,
262
+ feedNamespace,
263
+ });
264
+ const totalBlocks = yield* this.#feedStore.countNamespaceBlocks({
265
+ spaceId,
266
+ feedNamespace,
267
+ });
268
+ const { blocksToPull } = yield* this.#syncClient
269
+ .peekPull(ctx, {
270
+ spaceId,
271
+ feedNamespace,
272
+ limit: this.#messageBlocksLimit,
273
+ })
274
+ .pipe(
275
+ Effect.catchAll((cause) =>
276
+ Effect.gen(this, function* () {
277
+ this.#logSyncFailure('peekPull', { spaceId, feedNamespace, cause });
278
+ return { blocksToPull: 0 };
279
+ }),
280
+ ),
281
+ );
282
+ return {
283
+ namespace: feedNamespace,
284
+ blocksToPull: String(blocksToPull),
285
+ blocksToPush: String(blocksToPush),
286
+ totalBlocks: String(totalBlocks),
287
+ };
288
+ }),
289
+ { concurrency: 'unbounded' },
290
+ );
291
+ return { namespaces: namespaceStates };
292
+ }),
293
+ ),
294
+ );
295
+ }
296
+
297
+ /**
298
+ * Performs queue sync and blocks until there are no pending sync batches.
299
+ */
300
+ async syncBlocking(
301
+ ctx: Context,
302
+ {
303
+ spaceId,
304
+ subspaceTag,
305
+ shouldPush = true,
306
+ shouldPull = true,
307
+ }: {
308
+ spaceId: SpaceId;
309
+ subspaceTag: string;
310
+ shouldPush?: boolean;
311
+ shouldPull?: boolean;
312
+ },
313
+ ): Promise<void> {
314
+ invariant(SpaceId.isValid(spaceId));
315
+ invariant(FeedProtocol.isWellKnownNamespace(subspaceTag));
316
+ if (!shouldPush && !shouldPull) {
317
+ return;
318
+ }
319
+
320
+ await this.#runSerialized(() =>
321
+ RuntimeProvider.runPromise(this.#runtime)(
322
+ Effect.gen(this, function* () {
323
+ let done = false;
324
+ let iterations = 0;
325
+ while (!done) {
326
+ done = true;
327
+ if (shouldPull) {
328
+ const pullResult = yield* this.#syncClient.pull(ctx, {
329
+ spaceId,
330
+ feedNamespace: subspaceTag,
331
+ limit: this.#messageBlocksLimit,
332
+ });
333
+ done &&= pullResult.done;
334
+ }
335
+
336
+ if (shouldPush) {
337
+ const pushResult = yield* this.#syncClient.push(ctx, {
338
+ spaceId,
339
+ feedNamespace: subspaceTag,
340
+ limit: this.#messageBlocksLimit,
341
+ });
342
+ done &&= pushResult.done;
343
+ }
344
+ iterations++;
345
+ if (iterations > MAX_BLOCKING_SYNC_ITERATIONS) {
346
+ throw new Error('Blocking sync exceeded max iterations.');
347
+ }
348
+ }
349
+ }),
350
+ ),
351
+ );
352
+ }
353
+
354
+ async #runSerialized<A>(run: () => Promise<A>): Promise<A> {
355
+ using _guard = await this.#feedStoreMutex.acquire('feed-sync');
356
+ return run();
357
+ }
358
+
359
+ #schedulePushRetry({ hadFailure, needsMore }: { hadFailure: boolean; needsMore: boolean }): void {
360
+ if (!needsMore) {
361
+ this.#pushFailureBackoffMs = DEFAULT_PUSH_FAILURE_BACKOFF_MS;
362
+ return;
363
+ }
364
+ if (hadFailure) {
365
+ const delayMs = this.#pushFailureBackoffMs;
366
+ this.#pushFailureBackoffMs = Math.min(this.#pushFailureBackoffMs * 2, MAX_PUSH_FAILURE_BACKOFF_MS);
367
+ log.info('feed sync push retry scheduled with backoff', { delayMs });
368
+ scheduleTask(this._ctx, () => this.#pushTask.schedule(), delayMs);
369
+ return;
370
+ }
371
+ this.#pushFailureBackoffMs = DEFAULT_PUSH_FAILURE_BACKOFF_MS;
372
+ this.#pushTask.schedule();
373
+ }
374
+
375
+ #resetSpacesToPoll(): void {
376
+ this.#spacesToPoll.clear();
377
+ this.#getSpaceIds().forEach((spaceId) => {
378
+ this.#spacesToPoll.add(spaceId);
379
+ });
380
+ this.#lastFullPoll = Date.now();
381
+ }
382
+
383
+ #sendMessage(
384
+ ctx: Context,
385
+ message: FeedProtocol.QueryRequest | FeedProtocol.AppendRequest,
386
+ ): Effect.Effect<void, unknown, never> {
387
+ return Effect.gen(this, function* () {
388
+ const encoded = encoder.encode(message);
389
+ const serviceId = this.#getTargetServiceId(message);
390
+ const rpcTag = 'blocks' in message ? 'AppendRequest' : 'QueryRequest';
391
+ log('feed sync edge rpc outgoing', {
392
+ tag: rpcTag,
393
+ serviceId,
394
+ payloadByteLength: encoded.byteLength,
395
+ spaceId: message.spaceId,
396
+ feedNamespace: message.feedNamespace,
397
+ requestId: message.requestId,
398
+ });
399
+ yield* Effect.tryPromise(async () =>
400
+ this.#edgeClient.send(
401
+ ctx,
402
+ createBuf(MessageSchema, {
403
+ source: {
404
+ identityKey: this.#edgeClient.identityKey,
405
+ peerKey: this.#edgeClient.peerKey,
406
+ },
407
+ serviceId,
408
+ payload: { value: bufferToArray(encoded) },
409
+ }),
410
+ ),
411
+ ).pipe(
412
+ Effect.tapError((cause) =>
413
+ Effect.sync(() =>
414
+ log('feed sync edge send failed', {
415
+ serviceId,
416
+ tag: rpcTag,
417
+ cause: cause instanceof Error ? cause.message : String(cause),
418
+ }),
419
+ ),
420
+ ),
421
+ );
422
+ });
423
+ }
424
+
425
+ #logSyncFailure(
426
+ operation: 'pull' | 'push' | 'peekPull',
427
+ { spaceId, feedNamespace, cause }: { spaceId: SpaceId; feedNamespace: string; cause: unknown },
428
+ ): void {
429
+ log('feed sync operation failed', {
430
+ operation,
431
+ spaceId,
432
+ feedNamespace,
433
+ cause: cause instanceof Error ? cause.message : String(cause),
434
+ errorTag: cause instanceof Error ? cause.name : undefined,
435
+ });
436
+ }
437
+
438
+ #getTargetServiceId(message: FeedProtocol.QueryRequest | FeedProtocol.AppendRequest): string {
439
+ // TODO(dmaretskyi): Perhaps in the future we will want to include the queue namespace here as well.
440
+ // This would require putting it at the top level of the message.
441
+ // For now, we let the edge router handle it.
442
+ return FeedProtocol.encodeServiceId(message.feedNamespace, message.spaceId as SpaceId);
443
+ }
444
+
445
+ readonly #pollTask = new AsyncTask(async () =>
446
+ Effect.gen(this, function* () {
447
+ yield* Effect.forEach(
448
+ this.#spacesToPoll,
449
+ (spaceId) =>
450
+ Effect.gen(this, function* () {
451
+ let doneForAllNamespaces = true;
452
+ for (const feedNamespace of this.#syncNamespaces) {
453
+ const { done } = yield* this.#syncClient
454
+ .pull(this._ctx, {
455
+ spaceId,
456
+ feedNamespace,
457
+ limit: this.#messageBlocksLimit,
458
+ })
459
+ .pipe(
460
+ Effect.catchAll((cause) =>
461
+ Effect.gen(this, function* () {
462
+ this.#logSyncFailure('pull', { spaceId, feedNamespace, cause });
463
+ return { done: false };
464
+ }),
465
+ ),
466
+ );
467
+ if (!done) {
468
+ doneForAllNamespaces = false;
469
+ }
470
+ }
471
+ if (doneForAllNamespaces) {
472
+ this.#spacesToPoll.delete(spaceId);
473
+ }
474
+ }),
475
+ { concurrency: this.#syncConcurrency },
476
+ );
477
+
478
+ // If its time to do a full poll, reset the spaces to poll and schedule the next poll immediately.
479
+ if (this.#lastFullPoll == null || Date.now() - this.#lastFullPoll > this.#pollingInterval) {
480
+ this.#resetSpacesToPoll();
481
+ this.#pollTask.schedule();
482
+ } else if (this.#spacesToPoll.size > 0) {
483
+ // If there are some spaces still syncing, poll them immediately.
484
+ this.#pollTask.schedule();
485
+ } else {
486
+ // All spaces sync, and there's time before the next full poll, schedule it later.
487
+ this.#resetSpacesToPoll();
488
+ scheduleTask(
489
+ this._ctx,
490
+ () => this.#pollTask.schedule(),
491
+ Math.max(this.#pollingInterval - (Date.now() - (this.#lastFullPoll ?? 0)), 0),
492
+ );
493
+ }
494
+ }).pipe((effect) => this.#runSerialized(() => RuntimeProvider.runPromise(this.#runtime)(effect))),
495
+ );
496
+
497
+ readonly #pushTask = new AsyncTask(async () =>
498
+ Effect.gen(this, function* () {
499
+ yield* Effect.forEach(
500
+ this.#getSpaceIds(),
501
+ (spaceId) =>
502
+ Effect.gen(this, function* () {
503
+ let needsMorePush = false;
504
+ let hadPushFailure = false;
505
+ for (const feedNamespace of this.#syncNamespaces) {
506
+ const { done } = yield* this.#syncClient
507
+ .push(this._ctx, {
508
+ spaceId,
509
+ feedNamespace,
510
+ limit: this.#messageBlocksLimit,
511
+ })
512
+ .pipe(
513
+ Effect.tap(() =>
514
+ Effect.sync(() => {
515
+ this.#pushFailureBackoffMs = DEFAULT_PUSH_FAILURE_BACKOFF_MS;
516
+ }),
517
+ ),
518
+ Effect.catchAll((cause) =>
519
+ Effect.gen(this, function* () {
520
+ this.#logSyncFailure('push', { spaceId, feedNamespace, cause });
521
+ hadPushFailure = true;
522
+ return { done: false };
523
+ }),
524
+ ),
525
+ );
526
+ if (!done) {
527
+ needsMorePush = true;
528
+ }
529
+ }
530
+ this.#schedulePushRetry({ hadFailure: hadPushFailure, needsMore: needsMorePush });
531
+ }),
532
+ { concurrency: this.#syncConcurrency },
533
+ );
534
+ }).pipe((effect) => this.#runSerialized(() => RuntimeProvider.runPromise(this.#runtime)(effect))),
535
+ );
536
+ }
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './client-rpc-server';
6
+ export * from './feed-syncer';
6
7
  export * from './service-context';
7
8
  export * from './service-host';
8
9
  export * from './service-registry';
@@ -14,12 +14,18 @@ export const getPlatform = (): Platform => {
14
14
  userAgent,
15
15
  uptime: Math.floor((Date.now() - window.performance.timeOrigin) / 1_000),
16
16
  };
17
- } else {
17
+ } else if (typeof SharedWorkerGlobalScope !== 'undefined') {
18
18
  // Shared worker.
19
19
  return {
20
20
  type: Platform.PLATFORM_TYPE.SHARED_WORKER,
21
21
  uptime: Math.floor((Date.now() - performance.timeOrigin) / 1_000),
22
22
  };
23
+ } else {
24
+ // Dedicated worker.
25
+ return {
26
+ type: Platform.PLATFORM_TYPE.DEDICATED_WORKER,
27
+ uptime: Math.floor((Date.now() - performance.timeOrigin) / 1_000),
28
+ };
23
29
  }
24
30
  } else {
25
31
  // Node.
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
+ import { Context } from '@dxos/context';
7
8
  import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
8
9
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
9
10
  import { openAndClose } from '@dxos/test-utils';
@@ -19,7 +20,7 @@ describe('services/ServiceContext', () => {
19
20
  const device2 = await createOpenServiceContext(networkContext);
20
21
  await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
21
22
 
22
- const space1 = await device1.dataSpaceManager!.createSpace();
23
+ const space1 = await device1.dataSpaceManager!.createSpace(new Context());
23
24
  await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
24
25
  const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
25
26
  await space2!.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.timeframe);
@@ -35,7 +36,7 @@ describe('services/ServiceContext', () => {
35
36
 
36
37
  const identity2 = await createOpenServiceContext(networkContext);
37
38
  await identity2.createIdentity();
38
- const space1 = await identity2.dataSpaceManager!.createSpace();
39
+ const space1 = await identity2.dataSpaceManager!.createSpace(new Context());
39
40
  await Promise.all(
40
41
  performInvitation({
41
42
  host: identity2,