@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.
- package/.eslintrc +28 -0
- package/.mocharc.json +5 -0
- package/README.md +298 -0
- package/TODO.md +54 -0
- package/dist/DocCollection.d.ts +44 -0
- package/dist/DocCollection.d.ts.map +1 -0
- package/dist/DocCollection.js +85 -0
- package/dist/DocHandle.d.ts +78 -0
- package/dist/DocHandle.d.ts.map +1 -0
- package/dist/DocHandle.js +227 -0
- package/dist/EphemeralData.d.ts +27 -0
- package/dist/EphemeralData.d.ts.map +1 -0
- package/dist/EphemeralData.js +28 -0
- package/dist/Repo.d.ts +30 -0
- package/dist/Repo.d.ts.map +1 -0
- package/dist/Repo.js +97 -0
- package/dist/helpers/arraysAreEqual.d.ts +2 -0
- package/dist/helpers/arraysAreEqual.d.ts.map +1 -0
- package/dist/helpers/arraysAreEqual.js +1 -0
- package/dist/helpers/eventPromise.d.ts +5 -0
- package/dist/helpers/eventPromise.d.ts.map +1 -0
- package/dist/helpers/eventPromise.js +6 -0
- package/dist/helpers/headsAreSame.d.ts +3 -0
- package/dist/helpers/headsAreSame.d.ts.map +1 -0
- package/dist/helpers/headsAreSame.js +7 -0
- package/dist/helpers/mergeArrays.d.ts +2 -0
- package/dist/helpers/mergeArrays.d.ts.map +1 -0
- package/dist/helpers/mergeArrays.js +15 -0
- package/dist/helpers/pause.d.ts +3 -0
- package/dist/helpers/pause.d.ts.map +1 -0
- package/dist/helpers/pause.js +7 -0
- package/dist/helpers/withTimeout.d.ts +9 -0
- package/dist/helpers/withTimeout.d.ts.map +1 -0
- package/dist/helpers/withTimeout.js +22 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/network/NetworkAdapter.d.ts +37 -0
- package/dist/network/NetworkAdapter.d.ts.map +1 -0
- package/dist/network/NetworkAdapter.js +4 -0
- package/dist/network/NetworkSubsystem.d.ts +23 -0
- package/dist/network/NetworkSubsystem.d.ts.map +1 -0
- package/dist/network/NetworkSubsystem.js +89 -0
- package/dist/storage/StorageAdapter.d.ts +6 -0
- package/dist/storage/StorageAdapter.d.ts.map +1 -0
- package/dist/storage/StorageAdapter.js +2 -0
- package/dist/storage/StorageSubsystem.d.ts +12 -0
- package/dist/storage/StorageSubsystem.d.ts.map +1 -0
- package/dist/storage/StorageSubsystem.js +65 -0
- package/dist/synchronizer/CollectionSynchronizer.d.ts +24 -0
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -0
- package/dist/synchronizer/CollectionSynchronizer.js +92 -0
- package/dist/synchronizer/DocSynchronizer.d.ts +18 -0
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -0
- package/dist/synchronizer/DocSynchronizer.js +136 -0
- package/dist/synchronizer/Synchronizer.d.ts +10 -0
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -0
- package/dist/synchronizer/Synchronizer.js +3 -0
- package/dist/test-utilities/adapter-tests.d.ts +21 -0
- package/dist/test-utilities/adapter-tests.d.ts.map +1 -0
- package/dist/test-utilities/adapter-tests.js +117 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/fuzz/fuzz.ts +129 -0
- package/package.json +65 -0
- package/src/DocCollection.ts +123 -0
- package/src/DocHandle.ts +386 -0
- package/src/EphemeralData.ts +46 -0
- package/src/Repo.ts +155 -0
- package/src/helpers/arraysAreEqual.ts +2 -0
- package/src/helpers/eventPromise.ts +10 -0
- package/src/helpers/headsAreSame.ts +8 -0
- package/src/helpers/mergeArrays.ts +17 -0
- package/src/helpers/pause.ts +9 -0
- package/src/helpers/withTimeout.ts +28 -0
- package/src/index.ts +22 -0
- package/src/network/NetworkAdapter.ts +54 -0
- package/src/network/NetworkSubsystem.ts +130 -0
- package/src/storage/StorageAdapter.ts +5 -0
- package/src/storage/StorageSubsystem.ts +91 -0
- package/src/synchronizer/CollectionSynchronizer.ts +112 -0
- package/src/synchronizer/DocSynchronizer.ts +182 -0
- package/src/synchronizer/Synchronizer.ts +15 -0
- package/src/test-utilities/adapter-tests.ts +163 -0
- package/src/types.ts +3 -0
- package/test/CollectionSynchronizer.test.ts +73 -0
- package/test/DocCollection.test.ts +19 -0
- package/test/DocHandle.test.ts +281 -0
- package/test/DocSynchronizer.test.ts +68 -0
- package/test/EphemeralData.test.ts +44 -0
- package/test/Network.test.ts +13 -0
- package/test/Repo.test.ts +367 -0
- package/test/StorageSubsystem.test.ts +78 -0
- package/test/helpers/DummyNetworkAdapter.ts +8 -0
- package/test/helpers/DummyStorageAdapter.ts +23 -0
- package/test/helpers/getRandomItem.ts +4 -0
- package/test/types.ts +3 -0
- package/tsconfig.json +16 -0
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|