@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,10 @@
1
+ export type DocumentId = string & {
2
+ __documentId: true;
3
+ };
4
+ export type PeerId = string & {
5
+ __peerId: false;
6
+ };
7
+ export type ChannelId = string & {
8
+ __channelId: false;
9
+ };
10
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,YAAY,EAAE,IAAI,CAAA;CAAE,CAAA;AACxD,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,KAAK,CAAA;CAAE,CAAA;AACjD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,WAAW,EAAE,KAAK,CAAA;CAAE,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/fuzz/fuzz.ts ADDED
@@ -0,0 +1,129 @@
1
+ import assert from "assert"
2
+ import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel"
3
+ import * as Automerge from "@automerge/automerge"
4
+
5
+ import { ChannelId, DocHandle, DocumentId, PeerId, SharePolicy } from "../src"
6
+ import { eventPromise } from "../src/helpers/eventPromise.js"
7
+ import { pause } from "../src/helpers/pause.js"
8
+ import { Repo } from "../src/Repo.js"
9
+ import { DummyNetworkAdapter } from "../test/helpers/DummyNetworkAdapter.js"
10
+ import { DummyStorageAdapter } from "../test/helpers/DummyStorageAdapter.js"
11
+ import { getRandomItem } from "../test/helpers/getRandomItem.js"
12
+
13
+ interface TestDoc {
14
+ [key: string]: any
15
+ }
16
+
17
+ const setup = async () => {
18
+ // Set up three repos; connect Alice to Bob, and Bob to Charlie
19
+
20
+ const aliceBobChannel = new MessageChannel()
21
+ const bobCharlieChannel = new MessageChannel()
22
+
23
+ const { port1: aliceToBob, port2: bobToAlice } = aliceBobChannel
24
+ const { port1: bobToCharlie, port2: charlieToBob } = bobCharlieChannel
25
+
26
+ const excludedDocuments: DocumentId[] = []
27
+
28
+ const sharePolicy: SharePolicy = async (peerId, documentId) => {
29
+ if (documentId === undefined) return false
30
+
31
+ // make sure that charlie never gets excluded documents
32
+ if (excludedDocuments.includes(documentId) && peerId === "charlie")
33
+ return false
34
+
35
+ return true
36
+ }
37
+
38
+ const aliceRepo = new Repo({
39
+ network: [new MessageChannelNetworkAdapter(aliceToBob)],
40
+ peerId: "A" as PeerId,
41
+ sharePolicy,
42
+ })
43
+
44
+ const bobRepo = new Repo({
45
+ network: [
46
+ new MessageChannelNetworkAdapter(bobToAlice),
47
+ new MessageChannelNetworkAdapter(bobToCharlie),
48
+ ],
49
+ peerId: "B" as PeerId,
50
+ sharePolicy,
51
+ })
52
+
53
+ const charlieRepo = new Repo({
54
+ network: [new MessageChannelNetworkAdapter(charlieToBob)],
55
+ peerId: "C" as PeerId,
56
+ })
57
+
58
+ const aliceHandle = aliceRepo.create<TestDoc>()
59
+ aliceHandle.change(d => {
60
+ d.foo = "bar"
61
+ })
62
+
63
+ const notForCharlieHandle = aliceRepo.create<TestDoc>()
64
+ const notForCharlie = notForCharlieHandle.documentId
65
+ excludedDocuments.push(notForCharlie)
66
+ notForCharlieHandle.change(d => {
67
+ d.foo = "baz"
68
+ })
69
+
70
+ await Promise.all([
71
+ eventPromise(aliceRepo.networkSubsystem, "peer"),
72
+ eventPromise(bobRepo.networkSubsystem, "peer"),
73
+ eventPromise(charlieRepo.networkSubsystem, "peer"),
74
+ ])
75
+
76
+ const teardown = () => {
77
+ aliceBobChannel.port1.close()
78
+ bobCharlieChannel.port1.close()
79
+ }
80
+
81
+ return {
82
+ aliceRepo,
83
+ bobRepo,
84
+ charlieRepo,
85
+ aliceHandle,
86
+ notForCharlie,
87
+ teardown,
88
+ }
89
+ }
90
+
91
+ const { aliceRepo, bobRepo, charlieRepo, teardown } = await setup()
92
+
93
+ // HACK: yield to give repos time to get the one doc that aliceRepo created
94
+ await pause(50)
95
+
96
+ for (let i = 0; i < 100000; i++) {
97
+ // pick a repo
98
+ const repo = getRandomItem([aliceRepo, bobRepo, charlieRepo])
99
+ const docs = Object.values(repo.handles)
100
+ const doc = getRandomItem(docs) as DocHandle<TestDoc>
101
+
102
+ doc.change(d => {
103
+ d.timestamp = Date.now()
104
+ d.foo = { bar: Math.random().toString() }
105
+ })
106
+
107
+ await pause(0)
108
+ const a = await aliceRepo.find(doc.documentId).value()
109
+ const b = await bobRepo.find(doc.documentId).value()
110
+ const c = await charlieRepo.find(doc.documentId).value()
111
+ assert.deepStrictEqual(a, b, "A and B should be equal")
112
+ assert.deepStrictEqual(b, c, "B and C should be equal")
113
+
114
+ const bin = Automerge.save(b)
115
+ const load = Automerge.load(bin)
116
+ assert.deepStrictEqual(b, load)
117
+
118
+ console.log(
119
+ "Changes:",
120
+ Automerge.getAllChanges(a).length,
121
+ Automerge.getAllChanges(b).length,
122
+ Automerge.getAllChanges(c).length
123
+ )
124
+ }
125
+
126
+ console.log("DONE")
127
+ await pause(500)
128
+
129
+ teardown()
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@automerge/automerge-repo",
3
+ "version": "0.0.1",
4
+ "description": "A repository object to manage a collection of automerge documents",
5
+ "repository": "https://github.com/pvh/automerge-repo",
6
+ "author": "Peter van Hardenberg <pvh@pvh.ca>",
7
+ "license": "MIT",
8
+ "private": false,
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "watch": "npm-watch build",
14
+ "test:coverage": "c8 --reporter=lcov --reporter=html --reporter=text yarn test",
15
+ "test": "mocha --no-warnings --experimental-specifier-resolution=node --exit",
16
+ "test:watch": "npm-watch test",
17
+ "test:log": "cross-env DEBUG='automerge-repo:*' yarn test",
18
+ "fuzz": "ts-node --esm --experimentalSpecifierResolution=node fuzz/fuzz.ts"
19
+ },
20
+ "browser": {
21
+ "crypto": false
22
+ },
23
+ "devDependencies": {
24
+ "@types/debug": "^4.1.7",
25
+ "@types/uuid": "^8.3.4",
26
+ "@types/ws": "^8.5.3",
27
+ "@typescript-eslint/eslint-plugin": "^5.33.0",
28
+ "@typescript-eslint/parser": "^5.33.0",
29
+ "http-server": "^14.1.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@automerge/automerge": "^2.1.0-alpha.5"
33
+ },
34
+ "dependencies": {
35
+ "cbor-x": "^1.3.0",
36
+ "debug": "^4.3.4",
37
+ "eventemitter3": "^4.0.7",
38
+ "tiny-typed-emitter": "^2.1.0",
39
+ "ts-node": "^10.9.1",
40
+ "uuid": "^8.3.2",
41
+ "xstate": "^4.37.0"
42
+ },
43
+ "watch": {
44
+ "build": {
45
+ "patterns": "./src/**/*",
46
+ "extensions": [
47
+ ".ts"
48
+ ]
49
+ },
50
+ "test": {
51
+ "quiet": true,
52
+ "patterns": [
53
+ "./src/**/*",
54
+ "./test/**/*"
55
+ ],
56
+ "extensions": [
57
+ ".ts"
58
+ ]
59
+ }
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "gitHead": "e572f26ae416140b025c3ba557d9f781abbdada1"
65
+ }
@@ -0,0 +1,123 @@
1
+ import EventEmitter from "eventemitter3"
2
+ import { v4 as uuid } from "uuid"
3
+ import { DocHandle } from "./DocHandle.js"
4
+ import { type DocumentId } from "./types.js"
5
+ import { type SharePolicy } from "./Repo.js"
6
+
7
+ /**
8
+ * A DocCollection is a collection of DocHandles. It supports creating new documents and finding
9
+ * documents by ID.
10
+ * */
11
+ export class DocCollection extends EventEmitter<DocCollectionEvents> {
12
+ #handleCache: Record<DocumentId, DocHandle<any>> = {}
13
+
14
+ /** By default, we share generously with all peers. */
15
+ sharePolicy: SharePolicy = async () => true
16
+
17
+ constructor() {
18
+ super()
19
+ }
20
+
21
+ /** Returns an existing handle if we have it; creates one otherwise. */
22
+ #getHandle<T>(
23
+ /** The documentId of the handle to look up or create */
24
+ documentId: DocumentId,
25
+
26
+ /** If we know we're creating a new document, specify this so we can have access to it immediately */
27
+ isNew: boolean
28
+ ) {
29
+ // If we have the handle cached, return it
30
+ if (this.#handleCache[documentId]) return this.#handleCache[documentId]
31
+
32
+ // If not, create a new handle, cache it, and return it
33
+ const handle = new DocHandle<T>(documentId, { isNew })
34
+ this.#handleCache[documentId] = handle
35
+ return handle
36
+ }
37
+
38
+ /** Returns all the handles we have cached. */
39
+ get handles() {
40
+ return this.#handleCache
41
+ }
42
+
43
+ /**
44
+ * Creates a new document and returns a handle to it. The initial value of the document is
45
+ * an empty object `{}`. Its documentId is generated by the system. we emit a `document` event
46
+ * to advertise interest in the document.
47
+ */
48
+ create<T>(): DocHandle<T> {
49
+ // TODO:
50
+ // either
51
+ // - pass an initial value and do something like this to ensure that you get a valid initial value
52
+
53
+ // const myInitialValue = {
54
+ // tasks: [],
55
+ // filter: "all",
56
+ //
57
+ // const guaranteeInitialValue = (doc: any) => {
58
+ // if (!doc.tasks) doc.tasks = []
59
+ // if (!doc.filter) doc.filter = "all"
60
+
61
+ // return { ...myInitialValue, ...doc }
62
+ // }
63
+
64
+ // or
65
+ // - pass a "reify" function that takes a `<any>` and returns `<T>`
66
+
67
+ const documentId = uuid() as DocumentId
68
+ const handle = this.#getHandle<T>(documentId, true) as DocHandle<T>
69
+ this.emit("document", { handle })
70
+ return handle
71
+ }
72
+
73
+ /**
74
+ * Retrieves a document by id. It gets data from the local system, but also emits a `document`
75
+ * event to advertise interest in the document.
76
+ */
77
+ find<T>(
78
+ /** The documentId of the handle to retrieve */
79
+ documentId: DocumentId
80
+ ): DocHandle<T> {
81
+ // TODO: we want a way to make sure we don't yield intermediate document states during initial synchronization
82
+
83
+ // If we already have a handle, return it
84
+ if (this.#handleCache[documentId])
85
+ return this.#handleCache[documentId] as DocHandle<T>
86
+
87
+ // Otherwise, create a new handle
88
+ const handle = this.#getHandle<T>(documentId, false) as DocHandle<T>
89
+
90
+ // we don't directly initialize a value here because the StorageSubsystem and Synchronizers go
91
+ // and get the data asynchronously and block on read instead of on create
92
+
93
+ // emit a document event to advertise interest in this document
94
+ this.emit("document", { handle })
95
+
96
+ return handle
97
+ }
98
+
99
+ delete(
100
+ /** The documentId of the handle to delete */
101
+ documentId: DocumentId
102
+ ) {
103
+ const handle = this.#getHandle(documentId, false)
104
+ handle.delete()
105
+
106
+ delete this.#handleCache[documentId]
107
+ this.emit("delete-document", { documentId })
108
+ }
109
+ }
110
+
111
+ // events & payloads
112
+ interface DocCollectionEvents {
113
+ document: (arg: DocumentPayload) => void
114
+ "delete-document": (arg: DeleteDocumentPayload) => void
115
+ }
116
+
117
+ interface DocumentPayload {
118
+ handle: DocHandle<any>
119
+ }
120
+
121
+ interface DeleteDocumentPayload {
122
+ documentId: DocumentId
123
+ }