@automerge/automerge-repo-react-hooks 2.3.0-alpha.0 → 2.3.0
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/useSet.d.ts +2 -0
- package/dist/helpers/useSet.d.ts.map +1 -0
- package/dist/index.js +215 -205
- package/dist/useDocHandles.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/helpers/useSet.ts +19 -0
- package/src/useDocHandle.ts +2 -2
- package/src/useDocHandles.ts +5 -3
- package/test/helpers/useSet.test.tsx +82 -0
- package/test/useDocHandle.test.tsx +36 -5
- package/test/useDocHandles.test.tsx +297 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSet.d.ts","sourceRoot":"","sources":["../../src/helpers/useSet.ts"],"names":[],"mappings":"AAEA,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAY5C"}
|
package/dist/index.js
CHANGED
|
@@ -1,232 +1,242 @@
|
|
|
1
|
-
import q, { createContext as I, useContext as j, useRef as $, useState as
|
|
1
|
+
import q, { createContext as I, useContext as j, useRef as $, useState as E, useEffect as g, useCallback as R } from "react";
|
|
2
2
|
function O(o) {
|
|
3
|
-
let
|
|
3
|
+
let i = "pending", n, f;
|
|
4
4
|
const h = o.then(
|
|
5
5
|
(p) => {
|
|
6
|
-
|
|
6
|
+
i = "success", n = p;
|
|
7
7
|
},
|
|
8
8
|
(p) => {
|
|
9
|
-
|
|
9
|
+
i = "error", f = p;
|
|
10
10
|
}
|
|
11
11
|
);
|
|
12
12
|
return {
|
|
13
13
|
promise: o,
|
|
14
14
|
read() {
|
|
15
|
-
switch (
|
|
15
|
+
switch (i) {
|
|
16
16
|
case "pending":
|
|
17
17
|
throw h;
|
|
18
18
|
case "error":
|
|
19
|
-
throw
|
|
19
|
+
throw f;
|
|
20
20
|
case "success":
|
|
21
|
-
return
|
|
21
|
+
return n;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
const
|
|
26
|
+
const z = I(null);
|
|
27
27
|
function k() {
|
|
28
|
-
const o = j(
|
|
28
|
+
const o = j(z);
|
|
29
29
|
if (!o) throw new Error("Repo was not found on RepoContext.");
|
|
30
30
|
return o;
|
|
31
31
|
}
|
|
32
32
|
const x = /* @__PURE__ */ new Map();
|
|
33
|
-
function
|
|
34
|
-
const
|
|
33
|
+
function F(o, { suspense: i } = { suspense: !1 }) {
|
|
34
|
+
const n = k(), f = $(), [h, p] = E();
|
|
35
35
|
let u = h;
|
|
36
36
|
if (o && !u) {
|
|
37
|
-
const
|
|
38
|
-
|
|
37
|
+
const e = n.findWithProgress(o);
|
|
38
|
+
e.state === "ready" && (u = e.handle);
|
|
39
39
|
}
|
|
40
|
-
let
|
|
41
|
-
if (!
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
40
|
+
let r = o ? x.get(o) : void 0;
|
|
41
|
+
if (!r && o) {
|
|
42
|
+
f.current?.abort(), f.current = new AbortController();
|
|
43
|
+
const e = n.find(o, { signal: f.current.signal });
|
|
44
|
+
r = O(e), x.set(o, r);
|
|
45
45
|
}
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
p(
|
|
46
|
+
return g(() => {
|
|
47
|
+
u || i || !r || r.promise.then((e) => {
|
|
48
|
+
p(e);
|
|
49
49
|
}).catch(() => {
|
|
50
50
|
p(void 0);
|
|
51
51
|
});
|
|
52
|
-
}, [
|
|
52
|
+
}, [u, i, r]), u || !i || !r ? u : r.read();
|
|
53
53
|
}
|
|
54
|
-
function
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
h(
|
|
58
|
-
}, [
|
|
59
|
-
if (!
|
|
54
|
+
function X(o, i = { suspense: !1 }) {
|
|
55
|
+
const n = F(o, i), [f, h] = E(() => n?.doc()), [p, u] = E();
|
|
56
|
+
g(() => {
|
|
57
|
+
h(n?.doc());
|
|
58
|
+
}, [n]), g(() => {
|
|
59
|
+
if (!n)
|
|
60
60
|
return;
|
|
61
|
-
const
|
|
61
|
+
const e = () => h(n.doc()), t = () => {
|
|
62
62
|
u(new Error(`Document ${o} was deleted`));
|
|
63
63
|
};
|
|
64
|
-
return
|
|
65
|
-
|
|
64
|
+
return n.on("change", e), n.on("delete", t), () => {
|
|
65
|
+
n.removeListener("change", e), n.removeListener("delete", t);
|
|
66
66
|
};
|
|
67
|
-
}, [
|
|
68
|
-
const
|
|
69
|
-
(
|
|
70
|
-
|
|
67
|
+
}, [n, o]);
|
|
68
|
+
const r = R(
|
|
69
|
+
(e, t) => {
|
|
70
|
+
n.change(e, t);
|
|
71
71
|
},
|
|
72
|
-
[
|
|
72
|
+
[n]
|
|
73
73
|
);
|
|
74
74
|
if (p)
|
|
75
75
|
throw p;
|
|
76
|
-
return
|
|
76
|
+
return f ? [f, r] : [void 0, () => {
|
|
77
77
|
}];
|
|
78
78
|
}
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
function N(o) {
|
|
80
|
+
const [i, n] = E(() => new Set(o));
|
|
81
|
+
return g(() => {
|
|
82
|
+
const f = new Set(o);
|
|
83
|
+
W(i, f) || n(f);
|
|
84
|
+
}, [i, o]), i;
|
|
85
|
+
}
|
|
86
|
+
function W(o, i) {
|
|
87
|
+
return o.size === i.size && Array.from(o).every((n) => i.has(n));
|
|
88
|
+
}
|
|
89
|
+
function T(o, { suspense: i = !1 } = {}) {
|
|
90
|
+
const n = N(o), f = k(), [h, p] = E(() => {
|
|
91
|
+
const e = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const t of n.values()) {
|
|
93
|
+
let s;
|
|
84
94
|
try {
|
|
85
|
-
|
|
95
|
+
s = f.findWithProgress(t);
|
|
86
96
|
} catch {
|
|
87
97
|
continue;
|
|
88
98
|
}
|
|
89
|
-
|
|
99
|
+
s.state === "ready" && e.set(t, s.handle);
|
|
90
100
|
}
|
|
91
|
-
return
|
|
92
|
-
}),
|
|
93
|
-
for (const
|
|
94
|
-
let t =
|
|
95
|
-
if (!
|
|
101
|
+
return e;
|
|
102
|
+
}), u = [], r = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const e of n.values()) {
|
|
104
|
+
let t = h.get(e), s = x.get(e);
|
|
105
|
+
if (!s)
|
|
96
106
|
try {
|
|
97
|
-
const
|
|
98
|
-
|
|
107
|
+
const c = f.find(e);
|
|
108
|
+
s = O(c), x.set(e, s);
|
|
99
109
|
} catch {
|
|
100
110
|
continue;
|
|
101
111
|
}
|
|
102
112
|
try {
|
|
103
|
-
t ??=
|
|
104
|
-
} catch (
|
|
105
|
-
|
|
113
|
+
t ??= s.read(), r.set(e, t);
|
|
114
|
+
} catch (c) {
|
|
115
|
+
c instanceof Promise ? u.push(s) : r.set(e, void 0);
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
(
|
|
111
|
-
|
|
118
|
+
if (g(() => {
|
|
119
|
+
u.length > 0 ? Promise.allSettled(u.map((e) => e.promise)).then(
|
|
120
|
+
(e) => {
|
|
121
|
+
e.forEach((t) => {
|
|
112
122
|
if (t.status === "fulfilled") {
|
|
113
|
-
const
|
|
114
|
-
|
|
123
|
+
const s = t.value;
|
|
124
|
+
r.set(s.url, s);
|
|
115
125
|
}
|
|
116
|
-
}),
|
|
126
|
+
}), p(r);
|
|
117
127
|
}
|
|
118
|
-
) :
|
|
119
|
-
}, [
|
|
120
|
-
throw Promise.all(
|
|
121
|
-
return
|
|
128
|
+
) : p(r);
|
|
129
|
+
}, [i, n]), i && u.length > 0)
|
|
130
|
+
throw Promise.all(u.map((e) => e.promise));
|
|
131
|
+
return h;
|
|
122
132
|
}
|
|
123
|
-
function
|
|
124
|
-
const
|
|
133
|
+
function Y(o, { suspense: i = !0 } = {}) {
|
|
134
|
+
const n = T(o, { suspense: i }), [f, h] = E(() => {
|
|
125
135
|
const u = /* @__PURE__ */ new Map();
|
|
126
|
-
return
|
|
127
|
-
const
|
|
128
|
-
|
|
136
|
+
return n.forEach((r) => {
|
|
137
|
+
const e = r?.url;
|
|
138
|
+
e && u.set(e, r?.doc());
|
|
129
139
|
}), u;
|
|
130
140
|
});
|
|
131
|
-
|
|
141
|
+
g(() => {
|
|
132
142
|
const u = /* @__PURE__ */ new Map();
|
|
133
|
-
return
|
|
134
|
-
if (
|
|
135
|
-
const
|
|
143
|
+
return n.forEach((r, e) => {
|
|
144
|
+
if (r) {
|
|
145
|
+
const t = () => {
|
|
136
146
|
h((s) => {
|
|
137
147
|
const c = new Map(s);
|
|
138
|
-
return c.set(
|
|
148
|
+
return c.set(e, r.doc()), c;
|
|
139
149
|
});
|
|
140
150
|
};
|
|
141
151
|
h((s) => {
|
|
142
152
|
const c = new Map(s);
|
|
143
|
-
return c.set(
|
|
144
|
-
}),
|
|
153
|
+
return c.set(e, r.doc()), c;
|
|
154
|
+
}), r.on("change", t), u.set(e, t);
|
|
145
155
|
}
|
|
146
|
-
}), h((
|
|
147
|
-
const
|
|
148
|
-
for (const [
|
|
149
|
-
|
|
150
|
-
return
|
|
156
|
+
}), h((r) => {
|
|
157
|
+
const e = new Map(r);
|
|
158
|
+
for (const [t] of e)
|
|
159
|
+
n.has(t) || e.delete(t);
|
|
160
|
+
return e;
|
|
151
161
|
}), () => {
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
162
|
+
n.forEach((r, e) => {
|
|
163
|
+
const t = u.get(e);
|
|
164
|
+
r && t && r.removeListener("change", t);
|
|
155
165
|
});
|
|
156
166
|
};
|
|
157
|
-
}, [
|
|
158
|
-
const p =
|
|
159
|
-
(u,
|
|
160
|
-
const
|
|
161
|
-
|
|
167
|
+
}, [n]);
|
|
168
|
+
const p = R(
|
|
169
|
+
(u, r, e) => {
|
|
170
|
+
const t = n.get(u);
|
|
171
|
+
t && t.change(r, e);
|
|
162
172
|
},
|
|
163
|
-
[
|
|
173
|
+
[n]
|
|
164
174
|
);
|
|
165
|
-
return [
|
|
175
|
+
return [f, p];
|
|
166
176
|
}
|
|
167
|
-
function
|
|
177
|
+
function A(o) {
|
|
168
178
|
return o && o.__esModule && Object.prototype.hasOwnProperty.call(o, "default") ? o.default : o;
|
|
169
179
|
}
|
|
170
|
-
var
|
|
171
|
-
function
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
var o = q,
|
|
175
|
-
return typeof
|
|
176
|
-
},
|
|
177
|
-
var h = o.useState(
|
|
178
|
-
|
|
180
|
+
var S, M;
|
|
181
|
+
function B() {
|
|
182
|
+
if (M) return S;
|
|
183
|
+
M = 1;
|
|
184
|
+
var o = q, i = function(f) {
|
|
185
|
+
return typeof f == "function";
|
|
186
|
+
}, n = function(f) {
|
|
187
|
+
var h = o.useState(f), p = h[0], u = h[1], r = o.useRef(p), e = o.useCallback(function(t) {
|
|
188
|
+
r.current = i(t) ? t(r.current) : t, u(r.current);
|
|
179
189
|
}, []);
|
|
180
|
-
return [p,
|
|
190
|
+
return [p, e, r];
|
|
181
191
|
};
|
|
182
|
-
return
|
|
192
|
+
return S = n, S;
|
|
183
193
|
}
|
|
184
|
-
var
|
|
185
|
-
const
|
|
186
|
-
var
|
|
187
|
-
function
|
|
188
|
-
return
|
|
189
|
-
var
|
|
190
|
-
function
|
|
194
|
+
var G = B();
|
|
195
|
+
const D = /* @__PURE__ */ A(G);
|
|
196
|
+
var C = { exports: {} }, P;
|
|
197
|
+
function J() {
|
|
198
|
+
return P || (P = 1, function(o) {
|
|
199
|
+
var i = Object.prototype.hasOwnProperty, n = "~";
|
|
200
|
+
function f() {
|
|
191
201
|
}
|
|
192
|
-
Object.create && (
|
|
193
|
-
function h(
|
|
194
|
-
this.fn =
|
|
202
|
+
Object.create && (f.prototype = /* @__PURE__ */ Object.create(null), new f().__proto__ || (n = !1));
|
|
203
|
+
function h(e, t, s) {
|
|
204
|
+
this.fn = e, this.context = t, this.once = s || !1;
|
|
195
205
|
}
|
|
196
|
-
function p(
|
|
206
|
+
function p(e, t, s, c, w) {
|
|
197
207
|
if (typeof s != "function")
|
|
198
208
|
throw new TypeError("The listener must be a function");
|
|
199
|
-
var v = new h(s, c ||
|
|
200
|
-
return
|
|
209
|
+
var v = new h(s, c || e, w), l = n ? n + t : t;
|
|
210
|
+
return e._events[l] ? e._events[l].fn ? e._events[l] = [e._events[l], v] : e._events[l].push(v) : (e._events[l] = v, e._eventsCount++), e;
|
|
201
211
|
}
|
|
202
|
-
function u(
|
|
203
|
-
--
|
|
212
|
+
function u(e, t) {
|
|
213
|
+
--e._eventsCount === 0 ? e._events = new f() : delete e._events[t];
|
|
204
214
|
}
|
|
205
|
-
function
|
|
206
|
-
this._events = new
|
|
215
|
+
function r() {
|
|
216
|
+
this._events = new f(), this._eventsCount = 0;
|
|
207
217
|
}
|
|
208
|
-
|
|
209
|
-
var
|
|
210
|
-
if (this._eventsCount === 0) return
|
|
218
|
+
r.prototype.eventNames = function() {
|
|
219
|
+
var t = [], s, c;
|
|
220
|
+
if (this._eventsCount === 0) return t;
|
|
211
221
|
for (c in s = this._events)
|
|
212
|
-
|
|
213
|
-
return Object.getOwnPropertySymbols ?
|
|
214
|
-
},
|
|
215
|
-
var s =
|
|
222
|
+
i.call(s, c) && t.push(n ? c.slice(1) : c);
|
|
223
|
+
return Object.getOwnPropertySymbols ? t.concat(Object.getOwnPropertySymbols(s)) : t;
|
|
224
|
+
}, r.prototype.listeners = function(t) {
|
|
225
|
+
var s = n ? n + t : t, c = this._events[s];
|
|
216
226
|
if (!c) return [];
|
|
217
227
|
if (c.fn) return [c.fn];
|
|
218
|
-
for (var w = 0, v = c.length,
|
|
219
|
-
|
|
220
|
-
return
|
|
221
|
-
},
|
|
222
|
-
var s =
|
|
228
|
+
for (var w = 0, v = c.length, l = new Array(v); w < v; w++)
|
|
229
|
+
l[w] = c[w].fn;
|
|
230
|
+
return l;
|
|
231
|
+
}, r.prototype.listenerCount = function(t) {
|
|
232
|
+
var s = n ? n + t : t, c = this._events[s];
|
|
223
233
|
return c ? c.fn ? 1 : c.length : 0;
|
|
224
|
-
},
|
|
225
|
-
var m =
|
|
234
|
+
}, r.prototype.emit = function(t, s, c, w, v, l) {
|
|
235
|
+
var m = n ? n + t : t;
|
|
226
236
|
if (!this._events[m]) return !1;
|
|
227
|
-
var a = this._events[m],
|
|
237
|
+
var a = this._events[m], y = arguments.length, _, d;
|
|
228
238
|
if (a.fn) {
|
|
229
|
-
switch (a.once && this.removeListener(
|
|
239
|
+
switch (a.once && this.removeListener(t, a.fn, void 0, !0), y) {
|
|
230
240
|
case 1:
|
|
231
241
|
return a.fn.call(a.context), !0;
|
|
232
242
|
case 2:
|
|
@@ -238,15 +248,15 @@ function B() {
|
|
|
238
248
|
case 5:
|
|
239
249
|
return a.fn.call(a.context, s, c, w, v), !0;
|
|
240
250
|
case 6:
|
|
241
|
-
return a.fn.call(a.context, s, c, w, v,
|
|
251
|
+
return a.fn.call(a.context, s, c, w, v, l), !0;
|
|
242
252
|
}
|
|
243
|
-
for (d = 1, _ = new Array(
|
|
253
|
+
for (d = 1, _ = new Array(y - 1); d < y; d++)
|
|
244
254
|
_[d - 1] = arguments[d];
|
|
245
255
|
a.fn.apply(a.context, _);
|
|
246
256
|
} else {
|
|
247
|
-
var
|
|
248
|
-
for (d = 0; d <
|
|
249
|
-
switch (a[d].once && this.removeListener(
|
|
257
|
+
var H = a.length, b;
|
|
258
|
+
for (d = 0; d < H; d++)
|
|
259
|
+
switch (a[d].once && this.removeListener(t, a[d].fn, void 0, !0), y) {
|
|
250
260
|
case 1:
|
|
251
261
|
a[d].fn.call(a[d].context);
|
|
252
262
|
break;
|
|
@@ -260,107 +270,107 @@ function B() {
|
|
|
260
270
|
a[d].fn.call(a[d].context, s, c, w);
|
|
261
271
|
break;
|
|
262
272
|
default:
|
|
263
|
-
if (!_) for (
|
|
264
|
-
_[
|
|
273
|
+
if (!_) for (b = 1, _ = new Array(y - 1); b < y; b++)
|
|
274
|
+
_[b - 1] = arguments[b];
|
|
265
275
|
a[d].fn.apply(a[d].context, _);
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
278
|
return !0;
|
|
269
|
-
},
|
|
270
|
-
return p(this,
|
|
271
|
-
},
|
|
272
|
-
return p(this,
|
|
273
|
-
},
|
|
274
|
-
var v =
|
|
279
|
+
}, r.prototype.on = function(t, s, c) {
|
|
280
|
+
return p(this, t, s, c, !1);
|
|
281
|
+
}, r.prototype.once = function(t, s, c) {
|
|
282
|
+
return p(this, t, s, c, !0);
|
|
283
|
+
}, r.prototype.removeListener = function(t, s, c, w) {
|
|
284
|
+
var v = n ? n + t : t;
|
|
275
285
|
if (!this._events[v]) return this;
|
|
276
286
|
if (!s)
|
|
277
287
|
return u(this, v), this;
|
|
278
|
-
var
|
|
279
|
-
if (
|
|
280
|
-
|
|
288
|
+
var l = this._events[v];
|
|
289
|
+
if (l.fn)
|
|
290
|
+
l.fn === s && (!w || l.once) && (!c || l.context === c) && u(this, v);
|
|
281
291
|
else {
|
|
282
|
-
for (var m = 0, a = [],
|
|
283
|
-
(
|
|
292
|
+
for (var m = 0, a = [], y = l.length; m < y; m++)
|
|
293
|
+
(l[m].fn !== s || w && !l[m].once || c && l[m].context !== c) && a.push(l[m]);
|
|
284
294
|
a.length ? this._events[v] = a.length === 1 ? a[0] : a : u(this, v);
|
|
285
295
|
}
|
|
286
296
|
return this;
|
|
287
|
-
},
|
|
297
|
+
}, r.prototype.removeAllListeners = function(t) {
|
|
288
298
|
var s;
|
|
289
|
-
return
|
|
290
|
-
},
|
|
291
|
-
}(
|
|
299
|
+
return t ? (s = n ? n + t : t, this._events[s] && u(this, s)) : (this._events = new f(), this._eventsCount = 0), this;
|
|
300
|
+
}, r.prototype.off = r.prototype.removeListener, r.prototype.addListener = r.prototype.on, r.prefixed = n, r.EventEmitter = r, o.exports = r;
|
|
301
|
+
}(C)), C.exports;
|
|
292
302
|
}
|
|
293
|
-
var
|
|
294
|
-
const
|
|
303
|
+
var K = J();
|
|
304
|
+
const Q = /* @__PURE__ */ A(K), L = new Q(), Z = ({
|
|
295
305
|
handle: o,
|
|
296
|
-
localUserId:
|
|
297
|
-
offlineTimeout:
|
|
298
|
-
getTime:
|
|
306
|
+
localUserId: i,
|
|
307
|
+
offlineTimeout: n = 3e4,
|
|
308
|
+
getTime: f = () => (/* @__PURE__ */ new Date()).getTime()
|
|
299
309
|
}) => {
|
|
300
|
-
const [h, p, u] =
|
|
301
|
-
return
|
|
310
|
+
const [h, p, u] = D({}), [r, e, t] = D({});
|
|
311
|
+
return g(() => {
|
|
302
312
|
const s = (v) => {
|
|
303
|
-
const [
|
|
304
|
-
|
|
313
|
+
const [l, m] = v.message;
|
|
314
|
+
l !== i && (t.current[l] || L.emit("new_peer", v), p({
|
|
305
315
|
...u.current,
|
|
306
|
-
[
|
|
307
|
-
}),
|
|
308
|
-
...
|
|
309
|
-
[
|
|
316
|
+
[l]: m
|
|
317
|
+
}), e({
|
|
318
|
+
...t.current,
|
|
319
|
+
[l]: f()
|
|
310
320
|
}));
|
|
311
321
|
}, c = () => {
|
|
312
|
-
const v = u.current,
|
|
313
|
-
for (const a in
|
|
314
|
-
m -
|
|
315
|
-
p(v),
|
|
322
|
+
const v = u.current, l = t.current, m = f();
|
|
323
|
+
for (const a in l)
|
|
324
|
+
m - l[a] > n && (delete v[a], delete l[a]);
|
|
325
|
+
p(v), e(l);
|
|
316
326
|
};
|
|
317
327
|
o.on("ephemeral-message", s);
|
|
318
328
|
const w = setInterval(
|
|
319
329
|
c,
|
|
320
|
-
|
|
330
|
+
n
|
|
321
331
|
);
|
|
322
332
|
return () => {
|
|
323
333
|
o.removeListener("ephemeral-message", s), clearInterval(w);
|
|
324
334
|
};
|
|
325
|
-
}, [o,
|
|
326
|
-
},
|
|
335
|
+
}, [o, i, n, f]), [h, r];
|
|
336
|
+
}, U = ({
|
|
327
337
|
handle: o,
|
|
328
|
-
userId:
|
|
329
|
-
initialState:
|
|
330
|
-
heartbeatTime:
|
|
338
|
+
userId: i,
|
|
339
|
+
initialState: n,
|
|
340
|
+
heartbeatTime: f = 15e3
|
|
331
341
|
}) => {
|
|
332
|
-
const [h, p, u] =
|
|
333
|
-
const
|
|
334
|
-
p(
|
|
342
|
+
const [h, p, u] = D(n), r = (e) => {
|
|
343
|
+
const t = typeof e == "function" ? e(u.current) : e;
|
|
344
|
+
p(t), o.broadcast([i, t]);
|
|
335
345
|
};
|
|
336
|
-
return
|
|
337
|
-
if (!
|
|
346
|
+
return g(() => {
|
|
347
|
+
if (!i)
|
|
338
348
|
return;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
return () => void clearInterval(
|
|
343
|
-
}, [o,
|
|
344
|
-
let
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
() => o.broadcast([
|
|
349
|
+
const e = () => void o.broadcast([i, u.current]);
|
|
350
|
+
e();
|
|
351
|
+
const t = setInterval(e, f);
|
|
352
|
+
return () => void clearInterval(t);
|
|
353
|
+
}, [o, i, f]), g(() => {
|
|
354
|
+
let e;
|
|
355
|
+
const t = L.on("new_peer", () => {
|
|
356
|
+
e = setTimeout(
|
|
357
|
+
() => o.broadcast([i, u.current]),
|
|
348
358
|
500
|
|
349
359
|
// Wait for the peer to be ready
|
|
350
360
|
);
|
|
351
361
|
});
|
|
352
362
|
return () => {
|
|
353
|
-
|
|
363
|
+
t.off("new_peer"), e && clearTimeout(e);
|
|
354
364
|
};
|
|
355
|
-
}, [o,
|
|
365
|
+
}, [o, i, L]), [h, r];
|
|
356
366
|
};
|
|
357
367
|
export {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
368
|
+
z as RepoContext,
|
|
369
|
+
F as useDocHandle,
|
|
370
|
+
T as useDocHandles,
|
|
371
|
+
X as useDocument,
|
|
372
|
+
Y as useDocuments,
|
|
373
|
+
U as useLocalAwareness,
|
|
374
|
+
Z as useRemoteAwareness,
|
|
365
375
|
k as useRepo
|
|
366
376
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDocHandles.d.ts","sourceRoot":"","sources":["../src/useDocHandles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAA;
|
|
1
|
+
{"version":3,"file":"useDocHandles.d.ts","sourceRoot":"","sources":["../src/useDocHandles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAA;AAOxE,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,KAAK,YAAY,CAAC,CAAC,IAAI,GAAG,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;AAElE,wBAAgB,aAAa,CAAC,CAAC,EAC7B,GAAG,EAAE,YAAY,EAAE,EACnB,EAAE,QAAgB,EAAE,GAAE,mBAAwB,GAC7C,YAAY,CAAC,CAAC,CAAC,CAsFjB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo-react-hooks",
|
|
3
|
-
"version": "2.3.0
|
|
3
|
+
"version": "2.3.0",
|
|
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,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@automerge/automerge": "2.2.8 - 3",
|
|
18
|
-
"@automerge/automerge-repo": "2.3.0
|
|
18
|
+
"@automerge/automerge-repo": "2.3.0",
|
|
19
19
|
"eventemitter3": "^5.0.1",
|
|
20
20
|
"react-usestateref": "^1.0.8"
|
|
21
21
|
},
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "024b98b64a6add14dca52219e92acdfdb5bed230"
|
|
50
50
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
export function useSet<T>(items: T[]): Set<T> {
|
|
4
|
+
const [set, setSet] = useState<Set<T>>(() => {
|
|
5
|
+
return new Set<T>(items)
|
|
6
|
+
})
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const newSet = new Set(items)
|
|
9
|
+
if (identical(set, newSet)) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
setSet(newSet)
|
|
13
|
+
}, [set, items])
|
|
14
|
+
return set
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function identical<T>(s1: Set<T>, s2: Set<T>) {
|
|
18
|
+
return s1.size === s2.size && Array.from(s1).every(v => s2.has(v))
|
|
19
|
+
}
|
package/src/useDocHandle.ts
CHANGED
|
@@ -65,7 +65,7 @@ export function useDocHandle<T>(
|
|
|
65
65
|
* re-running this function until it succeeds, whereas the synchronous
|
|
66
66
|
* form uses a setState to track the value. */
|
|
67
67
|
useEffect(() => {
|
|
68
|
-
if (suspense || !wrapper) {
|
|
68
|
+
if (currentHandle || suspense || !wrapper) {
|
|
69
69
|
return
|
|
70
70
|
}
|
|
71
71
|
wrapper.promise
|
|
@@ -75,7 +75,7 @@ export function useDocHandle<T>(
|
|
|
75
75
|
.catch(() => {
|
|
76
76
|
setHandle(undefined)
|
|
77
77
|
})
|
|
78
|
-
}, [suspense, wrapper])
|
|
78
|
+
}, [currentHandle, suspense, wrapper])
|
|
79
79
|
|
|
80
80
|
if (currentHandle || !suspense || !wrapper) {
|
|
81
81
|
return currentHandle
|
package/src/useDocHandles.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { useState, useEffect } from "react"
|
|
|
3
3
|
import { useRepo } from "./useRepo.js"
|
|
4
4
|
import { PromiseWrapper, wrapPromise } from "./wrapPromise.js"
|
|
5
5
|
import { wrapperCache } from "./useDocHandle.js"
|
|
6
|
+
import { useSet } from "./helpers/useSet.js"
|
|
6
7
|
|
|
7
8
|
interface UseDocHandlesParams {
|
|
8
9
|
suspense?: boolean
|
|
@@ -14,12 +15,13 @@ export function useDocHandles<T>(
|
|
|
14
15
|
ids: AutomergeUrl[],
|
|
15
16
|
{ suspense = false }: UseDocHandlesParams = {}
|
|
16
17
|
): DocHandleMap<T> {
|
|
18
|
+
const idSet = useSet(ids)
|
|
17
19
|
const repo = useRepo()
|
|
18
20
|
const [handleMap, setHandleMap] = useState<DocHandleMap<T>>(() => {
|
|
19
21
|
const map = new Map()
|
|
20
22
|
|
|
21
23
|
// Initialize the map with any handles that are ready
|
|
22
|
-
for (const id of
|
|
24
|
+
for (const id of idSet.values()) {
|
|
23
25
|
let progress
|
|
24
26
|
try {
|
|
25
27
|
progress = repo.findWithProgress<T>(id)
|
|
@@ -39,7 +41,7 @@ export function useDocHandles<T>(
|
|
|
39
41
|
const nextHandleMap = new Map<AutomergeUrl, DocHandle<T> | undefined>()
|
|
40
42
|
|
|
41
43
|
// Check if we need any new wrappers
|
|
42
|
-
for (const id of
|
|
44
|
+
for (const id of idSet.values()) {
|
|
43
45
|
let handle = handleMap.get(id)
|
|
44
46
|
let wrapper = wrapperCache.get(id)
|
|
45
47
|
if (!wrapper) {
|
|
@@ -85,7 +87,7 @@ export function useDocHandles<T>(
|
|
|
85
87
|
} else {
|
|
86
88
|
setHandleMap(nextHandleMap)
|
|
87
89
|
}
|
|
88
|
-
}, [suspense,
|
|
90
|
+
}, [suspense, idSet])
|
|
89
91
|
|
|
90
92
|
// If any promises are pending, suspend with Promise.all
|
|
91
93
|
// Note that this behaviour is different from the synchronous
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { render } from "@testing-library/react"
|
|
3
|
+
import "@testing-library/jest-dom"
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, vi } from "vitest"
|
|
6
|
+
import { useSet } from "../../src/helpers/useSet"
|
|
7
|
+
|
|
8
|
+
describe("useSet", () => {
|
|
9
|
+
const Component = ({
|
|
10
|
+
args,
|
|
11
|
+
onSet,
|
|
12
|
+
}: {
|
|
13
|
+
args: number[]
|
|
14
|
+
onSet: (result: Set<number>) => void
|
|
15
|
+
}) => {
|
|
16
|
+
const result = useSet(args)
|
|
17
|
+
onSet(result)
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
it("builds a Set from the provided arguments", () => {
|
|
22
|
+
const onSet = vi.fn<(result: Set<number>) => void>()
|
|
23
|
+
|
|
24
|
+
const source = [1, 2, 3]
|
|
25
|
+
|
|
26
|
+
render(<Component args={source} onSet={onSet} />)
|
|
27
|
+
|
|
28
|
+
const result = onSet.mock.lastCall?.at(0)
|
|
29
|
+
expect(result?.size).toBe(source.length)
|
|
30
|
+
source.forEach(entry => {
|
|
31
|
+
expect(result?.has(entry)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("collapses duplicates", () => {
|
|
36
|
+
const onSet = vi.fn<(result: Set<number>) => void>()
|
|
37
|
+
|
|
38
|
+
const source = [1, 2, 2, 3]
|
|
39
|
+
|
|
40
|
+
render(<Component args={source} onSet={onSet} />)
|
|
41
|
+
|
|
42
|
+
const result = onSet.mock.lastCall?.at(0)
|
|
43
|
+
expect(result?.size).toBe(3)
|
|
44
|
+
source.forEach(entry => {
|
|
45
|
+
expect(result?.has(entry)).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("returns a new Set if the items change", () => {
|
|
50
|
+
const onSet1 = vi.fn<(result: Set<number>) => void>()
|
|
51
|
+
const source1 = [1, 2, 3]
|
|
52
|
+
|
|
53
|
+
const { rerender } = render(<Component args={source1} onSet={onSet1} />)
|
|
54
|
+
const result1 = onSet1.mock.lastCall?.at(0)
|
|
55
|
+
|
|
56
|
+
const onSet2 = vi.fn<(result: Set<number>) => void>()
|
|
57
|
+
const source2 = [2, 3, 4]
|
|
58
|
+
rerender(<Component args={source2} onSet={onSet2} />)
|
|
59
|
+
const result2 = onSet2.mock.lastCall?.at(0)
|
|
60
|
+
|
|
61
|
+
expect(result1).not.toBe(result2)
|
|
62
|
+
expect(result2?.size).toBe(source2.length)
|
|
63
|
+
source2.forEach(entry => {
|
|
64
|
+
expect(result2?.has(entry)).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("returns the same Set (same object by reference) if the items did not change", () => {
|
|
69
|
+
const onSet1 = vi.fn<(result: Set<number>) => void>()
|
|
70
|
+
const source1 = [1, 2, 3]
|
|
71
|
+
|
|
72
|
+
const { rerender } = render(<Component args={source1} onSet={onSet1} />)
|
|
73
|
+
const result1 = onSet1.mock.lastCall?.at(0)
|
|
74
|
+
|
|
75
|
+
const onSet2 = vi.fn<(result: Set<number>) => void>()
|
|
76
|
+
const source2 = [1, 2, 3]
|
|
77
|
+
rerender(<Component args={source2} onSet={onSet2} />)
|
|
78
|
+
const result2 = onSet2.mock.lastCall?.at(0)
|
|
79
|
+
|
|
80
|
+
expect(result1).toBe(result2)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Suspense } from "react"
|
|
1
|
+
import React, { act, Suspense } from "react"
|
|
2
2
|
import {
|
|
3
3
|
AutomergeUrl,
|
|
4
4
|
DocHandle,
|
|
@@ -11,6 +11,7 @@ import { describe, expect, it, vi } from "vitest"
|
|
|
11
11
|
import { useDocHandle } from "../src/useDocHandle"
|
|
12
12
|
import { ErrorBoundary } from "react-error-boundary"
|
|
13
13
|
import { setup, setupPairedRepos } from "./testSetup"
|
|
14
|
+
import { pause } from "../src/helpers/DummyNetworkAdapter"
|
|
14
15
|
|
|
15
16
|
describe("useDocHandle", () => {
|
|
16
17
|
const Component = ({
|
|
@@ -135,7 +136,7 @@ describe("useDocHandle", () => {
|
|
|
135
136
|
})
|
|
136
137
|
|
|
137
138
|
it("suspends while loading a handle", async () => {
|
|
138
|
-
const { repoCreator, wrapper } =
|
|
139
|
+
const { repoCreator, wrapper } = setupPairedRepos()
|
|
139
140
|
const handleA = repoCreator.create({ foo: "A" })
|
|
140
141
|
const onHandle = vi.fn()
|
|
141
142
|
|
|
@@ -159,7 +160,7 @@ describe("useDocHandle", () => {
|
|
|
159
160
|
})
|
|
160
161
|
|
|
161
162
|
it("handles rapid url changes during loading", async () => {
|
|
162
|
-
const { repoCreator, repoFinder, wrapper } =
|
|
163
|
+
const { repoCreator, repoFinder, wrapper } = setupPairedRepos()
|
|
163
164
|
const handleA = repoCreator.create({ foo: "A" })
|
|
164
165
|
const handleB = repoFinder.create({ foo: "B" })
|
|
165
166
|
const onHandle = vi.fn()
|
|
@@ -187,9 +188,9 @@ describe("useDocHandle", () => {
|
|
|
187
188
|
})
|
|
188
189
|
})
|
|
189
190
|
|
|
190
|
-
describe("
|
|
191
|
+
describe("with suspense: false", () => {
|
|
191
192
|
it("returns undefined while loading then resolves to handle", async () => {
|
|
192
|
-
const { repoCreator, wrapper } =
|
|
193
|
+
const { repoCreator, wrapper } = setupPairedRepos()
|
|
193
194
|
const handleA = repoCreator.create({ foo: "A" })
|
|
194
195
|
|
|
195
196
|
const onHandle = vi.fn()
|
|
@@ -281,5 +282,35 @@ describe("useDocHandle", () => {
|
|
|
281
282
|
// Then resolve to new handle
|
|
282
283
|
await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleB))
|
|
283
284
|
})
|
|
285
|
+
|
|
286
|
+
it("does not re-render unnecessarily when the handle does not change", async () => {
|
|
287
|
+
const { wrapper, handleA } = setup()
|
|
288
|
+
const onHandle = vi.fn()
|
|
289
|
+
|
|
290
|
+
const NonSuspenseComponent = ({
|
|
291
|
+
url,
|
|
292
|
+
onHandle,
|
|
293
|
+
}: {
|
|
294
|
+
url: AutomergeUrl
|
|
295
|
+
onHandle: (handle: DocHandle<unknown> | undefined) => void
|
|
296
|
+
}) => {
|
|
297
|
+
const handle = useDocHandle(url, { suspense: false })
|
|
298
|
+
onHandle(handle)
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// On first render, we should only render once
|
|
303
|
+
const { rerender } = render(
|
|
304
|
+
<NonSuspenseComponent url={handleA.url} onHandle={onHandle} />,
|
|
305
|
+
{ wrapper }
|
|
306
|
+
)
|
|
307
|
+
await act(pause) // allow time for extra re-renders
|
|
308
|
+
expect(onHandle).toHaveBeenCalledTimes(1)
|
|
309
|
+
|
|
310
|
+
// On explicit second render with no changes, we should render once more
|
|
311
|
+
rerender(<NonSuspenseComponent url={handleA.url} onHandle={onHandle} />)
|
|
312
|
+
await act(pause) // allow time for extra re-renders
|
|
313
|
+
expect(onHandle).toHaveBeenCalledTimes(2)
|
|
314
|
+
})
|
|
284
315
|
})
|
|
285
316
|
})
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import React, { act, Suspense } from "react"
|
|
2
|
+
import {
|
|
3
|
+
AutomergeUrl,
|
|
4
|
+
DocHandle,
|
|
5
|
+
generateAutomergeUrl,
|
|
6
|
+
} from "@automerge/automerge-repo"
|
|
7
|
+
import { render, screen, waitFor } from "@testing-library/react"
|
|
8
|
+
import "@testing-library/jest-dom"
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from "vitest"
|
|
11
|
+
import { ErrorBoundary } from "react-error-boundary"
|
|
12
|
+
import { setup, setupPairedRepos } from "./testSetup"
|
|
13
|
+
import { useDocHandles } from "../src/useDocHandles"
|
|
14
|
+
import { pause } from "../src/helpers/DummyNetworkAdapter"
|
|
15
|
+
|
|
16
|
+
describe("useDocHandles", () => {
|
|
17
|
+
function mockOnHandles() {
|
|
18
|
+
return vi.fn<
|
|
19
|
+
(result: Map<AutomergeUrl, DocHandle<unknown> | undefined>) => void
|
|
20
|
+
>()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("suspense", () => {
|
|
24
|
+
const Component = ({
|
|
25
|
+
urls,
|
|
26
|
+
onHandles,
|
|
27
|
+
}: {
|
|
28
|
+
urls: AutomergeUrl[]
|
|
29
|
+
onHandles: (
|
|
30
|
+
handles: Map<AutomergeUrl, DocHandle<unknown> | undefined>
|
|
31
|
+
) => void
|
|
32
|
+
}) => {
|
|
33
|
+
const handle = useDocHandles(urls, { suspense: true })
|
|
34
|
+
onHandles(handle)
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it("loads some handles", async () => {
|
|
39
|
+
const { handleA, handleB, wrapper } = setup()
|
|
40
|
+
const onHandles = mockOnHandles()
|
|
41
|
+
|
|
42
|
+
render(
|
|
43
|
+
<Suspense fallback={null}>
|
|
44
|
+
<Component urls={[handleA.url, handleB.url]} onHandles={onHandles} />
|
|
45
|
+
</Suspense>,
|
|
46
|
+
{ wrapper }
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const result = onHandles.mock.lastCall?.at(0)
|
|
50
|
+
|
|
51
|
+
expect(result?.size).toBe(2)
|
|
52
|
+
expect(result?.get(handleA.url)?.url).toEqual(handleA.url)
|
|
53
|
+
expect(result?.get(handleB.url)?.url).toEqual(handleB.url)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("updates the result map when the url changes", async () => {
|
|
57
|
+
const { wrapper, handleA, handleB } = setup()
|
|
58
|
+
const onHandles = mockOnHandles()
|
|
59
|
+
|
|
60
|
+
const { rerender } = render(
|
|
61
|
+
<Component urls={[handleA.url]} onHandles={onHandles} />,
|
|
62
|
+
{ wrapper }
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const result1 = onHandles.mock.lastCall?.at(0)
|
|
66
|
+
expect(result1?.size).toBe(1)
|
|
67
|
+
expect(result1?.get(handleA.url)?.url).toEqual(handleA.url)
|
|
68
|
+
|
|
69
|
+
rerender(<Component urls={[handleB.url]} onHandles={onHandles} />)
|
|
70
|
+
|
|
71
|
+
await act(pause)
|
|
72
|
+
|
|
73
|
+
const result2 = onHandles.mock.lastCall?.at(0)
|
|
74
|
+
expect(result2?.size).toBe(1)
|
|
75
|
+
expect(result2?.get(handleB.url)?.url).toEqual(handleB.url)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("does not update the result map when the urls do not change", async () => {
|
|
79
|
+
const { wrapper, handleA, handleB } = setup()
|
|
80
|
+
const onHandles = mockOnHandles()
|
|
81
|
+
|
|
82
|
+
const { rerender } = render(
|
|
83
|
+
<Component urls={[handleA.url, handleB.url]} onHandles={onHandles} />,
|
|
84
|
+
{ wrapper }
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const result1 = onHandles.mock.lastCall?.at(0)
|
|
88
|
+
expect(result1?.size).toBe(2)
|
|
89
|
+
expect(result1?.get(handleA.url)?.url).toEqual(handleA.url)
|
|
90
|
+
expect(result1?.get(handleB.url)?.url).toEqual(handleB.url)
|
|
91
|
+
|
|
92
|
+
rerender(
|
|
93
|
+
<Component urls={[handleA.url, handleB.url]} onHandles={onHandles} />
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
await act(pause)
|
|
97
|
+
|
|
98
|
+
const result2 = onHandles.mock.lastCall?.at(0)
|
|
99
|
+
expect(result2).toBe(result1)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("handles unavailable documents correctly", async () => {
|
|
103
|
+
// suppress console.error from the error boundary
|
|
104
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
105
|
+
|
|
106
|
+
const { handleA, wrapper } = setup()
|
|
107
|
+
const noSuchDocUrl = generateAutomergeUrl()
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
<ErrorBoundary fallback={<div data-testid="error">Error</div>}>
|
|
111
|
+
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
112
|
+
<Component
|
|
113
|
+
urls={[noSuchDocUrl, handleA.url]}
|
|
114
|
+
onHandles={() => {
|
|
115
|
+
throw new Error("Should not reach here")
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
</Suspense>
|
|
119
|
+
</ErrorBoundary>,
|
|
120
|
+
{ wrapper }
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(screen.getByTestId("error")).toBeInTheDocument()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
consoleSpy.mockRestore()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("handles slow network correctly", async () => {
|
|
131
|
+
const { repoCreator, wrapper } = setupPairedRepos()
|
|
132
|
+
const handleA = repoCreator.create({ foo: "A" })
|
|
133
|
+
const onHandles = mockOnHandles()
|
|
134
|
+
|
|
135
|
+
render(
|
|
136
|
+
<ErrorBoundary fallback={<div data-testid="error">Error</div>}>
|
|
137
|
+
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
138
|
+
<Component urls={[handleA.url]} onHandles={onHandles} />
|
|
139
|
+
</Suspense>
|
|
140
|
+
</ErrorBoundary>,
|
|
141
|
+
{ wrapper }
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Verify loading state is shown initially
|
|
145
|
+
expect(screen.getByTestId("loading")).toBeInTheDocument()
|
|
146
|
+
expect(onHandles).not.toHaveBeenCalled()
|
|
147
|
+
|
|
148
|
+
// Wait for successful resolution
|
|
149
|
+
await waitFor(() => {
|
|
150
|
+
// Loading state should be gone
|
|
151
|
+
expect(screen.queryByTestId("loading")).not.toBeInTheDocument()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const result = onHandles.mock.lastCall?.at(0)
|
|
155
|
+
expect(result?.size).toBe(1)
|
|
156
|
+
expect(result?.get(handleA.url)?.url).toEqual(handleA.url)
|
|
157
|
+
|
|
158
|
+
// Verify error boundary never rendered
|
|
159
|
+
expect(screen.queryByTestId("error")).not.toBeInTheDocument()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("suspends while loading a handle", async () => {
|
|
163
|
+
const { repoCreator, wrapper } = await setupPairedRepos()
|
|
164
|
+
const handleA = repoCreator.create({ foo: "A" })
|
|
165
|
+
const onHandles = mockOnHandles()
|
|
166
|
+
|
|
167
|
+
render(
|
|
168
|
+
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
169
|
+
<Component urls={[handleA.url]} onHandles={onHandles} />
|
|
170
|
+
</Suspense>,
|
|
171
|
+
{ wrapper }
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// Should show loading state
|
|
175
|
+
expect(screen.getByTestId("loading")).toBeInTheDocument()
|
|
176
|
+
expect(onHandles).not.toHaveBeenCalled()
|
|
177
|
+
|
|
178
|
+
// Should show content
|
|
179
|
+
await waitFor(() => {
|
|
180
|
+
expect(onHandles).toHaveBeenCalled()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const result = onHandles.mock.lastCall?.at(0)
|
|
184
|
+
expect(result?.size).toBe(1)
|
|
185
|
+
expect(result?.get(handleA.url)?.url).toEqual(handleA.url)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("handles rapid url changes during loading", async () => {
|
|
189
|
+
const { repoCreator, repoFinder, wrapper } = await setupPairedRepos()
|
|
190
|
+
const handleA = repoCreator.create({ foo: "A" })
|
|
191
|
+
const handleB = repoFinder.create({ foo: "B" })
|
|
192
|
+
const onHandles = mockOnHandles()
|
|
193
|
+
|
|
194
|
+
const { rerender } = render(
|
|
195
|
+
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
196
|
+
<Component urls={[handleA.url]} onHandles={onHandles} />
|
|
197
|
+
</Suspense>,
|
|
198
|
+
{ wrapper }
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// Quickly switch to B before A loads
|
|
202
|
+
rerender(
|
|
203
|
+
<Suspense fallback={<div data-testid="loading">Loading...</div>}>
|
|
204
|
+
<Component urls={[handleB.url]} onHandles={onHandles} />
|
|
205
|
+
</Suspense>
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// Should eventually resolve with B, not A
|
|
209
|
+
await waitFor(() => {
|
|
210
|
+
expect(onHandles).toHaveBeenCalled()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const result = onHandles.mock.lastCall?.at(0)
|
|
214
|
+
expect(result?.size).toBe(1)
|
|
215
|
+
expect(result?.get(handleB.url)?.url).toEqual(handleB.url)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe("useDocHandles with suspense: false", () => {
|
|
220
|
+
function Component({
|
|
221
|
+
urls,
|
|
222
|
+
onHandles,
|
|
223
|
+
}: {
|
|
224
|
+
urls: AutomergeUrl[]
|
|
225
|
+
onHandles: (
|
|
226
|
+
handles: Map<AutomergeUrl, DocHandle<unknown> | undefined>
|
|
227
|
+
) => void
|
|
228
|
+
}) {
|
|
229
|
+
const handle = useDocHandles(urls, { suspense: false })
|
|
230
|
+
onHandles(handle)
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
it("returns and empty map while loading then resolves to handle", async () => {
|
|
235
|
+
const { repoCreator, wrapper } = await setupPairedRepos()
|
|
236
|
+
const handleA = repoCreator.create({ foo: "A" })
|
|
237
|
+
|
|
238
|
+
const onHandles = mockOnHandles()
|
|
239
|
+
|
|
240
|
+
render(<Component urls={[handleA.url]} onHandles={onHandles} />, {
|
|
241
|
+
wrapper,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const result1 = onHandles.mock.lastCall?.at(0)
|
|
245
|
+
expect(result1?.size).toBe(0)
|
|
246
|
+
|
|
247
|
+
// Wait for handle to load
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(onHandles).toHaveBeenCalledWith(
|
|
250
|
+
expect.objectContaining({ size: 1 })
|
|
251
|
+
)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const result2 = onHandles.mock.lastCall?.at(0)
|
|
255
|
+
expect(result2?.get(handleA.url)?.url).toEqual(handleA.url)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it("handles unavailable documents by omitting them", async () => {
|
|
259
|
+
const { handleA, wrapper } = setup()
|
|
260
|
+
const noSuchDocUrl = generateAutomergeUrl()
|
|
261
|
+
const onHandles = mockOnHandles()
|
|
262
|
+
|
|
263
|
+
render(
|
|
264
|
+
<ErrorBoundary fallback={<div data-testid="error">Error</div>}>
|
|
265
|
+
<Component urls={[handleA.url, noSuchDocUrl]} onHandles={onHandles} />
|
|
266
|
+
</ErrorBoundary>,
|
|
267
|
+
{ wrapper }
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
const result = onHandles.mock.lastCall?.at(0)
|
|
271
|
+
expect(result?.size).toBe(1)
|
|
272
|
+
expect(result?.get(handleA.url)?.url).toEqual(handleA.url)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it("updates the handle map when urls change", async () => {
|
|
276
|
+
const { wrapper, handleA, handleB } = setup()
|
|
277
|
+
const onHandles = mockOnHandles()
|
|
278
|
+
|
|
279
|
+
const { rerender } = render(
|
|
280
|
+
<Component urls={[handleA.url]} onHandles={onHandles} />,
|
|
281
|
+
{ wrapper }
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const result1 = onHandles.mock.lastCall?.at(0)
|
|
285
|
+
expect(result1?.size).toBe(1)
|
|
286
|
+
expect(result1?.get(handleA.url)?.url).toEqual(handleA.url)
|
|
287
|
+
|
|
288
|
+
// Change URL
|
|
289
|
+
rerender(<Component urls={[handleB.url]} onHandles={onHandles} />)
|
|
290
|
+
await act(pause)
|
|
291
|
+
|
|
292
|
+
const result2 = onHandles.mock.lastCall?.at(0)
|
|
293
|
+
expect(result2?.size).toBe(1)
|
|
294
|
+
expect(result2?.get(handleB.url)?.url).toEqual(handleB.url)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
})
|