@dxos/echo-pipeline 0.1.24-next.6b1f434
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 +8 -0
- package/README.md +22 -0
- package/dist/lib/browser/chunk-3GAE5CKD.mjs +1621 -0
- package/dist/lib/browser/chunk-3GAE5CKD.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +114 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/testing/index.mjs +238 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node/index.cjs +1747 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node/testing/index.cjs +1835 -0
- package/dist/lib/node/testing/index.cjs.map +7 -0
- package/dist/types/src/common/codec.d.ts +12 -0
- package/dist/types/src/common/codec.d.ts.map +1 -0
- package/dist/types/src/common/feeds.d.ts +7 -0
- package/dist/types/src/common/feeds.d.ts.map +1 -0
- package/dist/types/src/common/index.d.ts +3 -0
- package/dist/types/src/common/index.d.ts.map +1 -0
- package/dist/types/src/dbhost/data-service-host.d.ts +22 -0
- package/dist/types/src/dbhost/data-service-host.d.ts.map +1 -0
- package/dist/types/src/dbhost/data-service.d.ts +21 -0
- package/dist/types/src/dbhost/data-service.d.ts.map +1 -0
- package/dist/types/src/dbhost/database-backend.d.ts +28 -0
- package/dist/types/src/dbhost/database-backend.d.ts.map +1 -0
- package/dist/types/src/dbhost/index.d.ts +6 -0
- package/dist/types/src/dbhost/index.d.ts.map +1 -0
- package/dist/types/src/dbhost/snapshot-manager.d.ts +17 -0
- package/dist/types/src/dbhost/snapshot-manager.d.ts.map +1 -0
- package/dist/types/src/dbhost/snapshot-store.d.ts +9 -0
- package/dist/types/src/dbhost/snapshot-store.d.ts.map +1 -0
- package/dist/types/src/dbhost/snapshot-store.test.d.ts +2 -0
- package/dist/types/src/dbhost/snapshot-store.test.d.ts.map +1 -0
- package/dist/types/src/errors/errors.d.ts +26 -0
- package/dist/types/src/errors/errors.d.ts.map +1 -0
- package/dist/types/src/errors/index.d.ts +2 -0
- package/dist/types/src/errors/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +7 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/metadata/index.d.ts +2 -0
- package/dist/types/src/metadata/index.d.ts.map +1 -0
- package/dist/types/src/metadata/metadata-store.d.ts +42 -0
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -0
- package/dist/types/src/pipeline/index.d.ts +3 -0
- package/dist/types/src/pipeline/index.d.ts.map +1 -0
- package/dist/types/src/pipeline/message-selector.d.ts +11 -0
- package/dist/types/src/pipeline/message-selector.d.ts.map +1 -0
- package/dist/types/src/pipeline/pipeline.d.ts +98 -0
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -0
- package/dist/types/src/pipeline/pipeline.test.d.ts +2 -0
- package/dist/types/src/pipeline/pipeline.test.d.ts.map +1 -0
- package/dist/types/src/pipeline/timeframe-clock.d.ts +19 -0
- package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -0
- package/dist/types/src/space/auth.d.ts +23 -0
- package/dist/types/src/space/auth.d.ts.map +1 -0
- package/dist/types/src/space/control-pipeline.d.ts +31 -0
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -0
- package/dist/types/src/space/control-pipeline.test.d.ts +2 -0
- package/dist/types/src/space/control-pipeline.test.d.ts.map +1 -0
- package/dist/types/src/space/data-pipeline-controller.d.ts +63 -0
- package/dist/types/src/space/data-pipeline-controller.d.ts.map +1 -0
- package/dist/types/src/space/genesis.d.ts +5 -0
- package/dist/types/src/space/genesis.d.ts.map +1 -0
- package/dist/types/src/space/index.d.ts +7 -0
- package/dist/types/src/space/index.d.ts.map +1 -0
- package/dist/types/src/space/replication.browser.test.d.ts +2 -0
- package/dist/types/src/space/replication.browser.test.d.ts.map +1 -0
- package/dist/types/src/space/space-manager.browser.test.d.ts +2 -0
- package/dist/types/src/space/space-manager.browser.test.d.ts.map +1 -0
- package/dist/types/src/space/space-manager.d.ts +41 -0
- package/dist/types/src/space/space-manager.d.ts.map +1 -0
- package/dist/types/src/space/space-protocol.browser.test.d.ts +2 -0
- package/dist/types/src/space/space-protocol.browser.test.d.ts.map +1 -0
- package/dist/types/src/space/space-protocol.d.ts +76 -0
- package/dist/types/src/space/space-protocol.d.ts.map +1 -0
- package/dist/types/src/space/space-protocol.test.d.ts +2 -0
- package/dist/types/src/space/space-protocol.test.d.ts.map +1 -0
- package/dist/types/src/space/space.d.ts +59 -0
- package/dist/types/src/space/space.d.ts.map +1 -0
- package/dist/types/src/space/space.test.d.ts +2 -0
- package/dist/types/src/space/space.test.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +4 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/test-agent-builder.d.ts +52 -0
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -0
- package/dist/types/src/testing/test-feed-builder.d.ts +9 -0
- package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -0
- package/dist/types/src/testing/util.d.ts +14 -0
- package/dist/types/src/testing/util.d.ts.map +1 -0
- package/dist/types/src/tests/database.test.d.ts +2 -0
- package/dist/types/src/tests/database.test.d.ts.map +1 -0
- package/package.json +60 -0
- package/src/common/codec.ts +18 -0
- package/src/common/feeds.ts +23 -0
- package/src/common/index.ts +6 -0
- package/src/dbhost/data-service-host.ts +103 -0
- package/src/dbhost/data-service.ts +66 -0
- package/src/dbhost/database-backend.ts +64 -0
- package/src/dbhost/index.ts +9 -0
- package/src/dbhost/snapshot-manager.ts +68 -0
- package/src/dbhost/snapshot-store.test.ts +30 -0
- package/src/dbhost/snapshot-store.ts +45 -0
- package/src/errors/errors.ts +52 -0
- package/src/errors/index.ts +5 -0
- package/src/index.ts +10 -0
- package/src/metadata/index.ts +5 -0
- package/src/metadata/metadata-store.ts +173 -0
- package/src/pipeline/index.ts +6 -0
- package/src/pipeline/message-selector.ts +38 -0
- package/src/pipeline/pipeline.test.ts +71 -0
- package/src/pipeline/pipeline.ts +249 -0
- package/src/pipeline/timeframe-clock.ts +56 -0
- package/src/space/auth.ts +88 -0
- package/src/space/control-pipeline.test.ts +162 -0
- package/src/space/control-pipeline.ts +103 -0
- package/src/space/data-pipeline-controller.ts +234 -0
- package/src/space/genesis.ts +29 -0
- package/src/space/index.ts +10 -0
- package/src/space/replication.browser.test.ts +64 -0
- package/src/space/space-manager.browser.test.ts +44 -0
- package/src/space/space-manager.ts +92 -0
- package/src/space/space-protocol.browser.test.ts +135 -0
- package/src/space/space-protocol.test.ts +136 -0
- package/src/space/space-protocol.ts +222 -0
- package/src/space/space.test.ts +144 -0
- package/src/space/space.ts +199 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/test-agent-builder.ts +193 -0
- package/src/testing/test-feed-builder.ts +19 -0
- package/src/testing/util.ts +77 -0
- package/src/tests/database.test.ts +53 -0
- package/testing.d.ts +11 -0
- package/testing.js +5 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TestBuilder } from '@dxos/feed-store/testing';
|
|
2
|
+
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
3
|
+
/**
|
|
4
|
+
* Builder with default encoder and generator.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TestFeedBuilder extends TestBuilder<FeedMessage> {
|
|
7
|
+
constructor();
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=test-feed-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-feed-builder.d.ts","sourceRoot":"","sources":["../../../../src/testing/test-feed-builder.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AAIxE;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW,CAAC,WAAW,CAAC;;CAM5D"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DatabaseBackendProxy, ItemManager } from '@dxos/echo-db';
|
|
2
|
+
import { ModelFactory } from '@dxos/model-factory';
|
|
3
|
+
import { DatabaseBackendHost, DataServiceHost } from '../dbhost';
|
|
4
|
+
import { DataPipelineControllerImpl } from '../space';
|
|
5
|
+
export declare const createMemoryDatabase: (modelFactory: ModelFactory) => Promise<{
|
|
6
|
+
backend: DatabaseBackendHost;
|
|
7
|
+
itemManager: ItemManager;
|
|
8
|
+
}>;
|
|
9
|
+
export declare const createRemoteDatabaseFromDataServiceHost: (modelFactory: ModelFactory, dataServiceHost: DataServiceHost) => Promise<{
|
|
10
|
+
itemManager: ItemManager;
|
|
11
|
+
backend: DatabaseBackendProxy;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const testLocalDatabase: (create: DataPipelineControllerImpl, check?: DataPipelineControllerImpl) => Promise<void>;
|
|
14
|
+
//# sourceMappingURL=util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../../src/testing/util.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGlE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAInD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAA6C,MAAM,WAAW,CAAC;AAC5G,OAAO,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;AAEtD,eAAO,MAAM,oBAAoB,iBAAwB,YAAY;;;EAqBpE,CAAC;AAEF,eAAO,MAAM,uCAAuC,iBACpC,YAAY,mBACT,eAAe;;;EAejC,CAAC;AAEF,eAAO,MAAM,iBAAiB,WACpB,0BAA0B,UAC3B,0BAA0B,kBAgBlC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.test.d.ts","sourceRoot":"","sources":["../../../../src/tests/database.test.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxos/echo-pipeline",
|
|
3
|
+
"version": "0.1.24-next.6b1f434",
|
|
4
|
+
"description": "ECHO database.",
|
|
5
|
+
"homepage": "https://dxos.org",
|
|
6
|
+
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "info@dxos.org",
|
|
9
|
+
"main": "dist/lib/node/index.cjs",
|
|
10
|
+
"browser": {
|
|
11
|
+
"./dist/lib/node/index.cjs": "./dist/lib/browser/index.mjs",
|
|
12
|
+
"./testing.js": "./dist/lib/browser/testing/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"types": "dist/types/src/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"testing.d.ts",
|
|
17
|
+
"testing.js",
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"debug": "^4.3.3",
|
|
23
|
+
"@dxos/async": "0.1.24-next.6b1f434",
|
|
24
|
+
"@dxos/codec-protobuf": "0.1.24-next.6b1f434",
|
|
25
|
+
"@dxos/context": "0.1.24-next.6b1f434",
|
|
26
|
+
"@dxos/credentials": "0.1.24-next.6b1f434",
|
|
27
|
+
"@dxos/crypto": "0.1.24-next.6b1f434",
|
|
28
|
+
"@dxos/debug": "0.1.24-next.6b1f434",
|
|
29
|
+
"@dxos/document-model": "0.1.24-next.6b1f434",
|
|
30
|
+
"@dxos/echo-db": "0.1.24-next.6b1f434",
|
|
31
|
+
"@dxos/feed-store": "0.1.24-next.6b1f434",
|
|
32
|
+
"@dxos/hypercore": "0.1.24-next.6b1f434",
|
|
33
|
+
"@dxos/keyring": "0.1.24-next.6b1f434",
|
|
34
|
+
"@dxos/keys": "0.1.24-next.6b1f434",
|
|
35
|
+
"@dxos/log": "0.1.24-next.6b1f434",
|
|
36
|
+
"@dxos/messaging": "0.1.24-next.6b1f434",
|
|
37
|
+
"@dxos/model-factory": "0.1.24-next.6b1f434",
|
|
38
|
+
"@dxos/network-manager": "0.1.24-next.6b1f434",
|
|
39
|
+
"@dxos/node-std": "0.1.24-next.6b1f434",
|
|
40
|
+
"@dxos/protocols": "0.1.24-next.6b1f434",
|
|
41
|
+
"@dxos/random-access-storage": "0.1.24-next.6b1f434",
|
|
42
|
+
"@dxos/rpc": "0.1.24-next.6b1f434",
|
|
43
|
+
"@dxos/teleport": "0.1.24-next.6b1f434",
|
|
44
|
+
"@dxos/teleport-extension-object-sync": "0.1.24-next.6b1f434",
|
|
45
|
+
"@dxos/teleport-extension-presence": "0.1.24-next.6b1f434",
|
|
46
|
+
"@dxos/teleport-extension-replicator": "0.1.24-next.6b1f434",
|
|
47
|
+
"@dxos/timeframe": "0.1.24-next.6b1f434",
|
|
48
|
+
"@dxos/typings": "0.1.24-next.6b1f434",
|
|
49
|
+
"@dxos/util": "0.1.24-next.6b1f434"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/debug": "^4.1.7",
|
|
53
|
+
"hypercore-protocol": "^8.0.7",
|
|
54
|
+
"source-map-support": "^0.5.12",
|
|
55
|
+
"wait-for-expect": "^3.0.2"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Codec } from '@dxos/codec-protobuf';
|
|
6
|
+
import { createCodecEncoding } from '@dxos/hypercore';
|
|
7
|
+
import { schema } from '@dxos/protocols';
|
|
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);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { FeedWriter } from '@dxos/feed-store';
|
|
8
|
+
import { MaybePromise } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maps the written arguments onto a different message type.
|
|
12
|
+
*/
|
|
13
|
+
export const createMappedFeedWriter = <Source extends {}, Target extends {}>(
|
|
14
|
+
mapper: (arg: Source) => MaybePromise<Target>,
|
|
15
|
+
writer: FeedWriter<Target>
|
|
16
|
+
): FeedWriter<Source> => {
|
|
17
|
+
assert(mapper);
|
|
18
|
+
assert(writer);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
write: async (data: Source) => await writer.write(await mapper(data))
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2021 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { Stream } from '@dxos/codec-protobuf';
|
|
8
|
+
import { tagMutationsInBatch, ItemDemuxer, ItemManager } from '@dxos/echo-db';
|
|
9
|
+
import { FeedWriter } from '@dxos/feed-store';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
13
|
+
import { EchoEvent, MutationReceipt, WriteRequest } from '@dxos/protocols/proto/dxos/echo/service';
|
|
14
|
+
import { ComplexMap } from '@dxos/util';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Provides methods for DataService for a single space.
|
|
18
|
+
* A DataServiceRouter must be placed before it to route requests to different DataServiceHost instances based on space id.
|
|
19
|
+
*/
|
|
20
|
+
// TODO(burdon): Move to client-services.
|
|
21
|
+
export class DataServiceHost {
|
|
22
|
+
private readonly _clientTagMap = new ComplexMap<[feedKey: PublicKey, seq: number], string>(
|
|
23
|
+
([feedKey, seq]) => `${feedKey.toHex()}:${seq}`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly _itemManager: ItemManager,
|
|
28
|
+
private readonly _itemDemuxer: ItemDemuxer,
|
|
29
|
+
private readonly _writeStream?: FeedWriter<DataMessage>
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Real-time subscription to data objects in a space.
|
|
34
|
+
*/
|
|
35
|
+
subscribe(): Stream<EchoEvent> {
|
|
36
|
+
return new Stream(({ next, ctx }) => {
|
|
37
|
+
// send current state
|
|
38
|
+
const objects = Array.from(this._itemManager.entities.values()).map((entity) => entity.createSnapshot());
|
|
39
|
+
|
|
40
|
+
next({
|
|
41
|
+
batch: {
|
|
42
|
+
objects
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// subscribe to mutations
|
|
47
|
+
|
|
48
|
+
this._itemDemuxer.mutation.on(ctx, (mutation) => {
|
|
49
|
+
log('Object update', { mutation });
|
|
50
|
+
|
|
51
|
+
const clientTag = this._clientTagMap.get([mutation.meta.feedKey, mutation.meta.seq]);
|
|
52
|
+
// TODO(dmaretskyi): Memorąąy leak with _clientTagMap not getting cleared.
|
|
53
|
+
|
|
54
|
+
// Assign feed metadata
|
|
55
|
+
const batch = {
|
|
56
|
+
objects: [
|
|
57
|
+
{
|
|
58
|
+
...mutation.data,
|
|
59
|
+
mutations: mutation.data.mutations?.map((m, mutationIdx) => ({
|
|
60
|
+
...m,
|
|
61
|
+
meta: mutation.meta
|
|
62
|
+
})),
|
|
63
|
+
meta: mutation.meta
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Assign client tag metadata
|
|
69
|
+
if (clientTag) {
|
|
70
|
+
tagMutationsInBatch(batch, clientTag);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
next({
|
|
74
|
+
clientTag,
|
|
75
|
+
feedKey: mutation.meta.feedKey,
|
|
76
|
+
seq: mutation.meta.seq,
|
|
77
|
+
batch
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async write(request: WriteRequest): Promise<MutationReceipt> {
|
|
84
|
+
assert(this._writeStream, 'Cannot write mutations in readonly mode');
|
|
85
|
+
assert(request.batch.objects?.length === 1, 'Only single object mutations are supported');
|
|
86
|
+
|
|
87
|
+
const receipt = await this._writeStream.write({
|
|
88
|
+
object: {
|
|
89
|
+
...request.batch.objects[0],
|
|
90
|
+
mutations: request.batch.objects[0].mutations?.map((m) => ({
|
|
91
|
+
...m,
|
|
92
|
+
meta: undefined
|
|
93
|
+
})),
|
|
94
|
+
meta: undefined
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (request.clientTag) {
|
|
98
|
+
this._clientTagMap.set([receipt.feedKey, receipt.seq], request.clientTag);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return receipt;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2021 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { Stream } from '@dxos/codec-protobuf';
|
|
8
|
+
import { raise } from '@dxos/debug';
|
|
9
|
+
import { PublicKey } from '@dxos/keys';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
import {
|
|
12
|
+
DataService,
|
|
13
|
+
MutationReceipt,
|
|
14
|
+
SubscribeRequest,
|
|
15
|
+
EchoEvent,
|
|
16
|
+
WriteRequest
|
|
17
|
+
} from '@dxos/protocols/proto/dxos/echo/service';
|
|
18
|
+
import { ComplexMap } from '@dxos/util';
|
|
19
|
+
|
|
20
|
+
import { DataServiceHost } from './data-service-host';
|
|
21
|
+
|
|
22
|
+
// TODO(burdon): Clear on close.
|
|
23
|
+
export class DataServiceSubscriptions {
|
|
24
|
+
private readonly _spaces = new ComplexMap<PublicKey, DataServiceHost>(PublicKey.hash);
|
|
25
|
+
|
|
26
|
+
clear() {
|
|
27
|
+
this._spaces.clear();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
registerSpace(spaceKey: PublicKey, host: DataServiceHost) {
|
|
31
|
+
log('Registering space', { spaceKey });
|
|
32
|
+
this._spaces.set(spaceKey, host);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
unregisterSpace(spaceKey: PublicKey) {
|
|
36
|
+
log('Unregistering space', { spaceKey });
|
|
37
|
+
this._spaces.delete(spaceKey);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getDataService(spaceKey: PublicKey) {
|
|
41
|
+
return this._spaces.get(spaceKey);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Routes DataService requests to different DataServiceHost instances based on space id.
|
|
47
|
+
*/
|
|
48
|
+
// TODO(burdon): Move to client-services.
|
|
49
|
+
export class DataServiceImpl implements DataService {
|
|
50
|
+
constructor(private readonly _subscriptions: DataServiceSubscriptions) {}
|
|
51
|
+
|
|
52
|
+
subscribe(request: SubscribeRequest): Stream<EchoEvent> {
|
|
53
|
+
assert(request.spaceKey);
|
|
54
|
+
const host =
|
|
55
|
+
this._subscriptions.getDataService(request.spaceKey) ?? raise(new Error(`space not found: ${request.spaceKey}`));
|
|
56
|
+
return host.subscribe();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
write(request: WriteRequest): Promise<MutationReceipt> {
|
|
60
|
+
assert(request.spaceKey);
|
|
61
|
+
assert(request.batch);
|
|
62
|
+
const host =
|
|
63
|
+
this._subscriptions.getDataService(request.spaceKey) ?? raise(new Error(`space not found: ${request.spaceKey}`));
|
|
64
|
+
return host.write(request);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
|
|
3
|
+
//
|
|
4
|
+
// Copyright 2023 DXOS.org
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
import { EchoProcessor, ItemDemuxer, ItemDemuxerOptions, ItemManager } from '@dxos/echo-db';
|
|
8
|
+
import { FeedWriter } from '@dxos/feed-store';
|
|
9
|
+
import { ModelFactory } from '@dxos/model-factory';
|
|
10
|
+
import { DataMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
11
|
+
import { EchoSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
12
|
+
|
|
13
|
+
import { DataServiceHost } from './data-service-host';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Database backend that operates on two streams: read and write.
|
|
17
|
+
* Mutations are read from the incoming streams and applied to the ItemManager via ItemDemuxer.
|
|
18
|
+
* Write operations result in mutations being written to the outgoing stream.
|
|
19
|
+
*/
|
|
20
|
+
export class DatabaseBackendHost {
|
|
21
|
+
private _echoProcessor!: EchoProcessor;
|
|
22
|
+
private _itemManager!: ItemManager;
|
|
23
|
+
private _itemDemuxer!: ItemDemuxer;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly _outboundStream: FeedWriter<DataMessage> | undefined,
|
|
27
|
+
private readonly _snapshot?: EchoSnapshot,
|
|
28
|
+
private readonly _options: ItemDemuxerOptions = {} // TODO(burdon): Pass in factory instead?
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
get isReadOnly(): boolean {
|
|
32
|
+
return !!this._outboundStream;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get echoProcessor() {
|
|
36
|
+
return this._echoProcessor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async open(itemManager: ItemManager, modelFactory: ModelFactory) {
|
|
40
|
+
this._itemManager = itemManager;
|
|
41
|
+
this._itemManager._debugLabel = 'host';
|
|
42
|
+
|
|
43
|
+
this._itemDemuxer = new ItemDemuxer(itemManager, modelFactory, this._options);
|
|
44
|
+
this._echoProcessor = this._itemDemuxer.open();
|
|
45
|
+
|
|
46
|
+
if (this._snapshot) {
|
|
47
|
+
await this._itemDemuxer.restoreFromSnapshot(this._snapshot);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async close() {}
|
|
52
|
+
|
|
53
|
+
getWriteStream(): FeedWriter<DataMessage> | undefined {
|
|
54
|
+
return this._outboundStream;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
createSnapshot() {
|
|
58
|
+
return this._itemDemuxer.createSnapshot();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createDataServiceHost() {
|
|
62
|
+
return new DataServiceHost(this._itemManager, this._itemDemuxer, this._outboundStream ?? undefined);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { trackLeaks } from '@dxos/async';
|
|
6
|
+
import { schema } from '@dxos/protocols';
|
|
7
|
+
import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
8
|
+
import { DataObject } from '@dxos/protocols/proto/dxos/mesh/teleport/objectsync';
|
|
9
|
+
import { ObjectSync } from '@dxos/teleport-extension-object-sync';
|
|
10
|
+
|
|
11
|
+
import { SnapshotStore } from './snapshot-store';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Snapshot manager for a specific space.
|
|
15
|
+
*/
|
|
16
|
+
@trackLeaks('open', 'close')
|
|
17
|
+
export class SnapshotManager {
|
|
18
|
+
private readonly _objectSync: ObjectSync;
|
|
19
|
+
|
|
20
|
+
// prettier-ignore
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly _snapshotStore: SnapshotStore
|
|
23
|
+
) {
|
|
24
|
+
this._objectSync = new ObjectSync({
|
|
25
|
+
getObject: async (id: string) => {
|
|
26
|
+
const snapshot = await this._snapshotStore.loadSnapshot(id);
|
|
27
|
+
if (!snapshot) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
payload: schema.getCodecForType('dxos.echo.snapshot.SpaceSnapshot').encodeAsAny(snapshot)
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
setObject: async (data: DataObject) => {
|
|
36
|
+
const snapshot = schema.getCodecForType('dxos.echo.snapshot.SpaceSnapshot').decode(data.payload);
|
|
37
|
+
await this._snapshotStore.saveSnapshot(snapshot);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get objectSync() {
|
|
43
|
+
return this._objectSync;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async open() {
|
|
47
|
+
await this._objectSync.open();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async close() {
|
|
51
|
+
await this._objectSync.close();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async load(id: string): Promise<SpaceSnapshot> {
|
|
55
|
+
const local = await this._snapshotStore.loadSnapshot(id);
|
|
56
|
+
if (local) {
|
|
57
|
+
return local;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const remote = await this._objectSync.download(id);
|
|
61
|
+
return schema.getCodecForType('dxos.echo.snapshot.SpaceSnapshot').decode(remote.payload);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async store(snapshot: SpaceSnapshot): Promise<string> {
|
|
65
|
+
const id = await this._snapshotStore.saveSnapshot(snapshot);
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import expect from 'expect';
|
|
6
|
+
|
|
7
|
+
import { PublicKey } from '@dxos/keys';
|
|
8
|
+
import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
9
|
+
import { createStorage } from '@dxos/random-access-storage';
|
|
10
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
11
|
+
|
|
12
|
+
import { SnapshotStore } from './snapshot-store';
|
|
13
|
+
|
|
14
|
+
describe('SnapshotStore', () => {
|
|
15
|
+
it('should save and load snapshot', async () => {
|
|
16
|
+
const store = new SnapshotStore(createStorage().createDirectory('snapshots'));
|
|
17
|
+
|
|
18
|
+
const snapshot: SpaceSnapshot = {
|
|
19
|
+
spaceKey: PublicKey.random().asBuffer(),
|
|
20
|
+
timeframe: new Timeframe(),
|
|
21
|
+
database: {
|
|
22
|
+
items: []
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const key = await store.saveSnapshot(snapshot);
|
|
27
|
+
const loaded = await store.loadSnapshot(key);
|
|
28
|
+
expect(loaded).toEqual(snapshot);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { subtleCrypto } from '@dxos/crypto';
|
|
6
|
+
import { schema } from '@dxos/protocols';
|
|
7
|
+
import { SpaceSnapshot } from '@dxos/protocols/proto/dxos/echo/snapshot';
|
|
8
|
+
import { Directory } from '@dxos/random-access-storage';
|
|
9
|
+
|
|
10
|
+
export class SnapshotStore {
|
|
11
|
+
// prettier-ignore
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly _directory: Directory
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
async saveSnapshot(snapshot: SpaceSnapshot): Promise<string> {
|
|
17
|
+
const encoded = schema.getCodecForType('dxos.echo.snapshot.SpaceSnapshot').encode(snapshot);
|
|
18
|
+
const key = await subtleCrypto.digest('SHA-256', encoded);
|
|
19
|
+
const keyString = Buffer.from(key).toString('hex');
|
|
20
|
+
|
|
21
|
+
const file = await this._directory.getOrCreateFile(keyString);
|
|
22
|
+
try {
|
|
23
|
+
await file.write(0, Buffer.from(encoded));
|
|
24
|
+
} finally {
|
|
25
|
+
await file.close();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return keyString;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async loadSnapshot(key: string): Promise<SpaceSnapshot | undefined> {
|
|
32
|
+
const file = await this._directory.getOrCreateFile(key);
|
|
33
|
+
try {
|
|
34
|
+
const { size } = await file.stat();
|
|
35
|
+
if (size === 0) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const buffer = await file.read(0, size);
|
|
40
|
+
return schema.getCodecForType('dxos.echo.snapshot.SpaceSnapshot').decode(buffer);
|
|
41
|
+
} finally {
|
|
42
|
+
await file.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2021 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { PublicKey } from '@dxos/keys';
|
|
6
|
+
import { ItemID } from '@dxos/protocols';
|
|
7
|
+
|
|
8
|
+
// TODO(burdon): Reconcile with @dxos/errors.
|
|
9
|
+
|
|
10
|
+
export class DBError extends Error {
|
|
11
|
+
constructor(readonly code: string, message?: string, readonly context?: any) {
|
|
12
|
+
super(message ? `${code}: ${message}` : code.toString());
|
|
13
|
+
// NOTE: Restores prototype chain (https://stackoverflow.com/a/48342359).
|
|
14
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class IdentityNotInitializedError extends DBError {
|
|
19
|
+
constructor() {
|
|
20
|
+
super('IDENTITY_NOT_INITIALIZED', 'Identity not initialized.');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class InvalidInvitationError extends DBError {
|
|
25
|
+
constructor() {
|
|
26
|
+
super('INVALID_INVITATION', 'Invitation is invalid.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class InvalidStorageVersionError extends DBError {
|
|
31
|
+
constructor(expected: number, actual: number) {
|
|
32
|
+
super('INVALID_STORAGE_VERSION', 'Invalid storage version.', { expected, actual });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class SpaceNotFoundError extends DBError {
|
|
37
|
+
constructor(spaceKey: PublicKey) {
|
|
38
|
+
super('SPACE_NOT_FOUND', 'Space not found.', { spaceKey });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class EntityNotFoundError extends DBError {
|
|
43
|
+
constructor(entityId: ItemID) {
|
|
44
|
+
super('ITEM_NOT_FOUND', 'Item not found.', { entityId });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class UnknownModelError extends DBError {
|
|
49
|
+
constructor(model: string) {
|
|
50
|
+
super('UNKNOWN_MODEL', 'Unknown model.', { model });
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/index.ts
ADDED