@dxos/client-services 0.8.4-main.a4bbb77 → 0.8.4-main.abd8ff62ef

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 (229) hide show
  1. package/dist/lib/browser/{chunk-FZDVFED2.mjs → chunk-KW4WMU5R.mjs} +2698 -4599
  2. package/dist/lib/browser/chunk-KW4WMU5R.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-QCWEHHJW.mjs +24 -0
  4. package/dist/lib/browser/chunk-QCWEHHJW.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-XJRPB3GA.mjs +22 -0
  6. package/dist/lib/browser/chunk-XJRPB3GA.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +490 -228
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs +88 -0
  11. package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +7 -0
  12. package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs +11 -0
  13. package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs.map +7 -0
  14. package/dist/lib/browser/packlets/locks/browser.mjs +86 -0
  15. package/dist/lib/browser/packlets/locks/browser.mjs.map +7 -0
  16. package/dist/lib/browser/packlets/locks/node.mjs +48 -0
  17. package/dist/lib/browser/packlets/locks/node.mjs.map +7 -0
  18. package/dist/lib/browser/testing/index.mjs +60 -90
  19. package/dist/lib/browser/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/chunk-2DT3MZRL.mjs +22 -0
  21. package/dist/lib/node-esm/chunk-2DT3MZRL.mjs.map +7 -0
  22. package/dist/lib/node-esm/chunk-2SZHAWBN.mjs +24 -0
  23. package/dist/lib/node-esm/chunk-2SZHAWBN.mjs.map +7 -0
  24. package/dist/lib/node-esm/{chunk-JDTIU3EP.mjs → chunk-NDMKP2CH.mjs} +2621 -4391
  25. package/dist/lib/node-esm/chunk-NDMKP2CH.mjs.map +7 -0
  26. package/dist/lib/node-esm/index.mjs +490 -228
  27. package/dist/lib/node-esm/index.mjs.map +4 -4
  28. package/dist/lib/node-esm/meta.json +1 -1
  29. package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs +88 -0
  30. package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +7 -0
  31. package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs +11 -0
  32. package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs.map +7 -0
  33. package/dist/lib/node-esm/packlets/locks/browser.mjs +86 -0
  34. package/dist/lib/node-esm/packlets/locks/browser.mjs.map +7 -0
  35. package/dist/lib/node-esm/packlets/locks/node.mjs +48 -0
  36. package/dist/lib/node-esm/packlets/locks/node.mjs.map +7 -0
  37. package/dist/lib/node-esm/testing/index.mjs +60 -90
  38. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  39. package/dist/types/src/index.d.ts +1 -0
  40. package/dist/types/src/index.d.ts.map +1 -1
  41. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +3 -2
  42. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -1
  43. package/dist/types/src/packlets/agents/edge-agent-service.d.ts +2 -1
  44. package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -1
  45. package/dist/types/src/packlets/devices/devices-service.d.ts.map +1 -1
  46. package/dist/types/src/packlets/devtools/devtools.d.ts +2 -2
  47. package/dist/types/src/packlets/devtools/devtools.d.ts.map +1 -1
  48. package/dist/types/src/packlets/devtools/feeds.d.ts.map +1 -1
  49. package/dist/types/src/packlets/devtools/keys.d.ts.map +1 -1
  50. package/dist/types/src/packlets/devtools/metadata.d.ts.map +1 -1
  51. package/dist/types/src/packlets/devtools/network.d.ts.map +1 -1
  52. package/dist/types/src/packlets/devtools/spaces.d.ts.map +1 -1
  53. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts.map +1 -1
  54. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  55. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -1
  56. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts +2 -3
  57. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -1
  58. package/dist/types/src/packlets/diagnostics/index.d.ts +1 -1
  59. package/dist/types/src/packlets/diagnostics/index.d.ts.map +1 -1
  60. package/dist/types/src/packlets/identity/authenticator.d.ts +2 -2
  61. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  62. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  63. package/dist/types/src/packlets/identity/identity-manager.d.ts +6 -6
  64. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  65. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +8 -7
  66. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -1
  67. package/dist/types/src/packlets/identity/identity-service.d.ts +6 -10
  68. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  69. package/dist/types/src/packlets/identity/identity.d.ts +8 -11
  70. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  71. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +6 -5
  72. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  73. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +1 -1
  74. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -1
  75. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  76. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  77. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +7 -4
  78. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  79. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -1
  80. package/dist/types/src/packlets/invitations/invitation-topology.d.ts.map +1 -1
  81. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +4 -4
  82. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  83. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +3 -3
  84. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
  85. package/dist/types/src/packlets/invitations/invitations-service.d.ts +3 -3
  86. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  87. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +4 -3
  88. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  89. package/dist/types/src/packlets/invitations/utils.d.ts.map +1 -1
  90. package/dist/types/src/packlets/locks/browser.d.ts.map +1 -1
  91. package/dist/types/src/packlets/locks/index.d.ts +1 -1
  92. package/dist/types/src/packlets/locks/index.d.ts.map +1 -1
  93. package/dist/types/src/packlets/locks/node.d.ts.map +1 -1
  94. package/dist/types/src/packlets/logging/logging-service.d.ts +4 -0
  95. package/dist/types/src/packlets/logging/logging-service.d.ts.map +1 -1
  96. package/dist/types/src/packlets/network/network-service.d.ts +5 -4
  97. package/dist/types/src/packlets/network/network-service.d.ts.map +1 -1
  98. package/dist/types/src/packlets/services/client-rpc-server.d.ts +5 -5
  99. package/dist/types/src/packlets/services/client-rpc-server.d.ts.map +1 -1
  100. package/dist/types/src/packlets/services/feed-syncer.d.ts +59 -0
  101. package/dist/types/src/packlets/services/feed-syncer.d.ts.map +1 -0
  102. package/dist/types/src/packlets/services/feed-syncer.test.d.ts +2 -0
  103. package/dist/types/src/packlets/services/feed-syncer.test.d.ts.map +1 -0
  104. package/dist/types/src/packlets/services/platform.d.ts.map +1 -1
  105. package/dist/types/src/packlets/services/service-context.d.ts +13 -9
  106. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  107. package/dist/types/src/packlets/services/service-host.d.ts +20 -7
  108. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  109. package/dist/types/src/packlets/services/service-registry.d.ts.map +1 -1
  110. package/dist/types/src/packlets/services/util.d.ts.map +1 -1
  111. package/dist/types/src/packlets/space-export/archive-format.d.ts +9 -0
  112. package/dist/types/src/packlets/space-export/archive-format.d.ts.map +1 -0
  113. package/dist/types/src/packlets/space-export/index.d.ts +4 -1
  114. package/dist/types/src/packlets/space-export/index.d.ts.map +1 -1
  115. package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts +23 -0
  116. package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts.map +1 -0
  117. package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts +36 -0
  118. package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts.map +1 -0
  119. package/dist/types/src/packlets/space-export/space-archive-reader.d.ts +9 -1
  120. package/dist/types/src/packlets/space-export/space-archive-reader.d.ts.map +1 -1
  121. package/dist/types/src/packlets/space-export/space-archive-writer.d.ts +7 -1
  122. package/dist/types/src/packlets/space-export/space-archive-writer.d.ts.map +1 -1
  123. package/dist/types/src/packlets/space-export/space-archive.test.d.ts +2 -0
  124. package/dist/types/src/packlets/space-export/space-archive.test.d.ts.map +1 -0
  125. package/dist/types/src/packlets/spaces/automerge-space-state.d.ts.map +1 -1
  126. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +28 -17
  127. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  128. package/dist/types/src/packlets/spaces/data-space.d.ts +26 -9
  129. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  130. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +2 -2
  131. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  132. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  133. package/dist/types/src/packlets/spaces/genesis.d.ts +2 -1
  134. package/dist/types/src/packlets/spaces/genesis.d.ts.map +1 -1
  135. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +6 -9
  136. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  137. package/dist/types/src/packlets/spaces/spaces-service.d.ts +10 -7
  138. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  139. package/dist/types/src/packlets/storage/level.d.ts.map +1 -1
  140. package/dist/types/src/packlets/storage/profile-archive.d.ts.map +1 -1
  141. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  142. package/dist/types/src/packlets/storage/util.d.ts.map +1 -1
  143. package/dist/types/src/packlets/system/system-service.d.ts +1 -1
  144. package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
  145. package/dist/types/src/packlets/testing/credential-utils.d.ts.map +1 -1
  146. package/dist/types/src/packlets/testing/invitation-utils.d.ts +6 -3
  147. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  148. package/dist/types/src/packlets/testing/test-builder.d.ts +6 -5
  149. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  150. package/dist/types/src/packlets/worker/worker-runtime.d.ts +41 -4
  151. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  152. package/dist/types/src/packlets/worker/worker-session.d.ts +2 -4
  153. package/dist/types/src/packlets/worker/worker-session.d.ts.map +1 -1
  154. package/dist/types/src/testing/setup.d.ts.map +1 -1
  155. package/dist/types/src/version.d.ts +1 -1
  156. package/dist/types/src/version.d.ts.map +1 -1
  157. package/dist/types/tsconfig.tsbuildinfo +1 -1
  158. package/package.json +70 -55
  159. package/src/index.ts +1 -0
  160. package/src/packlets/agents/edge-agent-manager.ts +8 -5
  161. package/src/packlets/agents/edge-agent-service.ts +15 -3
  162. package/src/packlets/devices/devices-service.test.ts +0 -1
  163. package/src/packlets/devices/devices-service.ts +1 -1
  164. package/src/packlets/devtools/devtools.ts +2 -3
  165. package/src/packlets/diagnostics/diagnostics.ts +1 -2
  166. package/src/packlets/diagnostics/index.ts +1 -1
  167. package/src/packlets/identity/authenticator.ts +2 -2
  168. package/src/packlets/identity/contacts-service.ts +0 -1
  169. package/src/packlets/identity/identity-manager.test.ts +5 -5
  170. package/src/packlets/identity/identity-manager.ts +23 -22
  171. package/src/packlets/identity/identity-recovery-manager.ts +22 -18
  172. package/src/packlets/identity/identity-service.test.ts +6 -27
  173. package/src/packlets/identity/identity-service.ts +13 -81
  174. package/src/packlets/identity/identity.test.ts +6 -6
  175. package/src/packlets/identity/identity.ts +11 -34
  176. package/src/packlets/invitations/device-invitation-protocol.ts +8 -7
  177. package/src/packlets/invitations/edge-invitation-handler.ts +9 -5
  178. package/src/packlets/invitations/invitation-guest-extenstion.ts +6 -4
  179. package/src/packlets/invitations/invitation-host-extension.ts +13 -14
  180. package/src/packlets/invitations/invitation-protocol.ts +7 -4
  181. package/src/packlets/invitations/invitation-state.ts +1 -15
  182. package/src/packlets/invitations/invitations-handler.test.ts +4 -5
  183. package/src/packlets/invitations/invitations-handler.ts +74 -22
  184. package/src/packlets/invitations/invitations-manager.ts +40 -15
  185. package/src/packlets/invitations/invitations-service.ts +9 -9
  186. package/src/packlets/invitations/space-invitation-protocol.test.ts +17 -16
  187. package/src/packlets/invitations/space-invitation-protocol.ts +11 -16
  188. package/src/packlets/locks/index.ts +1 -1
  189. package/src/packlets/logging/logging-service.ts +20 -16
  190. package/src/packlets/network/network-service.test.ts +0 -1
  191. package/src/packlets/network/network-service.ts +10 -8
  192. package/src/packlets/services/client-rpc-server.ts +19 -16
  193. package/src/packlets/services/feed-syncer.test.ts +340 -0
  194. package/src/packlets/services/feed-syncer.ts +337 -0
  195. package/src/packlets/services/platform.ts +7 -1
  196. package/src/packlets/services/service-context.test.ts +3 -2
  197. package/src/packlets/services/service-context.ts +138 -56
  198. package/src/packlets/services/service-host.test.ts +8 -8
  199. package/src/packlets/services/service-host.ts +70 -40
  200. package/src/packlets/services/service-registry.test.ts +0 -1
  201. package/src/packlets/space-export/archive-format.ts +42 -0
  202. package/src/packlets/space-export/index.ts +4 -1
  203. package/src/packlets/space-export/serialized-space-reader.ts +111 -0
  204. package/src/packlets/space-export/serialized-space-writer.ts +253 -0
  205. package/src/packlets/space-export/space-archive-reader.ts +64 -3
  206. package/src/packlets/space-export/space-archive-writer.ts +41 -3
  207. package/src/packlets/space-export/space-archive.test.ts +461 -0
  208. package/src/packlets/spaces/data-space-manager.test.ts +79 -13
  209. package/src/packlets/spaces/data-space-manager.ts +115 -115
  210. package/src/packlets/spaces/data-space.ts +58 -33
  211. package/src/packlets/spaces/edge-feed-replicator.test.ts +2 -2
  212. package/src/packlets/spaces/edge-feed-replicator.ts +12 -10
  213. package/src/packlets/spaces/epoch-migrations.ts +5 -5
  214. package/src/packlets/spaces/genesis.ts +6 -1
  215. package/src/packlets/spaces/notarization-plugin.test.ts +2 -2
  216. package/src/packlets/spaces/notarization-plugin.ts +10 -9
  217. package/src/packlets/spaces/spaces-service.test.ts +18 -11
  218. package/src/packlets/spaces/spaces-service.ts +123 -24
  219. package/src/packlets/storage/storage.ts +4 -4
  220. package/src/packlets/testing/invitation-utils.ts +10 -6
  221. package/src/packlets/testing/test-builder.ts +36 -10
  222. package/src/packlets/worker/worker-runtime.ts +188 -17
  223. package/src/packlets/worker/worker-session.ts +12 -18
  224. package/src/version.ts +1 -1
  225. package/dist/lib/browser/chunk-FZDVFED2.mjs.map +0 -7
  226. package/dist/lib/node-esm/chunk-JDTIU3EP.mjs.map +0 -7
  227. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts +0 -19
  228. package/dist/types/src/packlets/identity/default-space-state-machine.d.ts.map +0 -1
  229. package/src/packlets/identity/default-space-state-machine.ts +0 -44
