@dxos/echo-pipeline 0.6.2-main.c33bf0c → 0.6.2-main.d41f0d2

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.
@@ -1,5 +1,5 @@
1
- import { type RequestOptions, type Stream } from '@dxos/codec-protobuf';
2
- import { type DataService, type EchoEvent, type FlushRequest, type GetDocumentHeadsRequest, type GetDocumentHeadsResponse, type HostInfo, type MutationReceipt, type ReIndexHeadsRequest, type SubscribeRequest, type SyncRepoRequest, type SyncRepoResponse, type WaitUntilHeadsReplicatedRequest, type WriteRequest } from '@dxos/protocols/proto/dxos/echo/service';
1
+ import { type RequestOptions, Stream } from '@dxos/codec-protobuf';
2
+ import { type DataService, type FlushRequest, type HostInfo, type SubscribeRequest, type BatchedDocumentUpdates, type UpdateSubscriptionRequest, type GetDocumentHeadsRequest, type GetDocumentHeadsResponse, type ReIndexHeadsRequest, type SyncRepoRequest, type SyncRepoResponse, type WaitUntilHeadsReplicatedRequest, type WriteRequest } from '@dxos/protocols/proto/dxos/echo/service';
3
3
  import { type AutomergeHost } from '../automerge';
4
4
  export type DataServiceParams = {
5
5
  automergeHost: AutomergeHost;
@@ -9,11 +9,17 @@ export type DataServiceParams = {
9
9
  * Data sync between client and services.
10
10
  */
11
11
  export declare class DataServiceImpl implements DataService {
12
+ /**
13
+ * Map of subscriptions.
14
+ * subscriptionId -> DocumentsSynchronizer
15
+ */
16
+ private readonly _subscriptions;
12
17
  private readonly _automergeHost;
13
18
  private readonly _updateIndexes;
14
19
  constructor(params: DataServiceParams);
15
- subscribe(request: SubscribeRequest): Stream<EchoEvent>;
16
- write(request: WriteRequest): Promise<MutationReceipt>;
20
+ subscribe(request: SubscribeRequest): Stream<BatchedDocumentUpdates>;
21
+ updateSubscription(request: UpdateSubscriptionRequest): Promise<void>;
22
+ write(request: WriteRequest): Promise<void>;
17
23
  flush(request: FlushRequest): Promise<void>;
18
24
  getHostInfo(request: void): Promise<HostInfo>;
19
25
  syncRepo(request: SyncRepoRequest): Stream<SyncRepoResponse>;
@@ -1 +1 @@
1
- {"version":3,"file":"data-service.d.ts","sourceRoot":"","sources":["../../../../src/db-host/data-service.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,+BAA+B,EACpC,KAAK,YAAY,EAClB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,EAAE,KAAK,aAAa,EAAmB,MAAM,cAAc,CAAC;AAEnE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAEF;;GAEG;AAEH,qBAAa,eAAgB,YAAW,WAAW;IACjD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;gBAEzC,MAAM,EAAE,iBAAiB;IAKrC,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAAC,SAAS,CAAC;IAIvD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC;IAIhD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3C,WAAW,CAAC,OAAO,EAAE,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC;IAInD,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC,gBAAgB,CAAC;IAI5D,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAiBrF,wBAAwB,CAC5B,OAAO,EAAE,+BAA+B,EACxC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,GACnC,OAAO,CAAC,IAAI,CAAC;IAIV,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAInF,aAAa;CAGpB"}
1
+ {"version":3,"file":"data-service.d.ts","sourceRoot":"","sources":["../../../../src/db-host/data-service.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,+BAA+B,EACpC,KAAK,YAAY,EAClB,MAAM,yCAAyC,CAAC;AAGjD,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAEF;;GAEG;AAEH,qBAAa,eAAgB,YAAW,WAAW;IACjD;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4C;IAE3E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;gBAEzC,MAAM,EAAE,iBAAiB;IAKrC,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAAC,sBAAsB,CAAC;IAiB9D,kBAAkB,CAAC,OAAO,EAAE,yBAAyB;IAYrD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAU3C,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3C,WAAW,CAAC,OAAO,EAAE,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC;IAInD,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC,gBAAgB,CAAC;IAI5D,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAiBrF,wBAAwB,CAC5B,OAAO,EAAE,+BAA+B,EACxC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,GACnC,OAAO,CAAC,IAAI,CAAC;IAIV,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAInF,aAAa;CAGpB"}
@@ -0,0 +1,43 @@
1
+ import { type Heads } from '@dxos/automerge/automerge';
2
+ import { type Repo, type DocHandle, type DocumentId } from '@dxos/automerge/automerge-repo';
3
+ import { Resource } from '@dxos/context';
4
+ import { type SpaceDoc } from '@dxos/echo-protocol';
5
+ import { type BatchedDocumentUpdates, type DocumentUpdate } from '@dxos/protocols/proto/dxos/echo/service';
6
+ export type DocumentsSynchronizerParams = {
7
+ repo: Repo;
8
+ sendUpdates: (updates: BatchedDocumentUpdates) => void;
9
+ };
10
+ interface DocSyncState {
11
+ handle: DocHandle<SpaceDoc>;
12
+ lastSentHead?: Heads;
13
+ clearSubscriptions?: () => void;
14
+ }
15
+ /**
16
+ * Manages a connection and replication between worker's Automerge Repo and the client's Repo.
17
+ */
18
+ export declare class DocumentsSynchronizer extends Resource {
19
+ private readonly _params;
20
+ private readonly _syncStates;
21
+ /**
22
+ * Documents that have pending updates.
23
+ * Used to batch updates.
24
+ */
25
+ private readonly _pendingUpdates;
26
+ /**
27
+ * Job that schedules if there are pending updates.
28
+ */
29
+ private _sendUpdatesJob?;
30
+ constructor(_params: DocumentsSynchronizerParams);
31
+ addDocuments(documentIds: DocumentId[]): Promise<void>;
32
+ removeDocuments(documentIds: DocumentId[]): void;
33
+ protected _open(): Promise<void>;
34
+ protected _close(): Promise<void>;
35
+ write(updates: DocumentUpdate[]): void;
36
+ private _startSync;
37
+ _subscribeForChanges(syncState: DocSyncState): void;
38
+ private _checkAndSendUpdates;
39
+ private _getPendingChanges;
40
+ private _writeMutation;
41
+ }
42
+ export {};
43
+ //# sourceMappingURL=documents-synchronizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents-synchronizer.d.ts","sourceRoot":"","sources":["../../../../src/db-host/documents-synchronizer.ts"],"names":[],"mappings":"AAKA,OAAO,EAAa,KAAK,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC5F,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAI3G,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,IAAI,CAAC;IACX,WAAW,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACxD,CAAC;AAEF,UAAU,YAAY;IACpB,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC5B,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;CACjC;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,QAAQ;IAarC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuC;IACnE;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IAEzD;;OAEG;IACH,OAAO,CAAC,eAAe,CAAC,CAA8B;gBAEzB,OAAO,EAAE,2BAA2B;IAI3D,YAAY,CAAC,WAAW,EAAE,UAAU,EAAE;IAU5C,eAAe,CAAC,WAAW,EAAE,UAAU,EAAE;cAQhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;cAMtB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAKhD,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE;IAY/B,OAAO,CAAC,UAAU;IAWlB,oBAAoB,CAAC,SAAS,EAAE,YAAY;YAS9B,oBAAoB;IAqBlC,OAAO,CAAC,kBAAkB;IAe1B,OAAO,CAAC,cAAc;CAYvB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=documents-synchronizer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents-synchronizer.test.d.ts","sourceRoot":"","sources":["../../../../src/db-host/documents-synchronizer.test.ts"],"names":[],"mappings":""}
@@ -1,4 +1,5 @@
1
1
  export * from './snapshot-manager';
2
2
  export * from './snapshot-store';
3
3
  export * from './data-service';
4
+ export * from './documents-synchronizer';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/db-host/index.ts"],"names":[],"mappings":"AAIA,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/db-host/index.ts"],"names":[],"mappings":"AAIA,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-pipeline",
3
- "version": "0.6.2-main.c33bf0c",
3
+ "version": "0.6.2-main.d41f0d2",
4
4
  "description": "ECHO database.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -39,38 +39,38 @@
39
39
  "crc-32": "^1.2.2",
40
40
  "level": "^8.0.1",
41
41
  "level-transcoder": "^1.0.1",
42
- "@dxos/async": "0.6.2-main.c33bf0c",
43
- "@dxos/automerge": "0.6.2-main.c33bf0c",
44
- "@dxos/codec-protobuf": "0.6.2-main.c33bf0c",
45
- "@dxos/context": "0.6.2-main.c33bf0c",
46
- "@dxos/credentials": "0.6.2-main.c33bf0c",
47
- "@dxos/debug": "0.6.2-main.c33bf0c",
48
- "@dxos/crypto": "0.6.2-main.c33bf0c",
49
- "@dxos/echo-protocol": "0.6.2-main.c33bf0c",
50
- "@dxos/echo-schema": "0.6.2-main.c33bf0c",
51
- "@dxos/feed-store": "0.6.2-main.c33bf0c",
52
- "@dxos/hypercore": "0.6.2-main.c33bf0c",
53
- "@dxos/indexing": "0.6.2-main.c33bf0c",
54
- "@dxos/invariant": "0.6.2-main.c33bf0c",
55
- "@dxos/keyring": "0.6.2-main.c33bf0c",
56
- "@dxos/keys": "0.6.2-main.c33bf0c",
57
- "@dxos/log": "0.6.2-main.c33bf0c",
58
- "@dxos/kv-store": "0.6.2-main.c33bf0c",
59
- "@dxos/messaging": "0.6.2-main.c33bf0c",
60
- "@dxos/network-manager": "0.6.2-main.c33bf0c",
61
- "@dxos/node-std": "0.6.2-main.c33bf0c",
62
- "@dxos/protocols": "0.6.2-main.c33bf0c",
63
- "@dxos/random-access-storage": "0.6.2-main.c33bf0c",
64
- "@dxos/teleport": "0.6.2-main.c33bf0c",
65
- "@dxos/rpc": "0.6.2-main.c33bf0c",
66
- "@dxos/teleport-extension-gossip": "0.6.2-main.c33bf0c",
67
- "@dxos/teleport-extension-object-sync": "0.6.2-main.c33bf0c",
68
- "@dxos/teleport-extension-replicator": "0.6.2-main.c33bf0c",
69
- "@dxos/teleport-extension-automerge-replicator": "0.6.2-main.c33bf0c",
70
- "@dxos/timeframe": "0.6.2-main.c33bf0c",
71
- "@dxos/tracing": "0.6.2-main.c33bf0c",
72
- "@dxos/typings": "0.6.2-main.c33bf0c",
73
- "@dxos/util": "0.6.2-main.c33bf0c"
42
+ "@dxos/automerge": "0.6.2-main.d41f0d2",
43
+ "@dxos/async": "0.6.2-main.d41f0d2",
44
+ "@dxos/codec-protobuf": "0.6.2-main.d41f0d2",
45
+ "@dxos/context": "0.6.2-main.d41f0d2",
46
+ "@dxos/debug": "0.6.2-main.d41f0d2",
47
+ "@dxos/credentials": "0.6.2-main.d41f0d2",
48
+ "@dxos/echo-protocol": "0.6.2-main.d41f0d2",
49
+ "@dxos/crypto": "0.6.2-main.d41f0d2",
50
+ "@dxos/echo-schema": "0.6.2-main.d41f0d2",
51
+ "@dxos/feed-store": "0.6.2-main.d41f0d2",
52
+ "@dxos/hypercore": "0.6.2-main.d41f0d2",
53
+ "@dxos/keyring": "0.6.2-main.d41f0d2",
54
+ "@dxos/indexing": "0.6.2-main.d41f0d2",
55
+ "@dxos/invariant": "0.6.2-main.d41f0d2",
56
+ "@dxos/keys": "0.6.2-main.d41f0d2",
57
+ "@dxos/log": "0.6.2-main.d41f0d2",
58
+ "@dxos/kv-store": "0.6.2-main.d41f0d2",
59
+ "@dxos/messaging": "0.6.2-main.d41f0d2",
60
+ "@dxos/network-manager": "0.6.2-main.d41f0d2",
61
+ "@dxos/node-std": "0.6.2-main.d41f0d2",
62
+ "@dxos/random-access-storage": "0.6.2-main.d41f0d2",
63
+ "@dxos/protocols": "0.6.2-main.d41f0d2",
64
+ "@dxos/rpc": "0.6.2-main.d41f0d2",
65
+ "@dxos/teleport": "0.6.2-main.d41f0d2",
66
+ "@dxos/teleport-extension-automerge-replicator": "0.6.2-main.d41f0d2",
67
+ "@dxos/teleport-extension-gossip": "0.6.2-main.d41f0d2",
68
+ "@dxos/teleport-extension-replicator": "0.6.2-main.d41f0d2",
69
+ "@dxos/teleport-extension-object-sync": "0.6.2-main.d41f0d2",
70
+ "@dxos/timeframe": "0.6.2-main.d41f0d2",
71
+ "@dxos/tracing": "0.6.2-main.d41f0d2",
72
+ "@dxos/util": "0.6.2-main.d41f0d2",
73
+ "@dxos/typings": "0.6.2-main.d41f0d2"
74
74
  },
75
75
  "devDependencies": {
76
76
  "fast-check": "^3.19.0",
@@ -2,25 +2,29 @@
2
2
  // Copyright 2021 DXOS.org
3
3
  //
4
4
 
5
- import { type RequestOptions, type Stream } from '@dxos/codec-protobuf';
5
+ import { type DocumentId } from '@dxos/automerge/automerge-repo';
6
+ import { type RequestOptions, Stream } from '@dxos/codec-protobuf';
7
+ import { invariant } from '@dxos/invariant';
8
+ import { log } from '@dxos/log';
6
9
  import {
7
10
  type DataService,
8
11
  type DocHeadsList,
9
- type EchoEvent,
10
12
  type FlushRequest,
13
+ type HostInfo,
14
+ type SubscribeRequest,
15
+ type BatchedDocumentUpdates,
16
+ type UpdateSubscriptionRequest,
11
17
  type GetDocumentHeadsRequest,
12
18
  type GetDocumentHeadsResponse,
13
- type HostInfo,
14
- type MutationReceipt,
15
19
  type ReIndexHeadsRequest,
16
- type SubscribeRequest,
17
20
  type SyncRepoRequest,
18
21
  type SyncRepoResponse,
19
22
  type WaitUntilHeadsReplicatedRequest,
20
23
  type WriteRequest,
21
24
  } from '@dxos/protocols/proto/dxos/echo/service';
22
25
 
23
- import { type AutomergeHost, type DocumentId } from '../automerge';
26
+ import { DocumentsSynchronizer } from './documents-synchronizer';
27
+ import { type AutomergeHost } from '../automerge';
24
28
 
25
29
  export type DataServiceParams = {
26
30
  automergeHost: AutomergeHost;
@@ -32,6 +36,12 @@ export type DataServiceParams = {
32
36
  */
33
37
  // TODO(burdon): Move to client-services.
34
38
  export class DataServiceImpl implements DataService {
39
+ /**
40
+ * Map of subscriptions.
41
+ * subscriptionId -> DocumentsSynchronizer
42
+ */
43
+ private readonly _subscriptions = new Map<string, DocumentsSynchronizer>();
44
+
35
45
  private readonly _automergeHost: AutomergeHost;
36
46
  private readonly _updateIndexes: () => Promise<void>;
37
47
 
@@ -40,12 +50,43 @@ export class DataServiceImpl implements DataService {
40
50
  this._updateIndexes = params.updateIndexes;
41
51
  }
42
52
 
43
- subscribe(request: SubscribeRequest): Stream<EchoEvent> {
44
- throw new Error('Deprecated.');
53
+ subscribe(request: SubscribeRequest): Stream<BatchedDocumentUpdates> {
54
+ return new Stream<BatchedDocumentUpdates>(({ next, ready }) => {
55
+ const synchronizer = new DocumentsSynchronizer({
56
+ repo: this._automergeHost.repo,
57
+ sendUpdates: (updates) => next(updates),
58
+ });
59
+ synchronizer
60
+ .open()
61
+ .then(() => {
62
+ this._subscriptions.set(request.subscriptionId, synchronizer);
63
+ ready();
64
+ })
65
+ .catch((err) => log.catch(err));
66
+ return () => synchronizer.close();
67
+ });
45
68
  }
46
69
 
47
- write(request: WriteRequest): Promise<MutationReceipt> {
48
- throw new Error('Deprecated.');
70
+ async updateSubscription(request: UpdateSubscriptionRequest) {
71
+ const synchronizer = this._subscriptions.get(request.subscriptionId);
72
+ invariant(synchronizer, 'Subscription not found');
73
+
74
+ if (request.addIds?.length) {
75
+ await synchronizer.addDocuments(request.addIds as DocumentId[]);
76
+ }
77
+ if (request.removeIds?.length) {
78
+ await synchronizer.removeDocuments(request.removeIds as DocumentId[]);
79
+ }
80
+ }
81
+
82
+ async write(request: WriteRequest): Promise<void> {
83
+ if (!request.updates) {
84
+ return;
85
+ }
86
+ const synchronizer = this._subscriptions.get(request.subscriptionId);
87
+ invariant(synchronizer, 'Subscription not found');
88
+
89
+ synchronizer.write(request.updates);
49
90
  }
50
91
 
51
92
  async flush(request: FlushRequest): Promise<void> {
@@ -0,0 +1,40 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { sleep } from '@dxos/async';
8
+ import { next as A } from '@dxos/automerge/automerge';
9
+ import { generateAutomergeUrl, parseAutomergeUrl, Repo } from '@dxos/automerge/automerge-repo';
10
+ import { describe, openAndClose, test } from '@dxos/test';
11
+
12
+ import { DocumentsSynchronizer } from './documents-synchronizer';
13
+
14
+ describe('DocumentsSynchronizer', () => {
15
+ test('do not get init changes for client created docs', async () => {
16
+ let counter = 0;
17
+ const serverRepo = new Repo({ network: [] });
18
+ const synchronizer = new DocumentsSynchronizer({
19
+ repo: serverRepo,
20
+ sendUpdates: () => {
21
+ counter++;
22
+ },
23
+ });
24
+ await openAndClose(synchronizer);
25
+
26
+ synchronizer.write([
27
+ {
28
+ documentId: parseAutomergeUrl(generateAutomergeUrl()).documentId,
29
+ mutation: A.save(A.from({ text: 'hello' })),
30
+ isNew: true,
31
+ },
32
+ ]);
33
+
34
+ // Wait for the changes to be processed.
35
+ await sleep(100);
36
+
37
+ // No updates should be sent.
38
+ expect(counter).to.eq(0);
39
+ });
40
+ });
@@ -0,0 +1,156 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { UpdateScheduler } from '@dxos/async';
6
+ import { next as A, type Heads } from '@dxos/automerge/automerge';
7
+ import { type Repo, type DocHandle, type DocumentId } from '@dxos/automerge/automerge-repo';
8
+ import { Resource } from '@dxos/context';
9
+ import { type SpaceDoc } from '@dxos/echo-protocol';
10
+ import { invariant } from '@dxos/invariant';
11
+ import { log } from '@dxos/log';
12
+ import { type BatchedDocumentUpdates, type DocumentUpdate } from '@dxos/protocols/proto/dxos/echo/service';
13
+
14
+ const MAX_UPDATE_FREQ = 10; // [updates/sec]
15
+
16
+ export type DocumentsSynchronizerParams = {
17
+ repo: Repo;
18
+ sendUpdates: (updates: BatchedDocumentUpdates) => void;
19
+ };
20
+
21
+ interface DocSyncState {
22
+ handle: DocHandle<SpaceDoc>;
23
+ lastSentHead?: Heads;
24
+ clearSubscriptions?: () => void;
25
+ }
26
+
27
+ /**
28
+ * Manages a connection and replication between worker's Automerge Repo and the client's Repo.
29
+ */
30
+ export class DocumentsSynchronizer extends Resource {
31
+ private readonly _syncStates = new Map<DocumentId, DocSyncState>();
32
+ /**
33
+ * Documents that have pending updates.
34
+ * Used to batch updates.
35
+ */
36
+ private readonly _pendingUpdates = new Set<DocumentId>();
37
+
38
+ /**
39
+ * Job that schedules if there are pending updates.
40
+ */
41
+ private _sendUpdatesJob?: UpdateScheduler = undefined;
42
+
43
+ constructor(private readonly _params: DocumentsSynchronizerParams) {
44
+ super();
45
+ }
46
+
47
+ async addDocuments(documentIds: DocumentId[]) {
48
+ for (const documentId of documentIds) {
49
+ const doc = this._params.repo.find(documentId as DocumentId);
50
+ await doc.whenReady();
51
+ this._startSync(doc);
52
+ this._pendingUpdates.add(doc.documentId);
53
+ }
54
+ this._sendUpdatesJob!.trigger();
55
+ }
56
+
57
+ removeDocuments(documentIds: DocumentId[]) {
58
+ for (const documentId of documentIds) {
59
+ this._syncStates.get(documentId)?.clearSubscriptions?.();
60
+ this._syncStates.delete(documentId);
61
+ this._pendingUpdates.delete(documentId);
62
+ }
63
+ }
64
+
65
+ protected override async _open(): Promise<void> {
66
+ this._sendUpdatesJob = new UpdateScheduler(this._ctx, this._checkAndSendUpdates.bind(this), {
67
+ maxFrequency: MAX_UPDATE_FREQ,
68
+ });
69
+ }
70
+
71
+ protected override async _close(): Promise<void> {
72
+ await this._sendUpdatesJob!.join();
73
+ this._syncStates.clear();
74
+ }
75
+
76
+ write(updates: DocumentUpdate[]) {
77
+ for (const { documentId, mutation, isNew } of updates) {
78
+ if (isNew) {
79
+ const doc = this._params.repo.find(documentId as DocumentId);
80
+ doc.update((doc) => A.loadIncremental(doc, mutation));
81
+ this._startSync(doc);
82
+ } else {
83
+ this._writeMutation(documentId as DocumentId, mutation);
84
+ }
85
+ }
86
+ }
87
+
88
+ private _startSync(doc: DocHandle<SpaceDoc>) {
89
+ if (this._syncStates.has(doc.documentId)) {
90
+ log.info('Document already being synced', { documentId: doc.documentId });
91
+ return;
92
+ }
93
+
94
+ const syncState: DocSyncState = { handle: doc };
95
+ this._subscribeForChanges(syncState);
96
+ this._syncStates.set(doc.documentId, syncState);
97
+ }
98
+
99
+ _subscribeForChanges(syncState: DocSyncState) {
100
+ const handler = () => {
101
+ this._pendingUpdates.add(syncState.handle.documentId);
102
+ this._sendUpdatesJob!.trigger();
103
+ };
104
+ syncState.handle.on('heads-changed', handler);
105
+ syncState.clearSubscriptions = () => syncState.handle.off('heads-changed', handler);
106
+ }
107
+
108
+ private async _checkAndSendUpdates() {
109
+ const updates: DocumentUpdate[] = [];
110
+
111
+ const docsWithPendingUpdates = Array.from(this._pendingUpdates);
112
+ this._pendingUpdates.clear();
113
+
114
+ for (const documentId of docsWithPendingUpdates) {
115
+ const update = this._getPendingChanges(documentId);
116
+ if (update) {
117
+ updates.push({
118
+ documentId,
119
+ mutation: update,
120
+ });
121
+ }
122
+ }
123
+
124
+ if (updates.length > 0) {
125
+ this._params.sendUpdates({ updates });
126
+ }
127
+ }
128
+
129
+ private _getPendingChanges(documentId: DocumentId): Uint8Array | void {
130
+ const syncState = this._syncStates.get(documentId);
131
+ invariant(syncState, 'Sync state for document not found');
132
+ const doc = syncState.handle.docSync();
133
+ if (!doc) {
134
+ return;
135
+ }
136
+ const mutation = syncState.lastSentHead ? A.saveSince(doc, syncState.lastSentHead) : A.save(doc);
137
+ if (mutation.length === 0) {
138
+ return;
139
+ }
140
+ syncState.lastSentHead = A.getHeads(doc);
141
+ return mutation;
142
+ }
143
+
144
+ private _writeMutation(documentId: DocumentId, mutation: Uint8Array) {
145
+ const syncState = this._syncStates.get(documentId);
146
+ invariant(syncState, 'Sync state for document not found');
147
+ syncState.handle.update((doc) => {
148
+ const headsBefore = A.getHeads(doc);
149
+ const newDoc = A.loadIncremental(doc, mutation);
150
+ if (A.equals(headsBefore, syncState.lastSentHead)) {
151
+ syncState.lastSentHead = A.getHeads(newDoc);
152
+ }
153
+ return newDoc;
154
+ });
155
+ }
156
+ }
@@ -5,3 +5,4 @@
5
5
  export * from './snapshot-manager';
6
6
  export * from './snapshot-store';
7
7
  export * from './data-service';
8
+ export * from './documents-synchronizer';