@automerge/automerge-repo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.eslintrc +28 -0
  2. package/.mocharc.json +5 -0
  3. package/README.md +298 -0
  4. package/TODO.md +54 -0
  5. package/dist/DocCollection.d.ts +44 -0
  6. package/dist/DocCollection.d.ts.map +1 -0
  7. package/dist/DocCollection.js +85 -0
  8. package/dist/DocHandle.d.ts +78 -0
  9. package/dist/DocHandle.d.ts.map +1 -0
  10. package/dist/DocHandle.js +227 -0
  11. package/dist/EphemeralData.d.ts +27 -0
  12. package/dist/EphemeralData.d.ts.map +1 -0
  13. package/dist/EphemeralData.js +28 -0
  14. package/dist/Repo.d.ts +30 -0
  15. package/dist/Repo.d.ts.map +1 -0
  16. package/dist/Repo.js +97 -0
  17. package/dist/helpers/arraysAreEqual.d.ts +2 -0
  18. package/dist/helpers/arraysAreEqual.d.ts.map +1 -0
  19. package/dist/helpers/arraysAreEqual.js +1 -0
  20. package/dist/helpers/eventPromise.d.ts +5 -0
  21. package/dist/helpers/eventPromise.d.ts.map +1 -0
  22. package/dist/helpers/eventPromise.js +6 -0
  23. package/dist/helpers/headsAreSame.d.ts +3 -0
  24. package/dist/helpers/headsAreSame.d.ts.map +1 -0
  25. package/dist/helpers/headsAreSame.js +7 -0
  26. package/dist/helpers/mergeArrays.d.ts +2 -0
  27. package/dist/helpers/mergeArrays.d.ts.map +1 -0
  28. package/dist/helpers/mergeArrays.js +15 -0
  29. package/dist/helpers/pause.d.ts +3 -0
  30. package/dist/helpers/pause.d.ts.map +1 -0
  31. package/dist/helpers/pause.js +7 -0
  32. package/dist/helpers/withTimeout.d.ts +9 -0
  33. package/dist/helpers/withTimeout.d.ts.map +1 -0
  34. package/dist/helpers/withTimeout.js +22 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +10 -0
  38. package/dist/network/NetworkAdapter.d.ts +37 -0
  39. package/dist/network/NetworkAdapter.d.ts.map +1 -0
  40. package/dist/network/NetworkAdapter.js +4 -0
  41. package/dist/network/NetworkSubsystem.d.ts +23 -0
  42. package/dist/network/NetworkSubsystem.d.ts.map +1 -0
  43. package/dist/network/NetworkSubsystem.js +89 -0
  44. package/dist/storage/StorageAdapter.d.ts +6 -0
  45. package/dist/storage/StorageAdapter.d.ts.map +1 -0
  46. package/dist/storage/StorageAdapter.js +2 -0
  47. package/dist/storage/StorageSubsystem.d.ts +12 -0
  48. package/dist/storage/StorageSubsystem.d.ts.map +1 -0
  49. package/dist/storage/StorageSubsystem.js +65 -0
  50. package/dist/synchronizer/CollectionSynchronizer.d.ts +24 -0
  51. package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -0
  52. package/dist/synchronizer/CollectionSynchronizer.js +92 -0
  53. package/dist/synchronizer/DocSynchronizer.d.ts +18 -0
  54. package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -0
  55. package/dist/synchronizer/DocSynchronizer.js +136 -0
  56. package/dist/synchronizer/Synchronizer.d.ts +10 -0
  57. package/dist/synchronizer/Synchronizer.d.ts.map +1 -0
  58. package/dist/synchronizer/Synchronizer.js +3 -0
  59. package/dist/test-utilities/adapter-tests.d.ts +21 -0
  60. package/dist/test-utilities/adapter-tests.d.ts.map +1 -0
  61. package/dist/test-utilities/adapter-tests.js +117 -0
  62. package/dist/types.d.ts +10 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +1 -0
  65. package/fuzz/fuzz.ts +129 -0
  66. package/package.json +65 -0
  67. package/src/DocCollection.ts +123 -0
  68. package/src/DocHandle.ts +386 -0
  69. package/src/EphemeralData.ts +46 -0
  70. package/src/Repo.ts +155 -0
  71. package/src/helpers/arraysAreEqual.ts +2 -0
  72. package/src/helpers/eventPromise.ts +10 -0
  73. package/src/helpers/headsAreSame.ts +8 -0
  74. package/src/helpers/mergeArrays.ts +17 -0
  75. package/src/helpers/pause.ts +9 -0
  76. package/src/helpers/withTimeout.ts +28 -0
  77. package/src/index.ts +22 -0
  78. package/src/network/NetworkAdapter.ts +54 -0
  79. package/src/network/NetworkSubsystem.ts +130 -0
  80. package/src/storage/StorageAdapter.ts +5 -0
  81. package/src/storage/StorageSubsystem.ts +91 -0
  82. package/src/synchronizer/CollectionSynchronizer.ts +112 -0
  83. package/src/synchronizer/DocSynchronizer.ts +182 -0
  84. package/src/synchronizer/Synchronizer.ts +15 -0
  85. package/src/test-utilities/adapter-tests.ts +163 -0
  86. package/src/types.ts +3 -0
  87. package/test/CollectionSynchronizer.test.ts +73 -0
  88. package/test/DocCollection.test.ts +19 -0
  89. package/test/DocHandle.test.ts +281 -0
  90. package/test/DocSynchronizer.test.ts +68 -0
  91. package/test/EphemeralData.test.ts +44 -0
  92. package/test/Network.test.ts +13 -0
  93. package/test/Repo.test.ts +367 -0
  94. package/test/StorageSubsystem.test.ts +78 -0
  95. package/test/helpers/DummyNetworkAdapter.ts +8 -0
  96. package/test/helpers/DummyStorageAdapter.ts +23 -0
  97. package/test/helpers/getRandomItem.ts +4 -0
  98. package/test/types.ts +3 -0
  99. package/tsconfig.json +16 -0
