@dxos/echo-pipeline 0.8.3 → 0.8.4-main.05e74ebcff
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/neutral/chunk-46DCRDA7.mjs +271 -0
- package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
- package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-LR7UFVQX.mjs} +313 -709
- package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
- package/dist/lib/neutral/index.mjs +7332 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-host.d.ts +119 -54
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
- package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +46 -15
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/errors.d.ts +35 -0
- package/dist/types/src/automerge/errors.d.ts.map +1 -0
- package/dist/types/src/automerge/handle-state.d.ts +52 -0
- package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
- package/dist/types/src/automerge/heads-store.d.ts +9 -2
- package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +3 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +11 -5
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
- package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
- package/dist/types/src/common/codec.d.ts +1 -1
- package/dist/types/src/common/codec.d.ts.map +1 -1
- package/dist/types/src/common/feeds.d.ts.map +1 -1
- package/dist/types/src/common/space-id.d.ts.map +1 -1
- package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
- package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts +13 -7
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +9 -3
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +74 -28
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +2 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
- package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
- package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +19 -11
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
- package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
- package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
- package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/stub.d.ts +11 -0
- package/dist/types/src/db-host/stub.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
- package/dist/types/src/edge/index.d.ts +1 -0
- package/dist/types/src/edge/index.d.ts.map +1 -1
- package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
- package/dist/types/src/filter/filter-match.d.ts +4 -1
- package/dist/types/src/filter/filter-match.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline.d.ts +3 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +22 -10
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +66 -8
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +66 -11
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +12 -1
- package/dist/types/src/query/query-planner.d.ts.map +1 -1
- package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
- package/dist/types/src/space/auth.d.ts +3 -3
- package/dist/types/src/space/auth.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts +5 -4
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +8 -8
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +10 -9
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +6 -6
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +0 -2
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
- package/dist/types/src/testing/test-replicator.d.ts +14 -13
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +59 -55
- package/src/automerge/automerge-host-subduction.test.ts +263 -0
- package/src/automerge/automerge-host.test.ts +162 -39
- package/src/automerge/automerge-host.ts +679 -201
- package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
- package/src/automerge/automerge-repo.test.ts +349 -91
- package/src/automerge/automerge-subduction.test.ts +330 -0
- package/src/automerge/collection-synchronizer.test.ts +98 -17
- package/src/automerge/collection-synchronizer.ts +141 -37
- package/src/automerge/echo-data-monitor.ts +1 -1
- package/src/automerge/echo-network-adapter.test.ts +5 -4
- package/src/automerge/echo-network-adapter.ts +76 -31
- package/src/automerge/echo-replicator.ts +50 -15
- package/src/automerge/errors.ts +10 -0
- package/src/automerge/handle-state.ts +62 -0
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +3 -1
- package/src/automerge/leveldb-storage-adapter.ts +77 -11
- package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
- package/src/automerge/mesh-echo-replicator.ts +15 -11
- package/src/automerge/storage-adapter.test.ts +1 -1
- package/src/common/space-id.ts +1 -1
- package/src/db-host/automerge-data-source.test.ts +299 -0
- package/src/db-host/automerge-data-source.ts +187 -0
- package/src/db-host/data-service.ts +35 -26
- package/src/db-host/database-root.ts +20 -8
- package/src/db-host/documents-synchronizer.test.ts +145 -14
- package/src/db-host/documents-synchronizer.ts +163 -51
- package/src/db-host/echo-host.ts +384 -99
- package/src/db-host/index.ts +2 -0
- package/src/db-host/invalidation-hint.ts +50 -0
- package/src/db-host/local-queue-service.ts +127 -0
- package/src/db-host/query-invalidation.test.ts +270 -0
- package/src/db-host/query-service.ts +111 -106
- package/src/db-host/queue-data-source.ts +160 -0
- package/src/db-host/queue-service.test.ts +145 -0
- package/src/db-host/space-state-manager.ts +12 -10
- package/src/db-host/stub.ts +33 -0
- package/src/edge/echo-edge-replicator.test.ts +12 -10
- package/src/edge/echo-edge-replicator.ts +144 -58
- package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
- package/src/edge/echo-edge-subduction-replicator.ts +377 -0
- package/src/edge/index.ts +1 -0
- package/src/filter/filter-match.test.ts +68 -32
- package/src/filter/filter-match.ts +230 -21
- package/src/metadata/metadata-store.ts +11 -8
- package/src/pipeline/pipeline-stress.test.ts +1 -1
- package/src/pipeline/pipeline.test.ts +2 -2
- package/src/pipeline/pipeline.ts +9 -7
- package/src/query/errors.ts +3 -1
- package/src/query/plan.ts +84 -9
- package/src/query/query-executor.ts +1197 -165
- package/src/query/query-planner.test.ts +993 -81
- package/src/query/query-planner.ts +503 -27
- package/src/space/admission-discovery-extension.ts +3 -3
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +5 -4
- package/src/space/control-pipeline.ts +16 -13
- package/src/space/space-manager.browser.test.ts +1 -1
- package/src/space/space-manager.ts +16 -20
- package/src/space/space-protocol.browser.test.ts +15 -14
- package/src/space/space-protocol.test.ts +15 -14
- package/src/space/space-protocol.ts +22 -18
- package/src/space/space.test.ts +3 -3
- package/src/space/space.ts +11 -11
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +3 -3
- package/src/testing/test-network-adapter.ts +24 -3
- package/src/testing/test-replicator.ts +19 -20
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
- package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
- package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -4743
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
- package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
- package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
- package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
- package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
- package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
- package/dist/lib/node/filter/index.cjs +0 -32
- package/dist/lib/node/filter/index.cjs.map +0 -7
- package/dist/lib/node/index.cjs +0 -4699
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node/testing/index.cjs +0 -753
- package/dist/lib/node/testing/index.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
- package/dist/lib/node-esm/filter/index.mjs +0 -11
- package/dist/lib/node-esm/filter/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -4743
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -742
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
- package/dist/types/src/testing/test-data.d.ts +0 -18
- package/dist/types/src/testing/test-data.d.ts.map +0 -1
- package/dist/types/src/testing/test-schema.d.ts +0 -37
- package/dist/types/src/testing/test-schema.d.ts.map +0 -1
- package/src/db-host/documents-iterator.ts +0 -74
- package/src/testing/test-data.ts +0 -127
- package/src/testing/test-schema.ts +0 -53
- /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Heads } from '@automerge/automerge';
|
|
6
|
+
import { type DocumentId } from '@automerge/automerge-repo';
|
|
7
|
+
|
|
8
|
+
import { type Context } from '@dxos/context';
|
|
5
9
|
import { type PublicKey, type SpaceId } from '@dxos/keys';
|
|
6
|
-
import type
|
|
10
|
+
import { type AutomergeProtocolMessage } from '@dxos/protocols';
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
// TODO(burdon): Rename AutomergeReplicator?
|
|
13
|
+
export interface AutomergeReplicator {
|
|
9
14
|
/**
|
|
10
15
|
* Called on when replicator is added to EchoHost.
|
|
11
16
|
*/
|
|
12
|
-
connect(context:
|
|
17
|
+
connect(ctx: Context, context: AutomergeReplicatorContext): Promise<void>;
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* Called on when replicator is removed from EchoHost.
|
|
@@ -17,7 +22,16 @@ export interface EchoReplicator {
|
|
|
17
22
|
disconnect(): Promise<void>;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Replicator with explicit per-space wiring (used by edge replicators that maintain one logical
|
|
27
|
+
* connection per `SpaceId`). `DataSpaceManager` drives these lifecycle hooks on space open/close.
|
|
28
|
+
*/
|
|
29
|
+
export interface EdgeAutomergeReplicator extends AutomergeReplicator {
|
|
30
|
+
connectToSpace(ctx: Context, spaceId: SpaceId): Promise<void>;
|
|
31
|
+
disconnectFromSpace(spaceId: SpaceId): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AutomergeReplicatorContext {
|
|
21
35
|
/**
|
|
22
36
|
* Our own peer id.
|
|
23
37
|
*/
|
|
@@ -28,16 +42,20 @@ export interface EchoReplicatorContext {
|
|
|
28
42
|
*/
|
|
29
43
|
getContainingSpaceForDocument(documentId: string): Promise<PublicKey | null>;
|
|
30
44
|
getContainingSpaceIdForDocument(documentId: string): Promise<SpaceId | null>;
|
|
31
|
-
isDocumentInRemoteCollection(params: RemoteDocumentExistenceCheckParams): Promise<boolean>;
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Returns false if collection sync hasn't happened yet.
|
|
48
|
+
*/
|
|
49
|
+
isDocumentInRemoteCollection(params: RemoteDocumentExistenceCheckProps): Promise<boolean>;
|
|
50
|
+
|
|
51
|
+
onConnectionOpen(connection: AutomergeReplicatorConnection): void;
|
|
52
|
+
onConnectionClosed(connection: AutomergeReplicatorConnection): void;
|
|
53
|
+
onConnectionAuthScopeChanged(connection: AutomergeReplicatorConnection): void;
|
|
36
54
|
}
|
|
37
55
|
|
|
38
|
-
export interface
|
|
56
|
+
export interface AutomergeReplicatorConnection {
|
|
39
57
|
/**
|
|
40
|
-
*
|
|
58
|
+
* Remote peer id.
|
|
41
59
|
*/
|
|
42
60
|
get peerId(): string;
|
|
43
61
|
|
|
@@ -55,23 +73,40 @@ export interface ReplicatorConnection {
|
|
|
55
73
|
* @returns true if the document should be advertised to this peer.
|
|
56
74
|
* The remote peer can still request the document by its id bypassing this check.
|
|
57
75
|
*/
|
|
58
|
-
shouldAdvertise(params:
|
|
76
|
+
shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean>;
|
|
59
77
|
|
|
60
78
|
/**
|
|
61
79
|
* @returns true if the collection should be synced to this peer.
|
|
62
80
|
*/
|
|
63
|
-
shouldSyncCollection(params:
|
|
81
|
+
shouldSyncCollection(params: ShouldSyncCollectionProps): boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Batch syncing considered enabled if AutomergeReplicatorConnection implements `pushBatch` and `pullBatch` methods.
|
|
85
|
+
* @returns true if the batch syncing is enabled.
|
|
86
|
+
*/
|
|
87
|
+
get bundleSyncEnabled(): boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pushes the batch of documents to the remote peer.
|
|
91
|
+
*/
|
|
92
|
+
pushBundle?(ctx: Context, bundle: { documentId: DocumentId; data: Uint8Array; heads: Heads }[]): Promise<void>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pulls the batch of documents from the remote peer.
|
|
96
|
+
*/
|
|
97
|
+
// TODO(mykola): Use automerge-repo-bundles Bundle type here.
|
|
98
|
+
pullBundle?(ctx: Context, docHeads: Record<DocumentId, Heads>): Promise<Record<DocumentId, Uint8Array>>;
|
|
64
99
|
}
|
|
65
100
|
|
|
66
|
-
export type
|
|
101
|
+
export type ShouldAdvertiseProps = {
|
|
67
102
|
documentId: string;
|
|
68
103
|
};
|
|
69
104
|
|
|
70
|
-
export type
|
|
105
|
+
export type ShouldSyncCollectionProps = {
|
|
71
106
|
collectionId: string;
|
|
72
107
|
};
|
|
73
108
|
|
|
74
|
-
export type
|
|
109
|
+
export type RemoteDocumentExistenceCheckProps = {
|
|
75
110
|
peerId: string;
|
|
76
111
|
documentId: string;
|
|
77
112
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { BaseError } from '@dxos/errors';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when attempting to send a message to a peer that is not connected.
|
|
9
|
+
*/
|
|
10
|
+
export class PeerNotFoundError extends BaseError.extend('PeerNotFoundError', 'Peer not found') {}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type DocumentId, type QueryState, type Repo } from '@automerge/automerge-repo';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Variants reported by {@link QueryState}: `'loading' | 'ready' | 'unavailable' | 'failed'`.
|
|
9
|
+
*/
|
|
10
|
+
export type HandleQueryState = QueryState<unknown>['state'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns the {@link QueryState} variant for a document already known to the
|
|
14
|
+
* repo (i.e. one for which a handle has been registered via `Repo.find` /
|
|
15
|
+
* `Repo.findWithProgress` / `Repo.create`), or `undefined` if no handle/query
|
|
16
|
+
* exists for this document yet.
|
|
17
|
+
*
|
|
18
|
+
* Use this anywhere you need to ask "is this document loaded?" / "did sync
|
|
19
|
+
* give up?" — read the result against the {@link HandleQueryState} variants
|
|
20
|
+
* (e.g. `=== 'ready'`).
|
|
21
|
+
*
|
|
22
|
+
* # Why we can't just call `DocHandle.isReady()` / read `DocHandle.state`
|
|
23
|
+
*
|
|
24
|
+
* The current subduction fork of `@automerge/automerge-repo`
|
|
25
|
+
* (`2.6.0-subduction.10`) **gutted** the per-handle state machine. Inside the
|
|
26
|
+
* fork, every `DocHandle` state predicate is a constant placeholder:
|
|
27
|
+
*
|
|
28
|
+
* ```
|
|
29
|
+
* isReady = () => true
|
|
30
|
+
* isUnloaded = () => false
|
|
31
|
+
* isDeleted = () => false
|
|
32
|
+
* isUnavailable = () => false
|
|
33
|
+
* inState = (states) => states.includes('ready')
|
|
34
|
+
* get state = () => 'ready'
|
|
35
|
+
* whenReady = async () => {} // resolves immediately, ignores argument
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* That means `handle.isReady()` always lies "yes", `handle.state` always reads
|
|
39
|
+
* `'ready'`, and `await handle.whenReady()` never blocks — even for handles
|
|
40
|
+
* whose document has never been loaded from storage or synced from a peer.
|
|
41
|
+
* Reading `handle.doc()` on such a "ready" handle returns the empty initial
|
|
42
|
+
* doc (`A.init()` or `A.emptyChange(A.init())`) rather than throwing or
|
|
43
|
+
* returning undefined, which can silently corrupt downstream logic that
|
|
44
|
+
* trusted the handle's `isReady()` signal.
|
|
45
|
+
*
|
|
46
|
+
* Real liveness moved to the {@link DocumentQuery} returned by
|
|
47
|
+
* `Repo.findWithProgress`, whose state goes through
|
|
48
|
+
* `'loading' → 'ready' | 'unavailable' | 'failed'`. Production code that
|
|
49
|
+
* needs to know whether a document is actually loaded must consult the query
|
|
50
|
+
* state through this helper instead of any `DocHandle.*` predicate.
|
|
51
|
+
*
|
|
52
|
+
* TODO(mykola): Remove this helper once the fork either (a) restores the
|
|
53
|
+
* `DocHandle` state machine, or (b) every caller is migrated to query
|
|
54
|
+
* `DocumentProgress` directly with a richer API surface (await on state
|
|
55
|
+
* transitions, etc.).
|
|
56
|
+
*/
|
|
57
|
+
export const getHandleState = (repo: Repo, documentId: DocumentId): HandleQueryState | undefined => {
|
|
58
|
+
if (!repo.handles[documentId]) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return repo.findWithProgress(documentId).peek().state;
|
|
62
|
+
};
|
|
@@ -4,18 +4,45 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Heads } from '@automerge/automerge';
|
|
6
6
|
import type { DocumentId } from '@automerge/automerge-repo';
|
|
7
|
+
import { type MixedEncoding } from 'level-transcoder';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import type { ProtoCodec } from '@dxos/codec-protobuf';
|
|
9
10
|
import type { BatchLevel, SublevelDB } from '@dxos/kv-store';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { schema } from '@dxos/protocols/proto';
|
|
13
|
+
import type { Heads as HeadsProto } from '@dxos/protocols/proto/dxos/echo/query';
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
// NOTE: Lazy so that code that doesn't use indexing doesn't need to load the codec (breaks in workerd).
|
|
16
|
+
let headsCodec: ProtoCodec<HeadsProto>;
|
|
17
|
+
const getHeadsCodec = () => (headsCodec ??= schema.getCodecForType('dxos.echo.query.Heads'));
|
|
18
|
+
|
|
19
|
+
const headsEncoding: MixedEncoding<Heads, Uint8Array, Heads> = {
|
|
20
|
+
encode: (value: Heads): Uint8Array => getHeadsCodec().encode({ hashes: value }),
|
|
21
|
+
decode: (encodedValue: Uint8Array): Heads => {
|
|
22
|
+
try {
|
|
23
|
+
return getHeadsCodec().decode(encodedValue).hashes!;
|
|
24
|
+
} catch {
|
|
25
|
+
// Legacy encoding migration path.
|
|
26
|
+
log.warn('Detected legacy encoding of heads in storage.');
|
|
27
|
+
const concatenatedHeads = Buffer.from(encodedValue).toString('utf8').replace(/"/g, '');
|
|
28
|
+
const heads = [];
|
|
29
|
+
for (let i = 0; i < concatenatedHeads.length; i += 64) {
|
|
30
|
+
heads.push(concatenatedHeads.slice(i, i + 64));
|
|
31
|
+
}
|
|
32
|
+
return heads;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
format: 'buffer',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type HeadsStoreProps = {
|
|
12
39
|
db: SublevelDB;
|
|
13
40
|
};
|
|
14
41
|
|
|
15
42
|
export class HeadsStore {
|
|
16
43
|
private readonly _db: SublevelDB;
|
|
17
44
|
|
|
18
|
-
constructor({ db }:
|
|
45
|
+
constructor({ db }: HeadsStoreProps) {
|
|
19
46
|
this._db = db;
|
|
20
47
|
}
|
|
21
48
|
|
|
@@ -34,4 +61,16 @@ export class HeadsStore {
|
|
|
34
61
|
valueEncoding: headsEncoding,
|
|
35
62
|
});
|
|
36
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Iterate over all document IDs and their heads.
|
|
67
|
+
*/
|
|
68
|
+
async *iterateAll(): AsyncGenerator<{ documentId: DocumentId; heads: Heads }> {
|
|
69
|
+
for await (const [documentId, heads] of this._db.iterator<DocumentId, Heads>({
|
|
70
|
+
keyEncoding: 'utf8',
|
|
71
|
+
valueEncoding: headsEncoding,
|
|
72
|
+
})) {
|
|
73
|
+
yield { documentId, heads };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
37
76
|
}
|
package/src/automerge/index.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './automerge-host';
|
|
6
|
+
export * from './handle-state';
|
|
6
7
|
export * from './leveldb-storage-adapter';
|
|
7
8
|
export * from './mesh-echo-replicator';
|
|
8
|
-
export * from './echo-replicator';
|
|
9
|
+
export type * from './echo-replicator';
|
|
9
10
|
export { diffCollectionState } from './collection-synchronizer';
|
|
10
11
|
export * from './space-collection';
|
|
11
12
|
export * from './echo-data-monitor';
|
|
13
|
+
export * from './errors';
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
// s
|
|
4
4
|
|
|
5
|
-
import { type
|
|
5
|
+
import { type Chunk, type StorageAdapterInterface, type StorageKey } from '@automerge/automerge-repo';
|
|
6
6
|
import { type MixedEncoding } from 'level-transcoder';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Resource } from '@dxos/context';
|
|
9
9
|
import { type BatchLevel, type SublevelDB } from '@dxos/kv-store';
|
|
10
10
|
import { type MaybePromise } from '@dxos/util';
|
|
11
11
|
|
|
@@ -16,27 +16,56 @@ export interface StorageAdapterDataMonitor {
|
|
|
16
16
|
recordStoreDuration(durationMs: number): void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export type
|
|
19
|
+
export type LevelDBStorageAdapterProps = {
|
|
20
20
|
db: SublevelDB;
|
|
21
21
|
callbacks?: StorageCallbacks;
|
|
22
22
|
monitor?: StorageAdapterDataMonitor;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export type
|
|
25
|
+
export type BeforeSaveProps = { path: StorageKey; batch: BatchLevel };
|
|
26
26
|
|
|
27
27
|
export interface StorageCallbacks {
|
|
28
|
-
beforeSave(params:
|
|
28
|
+
beforeSave(params: BeforeSaveProps): MaybePromise<void>;
|
|
29
29
|
afterSave(path: StorageKey): MaybePromise<void>;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export class LevelDBStorageAdapter extends Resource implements StorageAdapterInterface {
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* In-flight `loadRange` / `removeRange` iterations. Awaited in `_close` so
|
|
35
|
+
* the adapter doesn't return from close while a `for await` on the sublevel
|
|
36
|
+
* is still pending — otherwise the kv owner upstream would close the
|
|
37
|
+
* sublevel and the iterator's next `.next()` would reject with
|
|
38
|
+
* `LEVEL_ITERATOR_NOT_OPEN`.
|
|
39
|
+
*
|
|
40
|
+
* Promises stored here resolve regardless of outcome (the wrapper
|
|
41
|
+
* `.catch`es) so `Promise.all` won't reject.
|
|
42
|
+
*/
|
|
43
|
+
readonly #inFlightIterations = new Set<Promise<unknown>>();
|
|
44
|
+
|
|
45
|
+
constructor(private readonly _params: LevelDBStorageAdapterProps) {
|
|
34
46
|
super();
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
protected override async _close(): Promise<void> {
|
|
50
|
+
// New `loadRange`/`removeRange` calls already short-circuit on `!isOpen`
|
|
51
|
+
// (the Resource base flips state before invoking `_close`). Wait for any
|
|
52
|
+
// iterations that started while we were still open to finish before
|
|
53
|
+
// returning, so the upstream `kv` owner can safely close the sublevel.
|
|
54
|
+
if (this.#inFlightIterations.size > 0) {
|
|
55
|
+
await Promise.all(this.#inFlightIterations);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#trackIteration<T>(work: Promise<T>): Promise<T> {
|
|
60
|
+
const settled = work.catch(() => undefined);
|
|
61
|
+
this.#inFlightIterations.add(settled);
|
|
62
|
+
void settled.finally(() => this.#inFlightIterations.delete(settled));
|
|
63
|
+
return work;
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
async load(keyArray: StorageKey): Promise<Uint8Array | undefined> {
|
|
38
67
|
try {
|
|
39
|
-
if (this.
|
|
68
|
+
if (!this.isOpen) {
|
|
40
69
|
// TODO(mykola): this should be an error.
|
|
41
70
|
return undefined;
|
|
42
71
|
}
|
|
@@ -54,7 +83,7 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
|
|
|
54
83
|
}
|
|
55
84
|
|
|
56
85
|
async save(keyArray: StorageKey, binary: Uint8Array): Promise<void> {
|
|
57
|
-
if (this.
|
|
86
|
+
if (!this.isOpen) {
|
|
58
87
|
return undefined;
|
|
59
88
|
}
|
|
60
89
|
const startMs = Date.now();
|
|
@@ -71,17 +100,50 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
|
|
|
71
100
|
this._params.monitor?.recordStoreDuration(Date.now() - startMs);
|
|
72
101
|
}
|
|
73
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Atomically persist multiple key/value entries in a single LevelDB batch write.
|
|
105
|
+
*/
|
|
106
|
+
async saveBatch(entries: Array<[StorageKey, Uint8Array]>): Promise<void> {
|
|
107
|
+
if (!this.isOpen || entries.length === 0) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const startMs = Date.now();
|
|
111
|
+
const batch = this._params.db.batch();
|
|
112
|
+
|
|
113
|
+
for (const [keyArray] of entries) {
|
|
114
|
+
await this._params.callbacks?.beforeSave?.({ path: keyArray, batch });
|
|
115
|
+
}
|
|
116
|
+
for (const [keyArray, binary] of entries) {
|
|
117
|
+
batch.put<StorageKey, Uint8Array>(keyArray, Buffer.from(binary), {
|
|
118
|
+
...encodingOptions,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
await batch.write();
|
|
122
|
+
|
|
123
|
+
let bytesStored = 0;
|
|
124
|
+
for (const [keyArray, binary] of entries) {
|
|
125
|
+
bytesStored += binary.byteLength;
|
|
126
|
+
await this._params.callbacks?.afterSave?.(keyArray);
|
|
127
|
+
}
|
|
128
|
+
this._params.monitor?.recordBytesStored(bytesStored);
|
|
129
|
+
this._params.monitor?.recordStoreDuration(Date.now() - startMs);
|
|
130
|
+
}
|
|
131
|
+
|
|
74
132
|
async remove(keyArray: StorageKey): Promise<void> {
|
|
75
|
-
if (this.
|
|
133
|
+
if (!this.isOpen) {
|
|
76
134
|
return undefined;
|
|
77
135
|
}
|
|
78
136
|
await this._params.db.del<StorageKey>(keyArray, { ...encodingOptions });
|
|
79
137
|
}
|
|
80
138
|
|
|
81
139
|
async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
|
|
82
|
-
if (this.
|
|
140
|
+
if (!this.isOpen) {
|
|
83
141
|
return [];
|
|
84
142
|
}
|
|
143
|
+
return this.#trackIteration(this.#doLoadRange(keyPrefix));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async #doLoadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
|
|
85
147
|
const startMs = Date.now();
|
|
86
148
|
const result: Chunk[] = [];
|
|
87
149
|
for await (const [key, value] of this._params.db.iterator<StorageKey, Uint8Array>({
|
|
@@ -100,9 +162,13 @@ export class LevelDBStorageAdapter extends Resource implements StorageAdapterInt
|
|
|
100
162
|
}
|
|
101
163
|
|
|
102
164
|
async removeRange(keyPrefix: StorageKey): Promise<void> {
|
|
103
|
-
if (this.
|
|
165
|
+
if (!this.isOpen) {
|
|
104
166
|
return undefined;
|
|
105
167
|
}
|
|
168
|
+
await this.#trackIteration(this.#doRemoveRange(keyPrefix));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async #doRemoveRange(keyPrefix: StorageKey): Promise<void> {
|
|
106
172
|
const batch = this._params.db.batch();
|
|
107
173
|
|
|
108
174
|
for await (const [key] of this._params.db.iterator<StorageKey, Uint8Array>({
|
|
@@ -12,20 +12,20 @@ import { log } from '@dxos/log';
|
|
|
12
12
|
import type { AutomergeProtocolMessage } from '@dxos/protocols';
|
|
13
13
|
import { AutomergeReplicator, type AutomergeReplicatorFactory } from '@dxos/teleport-extension-automerge-replicator';
|
|
14
14
|
|
|
15
|
-
import type {
|
|
15
|
+
import type { AutomergeReplicatorConnection, ShouldAdvertiseProps, ShouldSyncCollectionProps } from './echo-replicator';
|
|
16
16
|
|
|
17
17
|
const DEFAULT_FACTORY: AutomergeReplicatorFactory = (params) => new AutomergeReplicator(...params);
|
|
18
18
|
|
|
19
|
-
export type
|
|
19
|
+
export type MeshReplicatorConnectionProps = {
|
|
20
20
|
ownPeerId: string;
|
|
21
21
|
onRemoteConnected: () => void;
|
|
22
22
|
onRemoteDisconnected: () => void;
|
|
23
|
-
shouldAdvertise: (params:
|
|
24
|
-
shouldSyncCollection: (params:
|
|
23
|
+
shouldAdvertise: (params: ShouldAdvertiseProps) => Promise<boolean>;
|
|
24
|
+
shouldSyncCollection: (params: ShouldSyncCollectionProps) => boolean;
|
|
25
25
|
replicatorFactory?: AutomergeReplicatorFactory;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
export class MeshReplicatorConnection extends Resource implements
|
|
28
|
+
export class MeshReplicatorConnection extends Resource implements AutomergeReplicatorConnection {
|
|
29
29
|
public readable: ReadableStream<AutomergeProtocolMessage>;
|
|
30
30
|
public writable: WritableStream<AutomergeProtocolMessage>;
|
|
31
31
|
public remoteDeviceKey: PublicKey | null = null;
|
|
@@ -35,7 +35,7 @@ export class MeshReplicatorConnection extends Resource implements ReplicatorConn
|
|
|
35
35
|
private _remotePeerId: string | null = null;
|
|
36
36
|
private _isEnabled = false;
|
|
37
37
|
|
|
38
|
-
constructor(private readonly _params:
|
|
38
|
+
constructor(private readonly _params: MeshReplicatorConnectionProps) {
|
|
39
39
|
super();
|
|
40
40
|
|
|
41
41
|
let readableStreamController!: ReadableStreamDefaultController<AutomergeProtocolMessage>;
|
|
@@ -111,11 +111,15 @@ export class MeshReplicatorConnection extends Resource implements ReplicatorConn
|
|
|
111
111
|
return this._isEnabled;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
get bundleSyncEnabled(): boolean {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async shouldAdvertise(params: ShouldAdvertiseProps): Promise<boolean> {
|
|
115
119
|
return this._params.shouldAdvertise(params);
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
shouldSyncCollection(params:
|
|
122
|
+
shouldSyncCollection(params: ShouldSyncCollectionProps): boolean {
|
|
119
123
|
return this._params.shouldSyncCollection(params);
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -2,27 +2,29 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Context } from '@dxos/context';
|
|
5
6
|
import type { CollectionId } from '@dxos/echo-protocol';
|
|
6
7
|
import { invariant } from '@dxos/invariant';
|
|
7
8
|
import { PublicKey, type SpaceId } from '@dxos/keys';
|
|
8
9
|
import { log } from '@dxos/log';
|
|
9
|
-
import
|
|
10
|
-
type AutomergeReplicator,
|
|
11
|
-
type AutomergeReplicatorFactory,
|
|
12
|
-
} from '@dxos/teleport-extension-automerge-replicator';
|
|
10
|
+
import type * as TeleportAutomergeReplicator from '@dxos/teleport-extension-automerge-replicator';
|
|
13
11
|
import { ComplexSet, defaultMap } from '@dxos/util';
|
|
14
12
|
|
|
15
|
-
import {
|
|
13
|
+
import { createIdFromSpaceKey } from '../common/space-id';
|
|
14
|
+
import {
|
|
15
|
+
type AutomergeReplicator,
|
|
16
|
+
type AutomergeReplicatorContext,
|
|
17
|
+
type ShouldAdvertiseProps,
|
|
18
|
+
} from './echo-replicator';
|
|
16
19
|
import { MeshReplicatorConnection } from './mesh-echo-replicator-connection';
|
|
17
20
|
import { getSpaceIdFromCollectionId } from './space-collection';
|
|
18
|
-
import { createIdFromSpaceKey } from '../common/space-id';
|
|
19
21
|
|
|
20
22
|
// TODO(dmaretskyi): Move out of @dxos/echo-pipeline.
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Used to replicate with other peers over the network.
|
|
24
26
|
*/
|
|
25
|
-
export class MeshEchoReplicator implements
|
|
27
|
+
export class MeshEchoReplicator implements AutomergeReplicator {
|
|
26
28
|
/**
|
|
27
29
|
* We might have multiple connections open with a peer (one per space), but there'll be only one enabled
|
|
28
30
|
* connection at any given moment, because there's a single repo for all the spaces.
|
|
@@ -40,9 +42,9 @@ export class MeshEchoReplicator implements EchoReplicator {
|
|
|
40
42
|
*/
|
|
41
43
|
private readonly _authorizedDevices = new Map<SpaceId, ComplexSet<PublicKey>>();
|
|
42
44
|
|
|
43
|
-
private _context:
|
|
45
|
+
private _context: AutomergeReplicatorContext | null = null;
|
|
44
46
|
|
|
45
|
-
async connect(context:
|
|
47
|
+
async connect(_ctx: Context, context: AutomergeReplicatorContext): Promise<void> {
|
|
46
48
|
this._context = context;
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -63,7 +65,9 @@ export class MeshEchoReplicator implements EchoReplicator {
|
|
|
63
65
|
this._context = null;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
createExtension(
|
|
68
|
+
createExtension(
|
|
69
|
+
extensionFactory?: TeleportAutomergeReplicator.AutomergeReplicatorFactory,
|
|
70
|
+
): TeleportAutomergeReplicator.AutomergeReplicator {
|
|
67
71
|
invariant(this._context);
|
|
68
72
|
|
|
69
73
|
const connection: MeshReplicatorConnection = new MeshReplicatorConnection({
|
|
@@ -110,7 +114,7 @@ export class MeshEchoReplicator implements EchoReplicator {
|
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
},
|
|
113
|
-
shouldAdvertise: async (params:
|
|
117
|
+
shouldAdvertise: async (params: ShouldAdvertiseProps) => {
|
|
114
118
|
log('shouldAdvertise', { peerId: connection.peerId, documentId: params.documentId });
|
|
115
119
|
invariant(this._context);
|
|
116
120
|
try {
|
package/src/common/space-id.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const createIdFromSpaceKey = async (spaceKey: PublicKey): Promise<SpaceId
|
|
|
18
18
|
return cachedValue;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const digest = await subtleCrypto.digest('SHA-256', spaceKey.asUint8Array());
|
|
21
|
+
const digest = await subtleCrypto.digest('SHA-256', spaceKey.asUint8Array() as Uint8Array<ArrayBuffer>);
|
|
22
22
|
|
|
23
23
|
const bytes = new Uint8Array(digest).slice(0, SpaceId.byteLength);
|
|
24
24
|
const spaceId = SpaceId.encode(bytes);
|