@dxos/client-services 0.10.0 → 0.11.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.
- package/dist/lib/chunk-client-services-layer.mjs +11425 -0
- package/dist/lib/chunk-client-services-layer.mjs.map +1 -0
- package/dist/lib/chunk-decorate.mjs +9 -0
- package/dist/lib/index.mjs +731 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/packlets/diagnostics/browser-diagnostics-broadcast.mjs +72 -0
- package/dist/lib/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +1 -0
- package/dist/lib/packlets/diagnostics/diagnostics-broadcast.mjs +14 -0
- package/dist/lib/packlets/diagnostics/diagnostics-broadcast.mjs.map +1 -0
- package/dist/lib/packlets/locks/browser.mjs +105 -0
- package/dist/lib/packlets/locks/browser.mjs.map +1 -0
- package/dist/lib/packlets/locks/node.mjs +53 -0
- package/dist/lib/packlets/locks/node.mjs.map +1 -0
- package/dist/lib/testing.mjs +570 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +21 -4
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts +8 -7
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -1
- package/dist/types/src/packlets/devices/devices-service.d.ts +7 -5
- package/dist/types/src/packlets/devices/devices-service.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/devtools.d.ts +30 -29
- package/dist/types/src/packlets/devtools/devtools.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/feeds.d.ts +1 -1
- package/dist/types/src/packlets/devtools/feeds.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/network.d.ts +2 -1
- package/dist/types/src/packlets/devtools/network.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/spaces.d.ts.map +1 -1
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts +2 -2
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
- package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -1
- package/dist/types/src/packlets/diagnostics/diagnostics.d.ts +1 -8
- package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts +9 -6
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-manager.d.ts +32 -3
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +23 -4
- package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-service.d.ts +15 -20
- package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +2 -2
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +17 -0
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-manager.d.ts +25 -4
- package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-service.d.ts +11 -11
- package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/logging/logging-service.d.ts +14 -12
- package/dist/types/src/packlets/logging/logging-service.d.ts.map +1 -1
- package/dist/types/src/packlets/metadata/index.d.ts +3 -0
- package/dist/types/src/packlets/metadata/index.d.ts.map +1 -0
- package/dist/types/src/packlets/metadata/metadata-store.d.ts +107 -0
- package/dist/types/src/packlets/metadata/metadata-store.d.ts.map +1 -0
- package/dist/types/src/packlets/metadata/metadata-store.test.d.ts +2 -0
- package/dist/types/src/packlets/metadata/metadata-store.test.d.ts.map +1 -0
- package/dist/types/src/packlets/metadata/sqlite-metadata-store.d.ts +65 -0
- package/dist/types/src/packlets/metadata/sqlite-metadata-store.d.ts.map +1 -0
- package/dist/types/src/packlets/network/network-service.d.ts +15 -14
- package/dist/types/src/packlets/network/network-service.d.ts.map +1 -1
- package/dist/types/src/packlets/pipeline/codec.d.ts +11 -0
- package/dist/types/src/packlets/pipeline/codec.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/feeds.d.ts +7 -0
- package/dist/types/src/packlets/pipeline/feeds.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/index.d.ts +5 -0
- package/dist/types/src/packlets/pipeline/index.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/message-selector.d.ts +11 -0
- package/dist/types/src/packlets/pipeline/message-selector.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/pipeline-stress.test.d.ts +2 -0
- package/dist/types/src/packlets/pipeline/pipeline-stress.test.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/pipeline.d.ts +131 -0
- package/dist/types/src/packlets/pipeline/pipeline.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/pipeline.test.d.ts +2 -0
- package/dist/types/src/packlets/pipeline/pipeline.test.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/testing/index.d.ts +2 -0
- package/dist/types/src/packlets/pipeline/testing/index.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/testing/test-feed-builder.d.ts +9 -0
- package/dist/types/src/packlets/pipeline/testing/test-feed-builder.d.ts.map +1 -0
- package/dist/types/src/packlets/pipeline/timeframe-clock.d.ts +31 -0
- package/dist/types/src/packlets/pipeline/timeframe-clock.d.ts.map +1 -0
- package/dist/types/src/packlets/services/client-services-layer.d.ts +21 -0
- package/dist/types/src/packlets/services/client-services-layer.d.ts.map +1 -0
- package/dist/types/src/packlets/services/cross-device-space-synchronizer.d.ts +28 -0
- package/dist/types/src/packlets/services/cross-device-space-synchronizer.d.ts.map +1 -0
- package/dist/types/src/packlets/services/effect-rpc.test.d.ts +2 -0
- package/dist/types/src/packlets/services/effect-rpc.test.d.ts.map +1 -0
- package/dist/types/src/packlets/services/feed-syncer.d.ts +22 -2
- package/dist/types/src/packlets/services/feed-syncer.d.ts.map +1 -1
- package/dist/types/src/packlets/services/index.d.ts +2 -2
- package/dist/types/src/packlets/services/index.d.ts.map +1 -1
- package/dist/types/src/packlets/services/platform.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +38 -57
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +82 -31
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/services/sqlite-storage.d.ts +29 -0
- package/dist/types/src/packlets/services/sqlite-storage.d.ts.map +1 -1
- package/dist/types/src/packlets/services/sqlite-storage.test.d.ts +2 -0
- package/dist/types/src/packlets/services/sqlite-storage.test.d.ts.map +1 -0
- package/dist/types/src/packlets/space/admission-discovery-extension.d.ts +30 -0
- package/dist/types/src/packlets/space/admission-discovery-extension.d.ts.map +1 -0
- package/dist/types/src/packlets/space/auth.d.ts +24 -0
- package/dist/types/src/packlets/space/auth.d.ts.map +1 -0
- package/dist/types/src/packlets/space/control-pipeline.d.ts +46 -0
- package/dist/types/src/packlets/space/control-pipeline.d.ts.map +1 -0
- package/dist/types/src/packlets/space/control-pipeline.test.d.ts +2 -0
- package/dist/types/src/packlets/space/control-pipeline.test.d.ts.map +1 -0
- package/dist/types/src/packlets/space/index.d.ts +6 -0
- package/dist/types/src/packlets/space/index.d.ts.map +1 -0
- package/dist/types/src/packlets/space/replication.browser.test.d.ts +2 -0
- package/dist/types/src/packlets/space/replication.browser.test.d.ts.map +1 -0
- package/dist/types/src/packlets/space/space-manager.browser.test.d.ts +2 -0
- package/dist/types/src/packlets/space/space-manager.browser.test.d.ts.map +1 -0
- package/dist/types/src/packlets/space/space-manager.d.ts +72 -0
- package/dist/types/src/packlets/space/space-manager.d.ts.map +1 -0
- package/dist/types/src/packlets/space/space-protocol.browser.test.d.ts +2 -0
- package/dist/types/src/packlets/space/space-protocol.browser.test.d.ts.map +1 -0
- package/dist/types/src/packlets/space/space-protocol.d.ts +99 -0
- package/dist/types/src/packlets/space/space-protocol.d.ts.map +1 -0
- package/dist/types/src/packlets/space/space-protocol.test.d.ts +2 -0
- package/dist/types/src/packlets/space/space-protocol.test.d.ts.map +1 -0
- package/dist/types/src/packlets/space/space.d.ts +65 -0
- package/dist/types/src/packlets/space/space.d.ts.map +1 -0
- package/dist/types/src/packlets/space/space.test.d.ts +2 -0
- package/dist/types/src/packlets/space/space.test.d.ts.map +1 -0
- package/dist/types/src/packlets/space/testing/change-metadata.d.ts +8 -0
- package/dist/types/src/packlets/space/testing/change-metadata.d.ts.map +1 -0
- package/dist/types/src/packlets/space/testing/index.d.ts +4 -0
- package/dist/types/src/packlets/space/testing/index.d.ts.map +1 -0
- package/dist/types/src/packlets/space/testing/test-agent-builder.d.ts +62 -0
- package/dist/types/src/packlets/space/testing/test-agent-builder.d.ts.map +1 -0
- package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts +1 -1
- package/dist/types/src/packlets/space-export/serialized-space-reader.d.ts.map +1 -1
- package/dist/types/src/packlets/space-export/serialized-space-writer.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +40 -7
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +3 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/genesis.d.ts +1 -1
- package/dist/types/src/packlets/spaces/genesis.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +25 -23
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/level.d.ts.map +1 -1
- package/dist/types/src/packlets/system/system-service.d.ts +18 -18
- package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +7 -6
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-runtime.d.ts +41 -58
- package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-session.d.ts +21 -8
- package/dist/types/src/packlets/worker/worker-session.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +52 -48
- package/src/index.ts +2 -0
- package/src/packlets/agents/edge-agent-manager.ts +56 -15
- package/src/packlets/agents/edge-agent-service.ts +25 -15
- package/src/packlets/devices/devices-service.test.ts +20 -19
- package/src/packlets/devices/devices-service.ts +17 -17
- package/src/packlets/devtools/devtools.ts +133 -75
- package/src/packlets/devtools/feeds.ts +2 -1
- package/src/packlets/devtools/network.ts +42 -12
- package/src/packlets/devtools/spaces.ts +1 -1
- package/src/packlets/diagnostics/diagnostics-broadcast.ts +4 -2
- package/src/packlets/diagnostics/diagnostics-collector.ts +5 -18
- package/src/packlets/diagnostics/diagnostics.ts +0 -11
- package/src/packlets/identity/authenticator.ts +2 -1
- package/src/packlets/identity/contacts-service.ts +46 -37
- package/src/packlets/identity/identity-manager.test.ts +3 -1
- package/src/packlets/identity/identity-manager.ts +73 -5
- package/src/packlets/identity/identity-recovery-manager.ts +56 -7
- package/src/packlets/identity/identity-service.test.ts +27 -18
- package/src/packlets/identity/identity-service.ts +114 -71
- package/src/packlets/identity/identity.test.ts +4 -9
- package/src/packlets/identity/identity.ts +1 -2
- package/src/packlets/invitations/edge-invitation-handler.ts +0 -1
- package/src/packlets/invitations/invitations-handler.ts +39 -3
- package/src/packlets/invitations/invitations-manager.ts +52 -4
- package/src/packlets/invitations/invitations-service.ts +54 -28
- package/src/packlets/logging/logging-service.ts +41 -29
- package/src/packlets/logging/logging.test.ts +28 -15
- package/src/packlets/metadata/index.ts +6 -0
- package/src/packlets/metadata/metadata-store.test.ts +49 -0
- package/src/packlets/metadata/metadata-store.ts +446 -0
- package/src/packlets/metadata/sqlite-metadata-store.ts +409 -0
- package/src/packlets/network/network-service.test.ts +19 -12
- package/src/packlets/network/network-service.ts +96 -29
- package/src/packlets/pipeline/codec.ts +18 -0
- package/src/packlets/pipeline/feeds.ts +22 -0
- package/src/packlets/pipeline/index.ts +8 -0
- package/src/packlets/pipeline/message-selector.ts +35 -0
- package/src/packlets/pipeline/pipeline-stress.test.ts +230 -0
- package/src/packlets/pipeline/pipeline.test.ts +140 -0
- package/src/packlets/pipeline/pipeline.ts +439 -0
- package/src/packlets/pipeline/testing/index.ts +5 -0
- package/src/packlets/pipeline/testing/test-feed-builder.ts +19 -0
- package/src/packlets/pipeline/timeframe-clock.ts +81 -0
- package/src/packlets/services/client-services-layer.ts +186 -0
- package/src/packlets/services/cross-device-space-synchronizer.ts +132 -0
- package/src/packlets/services/effect-rpc.test.ts +314 -0
- package/src/packlets/services/feed-syncer.ts +57 -3
- package/src/packlets/services/index.ts +2 -2
- package/src/packlets/services/platform.ts +0 -6
- package/src/packlets/services/service-context.ts +219 -522
- package/src/packlets/services/service-host.test.ts +34 -11
- package/src/packlets/services/service-host.ts +693 -213
- package/src/packlets/services/spec.md +106 -0
- package/src/packlets/services/sqlite-storage.test.ts +48 -0
- package/src/packlets/services/sqlite-storage.ts +117 -20
- package/src/packlets/space/admission-discovery-extension.ts +91 -0
- package/src/packlets/space/auth.ts +92 -0
- package/src/packlets/space/control-pipeline.test.ts +165 -0
- package/src/packlets/space/control-pipeline.ts +229 -0
- package/src/packlets/space/index.ts +9 -0
- package/src/packlets/space/replication.browser.test.ts +58 -0
- package/src/packlets/space/space-manager.browser.test.ts +40 -0
- package/src/packlets/space/space-manager.ts +224 -0
- package/src/packlets/space/space-protocol.browser.test.ts +129 -0
- package/src/packlets/space/space-protocol.test.ts +158 -0
- package/src/packlets/space/space-protocol.ts +308 -0
- package/src/packlets/space/space.test.ts +181 -0
- package/src/packlets/space/space.ts +196 -0
- package/src/packlets/space/testing/change-metadata.ts +27 -0
- package/src/packlets/space/testing/index.ts +7 -0
- package/src/packlets/space/testing/test-agent-builder.ts +252 -0
- package/src/packlets/space-export/serialized-space-reader.ts +3 -1
- package/src/packlets/space-export/serialized-space-writer.ts +53 -30
- package/src/packlets/space-export/space-archive.test.ts +30 -0
- package/src/packlets/spaces/data-space-manager.test.ts +1 -1
- package/src/packlets/spaces/data-space-manager.ts +148 -33
- package/src/packlets/spaces/data-space.ts +29 -18
- package/src/packlets/spaces/edge-feed-replicator.test.ts +1 -1
- package/src/packlets/spaces/genesis.ts +1 -1
- package/src/packlets/spaces/spaces-service.test.ts +35 -22
- package/src/packlets/spaces/spaces-service.ts +259 -180
- package/src/packlets/storage/level.ts +3 -1
- package/src/packlets/system/system-service.test.ts +31 -13
- package/src/packlets/system/system-service.ts +65 -37
- package/src/packlets/testing/invitation-utils.ts +3 -3
- package/src/packlets/testing/test-builder.ts +35 -20
- package/src/packlets/worker/worker-runtime.ts +222 -260
- package/src/packlets/worker/worker-session.ts +108 -91
- package/src/testing/index.ts +1 -0
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-QCWEHHJW.mjs +0 -24
- package/dist/lib/browser/chunk-QCWEHHJW.mjs.map +0 -7
- package/dist/lib/browser/chunk-XJRPB3GA.mjs +0 -22
- package/dist/lib/browser/chunk-XJRPB3GA.mjs.map +0 -7
- package/dist/lib/browser/chunk-XU5R2BMF.mjs +0 -8837
- package/dist/lib/browser/chunk-XU5R2BMF.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -876
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs +0 -88
- package/dist/lib/browser/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +0 -7
- package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs +0 -11
- package/dist/lib/browser/packlets/diagnostics/diagnostics-broadcast.mjs.map +0 -7
- package/dist/lib/browser/packlets/locks/browser.mjs +0 -86
- package/dist/lib/browser/packlets/locks/browser.mjs.map +0 -7
- package/dist/lib/browser/packlets/locks/node.mjs +0 -48
- package/dist/lib/browser/packlets/locks/node.mjs.map +0 -7
- package/dist/lib/browser/testing/index.mjs +0 -436
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-2DT3MZRL.mjs +0 -22
- package/dist/lib/node-esm/chunk-2DT3MZRL.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-2SZHAWBN.mjs +0 -24
- package/dist/lib/node-esm/chunk-2SZHAWBN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-H5YTUD35.mjs +0 -8837
- package/dist/lib/node-esm/chunk-H5YTUD35.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -876
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs +0 -88
- package/dist/lib/node-esm/packlets/diagnostics/browser-diagnostics-broadcast.mjs.map +0 -7
- package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs +0 -11
- package/dist/lib/node-esm/packlets/diagnostics/diagnostics-broadcast.mjs.map +0 -7
- package/dist/lib/node-esm/packlets/locks/browser.mjs +0 -86
- package/dist/lib/node-esm/packlets/locks/browser.mjs.map +0 -7
- package/dist/lib/node-esm/packlets/locks/node.mjs +0 -48
- package/dist/lib/node-esm/packlets/locks/node.mjs.map +0 -7
- package/dist/lib/node-esm/testing/index.mjs +0 -436
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/packlets/services/client-rpc-server.d.ts +0 -24
- package/dist/types/src/packlets/services/client-rpc-server.d.ts.map +0 -1
- package/dist/types/src/packlets/services/service-registry.d.ts +0 -15
- package/dist/types/src/packlets/services/service-registry.d.ts.map +0 -1
- package/dist/types/src/packlets/services/service-registry.test.d.ts +0 -2
- package/dist/types/src/packlets/services/service-registry.test.d.ts.map +0 -1
- package/dist/types/src/testing/setup.d.ts +0 -3
- package/dist/types/src/testing/setup.d.ts.map +0 -1
- package/src/packlets/services/client-rpc-server.ts +0 -111
- package/src/packlets/services/service-registry.test.ts +0 -78
- package/src/packlets/services/service-registry.ts +0 -36
- package/src/testing/setup.ts +0 -11
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as SqlClient from '@effect/sql/SqlClient';
|
|
6
|
+
import type * as SqlError from '@effect/sql/SqlError';
|
|
7
|
+
import CRC32 from 'crc-32';
|
|
8
|
+
import * as Effect from 'effect/Effect';
|
|
9
|
+
import * as Layer from 'effect/Layer';
|
|
10
|
+
|
|
11
|
+
import { Event, scheduleTaskInterval, synchronized } from '@dxos/async';
|
|
12
|
+
import { Context } from '@dxos/context';
|
|
13
|
+
import { RuntimeProvider } from '@dxos/effect';
|
|
14
|
+
import { invariant } from '@dxos/invariant';
|
|
15
|
+
import { PublicKey } from '@dxos/keys';
|
|
16
|
+
import { log } from '@dxos/log';
|
|
17
|
+
import { DataCorruptionError, STORAGE_VERSION } from '@dxos/protocols';
|
|
18
|
+
import { schema } from '@dxos/protocols/proto';
|
|
19
|
+
import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
20
|
+
import {
|
|
21
|
+
type ControlPipelineSnapshot,
|
|
22
|
+
type EchoMetadata,
|
|
23
|
+
type EdgeReplicationSetting,
|
|
24
|
+
type IdentityRecord,
|
|
25
|
+
type LargeSpaceMetadata,
|
|
26
|
+
type SpaceCache,
|
|
27
|
+
type SpaceMetadata,
|
|
28
|
+
} from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
29
|
+
import { SqlTransaction } from '@dxos/sql-sqlite';
|
|
30
|
+
import { type Timeframe } from '@dxos/timeframe';
|
|
31
|
+
import { ComplexMap, arrayToBuffer, forEachAsync, isNonNullable } from '@dxos/util';
|
|
32
|
+
|
|
33
|
+
import { type IMetadataStore, IMetadataStoreService, hasInvitationExpired } from './metadata-store';
|
|
34
|
+
|
|
35
|
+
// SqlTransaction.SqlTransaction is the Tag class exported from the SqlTransaction namespace.
|
|
36
|
+
type SqlTransactionTag = SqlTransaction.SqlTransaction;
|
|
37
|
+
|
|
38
|
+
const EXPIRED_INVITATION_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
39
|
+
|
|
40
|
+
const emptyEchoMetadata = (): EchoMetadata => ({
|
|
41
|
+
version: STORAGE_VERSION,
|
|
42
|
+
spaces: [],
|
|
43
|
+
created: new Date(),
|
|
44
|
+
updated: new Date(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const emptyLargeSpaceMetadata = (): LargeSpaceMetadata => ({});
|
|
48
|
+
|
|
49
|
+
const EchoMetadataCodec = schema.getCodecForType('dxos.echo.metadata.EchoMetadata');
|
|
50
|
+
const LargeSpaceMetadataCodec = schema.getCodecForType('dxos.echo.metadata.LargeSpaceMetadata');
|
|
51
|
+
|
|
52
|
+
const MAIN_KEY = 'main';
|
|
53
|
+
const largeKey = (spaceKey: PublicKey) => `large:${spaceKey.toHex()}`;
|
|
54
|
+
|
|
55
|
+
// Legacy invitation type detection.
|
|
56
|
+
const isLegacyInvitationFormat = (invitation: Invitation): boolean => invitation.type === Invitation.Type.MULTIUSE;
|
|
57
|
+
|
|
58
|
+
export type SqliteMetadataStoreProps = {
|
|
59
|
+
runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransactionTag>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* SQLite-backed replacement for MetadataStore.
|
|
64
|
+
* Stores EchoMetadata as a single protobuf blob and LargeSpaceMetadata as per-space blobs.
|
|
65
|
+
*/
|
|
66
|
+
export class SqliteMetadataStore implements IMetadataStore {
|
|
67
|
+
readonly #runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransactionTag>;
|
|
68
|
+
|
|
69
|
+
#metadata: EchoMetadata = emptyEchoMetadata();
|
|
70
|
+
readonly #spaceLargeMetadata = new ComplexMap<PublicKey, LargeSpaceMetadata>(PublicKey.hash);
|
|
71
|
+
|
|
72
|
+
readonly update = new Event<EchoMetadata>();
|
|
73
|
+
readonly #invitationCleanupCtx = new Context();
|
|
74
|
+
|
|
75
|
+
constructor({ runtime }: SqliteMetadataStoreProps) {
|
|
76
|
+
this.#runtime = runtime;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates the space_metadata and space_large tables if they do not exist.
|
|
81
|
+
*/
|
|
82
|
+
readonly migrate: Effect.Effect<void, SqlError.SqlError, SqlClient.SqlClient | SqlTransactionTag> = Effect.fn(
|
|
83
|
+
'SqliteMetadataStore.migrate',
|
|
84
|
+
)(() =>
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
const sql = yield* SqlClient.SqlClient;
|
|
87
|
+
yield* sql`CREATE TABLE IF NOT EXISTS space_metadata (
|
|
88
|
+
key TEXT PRIMARY KEY,
|
|
89
|
+
value BLOB NOT NULL
|
|
90
|
+
)`;
|
|
91
|
+
yield* sql`CREATE TABLE IF NOT EXISTS space_large (
|
|
92
|
+
space_key TEXT PRIMARY KEY,
|
|
93
|
+
value BLOB NOT NULL
|
|
94
|
+
)`;
|
|
95
|
+
log('space_metadata and space_large tables ready');
|
|
96
|
+
}).pipe(Effect.withSpan('SqliteMetadataStore.migrate')),
|
|
97
|
+
)();
|
|
98
|
+
|
|
99
|
+
get metadata(): EchoMetadata {
|
|
100
|
+
return this.#metadata;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get version(): number {
|
|
104
|
+
return this.#metadata.version ?? 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get spaces(): SpaceMetadata[] {
|
|
108
|
+
return this.#metadata.spaces ?? [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async close(): Promise<void> {
|
|
112
|
+
await this.#invitationCleanupCtx.dispose();
|
|
113
|
+
await this._save();
|
|
114
|
+
this.#metadata = emptyEchoMetadata();
|
|
115
|
+
this.#spaceLargeMetadata.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Loads metadata from SQLite.
|
|
120
|
+
*/
|
|
121
|
+
@synchronized
|
|
122
|
+
async load(): Promise<void> {
|
|
123
|
+
const rows = await RuntimeProvider.runPromise(this.#runtime)(
|
|
124
|
+
Effect.gen(function* () {
|
|
125
|
+
const sql = yield* SqlClient.SqlClient;
|
|
126
|
+
return yield* sql<{ value: Uint8Array }>`SELECT value FROM space_metadata WHERE key = ${MAIN_KEY}`;
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (rows.length > 0) {
|
|
131
|
+
try {
|
|
132
|
+
this.#metadata = this.#decodeWithCrc(rows[0].value, EchoMetadataCodec) ?? emptyEchoMetadata();
|
|
133
|
+
this.#metadata.spaces?.forEach((space) => {
|
|
134
|
+
space.state ??= SpaceState.SPACE_ACTIVE;
|
|
135
|
+
});
|
|
136
|
+
} catch (err: any) {
|
|
137
|
+
log.error('failed to load metadata from SQLite', { err });
|
|
138
|
+
this.#metadata = emptyEchoMetadata();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Load large metadata for all known spaces.
|
|
143
|
+
const spaceKeys = [
|
|
144
|
+
this.#metadata.identity?.haloSpace.key,
|
|
145
|
+
...(this.#metadata.spaces?.map((s) => s.key) ?? []),
|
|
146
|
+
].filter(isNonNullable);
|
|
147
|
+
|
|
148
|
+
await forEachAsync(spaceKeys, async (key) => {
|
|
149
|
+
try {
|
|
150
|
+
await this.#loadSpaceLargeMetadata(key);
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
log.error('failed to load large space metadata', { err });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
scheduleTaskInterval(
|
|
157
|
+
this.#invitationCleanupCtx,
|
|
158
|
+
async () => {
|
|
159
|
+
for (const invitation of this.#metadata.invitations ?? []) {
|
|
160
|
+
if (hasInvitationExpired(invitation) || isLegacyInvitationFormat(invitation)) {
|
|
161
|
+
await this.removeInvitation(invitation.invitationId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
EXPIRED_INVITATION_CLEANUP_INTERVAL,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async flush(): Promise<void> {
|
|
170
|
+
// No-op: SQLite writes are immediate.
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
hasSpace(spaceKey: PublicKey): boolean {
|
|
174
|
+
if (this.#metadata.identity?.haloSpace.key.equals(spaceKey)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return !!this.spaces.find((space) => space.key.equals(spaceKey));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getIdentityRecord(): IdentityRecord | undefined {
|
|
181
|
+
return this.#metadata.identity;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async setIdentityRecord(record: IdentityRecord): Promise<void> {
|
|
185
|
+
invariant(!this.#metadata.identity, 'Cannot overwrite existing identity in metadata');
|
|
186
|
+
this.#metadata.identity = record;
|
|
187
|
+
await this._save();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getInvitations(): Invitation[] {
|
|
191
|
+
return this.#metadata.invitations ?? [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async addInvitation(invitation: Invitation): Promise<void> {
|
|
195
|
+
if (this.#metadata.invitations?.find((i) => i.invitationId === invitation.invitationId)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
(this.#metadata.invitations ??= []).push(invitation);
|
|
199
|
+
await this._save();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async removeInvitation(invitationId: string): Promise<void> {
|
|
203
|
+
this.#metadata.invitations = (this.#metadata.invitations ?? []).filter((i) => i.invitationId !== invitationId);
|
|
204
|
+
await this._save();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async addSpace(record: SpaceMetadata): Promise<void> {
|
|
208
|
+
invariant(
|
|
209
|
+
!(this.#metadata.spaces ?? []).find((space) => space.key.equals(record.key)),
|
|
210
|
+
'Cannot overwrite existing space in metadata',
|
|
211
|
+
);
|
|
212
|
+
(this.#metadata.spaces ??= []).push(record);
|
|
213
|
+
await this._save();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async setSpaceDataLatestTimeframe(spaceKey: PublicKey, timeframe: Timeframe): Promise<void> {
|
|
217
|
+
this.#getSpace(spaceKey).dataTimeframe = timeframe;
|
|
218
|
+
await this._save();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async setSpaceControlLatestTimeframe(spaceKey: PublicKey, timeframe: Timeframe): Promise<void> {
|
|
222
|
+
this.#getSpace(spaceKey).controlTimeframe = timeframe;
|
|
223
|
+
await this._save();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async setCache(spaceKey: PublicKey, cache: SpaceCache): Promise<void> {
|
|
227
|
+
this.#getSpace(spaceKey).cache = cache;
|
|
228
|
+
await this._save();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async setWritableFeedKeys(spaceKey: PublicKey, controlFeedKey: PublicKey, dataFeedKey: PublicKey): Promise<void> {
|
|
232
|
+
const space = this.#getSpace(spaceKey);
|
|
233
|
+
space.controlFeedKey = controlFeedKey;
|
|
234
|
+
space.dataFeedKey = dataFeedKey;
|
|
235
|
+
await this._save();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async setSpaceState(spaceKey: PublicKey, state: SpaceState): Promise<void> {
|
|
239
|
+
this.#getSpace(spaceKey).state = state;
|
|
240
|
+
await this._save();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
getSpaceControlPipelineSnapshot(spaceKey: PublicKey): ControlPipelineSnapshot | undefined {
|
|
244
|
+
return this.#getLargeSpaceMetadata(spaceKey).controlPipelineSnapshot;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async setSpaceControlPipelineSnapshot(spaceKey: PublicKey, snapshot: ControlPipelineSnapshot): Promise<void> {
|
|
248
|
+
this.#getLargeSpaceMetadata(spaceKey).controlPipelineSnapshot = snapshot;
|
|
249
|
+
await this._saveSpaceLargeMetadata(spaceKey);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
getSpaceEdgeReplicationSetting(spaceKey: PublicKey): EdgeReplicationSetting | undefined {
|
|
253
|
+
return this.hasSpace(spaceKey) ? this.#getSpace(spaceKey).edgeReplication : undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async setSpaceEdgeReplicationSetting(spaceKey: PublicKey, setting: EdgeReplicationSetting): Promise<void> {
|
|
257
|
+
this.#getSpace(spaceKey).edgeReplication = setting;
|
|
258
|
+
await this._save();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
get deletedSpaces(): PublicKey[] {
|
|
262
|
+
return this.#metadata.deletedSpaces ?? [];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async addDeletedSpace(spaceKey: PublicKey): Promise<void> {
|
|
266
|
+
if ((this.#metadata.deletedSpaces ?? []).some((key) => key.equals(spaceKey))) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
(this.#metadata.deletedSpaces ??= []).push(spaceKey);
|
|
270
|
+
await this._save();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async clear(): Promise<void> {
|
|
274
|
+
log('clearing all metadata');
|
|
275
|
+
await RuntimeProvider.runPromise(this.#runtime)(
|
|
276
|
+
Effect.gen(function* () {
|
|
277
|
+
const sql = yield* SqlClient.SqlClient;
|
|
278
|
+
yield* sql`DELETE FROM space_metadata`;
|
|
279
|
+
yield* sql`DELETE FROM space_large`;
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
this.#metadata = emptyEchoMetadata();
|
|
283
|
+
this.#spaceLargeMetadata.clear();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#getSpace(spaceKey: PublicKey): SpaceMetadata {
|
|
287
|
+
if (this.#metadata.identity?.haloSpace.key.equals(spaceKey)) {
|
|
288
|
+
return this.#metadata.identity.haloSpace;
|
|
289
|
+
}
|
|
290
|
+
const space = this.spaces.find((space) => space.key.equals(spaceKey));
|
|
291
|
+
invariant(space, 'Space not found');
|
|
292
|
+
return space;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#getLargeSpaceMetadata(key: PublicKey): LargeSpaceMetadata {
|
|
296
|
+
let entry = this.#spaceLargeMetadata.get(key);
|
|
297
|
+
if (!entry) {
|
|
298
|
+
entry = emptyLargeSpaceMetadata();
|
|
299
|
+
this.#spaceLargeMetadata.set(key, entry);
|
|
300
|
+
}
|
|
301
|
+
return entry;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@synchronized
|
|
305
|
+
private async _save(): Promise<void> {
|
|
306
|
+
const data: EchoMetadata = {
|
|
307
|
+
...this.#metadata,
|
|
308
|
+
version: STORAGE_VERSION,
|
|
309
|
+
created: this.#metadata.created ?? new Date(),
|
|
310
|
+
updated: new Date(),
|
|
311
|
+
};
|
|
312
|
+
this.update.emit(data);
|
|
313
|
+
const encoded = this.#encodeWithCrc(EchoMetadataCodec, data);
|
|
314
|
+
await RuntimeProvider.runPromise(this.#runtime)(
|
|
315
|
+
Effect.gen(function* () {
|
|
316
|
+
const sql = yield* SqlClient.SqlClient;
|
|
317
|
+
yield* sql`INSERT OR REPLACE INTO space_metadata (key, value) VALUES (${MAIN_KEY}, ${encoded})`;
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async #loadSpaceLargeMetadata(key: PublicKey): Promise<void> {
|
|
323
|
+
const keyStr = largeKey(key);
|
|
324
|
+
const rows = await RuntimeProvider.runPromise(this.#runtime)(
|
|
325
|
+
Effect.gen(function* () {
|
|
326
|
+
const sql = yield* SqlClient.SqlClient;
|
|
327
|
+
return yield* sql<{ value: Uint8Array }>`SELECT value FROM space_large WHERE space_key = ${keyStr}`;
|
|
328
|
+
}),
|
|
329
|
+
);
|
|
330
|
+
if (rows.length > 0) {
|
|
331
|
+
try {
|
|
332
|
+
const meta = this.#decodeWithCrc(rows[0].value, LargeSpaceMetadataCodec);
|
|
333
|
+
if (meta) {
|
|
334
|
+
this.#spaceLargeMetadata.set(key, meta);
|
|
335
|
+
}
|
|
336
|
+
} catch (err: any) {
|
|
337
|
+
log.error('failed to decode large space metadata', { err });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@synchronized
|
|
343
|
+
private async _saveSpaceLargeMetadata(key: PublicKey): Promise<void> {
|
|
344
|
+
const data = this.#getLargeSpaceMetadata(key);
|
|
345
|
+
const keyStr = largeKey(key);
|
|
346
|
+
const encoded = this.#encodeWithCrc(LargeSpaceMetadataCodec, data);
|
|
347
|
+
await RuntimeProvider.runPromise(this.#runtime)(
|
|
348
|
+
Effect.gen(function* () {
|
|
349
|
+
const sql = yield* SqlClient.SqlClient;
|
|
350
|
+
yield* sql`INSERT OR REPLACE INTO space_large (space_key, value) VALUES (${keyStr}, ${encoded})`;
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Serialize with CRC32 checksum prefix for integrity checking (matches MetadataStore format). */
|
|
356
|
+
#encodeWithCrc<T>(codec: { encode: (v: T) => Uint8Array }, data: T): Buffer {
|
|
357
|
+
const encoded = arrayToBuffer(codec.encode(data));
|
|
358
|
+
const checksum = CRC32.buf(encoded);
|
|
359
|
+
const result = Buffer.alloc(8 + encoded.length);
|
|
360
|
+
result.writeInt32LE(encoded.length, 0);
|
|
361
|
+
result.writeInt32LE(checksum, 4);
|
|
362
|
+
encoded.copy(result, 8);
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Deserialize with CRC32 integrity check (matches MetadataStore format). */
|
|
367
|
+
#decodeWithCrc<T>(data: Uint8Array, codec: { decode: (v: Uint8Array) => T }): T | undefined {
|
|
368
|
+
const buf = Buffer.from(data);
|
|
369
|
+
if (buf.length < 8) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
const dataSize = buf.readInt32LE(0);
|
|
373
|
+
const checksum = buf.readInt32LE(4);
|
|
374
|
+
if (dataSize < 0 || dataSize > buf.length - 8) {
|
|
375
|
+
throw new DataCorruptionError({
|
|
376
|
+
message: 'Metadata size header is invalid.',
|
|
377
|
+
context: { fileLength: buf.length, dataSize },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (buf.length !== dataSize + 8) {
|
|
381
|
+
throw new DataCorruptionError({
|
|
382
|
+
message: 'Metadata payload length does not match framing header.',
|
|
383
|
+
context: { fileLength: buf.length, dataSize },
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const payload = buf.subarray(8, dataSize + 8);
|
|
387
|
+
const calculated = CRC32.buf(payload);
|
|
388
|
+
if (calculated !== checksum) {
|
|
389
|
+
throw new DataCorruptionError({ message: 'Metadata checksum is invalid.' });
|
|
390
|
+
}
|
|
391
|
+
return codec.decode(payload);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Effect Layer constructing a {@link SqliteMetadataStore} from the ambient SQL runtime.
|
|
397
|
+
*/
|
|
398
|
+
export const SqliteMetadataStoreLayer = (): Layer.Layer<
|
|
399
|
+
IMetadataStoreService,
|
|
400
|
+
never,
|
|
401
|
+
SqlClient.SqlClient | SqlTransactionTag
|
|
402
|
+
> =>
|
|
403
|
+
Layer.effect(
|
|
404
|
+
IMetadataStoreService,
|
|
405
|
+
Effect.gen(function* () {
|
|
406
|
+
const runtime = yield* RuntimeProvider.currentRuntime<SqlClient.SqlClient | SqlTransactionTag>();
|
|
407
|
+
return new SqliteMetadataStore({ runtime });
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as Runtime from 'effect/Runtime';
|
|
5
6
|
import { afterEach, beforeEach, describe, expect, onTestFinished, test } from 'vitest';
|
|
6
7
|
|
|
7
8
|
import { Trigger } from '@dxos/async';
|
|
8
9
|
import { Context } from '@dxos/context';
|
|
9
|
-
import {
|
|
10
|
+
import { EffectEx } from '@dxos/effect';
|
|
11
|
+
import { subscribeStream } from '@dxos/protocols';
|
|
12
|
+
import { ConnectionState } from '@dxos/protocols/proto/dxos/client/services';
|
|
10
13
|
|
|
11
14
|
import { type ServiceContext } from '../services';
|
|
12
15
|
import { createServiceContext } from '../testing';
|
|
@@ -14,7 +17,7 @@ import { NetworkServiceImpl } from './network-service';
|
|
|
14
17
|
|
|
15
18
|
describe('NetworkService', () => {
|
|
16
19
|
let serviceContext: ServiceContext;
|
|
17
|
-
let networkService:
|
|
20
|
+
let networkService: NetworkServiceImpl;
|
|
18
21
|
|
|
19
22
|
beforeEach(async () => {
|
|
20
23
|
serviceContext = await createServiceContext();
|
|
@@ -27,26 +30,30 @@ describe('NetworkService', () => {
|
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
test('setNetworkOptions changes network status', async () => {
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
await EffectEx.runPromise(
|
|
34
|
+
networkService['NetworkService.updateConfig']({
|
|
35
|
+
swarm: ConnectionState.OFFLINE,
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
33
38
|
|
|
34
39
|
expect(serviceContext.networkManager.connectionState).to.equal(ConnectionState.OFFLINE);
|
|
35
40
|
});
|
|
36
41
|
|
|
37
42
|
test('subscribeToNetworkStatus returns current network status', async () => {
|
|
38
|
-
const
|
|
43
|
+
const stream = networkService['NetworkService.queryStatus']();
|
|
39
44
|
let result = new Trigger<ConnectionState | undefined>();
|
|
40
|
-
|
|
41
|
-
result.wake(swarm)
|
|
45
|
+
const cleanup = subscribeStream(Runtime.defaultRuntime, stream, {
|
|
46
|
+
onData: ({ swarm }) => result.wake(swarm),
|
|
42
47
|
});
|
|
43
|
-
onTestFinished(
|
|
48
|
+
onTestFinished(cleanup);
|
|
44
49
|
expect(await result.wait()).to.equal(ConnectionState.ONLINE);
|
|
45
50
|
|
|
46
51
|
result = new Trigger<ConnectionState | undefined>();
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
await EffectEx.runPromise(
|
|
53
|
+
networkService['NetworkService.updateConfig']({
|
|
54
|
+
swarm: ConnectionState.OFFLINE,
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
50
57
|
expect(await result.wait()).to.equal(ConnectionState.OFFLINE);
|
|
51
58
|
});
|
|
52
59
|
});
|
|
@@ -2,37 +2,40 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import * as EffectStream from 'effect/Stream';
|
|
7
|
+
|
|
7
8
|
import { Context } from '@dxos/context';
|
|
8
9
|
import { type EdgeConnection } from '@dxos/edge-client';
|
|
9
|
-
import { type SignalManager } from '@dxos/messaging';
|
|
10
|
+
import { type SignalManager, type UnsubscribeCallback } from '@dxos/messaging';
|
|
10
11
|
import { type SwarmNetworkManager } from '@dxos/network-manager';
|
|
11
12
|
import {
|
|
12
|
-
type NetworkService,
|
|
13
13
|
type NetworkStatus,
|
|
14
14
|
type SubscribeSwarmStateRequest,
|
|
15
15
|
type UpdateConfigRequest,
|
|
16
16
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
17
|
-
import { type
|
|
17
|
+
import { type SwarmResponse } from '@dxos/protocols/proto/dxos/edge/messenger';
|
|
18
18
|
import {
|
|
19
19
|
type JoinRequest,
|
|
20
20
|
type LeaveRequest,
|
|
21
21
|
type Message,
|
|
22
22
|
type QueryRequest,
|
|
23
|
+
type SubscribeMessagesRequest,
|
|
23
24
|
} from '@dxos/protocols/proto/dxos/edge/signal';
|
|
25
|
+
import { type NetworkService } from '@dxos/protocols/rpc';
|
|
24
26
|
|
|
25
|
-
export class NetworkServiceImpl implements NetworkService {
|
|
26
|
-
constructor(
|
|
27
|
+
export class NetworkServiceImpl implements NetworkService.Handlers {
|
|
28
|
+
'constructor'(
|
|
27
29
|
private readonly networkManager: SwarmNetworkManager,
|
|
28
30
|
private readonly signalManager: SignalManager,
|
|
29
31
|
private readonly edgeConnection?: EdgeConnection,
|
|
30
32
|
) {}
|
|
31
33
|
|
|
32
|
-
queryStatus(): Stream<NetworkStatus> {
|
|
33
|
-
return
|
|
34
|
+
['NetworkService.queryStatus'](): EffectStream.Stream<NetworkStatus, Error> {
|
|
35
|
+
return EffectStream.async<NetworkStatus, Error>((emit) => {
|
|
36
|
+
const ctx = Context.default();
|
|
34
37
|
const update = () => {
|
|
35
|
-
|
|
38
|
+
void emit.single({
|
|
36
39
|
swarm: this.networkManager.connectionState,
|
|
37
40
|
connectionInfo: this.networkManager.connectionLog?.swarms,
|
|
38
41
|
signaling: this.signalManager.getStatus?.().map(({ host, state }) => ({ server: host, state })),
|
|
@@ -42,45 +45,109 @@ export class NetworkServiceImpl implements NetworkService {
|
|
|
42
45
|
this.networkManager.connectionStateChanged.on(ctx, () => update());
|
|
43
46
|
this.signalManager.statusChanged?.on(ctx, () => update());
|
|
44
47
|
update();
|
|
48
|
+
|
|
49
|
+
return Effect.promise(() => ctx.dispose());
|
|
45
50
|
});
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
['NetworkService.updateConfig'](request: UpdateConfigRequest): Effect.Effect<void, Error> {
|
|
54
|
+
return Effect.tryPromise({
|
|
55
|
+
try: async () => {
|
|
56
|
+
await this.networkManager.setConnectionState(request.swarm);
|
|
57
|
+
},
|
|
58
|
+
catch: (error) => error as Error,
|
|
59
|
+
});
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
return
|
|
62
|
+
['NetworkService.joinSwarm'](request: JoinRequest): Effect.Effect<void, Error> {
|
|
63
|
+
return Effect.tryPromise({
|
|
64
|
+
try: async () => {
|
|
65
|
+
await this.signalManager.join(Context.default(), request);
|
|
66
|
+
},
|
|
67
|
+
catch: (error) => error as Error,
|
|
68
|
+
});
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
return
|
|
71
|
+
['NetworkService.leaveSwarm'](request: LeaveRequest): Effect.Effect<void, Error> {
|
|
72
|
+
return Effect.tryPromise({
|
|
73
|
+
try: async () => {
|
|
74
|
+
await this.signalManager.leave(Context.default(), request);
|
|
75
|
+
},
|
|
76
|
+
catch: (error) => error as Error,
|
|
77
|
+
});
|
|
58
78
|
}
|
|
59
79
|
|
|
60
|
-
|
|
61
|
-
return
|
|
80
|
+
['NetworkService.querySwarm'](request: QueryRequest): Effect.Effect<SwarmResponse, Error> {
|
|
81
|
+
return Effect.tryPromise({
|
|
82
|
+
try: async () => {
|
|
83
|
+
return this.signalManager.query(Context.default(), request);
|
|
84
|
+
},
|
|
85
|
+
catch: (error) => error as Error,
|
|
86
|
+
});
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
subscribeSwarmState(
|
|
65
|
-
|
|
89
|
+
['NetworkService.subscribeSwarmState'](
|
|
90
|
+
request: SubscribeSwarmStateRequest,
|
|
91
|
+
): EffectStream.Stream<SwarmResponse, Error> {
|
|
92
|
+
return EffectStream.async<SwarmResponse, Error>((emit) => {
|
|
93
|
+
const ctx = Context.default();
|
|
66
94
|
this.signalManager.swarmState?.on(ctx, (state) => {
|
|
67
95
|
if (request.topic.equals(state.swarmKey)) {
|
|
68
|
-
|
|
96
|
+
void emit.single(state);
|
|
69
97
|
}
|
|
70
98
|
});
|
|
99
|
+
|
|
100
|
+
return Effect.promise(() => ctx.dispose());
|
|
71
101
|
});
|
|
72
102
|
}
|
|
73
103
|
|
|
74
|
-
|
|
75
|
-
return
|
|
104
|
+
['NetworkService.sendMessage'](message: Message): Effect.Effect<void, Error> {
|
|
105
|
+
return Effect.tryPromise({
|
|
106
|
+
try: async () => {
|
|
107
|
+
await this.signalManager.sendMessage(Context.default(), message);
|
|
108
|
+
},
|
|
109
|
+
catch: (error) => error as Error,
|
|
110
|
+
});
|
|
76
111
|
}
|
|
77
112
|
|
|
78
|
-
subscribeMessages(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
['NetworkService.subscribeMessages'](request: SubscribeMessagesRequest): EffectStream.Stream<Message, Error> {
|
|
114
|
+
const { peer, tags = [] } = request;
|
|
115
|
+
return EffectStream.async<Message, Error>((emit) => {
|
|
116
|
+
const ctx = Context.default();
|
|
117
|
+
|
|
118
|
+
// This stream crosses the client-services RPC (protobufjs codec, e.g. dedicated worker → main
|
|
119
|
+
// thread). Its `Message.payload` is a `google.protobuf.Any` without `preserve_any`, and the
|
|
120
|
+
// codec refuses to encode an Any lacking '@type' — stamping the opaque form
|
|
121
|
+
// ('@type': 'google.protobuf.Any' + type_url/value) makes it pass through verbatim.
|
|
122
|
+
const encodableAny = (payload: Message['payload']): Message['payload'] => ({
|
|
123
|
+
...payload,
|
|
124
|
+
'@type': 'google.protobuf.Any',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// The subscription encapsulates routing (DX-1125): point-to-point messages addressed to `peer`,
|
|
128
|
+
// plus — when `tags` are set — swarm broadcasts whose tags intersect. The returned callback owns
|
|
129
|
+
// teardown, refcounted so it releases only this stream's tag registration.
|
|
130
|
+
let unsubscribe: UnsubscribeCallback | undefined;
|
|
131
|
+
void this.signalManager
|
|
132
|
+
.subscribeMessages({
|
|
133
|
+
peer,
|
|
134
|
+
tags,
|
|
135
|
+
onMessage: (message) => {
|
|
136
|
+
void emit.single({ ...message, payload: encodableAny(message.payload) });
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
.then((unsub) => {
|
|
140
|
+
if (ctx.disposed) {
|
|
141
|
+
void unsub();
|
|
142
|
+
} else {
|
|
143
|
+
unsubscribe = unsub;
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
.catch((err) => emit.fail(err instanceof Error ? err : new Error(String(err))));
|
|
147
|
+
|
|
148
|
+
return Effect.promise(async () => {
|
|
149
|
+
await unsubscribe?.();
|
|
150
|
+
await ctx.dispose();
|
|
84
151
|
});
|
|
85
152
|
});
|
|
86
153
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Codec } from '@dxos/codec-protobuf';
|
|
6
|
+
import { createCodecEncoding } from '@dxos/hypercore';
|
|
7
|
+
import { schema } from '@dxos/protocols/proto';
|
|
8
|
+
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Codec for feed messages.
|
|
12
|
+
*/
|
|
13
|
+
export const codec: Codec<FeedMessage> = schema.getCodecForType('dxos.echo.feed.FeedMessage');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Value encoding used by feed store.
|
|
17
|
+
*/
|
|
18
|
+
export const valueEncoding = createCodecEncoding(codec);
|