@@ -0,0 +1,227 @@
1
+ import * as A from "@automerge/automerge";
2
+ import debug from "debug";
3
+ import EventEmitter from "eventemitter3";
4
+ import { assign, createMachine, interpret, } from "xstate";
5
+ import { waitFor } from "xstate/lib/waitFor.js";
6
+ import { headsAreSame } from "./helpers/headsAreSame.js";
7
+ import { pause } from "./helpers/pause.js";
8
+ import { withTimeout, TimeoutError } from "./helpers/withTimeout.js";
9
+ /** DocHandle is a wrapper around a single Automerge document that lets us listen for changes. */
10
+ export class DocHandle//
11
+ extends EventEmitter {
12
+ documentId;
13
+ #log;
14
+ #machine;
15
+ #timeoutDelay;
16
+ constructor(documentId, { isNew = false, timeoutDelay = 700000 } = {}) {
17
+ super();
18
+ this.documentId = documentId;
19
+ this.#timeoutDelay = timeoutDelay;
20
+ this.#log = debug(`automerge-repo:dochandle:${documentId.slice(0, 5)}`);
21
+ // initial doc
22
+ const doc = A.init({
23
+ patchCallback: (patches, { before, after }) => this.emit("patch", { handle: this, patches, before, after }),
24
+ });
25
+ /**
26
+ * Internally we use a state machine to orchestrate document loading and/or syncing, in order to
27
+ * avoid requesting data we already have, or surfacing intermediate values to the consumer.
28
+ *
29
+ * ┌─────────┐ ┌────────────┐
30
+ * ┌───────┐ ┌──FIND──┤ loading ├─REQUEST──►│ requesting ├─UPDATE──┐
31
+ * │ idle ├──┤ └───┬─────┘ └────────────┘ │
32
+ * └───────┘ │ │ └─►┌─────────┐
33
+ * │ └───────LOAD───────────────────────────────►│ ready │
34
+ * └──CREATE───────────────────────────────────────────────►└─────────┘
35
+ */
36
+ this.#machine = interpret(createMachine({
37
+ predictableActionArguments: true,
38
+ id: "docHandle",
39
+ initial: IDLE,
40
+ context: { documentId, doc },
41
+ states: {
42
+ idle: {
43
+ on: {
44
+ // If we're creating a new document, we don't need to load anything
45
+ CREATE: { target: READY },
46
+ // If we're accessing an existing document, we need to request it from storage
47
+ // and/or the network
48
+ FIND: { target: LOADING },
49
+ DELETE: { actions: "onDelete", target: DELETED },
50
+ },
51
+ },
52
+ loading: {
53
+ on: {
54
+ // LOAD is called by the Repo if the document is found in storage
55
+ LOAD: { actions: "onLoad", target: READY },
56
+ // REQUEST is called by the Repo if the document is not found in storage
57
+ REQUEST: { target: REQUESTING },
58
+ DELETE: { actions: "onDelete", target: DELETED },
59
+ },
60
+ },
61
+ requesting: {
62
+ on: {
63
+ // UPDATE is called by the Repo when we receive changes from the network
64
+ UPDATE: { actions: "onUpdate" },
65
+ // REQUEST_COMPLETE is called from `onUpdate` when the doc has been fully loaded from the network
66
+ REQUEST_COMPLETE: { target: READY },
67
+ DELETE: { actions: "onDelete", target: DELETED },
68
+ },
69
+ },
70
+ ready: {
71
+ on: {
72
+ // UPDATE is called by the Repo when we receive changes from the network
73
+ UPDATE: { actions: "onUpdate", target: READY },
74
+ DELETE: { actions: "onDelete", target: DELETED },
75
+ },
76
+ },
77
+ error: {},
78
+ deleted: {},
79
+ },
80
+ }, {
81
+ actions: {
82
+ /** Apply the binary changes from storage and put the updated doc on context */
83
+ onLoad: assign((context, { payload }) => {
84
+ const { binary } = payload;
85
+ const { doc } = context;
86
+ const newDoc = A.loadIncremental(doc, binary);
87
+ return { doc: newDoc };
88
+ }),
89
+ /** Put the updated doc on context; if it's different, emit a `change` event */
90
+ onUpdate: assign((context, { payload }) => {
91
+ const { doc: oldDoc } = context;
92
+ const { callback } = payload;
93
+ const newDoc = callback(oldDoc);
94
+ const docChanged = !headsAreSame(newDoc, oldDoc);
95
+ if (docChanged) {
96
+ this.emit("change", { handle: this, doc: newDoc });
97
+ if (!this.isReady()) {
98
+ this.#machine.send(REQUEST_COMPLETE);
99
+ }
100
+ }
101
+ return { doc: newDoc };
102
+ }),
103
+ onDelete: assign(() => {
104
+ this.emit("delete", { handle: this });
105
+ return { doc: undefined };
106
+ }),
107
+ },
108
+ }))
109
+ .onTransition(({ value: state }, { type: event }) => this.#log(`${event} → ${state}`, this.#doc))
110
+ .start();
111
+ this.#machine.send(isNew ? CREATE : FIND);
112
+ }
113
+ get doc() {
114
+ if (!this.isReady()) {
115
+ throw new Error(`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`);
116
+ }
117
+ return this.#doc;
118
+ }
119
+ // PRIVATE
120
+ /** Returns the current document */
121
+ get #doc() {
122
+ return this.#machine?.getSnapshot().context.doc;
123
+ }
124
+ /** Returns the docHandle's state (READY, etc.) */
125
+ get #state() {
126
+ return this.#machine?.getSnapshot().value;
127
+ }
128
+ /** Returns a promise that resolves when the docHandle is in one of the given states */
129
+ #statePromise(awaitStates) {
130
+ if (!Array.isArray(awaitStates))
131
+ awaitStates = [awaitStates];
132
+ return Promise.any(awaitStates.map(state => waitFor(this.#machine, s => s.matches(state))));
133
+ }
134
+ // PUBLIC
135
+ isReady = () => this.#state === READY;
136
+ isReadyOrRequesting = () => this.#state === READY || this.#state === REQUESTING;
137
+ isDeleted = () => this.#state === DELETED;
138
+ /**
139
+ * Returns the current document, waiting for the handle to be ready if necessary.
140
+ */
141
+ async value(awaitStates = [READY]) {
142
+ await pause(); // yield one tick because reasons
143
+ try {
144
+ // wait for the document to enter one of the desired states
145
+ await withTimeout(this.#statePromise(awaitStates), this.#timeoutDelay);
146
+ }
147
+ catch (error) {
148
+ if (error instanceof TimeoutError)
149
+ throw new Error(`DocHandle: timed out loading ${this.documentId}`);
150
+ else
151
+ throw error;
152
+ }
153
+ // Return the document
154
+ return this.#doc;
155
+ }
156
+ async loadAttemptedValue() {
157
+ return this.value([READY, REQUESTING]);
158
+ }
159
+ /** `load` is called by the repo when the document is found in storage */
160
+ load(binary) {
161
+ if (binary.length) {
162
+ this.#machine.send(LOAD, { payload: { binary } });
163
+ }
164
+ }
165
+ /** `update` is called by the repo when we receive changes from the network */
166
+ update(callback) {
167
+ this.#machine.send(UPDATE, { payload: { callback } });
168
+ }
169
+ /** `change` is called by the repo when the document is changed locally */
170
+ change(callback, options = {}) {
171
+ if (!this.isReady()) {
172
+ throw new Error(`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`);
173
+ }
174
+ this.#machine.send(UPDATE, {
175
+ payload: {
176
+ callback: (doc) => {
177
+ return A.change(doc, options, callback);
178
+ },
179
+ },
180
+ });
181
+ }
182
+ changeAt(heads, callback, options = {}) {
183
+ if (!this.isReady()) {
184
+ throw new Error(`DocHandle#${this.documentId} is not ready. Check \`handle.isReady()\` before accessing the document.`);
185
+ }
186
+ this.#machine.send(UPDATE, {
187
+ payload: {
188
+ callback: (doc) => {
189
+ return A.changeAt(doc, heads, options, callback);
190
+ },
191
+ },
192
+ });
193
+ }
194
+ /** `request` is called by the repo when the document is not found in storage */
195
+ request() {
196
+ if (this.#state === LOADING)
197
+ this.#machine.send(REQUEST);
198
+ }
199
+ /** `delete` is called by the repo when the document is deleted */
200
+ delete() {
201
+ this.#machine.send(DELETE);
202
+ }
203
+ }
204
+ // STATE MACHINE TYPES
205
+ // state
206
+ export const HandleState = {
207
+ IDLE: "idle",
208
+ LOADING: "loading",
209
+ REQUESTING: "requesting",
210
+ READY: "ready",
211
+ ERROR: "error",
212
+ DELETED: "deleted",
213
+ };
214
+ // events
215
+ export const Event = {
216
+ CREATE: "CREATE",
217
+ LOAD: "LOAD",
218
+ FIND: "FIND",
219
+ REQUEST: "REQUEST",
220
+ REQUEST_COMPLETE: "REQUEST_COMPLETE",
221
+ UPDATE: "UPDATE",
222
+ TIMEOUT: "TIMEOUT",
223
+ DELETE: "DELETE",
224
+ };
225
+ // CONSTANTS
226
+ const { IDLE, LOADING, REQUESTING, READY, ERROR, DELETED } = HandleState;
227
+ const { CREATE, LOAD, FIND, REQUEST, UPDATE, TIMEOUT, DELETE, REQUEST_COMPLETE, } = Event;
@@ -0,0 +1,27 @@
1
+ import EventEmitter from "eventemitter3";
2
+ import { ChannelId, PeerId } from "./index.js";
3
+ import { MessagePayload } from "./network/NetworkAdapter.js";
4
+ /**
5
+ * EphemeralData provides a mechanism to broadcast short-lived data — cursor positions, presence,
6
+ * heartbeats, etc. — that is useful in the moment but not worth persisting.
7
+ */
8
+ export declare class EphemeralData extends EventEmitter<EphemeralDataMessageEvents> {
9
+ /** Broadcast an ephemeral message */
10
+ broadcast(channelId: ChannelId, message: unknown): void;
11
+ /** Receive an ephemeral message */
12
+ receive(senderId: PeerId, grossChannelId: ChannelId, message: Uint8Array): void;
13
+ }
14
+ export interface EphemeralDataPayload {
15
+ channelId: ChannelId;
16
+ peerId: PeerId;
17
+ data: {
18
+ peerId: PeerId;
19
+ channelId: ChannelId;
20
+ data: unknown;
21
+ };
22
+ }
23
+ export type EphemeralDataMessageEvents = {
24
+ message: (event: MessagePayload) => void;
25
+ data: (event: EphemeralDataPayload) => void;
26
+ };
27
+ //# sourceMappingURL=EphemeralData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EphemeralData.d.ts","sourceRoot":"","sources":["../src/EphemeralData.ts"],"names":[],"mappings":"AACA,OAAO,YAAY,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAE5D;;;GAGG;AACH,qBAAa,aAAc,SAAQ,YAAY,CAAC,0BAA0B,CAAC;IACzE,qCAAqC;IACrC,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO;IAWhD,mCAAmC;IACnC,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU;CASzE;AAID,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAA;CAC9D;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAA;IACxC,IAAI,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAA;CAC5C,CAAA"}
@@ -0,0 +1,28 @@
1
+ import { decode, encode } from "cbor-x";
2
+ import EventEmitter from "eventemitter3";
3
+ /**
4
+ * EphemeralData provides a mechanism to broadcast short-lived data — cursor positions, presence,
5
+ * heartbeats, etc. — that is useful in the moment but not worth persisting.
6
+ */
7
+ export class EphemeralData extends EventEmitter {
8
+ /** Broadcast an ephemeral message */
9
+ broadcast(channelId, message) {
10
+ const messageBytes = encode(message);
11
+ this.emit("message", {
12
+ targetId: "*",
13
+ channelId: ("m/" + channelId),
14
+ message: messageBytes,
15
+ broadcast: true,
16
+ });
17
+ }
18
+ /** Receive an ephemeral message */
19
+ receive(senderId, grossChannelId, message) {
20
+ const data = decode(message);
21
+ const channelId = grossChannelId.slice(2);
22
+ this.emit("data", {
23
+ peerId: senderId,
24
+ channelId,
25
+ data,
26
+ });
27
+ }
28
+ }
package/dist/Repo.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { DocCollection } from "./DocCollection.js";
2
+ import { EphemeralData } from "./EphemeralData.js";
3
+ import { NetworkAdapter } from "./network/NetworkAdapter.js";
4
+ import { NetworkSubsystem } from "./network/NetworkSubsystem.js";
5
+ import { StorageAdapter } from "./storage/StorageAdapter.js";
6
+ import { StorageSubsystem } from "./storage/StorageSubsystem.js";
7
+ import { DocumentId, PeerId } from "./types.js";
8
+ /** A Repo is a DocCollection with networking, syncing, and storage capabilities. */
9
+ export declare class Repo extends DocCollection {
10
+ #private;
11
+ networkSubsystem: NetworkSubsystem;
12
+ storageSubsystem?: StorageSubsystem;
13
+ ephemeralData: EphemeralData;
14
+ constructor({ storage, network, peerId, sharePolicy }: RepoConfig);
15
+ }
16
+ export interface RepoConfig {
17
+ /** Our unique identifier */
18
+ peerId?: PeerId;
19
+ /** A storage adapter can be provided, or not */
20
+ storage?: StorageAdapter;
21
+ /** One or more network adapters must be provided */
22
+ network: NetworkAdapter[];
23
+ /**
24
+ * Normal peers typically share generously with everyone (meaning we sync all our documents with
25
+ * all peers). A server only syncs documents that a peer explicitly requests by ID.
26
+ */
27
+ sharePolicy?: SharePolicy;
28
+ }
29
+ export type SharePolicy = (peerId: PeerId, documentId?: DocumentId) => Promise<boolean>;
30
+ //# sourceMappingURL=Repo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Repo.d.ts","sourceRoot":"","sources":["../src/Repo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAEhE,OAAO,EAAa,UAAU,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAM1D,oFAAoF;AACpF,qBAAa,IAAK,SAAQ,aAAa;;IAGrC,gBAAgB,EAAE,gBAAgB,CAAA;IAClC,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC,aAAa,EAAE,aAAa,CAAA;gBAEhB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,UAAU;CA+GlE;AAED,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,gDAAgD;IAChD,OAAO,CAAC,EAAE,cAAc,CAAA;IAExB,oDAAoD;IACpD,OAAO,EAAE,cAAc,EAAE,CAAA;IAEzB;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED,MAAM,MAAM,WAAW,GAAG,CACxB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,UAAU,KACpB,OAAO,CAAC,OAAO,CAAC,CAAA"}
package/dist/Repo.js ADDED
@@ -0,0 +1,97 @@
1
+ import { DocCollection } from "./DocCollection.js";
2
+ import { EphemeralData } from "./EphemeralData.js";
3
+ import { NetworkSubsystem } from "./network/NetworkSubsystem.js";
4
+ import { StorageSubsystem } from "./storage/StorageSubsystem.js";
5
+ import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
6
+ import debug from "debug";
7
+ const SYNC_CHANNEL = "sync_channel";
8
+ /** A Repo is a DocCollection with networking, syncing, and storage capabilities. */
9
+ export class Repo extends DocCollection {
10
+ #log;
11
+ networkSubsystem;
12
+ storageSubsystem;
13
+ ephemeralData;
14
+ constructor({ storage, network, peerId, sharePolicy }) {
15
+ super();
16
+ this.#log = debug(`automerge-repo:repo`);
17
+ this.sharePolicy = sharePolicy ?? this.sharePolicy;
18
+ // DOC COLLECTION
19
+ // The `document` event is fired by the DocCollection any time we create a new document or look
20
+ // up a document by ID. We listen for it in order to wire up storage and network synchronization.
21
+ this.on("document", async ({ handle }) => {
22
+ if (storageSubsystem) {
23
+ // Save when the document changes
24
+ handle.on("change", async ({ handle }) => {
25
+ const doc = await handle.value();
26
+ storageSubsystem.save(handle.documentId, doc);
27
+ });
28
+ // Try to load from disk
29
+ const binary = await storageSubsystem.loadBinary(handle.documentId);
30
+ handle.load(binary);
31
+ }
32
+ handle.request();
33
+ // Register the document with the synchronizer. This advertises our interest in the document.
34
+ synchronizer.addDocument(handle.documentId);
35
+ });
36
+ this.on("delete-document", ({ documentId }) => {
37
+ // TODO Pass the delete on to the network
38
+ // synchronizer.removeDocument(documentId)
39
+ if (storageSubsystem) {
40
+ storageSubsystem.remove(documentId);
41
+ }
42
+ });
43
+ // SYNCHRONIZER
44
+ // The synchronizer uses the network subsystem to keep documents in sync with peers.
45
+ const synchronizer = new CollectionSynchronizer(this);
46
+ // When the synchronizer emits sync messages, send them to peers
47
+ synchronizer.on("message", ({ targetId, channelId, message, broadcast }) => {
48
+ this.#log(`sending sync message to ${targetId}`);
49
+ networkSubsystem.sendMessage(targetId, channelId, message, broadcast);
50
+ });
51
+ // STORAGE
52
+ // The storage subsystem has access to some form of persistence, and deals with save and loading documents.
53
+ const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined;
54
+ this.storageSubsystem = storageSubsystem;
55
+ // NETWORK
56
+ // The network subsystem deals with sending and receiving messages to and from peers.
57
+ const networkSubsystem = new NetworkSubsystem(network, peerId);
58
+ this.networkSubsystem = networkSubsystem;
59
+ // When we get a new peer, register it with the synchronizer
60
+ networkSubsystem.on("peer", async ({ peerId }) => {
61
+ this.#log("peer connected", { peerId });
62
+ synchronizer.addPeer(peerId);
63
+ });
64
+ // When a peer disconnects, remove it from the synchronizer
65
+ networkSubsystem.on("peer-disconnected", ({ peerId }) => {
66
+ synchronizer.removePeer(peerId);
67
+ });
68
+ // Handle incoming messages
69
+ networkSubsystem.on("message", async (msg) => {
70
+ const { senderId, channelId, message } = msg;
71
+ // TODO: this demands a more principled way of associating channels with recipients
72
+ // Ephemeral channel ids start with "m/"
73
+ if (channelId.startsWith("m/")) {
74
+ // Ephemeral message
75
+ this.#log(`receiving ephemeral message from ${senderId}`);
76
+ ephemeralData.receive(senderId, channelId, message);
77
+ }
78
+ else {
79
+ // Sync message
80
+ this.#log(`receiving sync message from ${senderId}`);
81
+ await synchronizer.receiveSyncMessage(senderId, channelId, message);
82
+ }
83
+ });
84
+ // We establish a special channel for sync messages
85
+ networkSubsystem.join(SYNC_CHANNEL);
86
+ // EPHEMERAL DATA
87
+ // The ephemeral data subsystem uses the network to send and receive messages that are not
88
+ // persisted to storage, e.g. cursor position, presence, etc.
89
+ const ephemeralData = new EphemeralData();
90
+ this.ephemeralData = ephemeralData;
91
+ // Send ephemeral messages to peers
92
+ ephemeralData.on("message", ({ targetId, channelId, message, broadcast }) => {
93
+ this.#log(`sending ephemeral message to ${targetId}`);
94
+ networkSubsystem.sendMessage(targetId, channelId, message, broadcast);
95
+ });
96
+ }
97
+ }
@@ -0,0 +1,2 @@
1
+ export declare const arraysAreEqual: <T>(a: T[], b: T[]) => boolean;
2
+ //# sourceMappingURL=arraysAreEqual.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arraysAreEqual.d.ts","sourceRoot":"","sources":["../../src/helpers/arraysAreEqual.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,gCACiD,CAAA"}
@@ -0,0 +1 @@
1
+ export const arraysAreEqual = (a, b) => a.length === b.length && a.every((element, index) => element === b[index]);
@@ -0,0 +1,5 @@
1
+ import EventEmitter from "eventemitter3";
2
+ /** Returns a promise that resolves when the given event is emitted on the given emitter. */
3
+ export declare const eventPromise: (emitter: EventEmitter, event: string) => Promise<any>;
4
+ export declare const eventPromises: (emitters: EventEmitter[], event: string) => Promise<any[]>;
5
+ //# sourceMappingURL=eventPromise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventPromise.d.ts","sourceRoot":"","sources":["../../src/helpers/eventPromise.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAA;AAExC,4FAA4F;AAC5F,eAAO,MAAM,YAAY,YAAa,YAAY,SAAS,MAAM,iBACE,CAAA;AAEnE,eAAO,MAAM,aAAa,aAAc,YAAY,EAAE,SAAS,MAAM,mBAGpE,CAAA"}
@@ -0,0 +1,6 @@
1
+ /** Returns a promise that resolves when the given event is emitted on the given emitter. */
2
+ export const eventPromise = (emitter, event) => new Promise(resolve => emitter.once(event, d => resolve(d)));
3
+ export const eventPromises = (emitters, event) => {
4
+ const promises = emitters.map(emitter => eventPromise(emitter, event));
5
+ return Promise.all(promises);
6
+ };
@@ -0,0 +1,3 @@
1
+ import * as A from "@automerge/automerge";
2
+ export declare const headsAreSame: <T>(a: A.unstable.Doc<T>, b: A.unstable.Doc<T>) => boolean;
3
+ //# sourceMappingURL=headsAreSame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headsAreSame.d.ts","sourceRoot":"","sources":["../../src/helpers/headsAreSame.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,sBAAsB,CAAA;AAGzC,eAAO,MAAM,YAAY,4DAIxB,CAAA"}
@@ -0,0 +1,7 @@
1
+ import * as A from "@automerge/automerge";
2
+ import { arraysAreEqual } from "./arraysAreEqual.js";
3
+ export const headsAreSame = (a, b) => {
4
+ const aHeads = A.getHeads(a);
5
+ const bHeads = A.getHeads(b);
6
+ return arraysAreEqual(aHeads, bHeads);
7
+ };
@@ -0,0 +1,2 @@
1
+ export declare function mergeArrays(myArrays: Uint8Array[]): Uint8Array;
2
+ //# sourceMappingURL=mergeArrays.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeArrays.d.ts","sourceRoot":"","sources":["../../src/helpers/mergeArrays.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,EAAE,cAgBjD"}
@@ -0,0 +1,15 @@
1
+ export function mergeArrays(myArrays) {
2
+ // Get the total length of all arrays.
3
+ let length = 0;
4
+ myArrays.forEach(item => {
5
+ length += item.length;
6
+ });
7
+ // Create a new array with total length and merge all source arrays.
8
+ const mergedArray = new Uint8Array(length);
9
+ let offset = 0;
10
+ myArrays.forEach(item => {
11
+ mergedArray.set(item, offset);
12
+ offset += item.length;
13
+ });
14
+ return mergedArray;
15
+ }
@@ -0,0 +1,3 @@
1
+ export declare const pause: (t?: number) => Promise<void>;
2
+ export declare function rejectOnTimeout<T>(promise: Promise<T>, millis: number): Promise<T>;
3
+ //# sourceMappingURL=pause.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pause.d.ts","sourceRoot":"","sources":["../../src/helpers/pause.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,+BAC4C,CAAA;AAE9D,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAKlF"}
@@ -0,0 +1,7 @@
1
+ export const pause = (t = 0) => new Promise(resolve => setTimeout(() => resolve(), t));
2
+ export function rejectOnTimeout(promise, millis) {
3
+ return Promise.race([
4
+ promise,
5
+ pause(millis).then(() => { throw new Error("timeout exceeded"); }),
6
+ ]);
7
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * If `promise` is resolved before `t` ms elapse, the timeout is cleared and the result of the
3
+ * promise is returned. If the timeout ends first, a `TimeoutError` is thrown.
4
+ */
5
+ export declare const withTimeout: <T>(promise: Promise<T>, t: number) => Promise<T>;
6
+ export declare class TimeoutError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ //# sourceMappingURL=withTimeout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withTimeout.d.ts","sourceRoot":"","sources":["../../src/helpers/withTimeout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW,8BAEnB,MAAM,eAcV,CAAA;AAED,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * If `promise` is resolved before `t` ms elapse, the timeout is cleared and the result of the
3
+ * promise is returned. If the timeout ends first, a `TimeoutError` is thrown.
4
+ */
5
+ export const withTimeout = async (promise, t) => {
6
+ let timeoutId;
7
+ const timeoutPromise = new Promise((_, reject) => {
8
+ timeoutId = setTimeout(() => reject(new TimeoutError(`withTimeout: timed out after ${t}ms`)), t);
9
+ });
10
+ try {
11
+ return await Promise.race([promise, timeoutPromise]);
12
+ }
13
+ finally {
14
+ clearTimeout(timeoutId);
15
+ }
16
+ };
17
+ export class TimeoutError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "TimeoutError";
21
+ }
22
+ }
@@ -0,0 +1,13 @@
1
+ export { DocCollection } from "./DocCollection.js";
2
+ export { DocHandle, HandleState } from "./DocHandle.js";
3
+ export type { DocHandleChangePayload, DocHandleMessagePayload, DocHandlePatchPayload, } from "./DocHandle.js";
4
+ export { NetworkAdapter } from "./network/NetworkAdapter.js";
5
+ export type { InboundMessagePayload, MessagePayload, OpenPayload, PeerCandidatePayload, PeerDisconnectedPayload, } from "./network/NetworkAdapter.js";
6
+ export { NetworkSubsystem } from "./network/NetworkSubsystem.js";
7
+ export { Repo, type SharePolicy } from "./Repo.js";
8
+ export { StorageAdapter } from "./storage/StorageAdapter.js";
9
+ export { StorageSubsystem } from "./storage/StorageSubsystem.js";
10
+ export { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
11
+ export * from "./types.js";
12
+ export * from "./test-utilities/adapter-tests.js";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACvD,YAAY,EACV,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,YAAY,EACV,qBAAqB,EACrB,cAAc,EACd,WAAW,EACX,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,6BAA6B,CAAA;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAChE,OAAO,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAA;AACjF,cAAc,YAAY,CAAA;AAC1B,cAAc,mCAAmC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { DocCollection } from "./DocCollection.js";
2
+ export { DocHandle, HandleState } from "./DocHandle.js";
3
+ export { NetworkAdapter } from "./network/NetworkAdapter.js";
4
+ export { NetworkSubsystem } from "./network/NetworkSubsystem.js";
5
+ export { Repo } from "./Repo.js";
6
+ export { StorageAdapter } from "./storage/StorageAdapter.js";
7
+ export { StorageSubsystem } from "./storage/StorageSubsystem.js";
8
+ export { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js";
9
+ export * from "./types.js";
10
+ export * from "./test-utilities/adapter-tests.js";
@@ -0,0 +1,37 @@
1
+ import EventEmitter from "eventemitter3";
2
+ import { PeerId, ChannelId } from "../types.js";
3
+ export declare abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> {
4
+ peerId?: PeerId;
5
+ abstract connect(url?: string): void;
6
+ abstract sendMessage(peerId: PeerId, channelId: ChannelId, message: Uint8Array, broadcast: boolean): void;
7
+ abstract join(channelId: ChannelId): void;
8
+ abstract leave(channelId: ChannelId): void;
9
+ }
10
+ export interface NetworkAdapterEvents {
11
+ open: (payload: OpenPayload) => void;
12
+ close: () => void;
13
+ "peer-candidate": (payload: PeerCandidatePayload) => void;
14
+ "peer-disconnected": (payload: PeerDisconnectedPayload) => void;
15
+ message: (payload: InboundMessagePayload) => void;
16
+ }
17
+ export interface OpenPayload {
18
+ network: NetworkAdapter;
19
+ }
20
+ export interface PeerCandidatePayload {
21
+ peerId: PeerId;
22
+ channelId: ChannelId;
23
+ }
24
+ export interface MessagePayload {
25
+ targetId: PeerId;
26
+ channelId: ChannelId;
27
+ message: Uint8Array;
28
+ broadcast: boolean;
29
+ }
30
+ export interface InboundMessagePayload extends MessagePayload {
31
+ type?: string;
32
+ senderId: PeerId;
33
+ }
34
+ export interface PeerDisconnectedPayload {
35
+ peerId: PeerId;
36
+ }
37
+ //# sourceMappingURL=NetworkAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NetworkAdapter.d.ts","sourceRoot":"","sources":["../../src/network/NetworkAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE/C,8BAAsB,cAAe,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAEpC,QAAQ,CAAC,WAAW,CAClB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,OAAO,GACjB,IAAI;IAEP,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAEzC,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;CAC3C;AAID,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAA;IACpC,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,gBAAgB,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IACzD,mBAAmB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAC/D,OAAO,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAA;CAClD;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,cAAc,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,SAAS,CAAA;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,SAAS,CAAA;IACpB,OAAO,EAAE,UAAU,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAA;CACf"}
@@ -0,0 +1,4 @@
1
+ import EventEmitter from "eventemitter3";
2
+ export class NetworkAdapter extends EventEmitter {
3
+ peerId; // hmmm, maybe not
4
+ }