@@ -0,0 +1,340 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Encoder, decode as cborDecode } from 'cbor-x';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Layer from 'effect/Layer';
8
+ import * as ManagedRuntime from 'effect/ManagedRuntime';
9
+ import { describe, expect, onTestFinished, test, vi } from 'vitest';
10
+
11
+ import { Event } from '@dxos/async';
12
+ import { Context } from '@dxos/context';
13
+ import { type EdgeConnection, MessageSchema } from '@dxos/edge-client';
14
+ import { RuntimeProvider } from '@dxos/effect';
15
+ import { FeedStore, SyncServer } from '@dxos/feed';
16
+ import { ObjectId, SpaceId } from '@dxos/keys';
17
+ import { FeedProtocol } from '@dxos/protocols';
18
+ import { EdgeService } from '@dxos/protocols';
19
+ import { createBuf } from '@dxos/protocols/buf';
20
+ import { type Message as RouterMessage } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
21
+ import { EdgeStatus } from '@dxos/protocols/proto/dxos/client/services';
22
+ import { SqlTransaction } from '@dxos/sql-sqlite';
23
+ import { layerMemory } from '@dxos/sql-sqlite/platform';
24
+ import { bufferToArray } from '@dxos/util';
25
+
26
+ import { FeedSyncer } from './feed-syncer';
27
+
28
+ type ProtocolMessage = FeedProtocol.ProtocolMessage;
29
+
30
+ const encoder = new Encoder({ tagUint8Array: false, useRecords: false });
31
+ const syncNamespace = FeedProtocol.WellKnownNamespaces.data;
32
+ const syncNamespaces = [FeedProtocol.WellKnownNamespaces.data, FeedProtocol.WellKnownNamespaces.trace];
33
+
34
+ const createRuntime = () => {
35
+ const baseLayer = layerMemory;
36
+ const transactionLayer = SqlTransaction.layer.pipe(Layer.provide(baseLayer));
37
+ return ManagedRuntime.make(Layer.merge(baseLayer, transactionLayer).pipe(Layer.orDie));
38
+ };
39
+
40
+ const createFeedStore = (localActorId: string, assignPositions: boolean) =>
41
+ new FeedStore({ localActorId, assignPositions });
42
+
43
+ const createEdgeConnection = ({
44
+ syncServer,
45
+ serverRuntime,
46
+ messageListeners,
47
+ }: {
48
+ syncServer: SyncServer;
49
+ serverRuntime: ReturnType<typeof createRuntime>;
50
+ messageListeners: Set<(message: RouterMessage) => void>;
51
+ }): EdgeConnection => {
52
+ const reconnectListeners = new Set<() => void>();
53
+
54
+ return {
55
+ statusChanged: new Event<any>(),
56
+ info: {},
57
+ identityKey: 'client-identity',
58
+ peerKey: 'client-peer',
59
+ isOpen: true,
60
+ status: {
61
+ state: EdgeStatus.ConnectionState.CONNECTED,
62
+ rtt: 0,
63
+ uptime: 0,
64
+ rateBytesUp: 0,
65
+ rateBytesDown: 0,
66
+ messagesSent: 0,
67
+ messagesReceived: 0,
68
+ },
69
+ setIdentity: () => {},
70
+ open: async () => {},
71
+ close: async () => {},
72
+ send: async (ctx, routerMessage: RouterMessage) => {
73
+ const decoded = cborDecode(routerMessage.payload!.value) as ProtocolMessage;
74
+ await syncServer.handleMessage(ctx, decoded).pipe(RuntimeProvider.runPromise(serverRuntime.runtimeEffect));
75
+ },
76
+ onMessage: (listener: (message: RouterMessage) => void) => {
77
+ messageListeners.add(listener);
78
+ return () => messageListeners.delete(listener);
79
+ },
80
+ onReconnected: (listener: () => void) => {
81
+ reconnectListeners.add(listener);
82
+ return () => reconnectListeners.delete(listener);
83
+ },
84
+ };
85
+ };
86
+
87
+ const createFeedSyncHarness = async ({
88
+ spaceId,
89
+ pollingInterval,
90
+ syncNamespaces: namespaces = [syncNamespace],
91
+ }: {
92
+ spaceId: SpaceId;
93
+ pollingInterval?: number;
94
+ syncNamespaces?: string[];
95
+ }) => {
96
+ const serverRuntime = createRuntime();
97
+ const clientRuntime = createRuntime();
98
+ const serverFeedStore = createFeedStore('server', true);
99
+ const clientFeedStore = createFeedStore('client', false);
100
+
101
+ await serverFeedStore.migrate().pipe(RuntimeProvider.runPromise(serverRuntime.runtimeEffect));
102
+ await clientFeedStore.migrate().pipe(RuntimeProvider.runPromise(clientRuntime.runtimeEffect));
103
+
104
+ const messageListeners = new Set<(message: RouterMessage) => void>();
105
+ const syncServer = new SyncServer({
106
+ peerId: 'server',
107
+ feedStore: serverFeedStore,
108
+ sendMessage: (_ctx, message) =>
109
+ Effect.promise(async () => {
110
+ const routerMessage = createBuf(MessageSchema, {
111
+ source: {
112
+ identityKey: 'server-identity',
113
+ peerKey: 'server-peer',
114
+ },
115
+ serviceId: `${EdgeService.QUEUE_REPLICATOR}:test`,
116
+ payload: { value: bufferToArray(encoder.encode(message)) },
117
+ });
118
+
119
+ for (const listener of messageListeners) {
120
+ listener(routerMessage);
121
+ }
122
+ }),
123
+ });
124
+
125
+ const edgeClient = createEdgeConnection({ syncServer, serverRuntime, messageListeners });
126
+
127
+ const syncer = new FeedSyncer({
128
+ runtime: clientRuntime.runtimeEffect,
129
+ feedStore: clientFeedStore,
130
+ edgeClient: edgeClient as any,
131
+ peerId: 'client',
132
+ getSpaceIds: () => [spaceId],
133
+ syncNamespaces: namespaces,
134
+ pollingInterval,
135
+ });
136
+
137
+ const close = async () => {
138
+ await syncer.close();
139
+ await clientRuntime.dispose();
140
+ await serverRuntime.dispose();
141
+ };
142
+
143
+ onTestFinished(close);
144
+
145
+ return { serverRuntime, clientRuntime, serverFeedStore, clientFeedStore, syncer, close };
146
+ };
147
+
148
+ describe('FeedSyncer', () => {
149
+ test('syncs mixed pull and push traffic', async () => {
150
+ const spaceId = SpaceId.random();
151
+ const { serverRuntime, clientRuntime, serverFeedStore, clientFeedStore, syncer } = await createFeedSyncHarness({
152
+ spaceId,
153
+ });
154
+ const serverFeedId = ObjectId.random();
155
+ const clientFeedId = ObjectId.random();
156
+
157
+ await serverFeedStore
158
+ .appendLocal([
159
+ {
160
+ spaceId,
161
+ feedId: serverFeedId,
162
+ feedNamespace: syncNamespace,
163
+ data: new Uint8Array([1, 2, 3]),
164
+ },
165
+ ])
166
+ .pipe(RuntimeProvider.runPromise(serverRuntime.runtimeEffect));
167
+
168
+ await syncer.open(new Context());
169
+
170
+ await vi.waitFor(async () => {
171
+ const { blocks } = await clientFeedStore
172
+ .query({
173
+ spaceId,
174
+ feedNamespace: syncNamespace,
175
+ position: -1,
176
+ query: { feedIds: [serverFeedId] },
177
+ })
178
+ .pipe(RuntimeProvider.runPromise(clientRuntime.runtimeEffect));
179
+
180
+ expect(blocks).toHaveLength(1);
181
+ expect(blocks[0].data).toEqual(new Uint8Array([1, 2, 3]));
182
+ expect(blocks[0].position).toBeDefined();
183
+ });
184
+
185
+ await clientFeedStore
186
+ .appendLocal([
187
+ {
188
+ spaceId,
189
+ feedId: clientFeedId,
190
+ feedNamespace: syncNamespace,
191
+ data: new Uint8Array([9, 8, 7]),
192
+ },
193
+ ])
194
+ .pipe(RuntimeProvider.runPromise(clientRuntime.runtimeEffect));
195
+
196
+ await vi.waitFor(async () => {
197
+ const { blocks } = await serverFeedStore
198
+ .query({
199
+ spaceId,
200
+ feedNamespace: syncNamespace,
201
+ position: -1,
202
+ query: { feedIds: [clientFeedId] },
203
+ })
204
+ .pipe(RuntimeProvider.runPromise(serverRuntime.runtimeEffect));
205
+
206
+ expect(blocks).toHaveLength(1);
207
+ expect(blocks[0].data).toEqual(new Uint8Array([9, 8, 7]));
208
+ expect(blocks[0].position).toBeDefined();
209
+ });
210
+ });
211
+
212
+ test('requestPoll triggers best-effort pull for a space', async () => {
213
+ const spaceId = SpaceId.random();
214
+ const { serverRuntime, clientRuntime, serverFeedStore, clientFeedStore, syncer } = await createFeedSyncHarness({
215
+ spaceId,
216
+ pollingInterval: 60_000,
217
+ });
218
+ const serverFeedId = ObjectId.random();
219
+
220
+ await serverFeedStore
221
+ .appendLocal([
222
+ {
223
+ spaceId,
224
+ feedId: serverFeedId,
225
+ feedNamespace: syncNamespace,
226
+ data: new Uint8Array([1, 2, 3]),
227
+ },
228
+ ])
229
+ .pipe(RuntimeProvider.runPromise(serverRuntime.runtimeEffect));
230
+
231
+ await syncer.open(new Context());
232
+
233
+ await vi.waitFor(async () => {
234
+ const { blocks } = await clientFeedStore
235
+ .query({
236
+ spaceId,
237
+ feedNamespace: syncNamespace,
238
+ position: -1,
239
+ query: { feedIds: [serverFeedId] },
240
+ })
241
+ .pipe(RuntimeProvider.runPromise(clientRuntime.runtimeEffect));
242
+
243
+ expect(blocks).toHaveLength(1);
244
+ expect(blocks[0].data).toEqual(new Uint8Array([1, 2, 3]));
245
+ });
246
+
247
+ await serverFeedStore
248
+ .appendLocal([
249
+ {
250
+ spaceId,
251
+ feedId: serverFeedId,
252
+ feedNamespace: syncNamespace,
253
+ data: new Uint8Array([4, 5, 6]),
254
+ },
255
+ ])
256
+ .pipe(RuntimeProvider.runPromise(serverRuntime.runtimeEffect));
257
+
258
+ await new Promise((resolve) => setTimeout(resolve, 250));
259
+ {
260
+ const { blocks } = await clientFeedStore
261
+ .query({
262
+ spaceId,
263
+ feedNamespace: syncNamespace,
264
+ position: -1,
265
+ query: { feedIds: [serverFeedId] },
266
+ })
267
+ .pipe(RuntimeProvider.runPromise(clientRuntime.runtimeEffect));
268
+ expect(blocks).toHaveLength(1);
269
+ }
270
+
271
+ syncer.schedulePoll();
272
+
273
+ await vi.waitFor(async () => {
274
+ const { blocks } = await clientFeedStore
275
+ .query({
276
+ spaceId,
277
+ feedNamespace: syncNamespace,
278
+ position: -1,
279
+ query: { feedIds: [serverFeedId] },
280
+ })
281
+ .pipe(RuntimeProvider.runPromise(clientRuntime.runtimeEffect));
282
+
283
+ expect(blocks).toHaveLength(2);
284
+ expect(blocks[1].data).toEqual(new Uint8Array([4, 5, 6]));
285
+ });
286
+ });
287
+
288
+ test('syncs all configured namespaces', async () => {
289
+ const spaceId = SpaceId.random();
290
+ const { serverRuntime, clientRuntime, serverFeedStore, clientFeedStore, syncer } = await createFeedSyncHarness({
291
+ spaceId,
292
+ syncNamespaces,
293
+ });
294
+ const serverDataFeedId = ObjectId.random();
295
+ const serverTraceFeedId = ObjectId.random();
296
+
297
+ await serverFeedStore
298
+ .appendLocal([
299
+ {
300
+ spaceId,
301
+ feedId: serverDataFeedId,
302
+ feedNamespace: FeedProtocol.WellKnownNamespaces.data,
303
+ data: new Uint8Array([1, 2, 3]),
304
+ },
305
+ {
306
+ spaceId,
307
+ feedId: serverTraceFeedId,
308
+ feedNamespace: FeedProtocol.WellKnownNamespaces.trace,
309
+ data: new Uint8Array([7, 8, 9]),
310
+ },
311
+ ])
312
+ .pipe(RuntimeProvider.runPromise(serverRuntime.runtimeEffect));
313
+
314
+ await syncer.open(new Context());
315
+
316
+ await vi.waitFor(async () => {
317
+ const dataResult = await clientFeedStore
318
+ .query({
319
+ spaceId,
320
+ feedNamespace: FeedProtocol.WellKnownNamespaces.data,
321
+ position: -1,
322
+ query: { feedIds: [serverDataFeedId] },
323
+ })
324
+ .pipe(RuntimeProvider.runPromise(clientRuntime.runtimeEffect));
325
+ const traceResult = await clientFeedStore
326
+ .query({
327
+ spaceId,
328
+ feedNamespace: FeedProtocol.WellKnownNamespaces.trace,
329
+ position: -1,
330
+ query: { feedIds: [serverTraceFeedId] },
331
+ })
332
+ .pipe(RuntimeProvider.runPromise(clientRuntime.runtimeEffect));
333
+
334
+ expect(dataResult.blocks).toHaveLength(1);
335
+ expect(traceResult.blocks).toHaveLength(1);
336
+ expect(dataResult.blocks[0].data).toEqual(new Uint8Array([1, 2, 3]));
337
+ expect(traceResult.blocks[0].data).toEqual(new Uint8Array([7, 8, 9]));
338
+ });
339
+ });
340
+ });
@@ -0,0 +1,337 @@
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, 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 { FeedProtocol } from '@dxos/protocols';
18
+ import { EdgeService } from '@dxos/protocols';
19
+ import { createBuf } from '@dxos/protocols/buf';
20
+ import { type Message as RouterMessage } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
21
+ import type { SqlTransaction } from '@dxos/sql-sqlite';
22
+ import { bufferToArray } from '@dxos/util';
23
+
24
+ const encoder = new Encoder({ tagUint8Array: false, useRecords: false });
25
+
26
+ const DEFAULT_MESSAGE_BLOCKS_LIMIT = 50;
27
+ const DEFAULT_SYNC_CONCURRENCY = 5;
28
+ const DEFAULT_POLLING_INTERVAL = 5_000;
29
+ const DEFAULT_POLL_REQUEST_THROTTLE_MS = 250;
30
+ const MAX_BLOCKING_SYNC_ITERATIONS = 100;
31
+
32
+ interface FeedSyncerOptions {
33
+ runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
34
+ feedStore: FeedStore;
35
+ edgeClient: EdgeConnection;
36
+ peerId: string;
37
+ getSpaceIds: () => SpaceId[];
38
+
39
+ /**
40
+ * Namespaces to sync.
41
+ */
42
+ syncNamespaces: string[];
43
+
44
+ /**
45
+ * Maximum number of blocks to sync in a single message.
46
+ * @default 50
47
+ */
48
+ messageBlocksLimit?: number;
49
+
50
+ /**
51
+ * Maximum number of spaces to sync concurrently.
52
+ * @default 5
53
+ */
54
+ syncConcurrency?: number;
55
+
56
+ /**
57
+ * Interval between full polls.
58
+ * @default 10 seconds
59
+ */
60
+ pollingInterval?: number;
61
+
62
+ /**
63
+ * Minimum delay between externally requested best-effort polls.
64
+ * @default 250 ms
65
+ */
66
+ pollRequestThrottleMs?: number;
67
+ }
68
+
69
+ export class FeedSyncer extends Resource {
70
+ readonly #syncNamespaces: string[];
71
+ readonly #messageBlocksLimit: number;
72
+ readonly #syncConcurrency: number;
73
+ readonly #pollingInterval: number;
74
+ readonly #pollRequestThrottleMs: number;
75
+
76
+ readonly #runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
77
+ readonly #feedStore: FeedStore;
78
+ readonly #edgeClient: EdgeConnection;
79
+ readonly #syncClient: SyncClient;
80
+ readonly #getSpaceIds: () => SpaceId[];
81
+
82
+ #spacesToPoll = new Set<SpaceId>();
83
+ /** Last time full poll was completed. */
84
+ #lastFullPoll: number | null = null;
85
+ #throttledPollScheduled = false;
86
+ #lastRequestedPollAt: number | null = null;
87
+
88
+ constructor(options: FeedSyncerOptions) {
89
+ super();
90
+ this.#runtime = options.runtime;
91
+ this.#feedStore = options.feedStore;
92
+ this.#edgeClient = options.edgeClient;
93
+ this.#syncClient = new SyncClient({
94
+ peerId: options.peerId,
95
+ feedStore: options.feedStore,
96
+ sendMessage: this.#sendMessage.bind(this),
97
+ });
98
+ this.#getSpaceIds = options.getSpaceIds;
99
+ this.#syncNamespaces = options.syncNamespaces;
100
+ this.#messageBlocksLimit = options.messageBlocksLimit ?? DEFAULT_MESSAGE_BLOCKS_LIMIT;
101
+ this.#syncConcurrency = options.syncConcurrency ?? DEFAULT_SYNC_CONCURRENCY;
102
+ this.#pollingInterval = options.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
103
+ this.#pollRequestThrottleMs = options.pollRequestThrottleMs ?? DEFAULT_POLL_REQUEST_THROTTLE_MS;
104
+ }
105
+
106
+ protected override async _open(): Promise<void> {
107
+ this._ctx.onDispose(
108
+ this.#edgeClient.onMessage((msg: RouterMessage) => {
109
+ if (!msg.serviceId) {
110
+ return;
111
+ }
112
+ const service = msg.serviceId.split(':')[0];
113
+ if (service !== EdgeService.QUEUE_REPLICATOR) {
114
+ return;
115
+ }
116
+ const handleMessageEffect = Effect.gen(this, function* () {
117
+ const decoded = yield* Effect.try({
118
+ try: () => cborXdecode(msg.payload!.value),
119
+ catch: (error) => new Error(`Failed to decode feed sync message: ${error}`),
120
+ });
121
+ const payload = yield* Schema.validate(FeedProtocol.ProtocolMessage)(decoded);
122
+ yield* this.#syncClient.handleMessage(payload);
123
+ });
124
+
125
+ void RuntimeProvider.runPromise(this.#runtime)(handleMessageEffect);
126
+ }),
127
+ );
128
+
129
+ this._ctx.onDispose(
130
+ // NOTE: This will fire immediately if the connection is already open.
131
+ this.#edgeClient.onReconnected(async () => {}),
132
+ );
133
+
134
+ this.#feedStore.onNewBlocks.on(this._ctx, () => {
135
+ this.#pushTask.schedule();
136
+ });
137
+
138
+ await this.#pollTask.open();
139
+ await this.#pushTask.open();
140
+
141
+ this.#resetSpacesToPoll();
142
+ this.#pollTask.schedule();
143
+ }
144
+
145
+ protected override async _close(): Promise<void> {
146
+ await this.#pollTask.close();
147
+ await this.#pushTask.close();
148
+ }
149
+
150
+ /**
151
+ * Schedules a best-effort pull without blocking the caller.
152
+ */
153
+ schedulePoll(): void {
154
+ this.#resetSpacesToPoll();
155
+ if (this.#throttledPollScheduled) {
156
+ return;
157
+ }
158
+
159
+ const now = Date.now();
160
+ const delay =
161
+ this.#lastRequestedPollAt == null
162
+ ? 0
163
+ : Math.max(this.#pollRequestThrottleMs - (now - this.#lastRequestedPollAt), 0);
164
+ this.#throttledPollScheduled = true;
165
+ scheduleTask(
166
+ this._ctx,
167
+ () => {
168
+ this.#throttledPollScheduled = false;
169
+ this.#lastRequestedPollAt = Date.now();
170
+ this.#pollTask.schedule();
171
+ },
172
+ delay,
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Performs queue sync and blocks until there are no pending sync batches.
178
+ */
179
+ async syncBlocking(
180
+ ctx: Context,
181
+ {
182
+ spaceId,
183
+ subspaceTag,
184
+ shouldPush = true,
185
+ shouldPull = true,
186
+ }: {
187
+ spaceId: SpaceId;
188
+ subspaceTag: string;
189
+ shouldPush?: boolean;
190
+ shouldPull?: boolean;
191
+ },
192
+ ): Promise<void> {
193
+ invariant(SpaceId.isValid(spaceId));
194
+ invariant(FeedProtocol.isWellKnownNamespace(subspaceTag));
195
+ if (!shouldPush && !shouldPull) {
196
+ return;
197
+ }
198
+
199
+ await RuntimeProvider.runPromise(this.#runtime)(
200
+ Effect.gen(this, function* () {
201
+ let done = false;
202
+ let iterations = 0;
203
+ while (!done) {
204
+ done = true;
205
+ if (shouldPull) {
206
+ const pullResult = yield* this.#syncClient.pull(ctx, {
207
+ spaceId,
208
+ feedNamespace: subspaceTag,
209
+ limit: this.#messageBlocksLimit,
210
+ });
211
+ done &&= pullResult.done;
212
+ }
213
+
214
+ if (shouldPush) {
215
+ const pushResult = yield* this.#syncClient.push(ctx, {
216
+ spaceId,
217
+ feedNamespace: subspaceTag,
218
+ limit: this.#messageBlocksLimit,
219
+ });
220
+ done &&= pushResult.done;
221
+ }
222
+ iterations++;
223
+ if (iterations > MAX_BLOCKING_SYNC_ITERATIONS) {
224
+ throw new Error('Blocking sync exceeded max iterations.');
225
+ }
226
+ }
227
+ }),
228
+ );
229
+ }
230
+
231
+ #resetSpacesToPoll(): void {
232
+ this.#spacesToPoll.clear();
233
+ this.#getSpaceIds().forEach((spaceId) => {
234
+ this.#spacesToPoll.add(spaceId);
235
+ });
236
+ this.#lastFullPoll = Date.now();
237
+ }
238
+
239
+ #sendMessage(
240
+ ctx: Context,
241
+ message: FeedProtocol.QueryRequest | FeedProtocol.AppendRequest,
242
+ ): Effect.Effect<void, unknown, never> {
243
+ return Effect.gen(this, function* () {
244
+ const encoded = encoder.encode(message);
245
+ yield* Effect.tryPromise(async () =>
246
+ this.#edgeClient.send(
247
+ ctx,
248
+ createBuf(MessageSchema, {
249
+ source: {
250
+ identityKey: this.#edgeClient.identityKey,
251
+ peerKey: this.#edgeClient.peerKey,
252
+ },
253
+ serviceId: this.#getTargetServiceId(message),
254
+ payload: { value: bufferToArray(encoded) },
255
+ }),
256
+ ),
257
+ );
258
+ });
259
+ }
260
+
261
+ #getTargetServiceId(message: FeedProtocol.QueryRequest | FeedProtocol.AppendRequest): string {
262
+ // TODO(dmaretskyi): Perhaps in the future we will want to include the queue namespace here as well.
263
+ // This would require putting it at the top level of the message.
264
+ // For now, we let the edge router handle it.
265
+ return FeedProtocol.encodeServiceId(message.feedNamespace, message.spaceId as SpaceId);
266
+ }
267
+
268
+ readonly #pollTask = new AsyncTask(async () =>
269
+ Effect.gen(this, function* () {
270
+ yield* Effect.forEach(
271
+ this.#spacesToPoll,
272
+ (spaceId) =>
273
+ Effect.gen(this, function* () {
274
+ let doneForAllNamespaces = true;
275
+ for (const feedNamespace of this.#syncNamespaces) {
276
+ const { done } = yield* this.#syncClient.pull(this._ctx, {
277
+ spaceId,
278
+ feedNamespace,
279
+ limit: this.#messageBlocksLimit,
280
+ });
281
+ if (!done) {
282
+ doneForAllNamespaces = false;
283
+ }
284
+ }
285
+ if (doneForAllNamespaces) {
286
+ this.#spacesToPoll.delete(spaceId);
287
+ }
288
+ }),
289
+ { concurrency: this.#syncConcurrency },
290
+ );
291
+
292
+ // If its time to do a full poll, reset the spaces to poll and schedule the next poll immediately.
293
+ if (this.#lastFullPoll == null || Date.now() - this.#lastFullPoll > this.#pollingInterval) {
294
+ this.#resetSpacesToPoll();
295
+ this.#pollTask.schedule();
296
+ } else if (this.#spacesToPoll.size > 0) {
297
+ // If there are some spaces still syncing, poll them immediately.
298
+ this.#pollTask.schedule();
299
+ } else {
300
+ // All spaces sync, and there's time before the next full poll, schedule it later.
301
+ this.#resetSpacesToPoll();
302
+ scheduleTask(
303
+ this._ctx,
304
+ () => this.#pollTask.schedule(),
305
+ Math.max(this.#pollingInterval - (Date.now() - (this.#lastFullPoll ?? 0)), 0),
306
+ );
307
+ }
308
+ }).pipe(RuntimeProvider.runPromise(this.#runtime)),
309
+ );
310
+
311
+ readonly #pushTask = new AsyncTask(async () =>
312
+ Effect.gen(this, function* () {
313
+ yield* Effect.forEach(
314
+ this.#getSpaceIds(),
315
+ (spaceId) =>
316
+ Effect.gen(this, function* () {
317
+ let doneForAllNamespaces = true;
318
+ for (const feedNamespace of this.#syncNamespaces) {
319
+ const { done } = yield* this.#syncClient.push(this._ctx, {
320
+ spaceId,
321
+ feedNamespace,
322
+ limit: this.#messageBlocksLimit,
323
+ });
324
+ if (!done) {
325
+ doneForAllNamespaces = false;
326
+ }
327
+ }
328
+ if (!doneForAllNamespaces) {
329
+ // Keep pushing until all blocks are pushed.
330
+ this.#pushTask.schedule();
331
+ }
332
+ }),
333
+ { concurrency: this.#syncConcurrency },
334
+ );
335
+ }).pipe(RuntimeProvider.runPromise(this.#runtime)),
336
+ );
337
+ }
@@ -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.