@automerge/automerge-repo-react-hooks 2.0.0-beta.1 → 2.0.0-beta.5
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/dist/helpers/DummyNetworkAdapter.d.ts +22 -0
- package/dist/helpers/DummyNetworkAdapter.d.ts.map +1 -0
- package/dist/index.js +30 -25
- package/package.json +7 -7
- package/src/helpers/DummyNetworkAdapter.ts +66 -0
- package/src/useDocHandle.ts +13 -4
- package/test/useDocHandle.test.tsx +56 -54
- package/test/useDocument.test.tsx +49 -41
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Message, NetworkAdapter, PeerId } from '@automerge/automerge-repo/slim';
|
|
2
|
+
|
|
3
|
+
export declare const pause: (t?: number) => Promise<void>;
|
|
4
|
+
export declare class DummyNetworkAdapter extends NetworkAdapter {
|
|
5
|
+
#private;
|
|
6
|
+
isReady(): boolean;
|
|
7
|
+
whenReady(): Promise<void>;
|
|
8
|
+
forceReady(): void;
|
|
9
|
+
constructor(opts?: {
|
|
10
|
+
startReady: boolean;
|
|
11
|
+
sendMessage: (_message: Message) => void;
|
|
12
|
+
});
|
|
13
|
+
connect(peerId: PeerId): void;
|
|
14
|
+
disconnect(): void;
|
|
15
|
+
peerCandidate(peerId: PeerId): void;
|
|
16
|
+
send(message: Message): void;
|
|
17
|
+
receive(message: Message): void;
|
|
18
|
+
static createConnectedPair({ latency }?: {
|
|
19
|
+
latency?: number | undefined;
|
|
20
|
+
}): DummyNetworkAdapter[];
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=DummyNetworkAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DummyNetworkAdapter.d.ts","sourceRoot":"","sources":["../../src/helpers/DummyNetworkAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAA;AAEhF,eAAO,MAAM,KAAK,GAAI,UAAK,kBACmC,CAAA;AAI9D,qBAAa,mBAAoB,SAAQ,cAAc;;IAOrD,OAAO;IAGP,SAAS;IAUT,UAAU;gBAIR,IAAI;;gCAA+C,OAAO;KAAS;IAQrE,OAAO,CAAC,MAAM,EAAE,MAAM;IAGtB,UAAU;IACV,aAAa,CAAC,MAAM,EAAE,MAAM;IAG5B,IAAI,CAAC,OAAO,EAAE,OAAO;IAGrB,OAAO,CAAC,OAAO,EAAE,OAAO;IAGxB,MAAM,CAAC,mBAAmB,CAAC,EAAE,OAAY,EAAE;;KAAK;CAajD"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import q, { createContext as I, useContext as j, useRef as $, useState as x, useEffect as _, useCallback as
|
|
1
|
+
import q, { createContext as I, useContext as j, useRef as $, useState as x, useEffect as _, useCallback as S } from "react";
|
|
2
2
|
function O(s) {
|
|
3
3
|
let l = "pending", r, f;
|
|
4
4
|
const h = s.then(
|
|
@@ -32,21 +32,26 @@ function k() {
|
|
|
32
32
|
const E = /* @__PURE__ */ new Map();
|
|
33
33
|
function N(s, { suspense: l } = { suspense: !1 }) {
|
|
34
34
|
const r = k(), f = $(), [h, p] = x();
|
|
35
|
-
let u =
|
|
36
|
-
if (
|
|
35
|
+
let u = h;
|
|
36
|
+
if (s && !u) {
|
|
37
|
+
const t = r.findWithProgress(s);
|
|
38
|
+
t.state === "ready" && (u = t.handle);
|
|
39
|
+
}
|
|
40
|
+
let n = s ? E.get(s) : void 0;
|
|
41
|
+
if (!n && s) {
|
|
37
42
|
f.current?.abort(), f.current = new AbortController();
|
|
38
|
-
const
|
|
39
|
-
|
|
43
|
+
const t = r.find(s, { signal: f.current.signal });
|
|
44
|
+
n = O(t), E.set(s, n);
|
|
40
45
|
}
|
|
41
46
|
return _(() => {
|
|
42
|
-
l || !
|
|
43
|
-
p(
|
|
47
|
+
l || !n || n.promise.then((t) => {
|
|
48
|
+
p(t);
|
|
44
49
|
}).catch(() => {
|
|
45
50
|
p(void 0);
|
|
46
51
|
});
|
|
47
|
-
}, [l,
|
|
52
|
+
}, [l, n]), u || !l || !n ? u : n.read();
|
|
48
53
|
}
|
|
49
|
-
function
|
|
54
|
+
function Q(s, l = { suspense: !1 }) {
|
|
50
55
|
const r = N(s, l), [f, h] = x(() => r?.doc()), [p, u] = x();
|
|
51
56
|
_(() => {
|
|
52
57
|
h(r?.doc());
|
|
@@ -60,7 +65,7 @@ function V(s, l = { suspense: !1 }) {
|
|
|
60
65
|
r.removeListener("change", t), r.removeListener("delete", e);
|
|
61
66
|
};
|
|
62
67
|
}, [r, s]);
|
|
63
|
-
const n =
|
|
68
|
+
const n = S(
|
|
64
69
|
(t, e) => {
|
|
65
70
|
r.change(t, e);
|
|
66
71
|
},
|
|
@@ -104,7 +109,7 @@ function T(s, { suspense: l = !1 } = {}) {
|
|
|
104
109
|
throw Promise.all(p.map((n) => n.promise));
|
|
105
110
|
return f;
|
|
106
111
|
}
|
|
107
|
-
function
|
|
112
|
+
function V(s, { suspense: l = !0 } = {}) {
|
|
108
113
|
const r = T(s, { suspense: l }), [f, h] = x(() => /* @__PURE__ */ new Map());
|
|
109
114
|
_(() => {
|
|
110
115
|
const u = /* @__PURE__ */ new Map();
|
|
@@ -133,7 +138,7 @@ function W(s, { suspense: l = !0 } = {}) {
|
|
|
133
138
|
});
|
|
134
139
|
};
|
|
135
140
|
}, [r]);
|
|
136
|
-
const p =
|
|
141
|
+
const p = S(
|
|
137
142
|
(u, n, t) => {
|
|
138
143
|
const e = r.get(u);
|
|
139
144
|
e && e.change(n, t);
|
|
@@ -142,11 +147,11 @@ function W(s, { suspense: l = !0 } = {}) {
|
|
|
142
147
|
);
|
|
143
148
|
return [f, p];
|
|
144
149
|
}
|
|
145
|
-
function
|
|
150
|
+
function H(s) {
|
|
146
151
|
return s && s.__esModule && Object.prototype.hasOwnProperty.call(s, "default") ? s.default : s;
|
|
147
152
|
}
|
|
148
153
|
var C, R;
|
|
149
|
-
function
|
|
154
|
+
function W() {
|
|
150
155
|
if (R) return C;
|
|
151
156
|
R = 1;
|
|
152
157
|
var s = q, l = function(f) {
|
|
@@ -159,11 +164,11 @@ function z() {
|
|
|
159
164
|
};
|
|
160
165
|
return C = r, C;
|
|
161
166
|
}
|
|
162
|
-
var
|
|
163
|
-
const L = /* @__PURE__ */
|
|
164
|
-
var D = { exports: {} },
|
|
165
|
-
function
|
|
166
|
-
return
|
|
167
|
+
var z = W();
|
|
168
|
+
const L = /* @__PURE__ */ H(z);
|
|
169
|
+
var D = { exports: {} }, P;
|
|
170
|
+
function B() {
|
|
171
|
+
return P || (P = 1, function(s) {
|
|
167
172
|
var l = Object.prototype.hasOwnProperty, r = "~";
|
|
168
173
|
function f() {
|
|
169
174
|
}
|
|
@@ -222,8 +227,8 @@ function G() {
|
|
|
222
227
|
y[d - 1] = arguments[d];
|
|
223
228
|
a.fn.apply(a.context, y);
|
|
224
229
|
} else {
|
|
225
|
-
var
|
|
226
|
-
for (d = 0; d <
|
|
230
|
+
var A = a.length, b;
|
|
231
|
+
for (d = 0; d < A; d++)
|
|
227
232
|
switch (a[d].once && this.removeListener(e, a[d].fn, void 0, !0), g) {
|
|
228
233
|
case 1:
|
|
229
234
|
a[d].fn.call(a[d].context);
|
|
@@ -268,8 +273,8 @@ function G() {
|
|
|
268
273
|
}, n.prototype.off = n.prototype.removeListener, n.prototype.addListener = n.prototype.on, n.prefixed = r, n.EventEmitter = n, s.exports = n;
|
|
269
274
|
}(D)), D.exports;
|
|
270
275
|
}
|
|
271
|
-
var
|
|
272
|
-
const
|
|
276
|
+
var G = B();
|
|
277
|
+
const J = /* @__PURE__ */ H(G), M = new J(), X = ({
|
|
273
278
|
handle: s,
|
|
274
279
|
localUserId: l,
|
|
275
280
|
offlineTimeout: r = 3e4,
|
|
@@ -336,8 +341,8 @@ export {
|
|
|
336
341
|
F as RepoContext,
|
|
337
342
|
N as useDocHandle,
|
|
338
343
|
T as useDocHandles,
|
|
339
|
-
|
|
340
|
-
|
|
344
|
+
Q as useDocument,
|
|
345
|
+
V as useDocuments,
|
|
341
346
|
Y as useLocalAwareness,
|
|
342
347
|
X as useRemoteAwareness,
|
|
343
348
|
k as useRepo
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo-react-hooks",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.5",
|
|
4
4
|
"description": "Hooks to access an Automerge Repo from your react app.",
|
|
5
5
|
"repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo-react-hooks",
|
|
6
6
|
"author": "Peter van Hardenberg <pvh@pvh.ca>",
|
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@automerge/automerge": "^2.2.8",
|
|
18
|
-
"@automerge/automerge-repo": "2.0.0-beta.
|
|
18
|
+
"@automerge/automerge-repo": "2.0.0-beta.5",
|
|
19
19
|
"eventemitter3": "^5.0.1",
|
|
20
|
-
"react": "^18.3.0",
|
|
21
|
-
"react-dom": "^18.3.0",
|
|
22
20
|
"react-usestateref": "^1.0.8"
|
|
23
21
|
},
|
|
24
22
|
"devDependencies": {
|
|
@@ -26,14 +24,16 @@
|
|
|
26
24
|
"@testing-library/react": "^14.0.0",
|
|
27
25
|
"eslint-plugin-react-hooks": "^5.1.0",
|
|
28
26
|
"jsdom": "^22.1.0",
|
|
27
|
+
"react": "^18.2.0",
|
|
28
|
+
"react-dom": "^18.2.0",
|
|
29
29
|
"react-error-boundary": "^5.0.0",
|
|
30
30
|
"rollup-plugin-visualizer": "^5.9.3",
|
|
31
31
|
"vite": "^6.0.7",
|
|
32
32
|
"vite-plugin-dts": "^3.9.1"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"react": "
|
|
36
|
-
"react-dom": "
|
|
35
|
+
"react": "^18.0.0",
|
|
36
|
+
"react-dom": "^18.0.0"
|
|
37
37
|
},
|
|
38
38
|
"watch": {
|
|
39
39
|
"build": {
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "0a1f8195dbe0a84c21611c5f58003b68f35b9dc1"
|
|
50
50
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Message, NetworkAdapter, PeerId } from "@automerge/automerge-repo/slim"
|
|
2
|
+
|
|
3
|
+
export const pause = (t = 0) =>
|
|
4
|
+
new Promise<void>(resolve => setTimeout(() => resolve(), t))
|
|
5
|
+
|
|
6
|
+
type SendMessageFn = (message: Message) => void
|
|
7
|
+
|
|
8
|
+
export class DummyNetworkAdapter extends NetworkAdapter {
|
|
9
|
+
#sendMessage: SendMessageFn
|
|
10
|
+
#ready = false
|
|
11
|
+
#readyResolver: ((value: void) => void) | undefined
|
|
12
|
+
#readyPromise = new Promise<void>(resolve => {
|
|
13
|
+
this.#readyResolver = resolve
|
|
14
|
+
})
|
|
15
|
+
isReady() {
|
|
16
|
+
return this.#ready
|
|
17
|
+
}
|
|
18
|
+
whenReady() {
|
|
19
|
+
return this.#readyPromise
|
|
20
|
+
}
|
|
21
|
+
#forceReady() {
|
|
22
|
+
if (!this.#ready) {
|
|
23
|
+
this.#ready = true
|
|
24
|
+
this.#readyResolver?.()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// A public wrapper for use in tests!
|
|
28
|
+
forceReady() {
|
|
29
|
+
this.#forceReady()
|
|
30
|
+
}
|
|
31
|
+
constructor(
|
|
32
|
+
opts = { startReady: true, sendMessage: (_message: Message) => {} }
|
|
33
|
+
) {
|
|
34
|
+
super()
|
|
35
|
+
if (opts.startReady) {
|
|
36
|
+
this.#forceReady()
|
|
37
|
+
}
|
|
38
|
+
this.#sendMessage = opts.sendMessage
|
|
39
|
+
}
|
|
40
|
+
connect(peerId: PeerId) {
|
|
41
|
+
this.peerId = peerId
|
|
42
|
+
}
|
|
43
|
+
disconnect() {}
|
|
44
|
+
peerCandidate(peerId: PeerId) {
|
|
45
|
+
this.emit("peer-candidate", { peerId, peerMetadata: {} })
|
|
46
|
+
}
|
|
47
|
+
send(message: Message) {
|
|
48
|
+
this.#sendMessage?.(message)
|
|
49
|
+
}
|
|
50
|
+
receive(message: Message) {
|
|
51
|
+
this.emit("message", message)
|
|
52
|
+
}
|
|
53
|
+
static createConnectedPair({ latency = 10 } = {}) {
|
|
54
|
+
const adapter1 = new DummyNetworkAdapter({
|
|
55
|
+
startReady: true,
|
|
56
|
+
sendMessage: (message: Message) =>
|
|
57
|
+
pause(latency).then(() => adapter2.receive(message)),
|
|
58
|
+
})
|
|
59
|
+
const adapter2 = new DummyNetworkAdapter({
|
|
60
|
+
startReady: true,
|
|
61
|
+
sendMessage: message =>
|
|
62
|
+
pause(latency).then(() => adapter1.receive(message)),
|
|
63
|
+
})
|
|
64
|
+
return [adapter1, adapter2]
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/useDocHandle.ts
CHANGED
|
@@ -39,6 +39,15 @@ export function useDocHandle<T>(
|
|
|
39
39
|
const controllerRef = useRef<AbortController>()
|
|
40
40
|
const [handle, setHandle] = useState<DocHandle<T> | undefined>()
|
|
41
41
|
|
|
42
|
+
let currentHandle: DocHandle<T> | undefined = handle
|
|
43
|
+
if (id && !currentHandle) {
|
|
44
|
+
// if we haven't saved a handle yet, check if one is immediately available
|
|
45
|
+
const progress = repo.findWithProgress<T>(id)
|
|
46
|
+
if (progress.state === "ready") {
|
|
47
|
+
currentHandle = progress.handle
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
let wrapper = id ? wrapperCache.get(id) : undefined
|
|
43
52
|
if (!wrapper && id) {
|
|
44
53
|
controllerRef.current?.abort()
|
|
@@ -68,9 +77,9 @@ export function useDocHandle<T>(
|
|
|
68
77
|
})
|
|
69
78
|
}, [suspense, wrapper])
|
|
70
79
|
|
|
71
|
-
if (suspense
|
|
72
|
-
return
|
|
73
|
-
} else {
|
|
74
|
-
return handle
|
|
80
|
+
if (currentHandle || !suspense || !wrapper) {
|
|
81
|
+
return currentHandle
|
|
75
82
|
}
|
|
83
|
+
|
|
84
|
+
return wrapper.read() as DocHandle<T>
|
|
76
85
|
}
|
|
@@ -13,6 +13,7 @@ import { describe, expect, it, vi } from "vitest"
|
|
|
13
13
|
import { useDocHandle } from "../src/useDocHandle"
|
|
14
14
|
import { RepoContext } from "../src/useRepo"
|
|
15
15
|
import { ErrorBoundary } from "react-error-boundary"
|
|
16
|
+
import { DummyNetworkAdapter } from "../src/helpers/DummyNetworkAdapter"
|
|
16
17
|
|
|
17
18
|
interface ExampleDoc {
|
|
18
19
|
foo: string
|
|
@@ -44,6 +45,39 @@ describe("useDocHandle", () => {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function setupPairedRepos(latency = 10) {
|
|
49
|
+
// Create two connected repos with network delay
|
|
50
|
+
const [adapterCreator, adapterFinder] =
|
|
51
|
+
DummyNetworkAdapter.createConnectedPair({
|
|
52
|
+
latency,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// TODO: fix types here
|
|
56
|
+
const repoCreator = new Repo({
|
|
57
|
+
peerId: "peer-creator" as PeerId,
|
|
58
|
+
network: [adapterCreator],
|
|
59
|
+
})
|
|
60
|
+
const repoFinder = new Repo({
|
|
61
|
+
peerId: "peer-finder" as PeerId,
|
|
62
|
+
network: [adapterFinder],
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// TODO: dummynetwork adapter should probably take care of this
|
|
66
|
+
// Initialize the network.
|
|
67
|
+
adapterCreator.peerCandidate(`peer-finder` as PeerId)
|
|
68
|
+
adapterFinder.peerCandidate(`peer-creator` as PeerId)
|
|
69
|
+
|
|
70
|
+
const wrapper = ({ children }) => {
|
|
71
|
+
return (
|
|
72
|
+
<RepoContext.Provider value={repoFinder}>
|
|
73
|
+
{children}
|
|
74
|
+
</RepoContext.Provider>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { repoCreator, repoFinder, wrapper }
|
|
79
|
+
}
|
|
80
|
+
|
|
47
81
|
const Component = ({
|
|
48
82
|
url,
|
|
49
83
|
onHandle,
|
|
@@ -133,16 +167,10 @@ describe("useDocHandle", () => {
|
|
|
133
167
|
})
|
|
134
168
|
|
|
135
169
|
it("handles slow network correctly", async () => {
|
|
136
|
-
const {
|
|
170
|
+
const { repoCreator, wrapper } = await setupPairedRepos()
|
|
171
|
+
const handleA = repoCreator.create({ foo: "A" })
|
|
137
172
|
const onHandle = vi.fn()
|
|
138
173
|
|
|
139
|
-
// Mock find to simulate slow network
|
|
140
|
-
const originalFind = repo.find.bind(repo)
|
|
141
|
-
repo.find = vi.fn().mockImplementation(async (...args) => {
|
|
142
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
143
|
-
return originalFind(...args)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
174
|
render(
|
|
147
175
|
<ErrorBoundary fallback={<div data-testid="error">Error</div>}>
|
|
148
176
|
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
@@ -163,25 +191,18 @@ describe("useDocHandle", () => {
|
|
|
163
191
|
})
|
|
164
192
|
|
|
165
193
|
// Verify callback was called with correct handle
|
|
166
|
-
expect(onHandle).toHaveBeenCalledWith(
|
|
194
|
+
expect(onHandle).toHaveBeenCalledWith(
|
|
195
|
+
expect.objectContaining({ url: handleA.url })
|
|
196
|
+
)
|
|
167
197
|
|
|
168
198
|
// Verify error boundary never rendered
|
|
169
199
|
expect(screen.queryByTestId("error")).not.toBeInTheDocument()
|
|
170
200
|
})
|
|
171
201
|
|
|
172
202
|
it("suspends while loading a handle", async () => {
|
|
173
|
-
const {
|
|
203
|
+
const { repoCreator, wrapper } = await setupPairedRepos()
|
|
204
|
+
const handleA = repoCreator.create({ foo: "A" })
|
|
174
205
|
const onHandle = vi.fn()
|
|
175
|
-
let promiseResolve: (value: DocHandle<ExampleDoc>) => void
|
|
176
|
-
|
|
177
|
-
// Mock find to return a delayed promise
|
|
178
|
-
const originalFind = repo.find.bind(repo)
|
|
179
|
-
repo.find = vi.fn().mockImplementation(
|
|
180
|
-
() =>
|
|
181
|
-
new Promise(resolve => {
|
|
182
|
-
promiseResolve = resolve
|
|
183
|
-
})
|
|
184
|
-
)
|
|
185
206
|
|
|
186
207
|
render(
|
|
187
208
|
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
@@ -194,31 +215,19 @@ describe("useDocHandle", () => {
|
|
|
194
215
|
expect(screen.getByTestId("loading")).toBeInTheDocument()
|
|
195
216
|
expect(onHandle).not.toHaveBeenCalled()
|
|
196
217
|
|
|
197
|
-
// Resolve the find
|
|
198
|
-
promiseResolve!(await originalFind(handleA.url))
|
|
199
|
-
|
|
200
218
|
// Should show content
|
|
201
219
|
await waitFor(() => {
|
|
202
|
-
expect(onHandle).toHaveBeenCalledWith(
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
expect(onHandle).toHaveBeenCalledWith(
|
|
221
|
+
expect.objectContaining({ url: handleA.url })
|
|
222
|
+
)
|
|
205
223
|
})
|
|
206
224
|
})
|
|
207
225
|
|
|
208
226
|
it("handles rapid url changes during loading", async () => {
|
|
209
|
-
const {
|
|
227
|
+
const { repoCreator, repoFinder, wrapper } = await setupPairedRepos()
|
|
228
|
+
const handleA = repoCreator.create({ foo: "A" })
|
|
229
|
+
const handleB = repoFinder.create({ foo: "B" })
|
|
210
230
|
const onHandle = vi.fn()
|
|
211
|
-
const delays: Record<string, number> = {
|
|
212
|
-
[handleA.url]: 100,
|
|
213
|
-
[handleB.url]: 50,
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Mock find to simulate different network delays
|
|
217
|
-
const originalFind = repo.find.bind(repo)
|
|
218
|
-
repo.find = vi.fn().mockImplementation(async (url: string) => {
|
|
219
|
-
await new Promise(resolve => setTimeout(resolve, delays[url]))
|
|
220
|
-
return originalFind(url)
|
|
221
|
-
})
|
|
222
231
|
|
|
223
232
|
const { rerender } = render(
|
|
224
233
|
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
@@ -237,21 +246,18 @@ describe("useDocHandle", () => {
|
|
|
237
246
|
// Should eventually resolve with B, not A
|
|
238
247
|
await waitFor(() => {
|
|
239
248
|
expect(onHandle).toHaveBeenLastCalledWith(handleB)
|
|
240
|
-
expect(onHandle).not.toHaveBeenCalledWith(
|
|
249
|
+
expect(onHandle).not.toHaveBeenCalledWith(
|
|
250
|
+
expect.objectContaining({ url: handleA.url })
|
|
251
|
+
)
|
|
241
252
|
})
|
|
242
253
|
})
|
|
243
254
|
|
|
244
255
|
describe("useDocHandle with suspense: false", () => {
|
|
245
256
|
it("returns undefined while loading then resolves to handle", async () => {
|
|
246
|
-
const {
|
|
247
|
-
const
|
|
257
|
+
const { repoCreator, wrapper } = await setupPairedRepos()
|
|
258
|
+
const handleA = repoCreator.create({ foo: "A" })
|
|
248
259
|
|
|
249
|
-
|
|
250
|
-
const originalFind = repo.find.bind(repo)
|
|
251
|
-
repo.find = vi.fn().mockImplementation(async (...args) => {
|
|
252
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
253
|
-
return originalFind(...args)
|
|
254
|
-
})
|
|
260
|
+
const onHandle = vi.fn()
|
|
255
261
|
|
|
256
262
|
const NonSuspenseComponent = ({
|
|
257
263
|
url,
|
|
@@ -274,11 +280,10 @@ describe("useDocHandle", () => {
|
|
|
274
280
|
|
|
275
281
|
// Wait for handle to load
|
|
276
282
|
await waitFor(() => {
|
|
277
|
-
expect(onHandle).
|
|
283
|
+
expect(onHandle).toHaveBeenCalledWith(
|
|
284
|
+
expect.objectContaining({ url: handleA.url })
|
|
285
|
+
)
|
|
278
286
|
})
|
|
279
|
-
|
|
280
|
-
// Restore original find implementation
|
|
281
|
-
repo.find = originalFind
|
|
282
287
|
})
|
|
283
288
|
|
|
284
289
|
it("handles unavailable documents by returning undefined", async () => {
|
|
@@ -338,9 +343,6 @@ describe("useDocHandle", () => {
|
|
|
338
343
|
// Change URL
|
|
339
344
|
rerender(<NonSuspenseComponent url={handleB.url} onHandle={onHandle} />)
|
|
340
345
|
|
|
341
|
-
// Should temporarily return to undefined
|
|
342
|
-
expect(onHandle).toHaveBeenCalledWith(undefined)
|
|
343
|
-
|
|
344
346
|
// Then resolve to new handle
|
|
345
347
|
await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleB))
|
|
346
348
|
})
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
generateAutomergeUrl,
|
|
5
5
|
PeerId,
|
|
6
6
|
Repo,
|
|
7
|
+
NetworkAdapter,
|
|
8
|
+
Message,
|
|
7
9
|
} from "@automerge/automerge-repo"
|
|
8
10
|
import { render, screen, waitFor } from "@testing-library/react"
|
|
9
11
|
import React, { Suspense } from "react"
|
|
@@ -13,7 +15,7 @@ import "@testing-library/jest-dom"
|
|
|
13
15
|
import { useDocument } from "../src/useDocument"
|
|
14
16
|
import { RepoContext } from "../src/useRepo"
|
|
15
17
|
import { ErrorBoundary } from "react-error-boundary"
|
|
16
|
-
|
|
18
|
+
import { DummyNetworkAdapter, pause } from "../src/helpers/DummyNetworkAdapter"
|
|
17
19
|
interface ExampleDoc {
|
|
18
20
|
foo: string
|
|
19
21
|
}
|
|
@@ -48,6 +50,37 @@ describe("useDocument", () => {
|
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function setupPairedRepos(latency = 10) {
|
|
54
|
+
// Create two connected repos with network delay
|
|
55
|
+
const [adapterCreator, adapterFinder] =
|
|
56
|
+
DummyNetworkAdapter.createConnectedPair({
|
|
57
|
+
latency,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const repoCreator = new Repo({
|
|
61
|
+
peerId: "peer-creator" as PeerId,
|
|
62
|
+
network: [adapterCreator],
|
|
63
|
+
})
|
|
64
|
+
const repoFinder = new Repo({
|
|
65
|
+
peerId: "peer-finder" as PeerId,
|
|
66
|
+
network: [adapterFinder],
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// TODO: dummynetwork adapter should probably take care of this
|
|
70
|
+
// Initialize the network.
|
|
71
|
+
adapterCreator.peerCandidate(`peer-finder` as PeerId)
|
|
72
|
+
adapterFinder.peerCandidate(`peer-creator` as PeerId)
|
|
73
|
+
|
|
74
|
+
const wrapper = ({ children }) => {
|
|
75
|
+
return (
|
|
76
|
+
<RepoContext.Provider value={repoFinder}>
|
|
77
|
+
{children}
|
|
78
|
+
</RepoContext.Provider>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { repoCreator, repoFinder, wrapper }
|
|
83
|
+
}
|
|
51
84
|
const Component = ({
|
|
52
85
|
url,
|
|
53
86
|
onDoc,
|
|
@@ -71,13 +104,9 @@ describe("useDocument", () => {
|
|
|
71
104
|
{ wrapper }
|
|
72
105
|
)
|
|
73
106
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Wait for content to appear and check it's correct
|
|
78
|
-
await waitFor(() => {
|
|
79
|
-
expect(screen.getByTestId("content")).toHaveTextContent("A")
|
|
80
|
-
})
|
|
107
|
+
// Because this document is already loaded locally (we made it)
|
|
108
|
+
// we should see results immediately.
|
|
109
|
+
expect(screen.getByTestId("content")).toHaveTextContent("A")
|
|
81
110
|
|
|
82
111
|
// Now check our spy got called with the document
|
|
83
112
|
expect(onDoc).toHaveBeenCalledWith({ foo: "A" })
|
|
@@ -95,9 +124,7 @@ describe("useDocument", () => {
|
|
|
95
124
|
)
|
|
96
125
|
|
|
97
126
|
// Wait for initial render
|
|
98
|
-
|
|
99
|
-
expect(screen.getByTestId("content")).toHaveTextContent("A")
|
|
100
|
-
})
|
|
127
|
+
expect(screen.getByTestId("content")).toHaveTextContent("A")
|
|
101
128
|
expect(onDoc).toHaveBeenCalledWith({ foo: "A" })
|
|
102
129
|
|
|
103
130
|
// Change the document
|
|
@@ -156,9 +183,7 @@ describe("useDocument", () => {
|
|
|
156
183
|
)
|
|
157
184
|
|
|
158
185
|
// Wait for first document
|
|
159
|
-
|
|
160
|
-
expect(screen.getByTestId("content")).toHaveTextContent("A")
|
|
161
|
-
})
|
|
186
|
+
expect(screen.getByTestId("content")).toHaveTextContent("A")
|
|
162
187
|
expect(onDoc).toHaveBeenCalledWith({ foo: "A" })
|
|
163
188
|
|
|
164
189
|
// Switch to second document
|
|
@@ -169,9 +194,7 @@ describe("useDocument", () => {
|
|
|
169
194
|
)
|
|
170
195
|
|
|
171
196
|
// Should show loading then new content
|
|
172
|
-
|
|
173
|
-
expect(screen.getByTestId("content")).toHaveTextContent("B")
|
|
174
|
-
})
|
|
197
|
+
expect(screen.getByTestId("content")).toHaveTextContent("B")
|
|
175
198
|
expect(onDoc).toHaveBeenCalledWith({ foo: "B" })
|
|
176
199
|
})
|
|
177
200
|
|
|
@@ -202,28 +225,23 @@ describe("useDocument", () => {
|
|
|
202
225
|
|
|
203
226
|
// Test slow-loading document
|
|
204
227
|
it("should handle slow-loading documents", async () => {
|
|
205
|
-
const {
|
|
206
|
-
const onDoc = vi.fn()
|
|
228
|
+
const { repoCreator, wrapper } = setupPairedRepos()
|
|
207
229
|
|
|
208
|
-
// Create
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
repo.find = vi.fn().mockImplementation(async (...args) => {
|
|
212
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
213
|
-
return originalFind(...args)
|
|
214
|
-
})
|
|
230
|
+
// Create document in first repo
|
|
231
|
+
const handle = repoCreator.create({ foo: "slow" })
|
|
232
|
+
const onDoc = vi.fn()
|
|
215
233
|
|
|
216
234
|
render(
|
|
217
235
|
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
218
|
-
<Component url={
|
|
236
|
+
<Component url={handle.url} onDoc={onDoc} />
|
|
219
237
|
</Suspense>,
|
|
220
238
|
{ wrapper }
|
|
221
239
|
)
|
|
222
240
|
|
|
223
|
-
// Should show loading state
|
|
241
|
+
// Should show loading state initially
|
|
224
242
|
expect(screen.getByTestId("loading")).toBeInTheDocument()
|
|
225
243
|
|
|
226
|
-
// Eventually shows content
|
|
244
|
+
// Eventually shows content after network delay
|
|
227
245
|
await waitFor(() => {
|
|
228
246
|
expect(screen.getByTestId("content")).toHaveTextContent("slow")
|
|
229
247
|
})
|
|
@@ -263,17 +281,10 @@ describe("useDocument", () => {
|
|
|
263
281
|
|
|
264
282
|
// Test document changes during loading
|
|
265
283
|
it("should handle document changes while loading", async () => {
|
|
266
|
-
const { wrapper,
|
|
284
|
+
const { wrapper, repoCreator } = setupPairedRepos()
|
|
267
285
|
const onDoc = vi.fn()
|
|
268
286
|
|
|
269
|
-
const handle =
|
|
270
|
-
let resolveFind: (value: any) => void
|
|
271
|
-
const originalFind = repo.find.bind(repo)
|
|
272
|
-
repo.find = vi.fn().mockImplementation(async (...args) => {
|
|
273
|
-
return new Promise(resolve => {
|
|
274
|
-
resolveFind = resolve
|
|
275
|
-
})
|
|
276
|
-
})
|
|
287
|
+
const handle = repoCreator.create({ foo: "initial" })
|
|
277
288
|
|
|
278
289
|
render(
|
|
279
290
|
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
@@ -285,9 +296,6 @@ describe("useDocument", () => {
|
|
|
285
296
|
// Modify document while it's still loading
|
|
286
297
|
handle.change(doc => (doc.foo = "changed"))
|
|
287
298
|
|
|
288
|
-
// Resolve the find
|
|
289
|
-
resolveFind!(await originalFind(handle.url))
|
|
290
|
-
|
|
291
299
|
// Should show final state
|
|
292
300
|
await waitFor(() => {
|
|
293
301
|
expect(screen.getByTestId("content")).toHaveTextContent("changed")
|