@automerge/automerge-repo-react-hooks 2.4.0-alpha.2 → 2.5.0-alpha.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/index.js +146 -141
- package/dist/useLocalAwareness.d.ts +1 -1
- package/dist/useLocalAwareness.d.ts.map +1 -1
- package/dist/useRemoteAwareness.d.ts +1 -1
- package/dist/useRemoteAwareness.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/useLocalAwareness.ts +11 -5
- package/src/useRemoteAwareness.ts +13 -5
- package/test/useLocalAwareness.test.tsx +275 -0
- package/test/useRemoteAwareness.test.tsx +326 -0
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import q, { createContext as I, useContext as j, useRef as $, useState as E, useEffect as
|
|
2
|
-
function O(
|
|
3
|
-
let
|
|
4
|
-
const h =
|
|
1
|
+
import q, { createContext as I, useContext as j, useRef as $, useState as E, useEffect as y, useCallback as R } from "react";
|
|
2
|
+
function O(s) {
|
|
3
|
+
let u = "pending", n, f;
|
|
4
|
+
const h = s.then(
|
|
5
5
|
(p) => {
|
|
6
|
-
|
|
6
|
+
u = "success", n = p;
|
|
7
7
|
},
|
|
8
8
|
(p) => {
|
|
9
|
-
|
|
9
|
+
u = "error", f = p;
|
|
10
10
|
}
|
|
11
11
|
);
|
|
12
12
|
return {
|
|
13
|
-
promise:
|
|
13
|
+
promise: s,
|
|
14
14
|
read() {
|
|
15
|
-
switch (
|
|
15
|
+
switch (u) {
|
|
16
16
|
case "pending":
|
|
17
17
|
throw h;
|
|
18
18
|
case "error":
|
|
@@ -25,46 +25,46 @@ function O(o) {
|
|
|
25
25
|
}
|
|
26
26
|
const z = I(null);
|
|
27
27
|
function k() {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
30
|
-
return
|
|
28
|
+
const s = j(z);
|
|
29
|
+
if (!s) throw new Error("Repo was not found on RepoContext.");
|
|
30
|
+
return s;
|
|
31
31
|
}
|
|
32
32
|
const x = /* @__PURE__ */ new Map();
|
|
33
|
-
function F(
|
|
33
|
+
function F(s, { suspense: u } = { suspense: !1 }) {
|
|
34
34
|
const n = k(), f = $(), [h, p] = E();
|
|
35
|
-
let
|
|
36
|
-
if (
|
|
37
|
-
const e = n.findWithProgress(
|
|
38
|
-
e.state === "ready" && (
|
|
35
|
+
let i = h;
|
|
36
|
+
if (s && !i) {
|
|
37
|
+
const e = n.findWithProgress(s);
|
|
38
|
+
e.state === "ready" && (i = e.handle);
|
|
39
39
|
}
|
|
40
|
-
let r =
|
|
41
|
-
if (!r &&
|
|
40
|
+
let r = s ? x.get(s) : void 0;
|
|
41
|
+
if (!r && s) {
|
|
42
42
|
f.current?.abort(), f.current = new AbortController();
|
|
43
|
-
const e = n.find(
|
|
44
|
-
r = O(e), x.set(
|
|
43
|
+
const e = n.find(s, { signal: f.current.signal });
|
|
44
|
+
r = O(e), x.set(s, r);
|
|
45
45
|
}
|
|
46
|
-
return
|
|
47
|
-
|
|
46
|
+
return y(() => {
|
|
47
|
+
i || u || !r || r.promise.then((e) => {
|
|
48
48
|
p(e);
|
|
49
49
|
}).catch(() => {
|
|
50
50
|
p(void 0);
|
|
51
51
|
});
|
|
52
|
-
}, [
|
|
52
|
+
}, [i, u, r]), i || !u || !r ? i : r.read();
|
|
53
53
|
}
|
|
54
|
-
function X(
|
|
55
|
-
const n = F(
|
|
56
|
-
|
|
54
|
+
function X(s, u = { suspense: !1 }) {
|
|
55
|
+
const n = F(s, u), [f, h] = E(() => n?.doc()), [p, i] = E();
|
|
56
|
+
y(() => {
|
|
57
57
|
h(n?.doc());
|
|
58
|
-
}, [n]),
|
|
58
|
+
}, [n]), y(() => {
|
|
59
59
|
if (!n)
|
|
60
60
|
return;
|
|
61
61
|
const e = () => h(n.doc()), t = () => {
|
|
62
|
-
|
|
62
|
+
i(new Error(`Document ${s} was deleted`));
|
|
63
63
|
};
|
|
64
64
|
return n.on("change", e), n.on("delete", t), () => {
|
|
65
65
|
n.removeListener("change", e), n.removeListener("delete", t);
|
|
66
66
|
};
|
|
67
|
-
}, [n,
|
|
67
|
+
}, [n, s]);
|
|
68
68
|
const r = R(
|
|
69
69
|
(e, t) => {
|
|
70
70
|
n.change(e, t);
|
|
@@ -76,82 +76,82 @@ function X(o, i = { suspense: !1 }) {
|
|
|
76
76
|
return f ? [f, r] : [void 0, () => {
|
|
77
77
|
}];
|
|
78
78
|
}
|
|
79
|
-
function N(
|
|
80
|
-
const [
|
|
81
|
-
return
|
|
82
|
-
const f = new Set(
|
|
83
|
-
W(
|
|
84
|
-
}, [
|
|
79
|
+
function N(s) {
|
|
80
|
+
const [u, n] = E(() => new Set(s));
|
|
81
|
+
return y(() => {
|
|
82
|
+
const f = new Set(s);
|
|
83
|
+
W(u, f) || n(f);
|
|
84
|
+
}, [u, s]), u;
|
|
85
85
|
}
|
|
86
|
-
function W(
|
|
87
|
-
return
|
|
86
|
+
function W(s, u) {
|
|
87
|
+
return s.size === u.size && Array.from(s).every((n) => u.has(n));
|
|
88
88
|
}
|
|
89
|
-
function T(
|
|
90
|
-
const n = N(
|
|
89
|
+
function T(s, { suspense: u = !1 } = {}) {
|
|
90
|
+
const n = N(s), f = k(), [h, p] = E(() => {
|
|
91
91
|
const e = /* @__PURE__ */ new Map();
|
|
92
92
|
for (const t of n.values()) {
|
|
93
|
-
let
|
|
93
|
+
let o;
|
|
94
94
|
try {
|
|
95
|
-
|
|
95
|
+
o = f.findWithProgress(t);
|
|
96
96
|
} catch {
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
o.state === "ready" && e.set(t, o.handle);
|
|
100
100
|
}
|
|
101
101
|
return e;
|
|
102
|
-
}),
|
|
102
|
+
}), i = [], r = /* @__PURE__ */ new Map();
|
|
103
103
|
for (const e of n.values()) {
|
|
104
|
-
let t = h.get(e),
|
|
105
|
-
if (!
|
|
104
|
+
let t = h.get(e), o = x.get(e);
|
|
105
|
+
if (!o)
|
|
106
106
|
try {
|
|
107
107
|
const c = f.find(e);
|
|
108
|
-
|
|
108
|
+
o = O(c), x.set(e, o);
|
|
109
109
|
} catch {
|
|
110
110
|
continue;
|
|
111
111
|
}
|
|
112
112
|
try {
|
|
113
|
-
t ??=
|
|
113
|
+
t ??= o.read(), r.set(e, t);
|
|
114
114
|
} catch (c) {
|
|
115
|
-
c instanceof Promise ?
|
|
115
|
+
c instanceof Promise ? i.push(o) : r.set(e, void 0);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
if (
|
|
119
|
-
|
|
118
|
+
if (y(() => {
|
|
119
|
+
i.length > 0 ? Promise.allSettled(i.map((e) => e.promise)).then(
|
|
120
120
|
(e) => {
|
|
121
121
|
e.forEach((t) => {
|
|
122
122
|
if (t.status === "fulfilled") {
|
|
123
|
-
const
|
|
124
|
-
r.set(
|
|
123
|
+
const o = t.value;
|
|
124
|
+
r.set(o.url, o);
|
|
125
125
|
}
|
|
126
126
|
}), p(r);
|
|
127
127
|
}
|
|
128
128
|
) : p(r);
|
|
129
|
-
}, [
|
|
130
|
-
throw Promise.all(
|
|
129
|
+
}, [u, n]), u && i.length > 0)
|
|
130
|
+
throw Promise.all(i.map((e) => e.promise));
|
|
131
131
|
return h;
|
|
132
132
|
}
|
|
133
|
-
function Y(
|
|
134
|
-
const n = T(
|
|
135
|
-
const
|
|
133
|
+
function Y(s, { suspense: u = !0 } = {}) {
|
|
134
|
+
const n = T(s, { suspense: u }), [f, h] = E(() => {
|
|
135
|
+
const i = /* @__PURE__ */ new Map();
|
|
136
136
|
return n.forEach((r) => {
|
|
137
137
|
const e = r?.url;
|
|
138
|
-
e &&
|
|
139
|
-
}),
|
|
138
|
+
e && i.set(e, r?.doc());
|
|
139
|
+
}), i;
|
|
140
140
|
});
|
|
141
|
-
|
|
142
|
-
const
|
|
141
|
+
y(() => {
|
|
142
|
+
const i = /* @__PURE__ */ new Map();
|
|
143
143
|
return n.forEach((r, e) => {
|
|
144
144
|
if (r) {
|
|
145
145
|
const t = () => {
|
|
146
|
-
h((
|
|
147
|
-
const c = new Map(
|
|
146
|
+
h((o) => {
|
|
147
|
+
const c = new Map(o);
|
|
148
148
|
return c.set(e, r.doc()), c;
|
|
149
149
|
});
|
|
150
150
|
};
|
|
151
|
-
h((
|
|
152
|
-
const c = new Map(
|
|
151
|
+
h((o) => {
|
|
152
|
+
const c = new Map(o);
|
|
153
153
|
return c.set(e, r.doc()), c;
|
|
154
|
-
}), r.on("change", t),
|
|
154
|
+
}), r.on("change", t), i.set(e, t);
|
|
155
155
|
}
|
|
156
156
|
}), h((r) => {
|
|
157
157
|
const e = new Map(r);
|
|
@@ -160,32 +160,32 @@ function Y(o, { suspense: i = !0 } = {}) {
|
|
|
160
160
|
return e;
|
|
161
161
|
}), () => {
|
|
162
162
|
n.forEach((r, e) => {
|
|
163
|
-
const t =
|
|
163
|
+
const t = i.get(e);
|
|
164
164
|
r && t && r.removeListener("change", t);
|
|
165
165
|
});
|
|
166
166
|
};
|
|
167
167
|
}, [n]);
|
|
168
168
|
const p = R(
|
|
169
|
-
(
|
|
170
|
-
const t = n.get(
|
|
169
|
+
(i, r, e) => {
|
|
170
|
+
const t = n.get(i);
|
|
171
171
|
t && t.change(r, e);
|
|
172
172
|
},
|
|
173
173
|
[n]
|
|
174
174
|
);
|
|
175
175
|
return [f, p];
|
|
176
176
|
}
|
|
177
|
-
function A(
|
|
178
|
-
return
|
|
177
|
+
function A(s) {
|
|
178
|
+
return s && s.__esModule && Object.prototype.hasOwnProperty.call(s, "default") ? s.default : s;
|
|
179
179
|
}
|
|
180
180
|
var S, M;
|
|
181
181
|
function B() {
|
|
182
182
|
if (M) return S;
|
|
183
183
|
M = 1;
|
|
184
|
-
var
|
|
184
|
+
var s = q, u = function(f) {
|
|
185
185
|
return typeof f == "function";
|
|
186
186
|
}, n = function(f) {
|
|
187
|
-
var h =
|
|
188
|
-
r.current =
|
|
187
|
+
var h = s.useState(f), p = h[0], i = h[1], r = s.useRef(p), e = s.useCallback(function(t) {
|
|
188
|
+
r.current = u(t) ? t(r.current) : t, i(r.current);
|
|
189
189
|
}, []);
|
|
190
190
|
return [p, e, r];
|
|
191
191
|
};
|
|
@@ -195,166 +195,171 @@ var G = B();
|
|
|
195
195
|
const D = /* @__PURE__ */ A(G);
|
|
196
196
|
var C = { exports: {} }, P;
|
|
197
197
|
function J() {
|
|
198
|
-
return P || (P = 1, function(
|
|
199
|
-
var
|
|
198
|
+
return P || (P = 1, function(s) {
|
|
199
|
+
var u = Object.prototype.hasOwnProperty, n = "~";
|
|
200
200
|
function f() {
|
|
201
201
|
}
|
|
202
202
|
Object.create && (f.prototype = /* @__PURE__ */ Object.create(null), new f().__proto__ || (n = !1));
|
|
203
|
-
function h(e, t,
|
|
204
|
-
this.fn = e, this.context = t, this.once =
|
|
203
|
+
function h(e, t, o) {
|
|
204
|
+
this.fn = e, this.context = t, this.once = o || !1;
|
|
205
205
|
}
|
|
206
|
-
function p(e, t,
|
|
207
|
-
if (typeof
|
|
206
|
+
function p(e, t, o, c, w) {
|
|
207
|
+
if (typeof o != "function")
|
|
208
208
|
throw new TypeError("The listener must be a function");
|
|
209
|
-
var v = new h(
|
|
209
|
+
var v = new h(o, c || e, w), l = n ? n + t : t;
|
|
210
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;
|
|
211
211
|
}
|
|
212
|
-
function
|
|
212
|
+
function i(e, t) {
|
|
213
213
|
--e._eventsCount === 0 ? e._events = new f() : delete e._events[t];
|
|
214
214
|
}
|
|
215
215
|
function r() {
|
|
216
216
|
this._events = new f(), this._eventsCount = 0;
|
|
217
217
|
}
|
|
218
218
|
r.prototype.eventNames = function() {
|
|
219
|
-
var t = [],
|
|
219
|
+
var t = [], o, c;
|
|
220
220
|
if (this._eventsCount === 0) return t;
|
|
221
|
-
for (c in
|
|
222
|
-
|
|
223
|
-
return Object.getOwnPropertySymbols ? t.concat(Object.getOwnPropertySymbols(
|
|
221
|
+
for (c in o = this._events)
|
|
222
|
+
u.call(o, c) && t.push(n ? c.slice(1) : c);
|
|
223
|
+
return Object.getOwnPropertySymbols ? t.concat(Object.getOwnPropertySymbols(o)) : t;
|
|
224
224
|
}, r.prototype.listeners = function(t) {
|
|
225
|
-
var
|
|
225
|
+
var o = n ? n + t : t, c = this._events[o];
|
|
226
226
|
if (!c) return [];
|
|
227
227
|
if (c.fn) return [c.fn];
|
|
228
228
|
for (var w = 0, v = c.length, l = new Array(v); w < v; w++)
|
|
229
229
|
l[w] = c[w].fn;
|
|
230
230
|
return l;
|
|
231
231
|
}, r.prototype.listenerCount = function(t) {
|
|
232
|
-
var
|
|
232
|
+
var o = n ? n + t : t, c = this._events[o];
|
|
233
233
|
return c ? c.fn ? 1 : c.length : 0;
|
|
234
|
-
}, r.prototype.emit = function(t,
|
|
234
|
+
}, r.prototype.emit = function(t, o, c, w, v, l) {
|
|
235
235
|
var m = n ? n + t : t;
|
|
236
236
|
if (!this._events[m]) return !1;
|
|
237
|
-
var a = this._events[m],
|
|
237
|
+
var a = this._events[m], g = arguments.length, _, d;
|
|
238
238
|
if (a.fn) {
|
|
239
|
-
switch (a.once && this.removeListener(t, a.fn, void 0, !0),
|
|
239
|
+
switch (a.once && this.removeListener(t, a.fn, void 0, !0), g) {
|
|
240
240
|
case 1:
|
|
241
241
|
return a.fn.call(a.context), !0;
|
|
242
242
|
case 2:
|
|
243
|
-
return a.fn.call(a.context,
|
|
243
|
+
return a.fn.call(a.context, o), !0;
|
|
244
244
|
case 3:
|
|
245
|
-
return a.fn.call(a.context,
|
|
245
|
+
return a.fn.call(a.context, o, c), !0;
|
|
246
246
|
case 4:
|
|
247
|
-
return a.fn.call(a.context,
|
|
247
|
+
return a.fn.call(a.context, o, c, w), !0;
|
|
248
248
|
case 5:
|
|
249
|
-
return a.fn.call(a.context,
|
|
249
|
+
return a.fn.call(a.context, o, c, w, v), !0;
|
|
250
250
|
case 6:
|
|
251
|
-
return a.fn.call(a.context,
|
|
251
|
+
return a.fn.call(a.context, o, c, w, v, l), !0;
|
|
252
252
|
}
|
|
253
|
-
for (d = 1, _ = new Array(
|
|
253
|
+
for (d = 1, _ = new Array(g - 1); d < g; d++)
|
|
254
254
|
_[d - 1] = arguments[d];
|
|
255
255
|
a.fn.apply(a.context, _);
|
|
256
256
|
} else {
|
|
257
257
|
var H = a.length, b;
|
|
258
258
|
for (d = 0; d < H; d++)
|
|
259
|
-
switch (a[d].once && this.removeListener(t, a[d].fn, void 0, !0),
|
|
259
|
+
switch (a[d].once && this.removeListener(t, a[d].fn, void 0, !0), g) {
|
|
260
260
|
case 1:
|
|
261
261
|
a[d].fn.call(a[d].context);
|
|
262
262
|
break;
|
|
263
263
|
case 2:
|
|
264
|
-
a[d].fn.call(a[d].context,
|
|
264
|
+
a[d].fn.call(a[d].context, o);
|
|
265
265
|
break;
|
|
266
266
|
case 3:
|
|
267
|
-
a[d].fn.call(a[d].context,
|
|
267
|
+
a[d].fn.call(a[d].context, o, c);
|
|
268
268
|
break;
|
|
269
269
|
case 4:
|
|
270
|
-
a[d].fn.call(a[d].context,
|
|
270
|
+
a[d].fn.call(a[d].context, o, c, w);
|
|
271
271
|
break;
|
|
272
272
|
default:
|
|
273
|
-
if (!_) for (b = 1, _ = new Array(
|
|
273
|
+
if (!_) for (b = 1, _ = new Array(g - 1); b < g; b++)
|
|
274
274
|
_[b - 1] = arguments[b];
|
|
275
275
|
a[d].fn.apply(a[d].context, _);
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
return !0;
|
|
279
|
-
}, r.prototype.on = function(t,
|
|
280
|
-
return p(this, t,
|
|
281
|
-
}, r.prototype.once = function(t,
|
|
282
|
-
return p(this, t,
|
|
283
|
-
}, r.prototype.removeListener = function(t,
|
|
279
|
+
}, r.prototype.on = function(t, o, c) {
|
|
280
|
+
return p(this, t, o, c, !1);
|
|
281
|
+
}, r.prototype.once = function(t, o, c) {
|
|
282
|
+
return p(this, t, o, c, !0);
|
|
283
|
+
}, r.prototype.removeListener = function(t, o, c, w) {
|
|
284
284
|
var v = n ? n + t : t;
|
|
285
285
|
if (!this._events[v]) return this;
|
|
286
|
-
if (!
|
|
287
|
-
return
|
|
286
|
+
if (!o)
|
|
287
|
+
return i(this, v), this;
|
|
288
288
|
var l = this._events[v];
|
|
289
289
|
if (l.fn)
|
|
290
|
-
l.fn ===
|
|
290
|
+
l.fn === o && (!w || l.once) && (!c || l.context === c) && i(this, v);
|
|
291
291
|
else {
|
|
292
|
-
for (var m = 0, a = [],
|
|
293
|
-
(l[m].fn !==
|
|
294
|
-
a.length ? this._events[v] = a.length === 1 ? a[0] : a :
|
|
292
|
+
for (var m = 0, a = [], g = l.length; m < g; m++)
|
|
293
|
+
(l[m].fn !== o || w && !l[m].once || c && l[m].context !== c) && a.push(l[m]);
|
|
294
|
+
a.length ? this._events[v] = a.length === 1 ? a[0] : a : i(this, v);
|
|
295
295
|
}
|
|
296
296
|
return this;
|
|
297
297
|
}, r.prototype.removeAllListeners = function(t) {
|
|
298
|
-
var
|
|
299
|
-
return t ? (
|
|
300
|
-
}, r.prototype.off = r.prototype.removeListener, r.prototype.addListener = r.prototype.on, r.prefixed = n, r.EventEmitter = r,
|
|
298
|
+
var o;
|
|
299
|
+
return t ? (o = n ? n + t : t, this._events[o] && i(this, o)) : (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, s.exports = r;
|
|
301
301
|
}(C)), C.exports;
|
|
302
302
|
}
|
|
303
303
|
var K = J();
|
|
304
304
|
const Q = /* @__PURE__ */ A(K), L = new Q(), Z = ({
|
|
305
|
-
handle:
|
|
306
|
-
localUserId:
|
|
305
|
+
handle: s,
|
|
306
|
+
localUserId: u,
|
|
307
307
|
offlineTimeout: n = 3e4,
|
|
308
308
|
getTime: f = () => (/* @__PURE__ */ new Date()).getTime()
|
|
309
309
|
}) => {
|
|
310
|
-
const [h, p,
|
|
311
|
-
return
|
|
312
|
-
|
|
310
|
+
const [h, p, i] = D({}), [r, e, t] = D({});
|
|
311
|
+
return y(() => {
|
|
312
|
+
if (!s)
|
|
313
|
+
return;
|
|
314
|
+
const o = (v) => {
|
|
313
315
|
const [l, m] = v.message;
|
|
314
|
-
l !==
|
|
315
|
-
...
|
|
316
|
+
l !== u && (t.current[l] || L.emit("new_peer", v), p({
|
|
317
|
+
...i.current,
|
|
316
318
|
[l]: m
|
|
317
319
|
}), e({
|
|
318
320
|
...t.current,
|
|
319
321
|
[l]: f()
|
|
320
322
|
}));
|
|
321
323
|
}, c = () => {
|
|
322
|
-
const v =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
324
|
+
const v = { ...i.current }, l = { ...t.current }, m = f();
|
|
325
|
+
let a = !1;
|
|
326
|
+
for (const g in l)
|
|
327
|
+
m - l[g] > n && (delete v[g], delete l[g], a = !0);
|
|
328
|
+
a && (p(v), e(l));
|
|
326
329
|
};
|
|
327
|
-
|
|
330
|
+
s.on("ephemeral-message", o);
|
|
328
331
|
const w = setInterval(
|
|
329
332
|
c,
|
|
330
333
|
n
|
|
331
334
|
);
|
|
332
335
|
return () => {
|
|
333
|
-
|
|
336
|
+
s.removeListener("ephemeral-message", o), clearInterval(w);
|
|
334
337
|
};
|
|
335
|
-
}, [
|
|
338
|
+
}, [s, u, n, f]), [h, r];
|
|
336
339
|
}, U = ({
|
|
337
|
-
handle:
|
|
338
|
-
userId:
|
|
340
|
+
handle: s,
|
|
341
|
+
userId: u,
|
|
339
342
|
initialState: n,
|
|
340
343
|
heartbeatTime: f = 15e3
|
|
341
344
|
}) => {
|
|
342
|
-
const [h, p,
|
|
343
|
-
const t = typeof e == "function" ? e(
|
|
344
|
-
p(t),
|
|
345
|
+
const [h, p, i] = D(n), r = (e) => {
|
|
346
|
+
const t = typeof e == "function" ? e(i.current) : e;
|
|
347
|
+
p(t), s && s.broadcast([u, t]);
|
|
345
348
|
};
|
|
346
|
-
return
|
|
347
|
-
if (!
|
|
349
|
+
return y(() => {
|
|
350
|
+
if (!u || !s)
|
|
348
351
|
return;
|
|
349
|
-
const e = () => void
|
|
352
|
+
const e = () => void s.broadcast([u, i.current]);
|
|
350
353
|
e();
|
|
351
354
|
const t = setInterval(e, f);
|
|
352
355
|
return () => void clearInterval(t);
|
|
353
|
-
}, [
|
|
356
|
+
}, [s, u, f]), y(() => {
|
|
357
|
+
if (!s || !u)
|
|
358
|
+
return;
|
|
354
359
|
let e;
|
|
355
360
|
const t = L.on("new_peer", () => {
|
|
356
361
|
e = setTimeout(
|
|
357
|
-
() =>
|
|
362
|
+
() => s.broadcast([u, i.current]),
|
|
358
363
|
500
|
|
359
364
|
// Wait for the peer to be ready
|
|
360
365
|
);
|
|
@@ -362,7 +367,7 @@ const Q = /* @__PURE__ */ A(K), L = new Q(), Z = ({
|
|
|
362
367
|
return () => {
|
|
363
368
|
t.off("new_peer"), e && clearTimeout(e);
|
|
364
369
|
};
|
|
365
|
-
}, [
|
|
370
|
+
}, [s, u, L]), [h, r];
|
|
366
371
|
};
|
|
367
372
|
export {
|
|
368
373
|
z as RepoContext,
|
|
@@ -2,7 +2,7 @@ import { DocHandle } from '@automerge/automerge-repo/slim';
|
|
|
2
2
|
|
|
3
3
|
export interface UseLocalAwarenessProps {
|
|
4
4
|
/** The document handle to send ephemeral state on */
|
|
5
|
-
handle
|
|
5
|
+
handle?: DocHandle<unknown>;
|
|
6
6
|
/** Our user ID **/
|
|
7
7
|
userId: string;
|
|
8
8
|
/** The initial state object/primitive we should advertise */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLocalAwareness.d.ts","sourceRoot":"","sources":["../src/useLocalAwareness.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAA;AAE1D,MAAM,WAAW,sBAAsB;IACrC,qDAAqD;IACrD,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"useLocalAwareness.d.ts","sourceRoot":"","sources":["../src/useLocalAwareness.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAA;AAE1D,MAAM,WAAW,sBAAsB;IACrC,qDAAqD;IACrD,MAAM,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;IAC3B,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,6DAA6D;IAC7D,YAAY,EAAE,GAAG,CAAA;IACjB,wCAAwC;IACxC,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AACD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAAI,kDAK/B,sBAAsB,UAyDxB,CAAA"}
|
|
@@ -4,7 +4,7 @@ import { EventEmitter } from 'eventemitter3';
|
|
|
4
4
|
export declare const peerEvents: EventEmitter<string | symbol, any>;
|
|
5
5
|
export interface UseRemoteAwarenessProps<T> {
|
|
6
6
|
/** The handle to receive ephemeral state on */
|
|
7
|
-
handle
|
|
7
|
+
handle?: DocHandle<T>;
|
|
8
8
|
/** Our user ID */
|
|
9
9
|
localUserId?: string;
|
|
10
10
|
/** How long to wait (in ms) before marking a peer as offline */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRemoteAwareness.d.ts","sourceRoot":"","sources":["../src/useRemoteAwareness.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EAEV,MAAM,gCAAgC,CAAA;AAGvC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAG5C,eAAO,MAAM,UAAU,oCAAqB,CAAA;AAE5C,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,+CAA+C;IAC/C,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"useRemoteAwareness.d.ts","sourceRoot":"","sources":["../src/useRemoteAwareness.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EAEV,MAAM,gCAAgC,CAAA;AAGvC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAG5C,eAAO,MAAM,UAAU,oCAAqB,CAAA;AAE5C,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,+CAA+C;IAC/C,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACrB,kBAAkB;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,MAAM,CAAA;CACvB;AAED,wCAAwC;AACxC,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAC5C,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAE/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAAE,mDAKnC,uBAAuB,CAAC,CAAC,CAAC,KAAG,CAAC,UAAU,EAAE,UAAU,CAyDtD,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo-react-hooks",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0-alpha.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.
|
|
18
|
+
"@automerge/automerge-repo": "2.5.0-alpha.0",
|
|
19
19
|
"eventemitter3": "^5.0.1",
|
|
20
20
|
"react-usestateref": "^1.0.8"
|
|
21
21
|
},
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@testing-library/jest-dom": "^6.6.3",
|
|
24
24
|
"@testing-library/react": "^14.0.0",
|
|
25
25
|
"eslint-plugin-react-hooks": "^5.1.0",
|
|
26
|
-
"jsdom": "^
|
|
26
|
+
"jsdom": "^27.0.0",
|
|
27
27
|
"react": "^18.2.0",
|
|
28
28
|
"react-dom": "^18.2.0",
|
|
29
29
|
"react-error-boundary": "^5.0.0",
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "6d94459eecf708f9c508be7e22c2035abffc03a4"
|
|
50
50
|
}
|
package/src/useLocalAwareness.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { DocHandle } from "@automerge/automerge-repo/slim"
|
|
|
5
5
|
|
|
6
6
|
export interface UseLocalAwarenessProps {
|
|
7
7
|
/** The document handle to send ephemeral state on */
|
|
8
|
-
handle
|
|
8
|
+
handle?: DocHandle<unknown>
|
|
9
9
|
/** Our user ID **/
|
|
10
10
|
userId: string
|
|
11
11
|
/** The initial state object/primitive we should advertise */
|
|
@@ -43,13 +43,15 @@ export const useLocalAwareness = ({
|
|
|
43
43
|
: stateOrUpdater
|
|
44
44
|
setLocalState(state)
|
|
45
45
|
// TODO: Send deltas instead of entire state
|
|
46
|
-
handle
|
|
46
|
+
if (handle) {
|
|
47
|
+
handle.broadcast([userId, state])
|
|
48
|
+
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
useEffect(() => {
|
|
50
|
-
// Don't broadcast if userId isn't set: this avoids bogus broadcasts
|
|
51
|
-
// during the loading of a userId document.
|
|
52
|
-
if (!userId) {
|
|
52
|
+
// Don't broadcast if userId or handle isn't set: this avoids bogus broadcasts
|
|
53
|
+
// during the loading of a userId document or handle.
|
|
54
|
+
if (!userId || !handle) {
|
|
53
55
|
return
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -64,6 +66,10 @@ export const useLocalAwareness = ({
|
|
|
64
66
|
|
|
65
67
|
useEffect(() => {
|
|
66
68
|
// Send entire state to new peers
|
|
69
|
+
if (!handle || !userId) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
let broadcastTimeoutId: ReturnType<typeof setTimeout>
|
|
68
74
|
const newPeerEvents = peerEvents.on("new_peer", () => {
|
|
69
75
|
broadcastTimeoutId = setTimeout(
|
|
@@ -11,7 +11,7 @@ export const peerEvents = new EventEmitter()
|
|
|
11
11
|
|
|
12
12
|
export interface UseRemoteAwarenessProps<T> {
|
|
13
13
|
/** The handle to receive ephemeral state on */
|
|
14
|
-
handle
|
|
14
|
+
handle?: DocHandle<T>
|
|
15
15
|
/** Our user ID */
|
|
16
16
|
localUserId?: string
|
|
17
17
|
/** How long to wait (in ms) before marking a peer as offline */
|
|
@@ -48,6 +48,10 @@ export const useRemoteAwareness = <T>({
|
|
|
48
48
|
const [peerStates, setPeerStates, peerStatesRef] = useStateRef<PeerStates>({})
|
|
49
49
|
const [heartbeats, setHeartbeats, heartbeatsRef] = useStateRef<Heartbeats>({})
|
|
50
50
|
useEffect(() => {
|
|
51
|
+
if (!handle) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
// Receive incoming message
|
|
52
56
|
const handleIncomingUpdate = (
|
|
53
57
|
event: DocHandleEphemeralMessagePayload<T>
|
|
@@ -66,17 +70,21 @@ export const useRemoteAwareness = <T>({
|
|
|
66
70
|
}
|
|
67
71
|
// Remove peers we haven't seen recently
|
|
68
72
|
const pruneOfflinePeers = () => {
|
|
69
|
-
const peerStates = peerStatesRef.current
|
|
70
|
-
const heartbeats = heartbeatsRef.current
|
|
73
|
+
const peerStates = { ...peerStatesRef.current }
|
|
74
|
+
const heartbeats = { ...heartbeatsRef.current }
|
|
71
75
|
const time = getTime()
|
|
76
|
+
let hasChanges = false
|
|
72
77
|
for (const key in heartbeats) {
|
|
73
78
|
if (time - heartbeats[key] > offlineTimeout) {
|
|
74
79
|
delete peerStates[key]
|
|
75
80
|
delete heartbeats[key]
|
|
81
|
+
hasChanges = true
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
if (hasChanges) {
|
|
85
|
+
setPeerStates(peerStates)
|
|
86
|
+
setHeartbeats(heartbeats)
|
|
87
|
+
}
|
|
80
88
|
}
|
|
81
89
|
handle.on("ephemeral-message", handleIncomingUpdate)
|
|
82
90
|
// Check for offline peers every `offlineTimeout` ms
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { DocHandle } from "@automerge/automerge-repo"
|
|
2
|
+
import { render, waitFor } from "@testing-library/react"
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { describe, expect, it, vi } from "vitest"
|
|
5
|
+
import "@testing-library/jest-dom"
|
|
6
|
+
|
|
7
|
+
import { useLocalAwareness } from "../src/useLocalAwareness"
|
|
8
|
+
import { setup, ExampleDoc } from "./testSetup"
|
|
9
|
+
|
|
10
|
+
describe("useLocalAwareness", () => {
|
|
11
|
+
describe("with defined handle", () => {
|
|
12
|
+
const Component = ({
|
|
13
|
+
handle,
|
|
14
|
+
userId,
|
|
15
|
+
initialState,
|
|
16
|
+
}: {
|
|
17
|
+
handle: DocHandle<ExampleDoc>
|
|
18
|
+
userId: string
|
|
19
|
+
initialState: any
|
|
20
|
+
}) => {
|
|
21
|
+
const [state, setState] = useLocalAwareness({
|
|
22
|
+
handle,
|
|
23
|
+
userId,
|
|
24
|
+
initialState,
|
|
25
|
+
})
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<div data-testid="state">{JSON.stringify(state)}</div>
|
|
29
|
+
<button onClick={() => setState({ updated: true })}>Update</button>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it("should initialize with initial state", () => {
|
|
35
|
+
const { handleA, wrapper } = setup()
|
|
36
|
+
const initialState = { foo: "bar" }
|
|
37
|
+
|
|
38
|
+
const { getByTestId } = render(
|
|
39
|
+
<Component
|
|
40
|
+
handle={handleA}
|
|
41
|
+
userId="user1"
|
|
42
|
+
initialState={initialState}
|
|
43
|
+
/>,
|
|
44
|
+
{ wrapper }
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
expect(getByTestId("state")).toHaveTextContent(
|
|
48
|
+
JSON.stringify(initialState)
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("should broadcast state changes when handle is defined", async () => {
|
|
53
|
+
const { handleA, wrapper } = setup()
|
|
54
|
+
const broadcastSpy = vi.spyOn(handleA, "broadcast")
|
|
55
|
+
|
|
56
|
+
const { getByText, getByTestId } = render(
|
|
57
|
+
<Component
|
|
58
|
+
handle={handleA}
|
|
59
|
+
userId="user1"
|
|
60
|
+
initialState={{ initial: true }}
|
|
61
|
+
/>,
|
|
62
|
+
{ wrapper }
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Wait for initial heartbeat
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
expect(broadcastSpy).toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Clear previous calls
|
|
71
|
+
broadcastSpy.mockClear()
|
|
72
|
+
|
|
73
|
+
// Click update button
|
|
74
|
+
React.act(() => {
|
|
75
|
+
getByText("Update").click()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Should broadcast the update
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(broadcastSpy).toHaveBeenCalledWith(["user1", { updated: true }])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(getByTestId("state")).toHaveTextContent(
|
|
84
|
+
JSON.stringify({ updated: true })
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("should send periodic heartbeats", async () => {
|
|
89
|
+
const { handleA, wrapper } = setup()
|
|
90
|
+
const broadcastSpy = vi.spyOn(handleA, "broadcast")
|
|
91
|
+
const initialState = { heartbeat: true }
|
|
92
|
+
|
|
93
|
+
render(
|
|
94
|
+
<Component
|
|
95
|
+
handle={handleA}
|
|
96
|
+
userId="user1"
|
|
97
|
+
initialState={initialState}
|
|
98
|
+
/>,
|
|
99
|
+
{ wrapper }
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// Should send initial heartbeat
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(broadcastSpy).toHaveBeenCalledWith(["user1", initialState])
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("should not broadcast if userId is not set", async () => {
|
|
109
|
+
const { handleA, wrapper } = setup()
|
|
110
|
+
const broadcastSpy = vi.spyOn(handleA, "broadcast")
|
|
111
|
+
|
|
112
|
+
render(
|
|
113
|
+
<Component handle={handleA} userId="" initialState={{ test: true }} />,
|
|
114
|
+
{ wrapper }
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Wait a bit
|
|
118
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
119
|
+
|
|
120
|
+
// Should not have broadcast
|
|
121
|
+
expect(broadcastSpy).not.toHaveBeenCalled()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("should cleanup on unmount", async () => {
|
|
125
|
+
const { handleA, wrapper } = setup()
|
|
126
|
+
|
|
127
|
+
const { unmount } = render(
|
|
128
|
+
<Component
|
|
129
|
+
handle={handleA}
|
|
130
|
+
userId="user1"
|
|
131
|
+
initialState={{ test: true }}
|
|
132
|
+
/>,
|
|
133
|
+
{ wrapper }
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Wait for initial heartbeat
|
|
137
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
138
|
+
|
|
139
|
+
// Unmount
|
|
140
|
+
unmount()
|
|
141
|
+
|
|
142
|
+
// No errors should occur
|
|
143
|
+
expect(true).toBe(true)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("with undefined handle", () => {
|
|
148
|
+
const Component = ({
|
|
149
|
+
handle,
|
|
150
|
+
userId,
|
|
151
|
+
initialState,
|
|
152
|
+
}: {
|
|
153
|
+
handle?: DocHandle<ExampleDoc>
|
|
154
|
+
userId: string
|
|
155
|
+
initialState: any
|
|
156
|
+
}) => {
|
|
157
|
+
const [state, setState] = useLocalAwareness({
|
|
158
|
+
handle,
|
|
159
|
+
userId,
|
|
160
|
+
initialState,
|
|
161
|
+
})
|
|
162
|
+
return (
|
|
163
|
+
<div>
|
|
164
|
+
<div data-testid="state">{JSON.stringify(state)}</div>
|
|
165
|
+
<button onClick={() => setState({ updated: true })}>Update</button>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
it("should not crash when handle is undefined", () => {
|
|
171
|
+
const { wrapper } = setup()
|
|
172
|
+
|
|
173
|
+
expect(() => {
|
|
174
|
+
render(<Component userId="user1" initialState={{ test: true }} />, {
|
|
175
|
+
wrapper,
|
|
176
|
+
})
|
|
177
|
+
}).not.toThrow()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("should still maintain local state when handle is undefined", () => {
|
|
181
|
+
const { wrapper } = setup()
|
|
182
|
+
const initialState = { foo: "bar" }
|
|
183
|
+
|
|
184
|
+
const { getByTestId } = render(
|
|
185
|
+
<Component userId="user1" initialState={initialState} />,
|
|
186
|
+
{ wrapper }
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
expect(getByTestId("state")).toHaveTextContent(
|
|
190
|
+
JSON.stringify(initialState)
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("should update local state without broadcasting when handle is undefined", async () => {
|
|
195
|
+
const { wrapper } = setup()
|
|
196
|
+
|
|
197
|
+
const { getByText, getByTestId } = render(
|
|
198
|
+
<Component userId="user1" initialState={{ initial: true }} />,
|
|
199
|
+
{ wrapper }
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Click update button
|
|
203
|
+
React.act(() => {
|
|
204
|
+
getByText("Update").click()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// State should update
|
|
208
|
+
await waitFor(() => {
|
|
209
|
+
expect(getByTestId("state")).toHaveTextContent(
|
|
210
|
+
JSON.stringify({ updated: true })
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("should handle transition from undefined to defined handle", async () => {
|
|
216
|
+
const { handleA, wrapper } = setup()
|
|
217
|
+
const broadcastSpy = vi.spyOn(handleA, "broadcast")
|
|
218
|
+
|
|
219
|
+
const { rerender, getByText } = render(
|
|
220
|
+
<Component userId="user1" initialState={{ test: true }} />,
|
|
221
|
+
{ wrapper }
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// Wait a bit with undefined handle
|
|
225
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
226
|
+
|
|
227
|
+
// Should not have broadcast yet
|
|
228
|
+
expect(broadcastSpy).not.toHaveBeenCalled()
|
|
229
|
+
|
|
230
|
+
// Now provide a handle
|
|
231
|
+
rerender(
|
|
232
|
+
<Component
|
|
233
|
+
handle={handleA}
|
|
234
|
+
userId="user1"
|
|
235
|
+
initialState={{ test: true }}
|
|
236
|
+
/>
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
// Should start broadcasting
|
|
240
|
+
await waitFor(() => {
|
|
241
|
+
expect(broadcastSpy).toHaveBeenCalled()
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it("should handle transition from defined to undefined handle", async () => {
|
|
246
|
+
const { handleA, wrapper } = setup()
|
|
247
|
+
const broadcastSpy = vi.spyOn(handleA, "broadcast")
|
|
248
|
+
|
|
249
|
+
const { rerender } = render(
|
|
250
|
+
<Component
|
|
251
|
+
handle={handleA}
|
|
252
|
+
userId="user1"
|
|
253
|
+
initialState={{ test: true }}
|
|
254
|
+
/>,
|
|
255
|
+
{ wrapper }
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// Wait for initial broadcast
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(broadcastSpy).toHaveBeenCalled()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
broadcastSpy.mockClear()
|
|
264
|
+
|
|
265
|
+
// Now remove the handle
|
|
266
|
+
rerender(<Component userId="user1" initialState={{ test: true }} />)
|
|
267
|
+
|
|
268
|
+
// Wait a bit
|
|
269
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
270
|
+
|
|
271
|
+
// Should not broadcast anymore
|
|
272
|
+
expect(broadcastSpy).not.toHaveBeenCalled()
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
})
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { DocHandle } from "@automerge/automerge-repo"
|
|
2
|
+
import { render, waitFor } from "@testing-library/react"
|
|
3
|
+
import React from "react"
|
|
4
|
+
import { describe, expect, it, vi } from "vitest"
|
|
5
|
+
import "@testing-library/jest-dom"
|
|
6
|
+
|
|
7
|
+
import { useRemoteAwareness } from "../src/useRemoteAwareness"
|
|
8
|
+
import { setup, ExampleDoc } from "./testSetup"
|
|
9
|
+
|
|
10
|
+
describe("useRemoteAwareness", () => {
|
|
11
|
+
describe("with defined handle", () => {
|
|
12
|
+
const Component = ({
|
|
13
|
+
handle,
|
|
14
|
+
localUserId,
|
|
15
|
+
}: {
|
|
16
|
+
handle: DocHandle<ExampleDoc>
|
|
17
|
+
localUserId?: string
|
|
18
|
+
}) => {
|
|
19
|
+
const [peerStates, heartbeats] = useRemoteAwareness({
|
|
20
|
+
handle,
|
|
21
|
+
localUserId,
|
|
22
|
+
})
|
|
23
|
+
return (
|
|
24
|
+
<div>
|
|
25
|
+
<div data-testid="peer-states">{JSON.stringify(peerStates)}</div>
|
|
26
|
+
<div data-testid="heartbeats">{JSON.stringify(heartbeats)}</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
it("should initialize with empty peer states", () => {
|
|
32
|
+
const { handleA, wrapper } = setup()
|
|
33
|
+
|
|
34
|
+
const { getByTestId } = render(
|
|
35
|
+
<Component handle={handleA} localUserId="local-user" />,
|
|
36
|
+
{ wrapper }
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
expect(getByTestId("peer-states")).toHaveTextContent("{}")
|
|
40
|
+
expect(getByTestId("heartbeats")).toHaveTextContent("{}")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("should receive and store remote peer states", async () => {
|
|
44
|
+
const { handleA, wrapper } = setup()
|
|
45
|
+
|
|
46
|
+
const { getByTestId } = render(
|
|
47
|
+
<Component handle={handleA} localUserId="local-user" />,
|
|
48
|
+
{ wrapper }
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Simulate receiving a message from a remote peer
|
|
52
|
+
const mockEvent = {
|
|
53
|
+
handle: handleA,
|
|
54
|
+
message: ["remote-user", { status: "online" }],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// @ts-ignore - accessing private emit for testing
|
|
58
|
+
React.act(() => {
|
|
59
|
+
handleA.emit("ephemeral-message", mockEvent)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
expect(getByTestId("peer-states")).toHaveTextContent(
|
|
64
|
+
JSON.stringify({ "remote-user": { status: "online" } })
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("should filter out messages from local user", async () => {
|
|
70
|
+
const { handleA, wrapper } = setup()
|
|
71
|
+
|
|
72
|
+
const { getByTestId } = render(
|
|
73
|
+
<Component handle={handleA} localUserId="local-user" />,
|
|
74
|
+
{ wrapper }
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// Simulate receiving a message from the local user (should be ignored)
|
|
78
|
+
const mockEvent = {
|
|
79
|
+
handle: handleA,
|
|
80
|
+
message: ["local-user", { status: "online" }],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// @ts-ignore - accessing private emit for testing
|
|
84
|
+
React.act(() => {
|
|
85
|
+
handleA.emit("ephemeral-message", mockEvent)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Wait a bit
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
90
|
+
|
|
91
|
+
// Should still be empty
|
|
92
|
+
expect(getByTestId("peer-states")).toHaveTextContent("{}")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should update heartbeat timestamps when receiving messages", async () => {
|
|
96
|
+
const { handleA, wrapper } = setup()
|
|
97
|
+
const mockGetTime = vi.fn(() => 1000)
|
|
98
|
+
|
|
99
|
+
const ComponentWithTime = () => {
|
|
100
|
+
const [peerStates, heartbeats] = useRemoteAwareness({
|
|
101
|
+
handle: handleA,
|
|
102
|
+
localUserId: "local-user",
|
|
103
|
+
getTime: mockGetTime,
|
|
104
|
+
})
|
|
105
|
+
return (
|
|
106
|
+
<div>
|
|
107
|
+
<div data-testid="heartbeats">{JSON.stringify(heartbeats)}</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { getByTestId } = render(<ComponentWithTime />, { wrapper })
|
|
113
|
+
|
|
114
|
+
// Simulate receiving a message
|
|
115
|
+
const mockEvent = {
|
|
116
|
+
handle: handleA,
|
|
117
|
+
message: ["remote-user", { status: "online" }],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// @ts-ignore - accessing private emit for testing
|
|
121
|
+
React.act(() => {
|
|
122
|
+
handleA.emit("ephemeral-message", mockEvent)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(getByTestId("heartbeats")).toHaveTextContent(
|
|
127
|
+
JSON.stringify({ "remote-user": 1000 })
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("should prune offline peers after timeout", async () => {
|
|
133
|
+
const { handleA, wrapper } = setup()
|
|
134
|
+
let currentTime = 1000
|
|
135
|
+
const mockGetTime = vi.fn(() => currentTime)
|
|
136
|
+
|
|
137
|
+
const ComponentWithTime = () => {
|
|
138
|
+
const [peerStates] = useRemoteAwareness({
|
|
139
|
+
handle: handleA,
|
|
140
|
+
localUserId: "local-user",
|
|
141
|
+
offlineTimeout: 100, // Short timeout for testing
|
|
142
|
+
getTime: mockGetTime,
|
|
143
|
+
})
|
|
144
|
+
return (
|
|
145
|
+
<div>
|
|
146
|
+
<div data-testid="peer-states">{JSON.stringify(peerStates)}</div>
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { getByTestId } = render(<ComponentWithTime />, { wrapper })
|
|
152
|
+
|
|
153
|
+
// Simulate receiving a message
|
|
154
|
+
const mockEvent = {
|
|
155
|
+
handle: handleA,
|
|
156
|
+
message: ["remote-user", { status: "online" }],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// @ts-ignore - accessing private emit for testing
|
|
160
|
+
React.act(() => {
|
|
161
|
+
handleA.emit("ephemeral-message", mockEvent)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Should have the peer
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
expect(getByTestId("peer-states")).toHaveTextContent(
|
|
167
|
+
JSON.stringify({ "remote-user": { status: "online" } })
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// Advance time past the offline timeout
|
|
172
|
+
currentTime = 1200
|
|
173
|
+
|
|
174
|
+
// Wait for the pruning interval to run (it runs every 100ms)
|
|
175
|
+
await new Promise(resolve => setTimeout(resolve, 150))
|
|
176
|
+
|
|
177
|
+
// Should now be pruned
|
|
178
|
+
expect(getByTestId("peer-states")).toHaveTextContent("{}")
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("should cleanup listeners on unmount", async () => {
|
|
182
|
+
const { handleA, wrapper } = setup()
|
|
183
|
+
const removeListenerSpy = vi.spyOn(handleA, "removeListener")
|
|
184
|
+
|
|
185
|
+
const { unmount } = render(
|
|
186
|
+
<Component handle={handleA} localUserId="local-user" />,
|
|
187
|
+
{ wrapper }
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// Unmount
|
|
191
|
+
unmount()
|
|
192
|
+
|
|
193
|
+
// Should have removed listener
|
|
194
|
+
expect(removeListenerSpy).toHaveBeenCalledWith(
|
|
195
|
+
"ephemeral-message",
|
|
196
|
+
expect.any(Function)
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe("with undefined handle", () => {
|
|
202
|
+
const Component = ({
|
|
203
|
+
handle,
|
|
204
|
+
localUserId,
|
|
205
|
+
}: {
|
|
206
|
+
handle?: DocHandle<ExampleDoc>
|
|
207
|
+
localUserId?: string
|
|
208
|
+
}) => {
|
|
209
|
+
const [peerStates, heartbeats] = useRemoteAwareness({
|
|
210
|
+
handle,
|
|
211
|
+
localUserId,
|
|
212
|
+
})
|
|
213
|
+
return (
|
|
214
|
+
<div>
|
|
215
|
+
<div data-testid="peer-states">{JSON.stringify(peerStates)}</div>
|
|
216
|
+
<div data-testid="heartbeats">{JSON.stringify(heartbeats)}</div>
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
it("should not crash when handle is undefined", () => {
|
|
222
|
+
const { wrapper } = setup()
|
|
223
|
+
|
|
224
|
+
expect(() => {
|
|
225
|
+
render(<Component localUserId="local-user" />, { wrapper })
|
|
226
|
+
}).not.toThrow()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it("should return empty peer states when handle is undefined", () => {
|
|
230
|
+
const { wrapper } = setup()
|
|
231
|
+
|
|
232
|
+
const { getByTestId } = render(<Component localUserId="local-user" />, {
|
|
233
|
+
wrapper,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
expect(getByTestId("peer-states")).toHaveTextContent("{}")
|
|
237
|
+
expect(getByTestId("heartbeats")).toHaveTextContent("{}")
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it("should handle transition from undefined to defined handle", async () => {
|
|
241
|
+
const { handleA, wrapper } = setup()
|
|
242
|
+
|
|
243
|
+
const { rerender, getByTestId } = render(
|
|
244
|
+
<Component localUserId="local-user" />,
|
|
245
|
+
{ wrapper }
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// Should have empty states
|
|
249
|
+
expect(getByTestId("peer-states")).toHaveTextContent("{}")
|
|
250
|
+
|
|
251
|
+
// Now provide a handle
|
|
252
|
+
rerender(<Component handle={handleA} localUserId="local-user" />)
|
|
253
|
+
|
|
254
|
+
// Simulate receiving a message
|
|
255
|
+
const mockEvent = {
|
|
256
|
+
handle: handleA,
|
|
257
|
+
message: ["remote-user", { status: "online" }],
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// @ts-ignore - accessing private emit for testing
|
|
261
|
+
React.act(() => {
|
|
262
|
+
handleA.emit("ephemeral-message", mockEvent)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// Should now receive and display the peer state
|
|
266
|
+
await waitFor(() => {
|
|
267
|
+
expect(getByTestId("peer-states")).toHaveTextContent(
|
|
268
|
+
JSON.stringify({ "remote-user": { status: "online" } })
|
|
269
|
+
)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it("should handle transition from defined to undefined handle", async () => {
|
|
274
|
+
const { handleA, wrapper } = setup()
|
|
275
|
+
|
|
276
|
+
const { rerender, getByTestId } = render(
|
|
277
|
+
<Component handle={handleA} localUserId="local-user" />,
|
|
278
|
+
{ wrapper }
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
// Simulate receiving a message
|
|
282
|
+
const mockEvent = {
|
|
283
|
+
handle: handleA,
|
|
284
|
+
message: ["remote-user", { status: "online" }],
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// @ts-ignore - accessing private emit for testing
|
|
288
|
+
React.act(() => {
|
|
289
|
+
handleA.emit("ephemeral-message", mockEvent)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// Should have the peer
|
|
293
|
+
await waitFor(() => {
|
|
294
|
+
expect(getByTestId("peer-states")).toHaveTextContent(
|
|
295
|
+
JSON.stringify({ "remote-user": { status: "online" } })
|
|
296
|
+
)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// Now remove the handle
|
|
300
|
+
rerender(<Component localUserId="local-user" />)
|
|
301
|
+
|
|
302
|
+
// The peer states should remain (they don't get cleared automatically)
|
|
303
|
+
// but new messages won't be received
|
|
304
|
+
expect(getByTestId("peer-states")).toHaveTextContent(
|
|
305
|
+
JSON.stringify({ "remote-user": { status: "online" } })
|
|
306
|
+
)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it("should not attempt to add listeners when handle is undefined", async () => {
|
|
310
|
+
const { wrapper } = setup()
|
|
311
|
+
|
|
312
|
+
// This should not throw any errors
|
|
313
|
+
const { unmount } = render(<Component localUserId="local-user" />, {
|
|
314
|
+
wrapper,
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// Wait a bit
|
|
318
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
319
|
+
|
|
320
|
+
// Unmount should also not throw
|
|
321
|
+
unmount()
|
|
322
|
+
|
|
323
|
+
expect(true).toBe(true)
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
})
|