@automerge/automerge-repo-react-hooks 2.4.0 → 2.5.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,18 +1,18 @@
1
- import q, { createContext as I, useContext as j, useRef as $, useState as E, useEffect as g, useCallback as R } from "react";
2
- function O(o) {
3
- let i = "pending", n, f;
4
- const h = o.then(
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
- i = "success", n = p;
6
+ u = "success", n = p;
7
7
  },
8
8
  (p) => {
9
- i = "error", f = p;
9
+ u = "error", f = p;
10
10
  }
11
11
  );
12
12
  return {
13
- promise: o,
13
+ promise: s,
14
14
  read() {
15
- switch (i) {
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 o = j(z);
29
- if (!o) throw new Error("Repo was not found on RepoContext.");
30
- return o;
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(o, { suspense: i } = { suspense: !1 }) {
33
+ function F(s, { suspense: u } = { suspense: !1 }) {
34
34
  const n = k(), f = $(), [h, p] = E();
35
- let u = h;
36
- if (o && !u) {
37
- const e = n.findWithProgress(o);
38
- e.state === "ready" && (u = e.handle);
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 = o ? x.get(o) : void 0;
41
- if (!r && o) {
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(o, { signal: f.current.signal });
44
- r = O(e), x.set(o, r);
43
+ const e = n.find(s, { signal: f.current.signal });
44
+ r = O(e), x.set(s, r);
45
45
  }
46
- return g(() => {
47
- u || i || !r || r.promise.then((e) => {
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
- }, [u, i, r]), u || !i || !r ? u : r.read();
52
+ }, [i, u, r]), i || !u || !r ? i : r.read();
53
53
  }
54
- function X(o, i = { suspense: !1 }) {
55
- const n = F(o, i), [f, h] = E(() => n?.doc()), [p, u] = E();
56
- g(() => {
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]), g(() => {
58
+ }, [n]), y(() => {
59
59
  if (!n)
60
60
  return;
61
61
  const e = () => h(n.doc()), t = () => {
62
- u(new Error(`Document ${o} was deleted`));
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, o]);
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(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;
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(o, i) {
87
- return o.size === i.size && Array.from(o).every((n) => i.has(n));
86
+ function W(s, u) {
87
+ return s.size === u.size && Array.from(s).every((n) => u.has(n));
88
88
  }
89
- function T(o, { suspense: i = !1 } = {}) {
90
- const n = N(o), f = k(), [h, p] = E(() => {
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 s;
93
+ let o;
94
94
  try {
95
- s = f.findWithProgress(t);
95
+ o = f.findWithProgress(t);
96
96
  } catch {
97
97
  continue;
98
98
  }
99
- s.state === "ready" && e.set(t, s.handle);
99
+ o.state === "ready" && e.set(t, o.handle);
100
100
  }
101
101
  return e;
102
- }), u = [], r = /* @__PURE__ */ new Map();
102
+ }), i = [], r = /* @__PURE__ */ new Map();
103
103
  for (const e of n.values()) {
104
- let t = h.get(e), s = x.get(e);
105
- if (!s)
104
+ let t = h.get(e), o = x.get(e);
105
+ if (!o)
106
106
  try {
107
107
  const c = f.find(e);
108
- s = O(c), x.set(e, s);
108
+ o = O(c), x.set(e, o);
109
109
  } catch {
110
110
  continue;
111
111
  }
112
112
  try {
113
- t ??= s.read(), r.set(e, t);
113
+ t ??= o.read(), r.set(e, t);
114
114
  } catch (c) {
115
- c instanceof Promise ? u.push(s) : r.set(e, void 0);
115
+ c instanceof Promise ? i.push(o) : r.set(e, void 0);
116
116
  }
117
117
  }
118
- if (g(() => {
119
- u.length > 0 ? Promise.allSettled(u.map((e) => e.promise)).then(
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 s = t.value;
124
- r.set(s.url, s);
123
+ const o = t.value;
124
+ r.set(o.url, o);
125
125
  }
126
126
  }), p(r);
127
127
  }
128
128
  ) : p(r);
129
- }, [i, n]), i && u.length > 0)
130
- throw Promise.all(u.map((e) => e.promise));
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(o, { suspense: i = !0 } = {}) {
134
- const n = T(o, { suspense: i }), [f, h] = E(() => {
135
- const u = /* @__PURE__ */ new Map();
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 && u.set(e, r?.doc());
139
- }), u;
138
+ e && i.set(e, r?.doc());
139
+ }), i;
140
140
  });
141
- g(() => {
142
- const u = /* @__PURE__ */ new Map();
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((s) => {
147
- const c = new Map(s);
146
+ h((o) => {
147
+ const c = new Map(o);
148
148
  return c.set(e, r.doc()), c;
149
149
  });
150
150
  };
151
- h((s) => {
152
- const c = new Map(s);
151
+ h((o) => {
152
+ const c = new Map(o);
153
153
  return c.set(e, r.doc()), c;
154
- }), r.on("change", t), u.set(e, 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 = u.get(e);
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
- (u, r, e) => {
170
- const t = n.get(u);
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(o) {
178
- return o && o.__esModule && Object.prototype.hasOwnProperty.call(o, "default") ? o.default : o;
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 o = q, i = function(f) {
184
+ var s = q, u = function(f) {
185
185
  return typeof f == "function";
186
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);
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(o) {
199
- var i = Object.prototype.hasOwnProperty, n = "~";
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, s) {
204
- this.fn = e, this.context = t, this.once = s || !1;
203
+ function h(e, t, o) {
204
+ this.fn = e, this.context = t, this.once = o || !1;
205
205
  }
206
- function p(e, t, s, c, w) {
207
- if (typeof s != "function")
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(s, c || e, w), l = n ? n + t : t;
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 u(e, t) {
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 = [], s, c;
219
+ var t = [], o, c;
220
220
  if (this._eventsCount === 0) return t;
221
- for (c in s = this._events)
222
- i.call(s, c) && t.push(n ? c.slice(1) : c);
223
- return Object.getOwnPropertySymbols ? t.concat(Object.getOwnPropertySymbols(s)) : t;
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 s = n ? n + t : t, c = this._events[s];
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 s = n ? n + t : t, c = this._events[s];
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, s, c, w, v, l) {
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], y = arguments.length, _, d;
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), y) {
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, s), !0;
243
+ return a.fn.call(a.context, o), !0;
244
244
  case 3:
245
- return a.fn.call(a.context, s, c), !0;
245
+ return a.fn.call(a.context, o, c), !0;
246
246
  case 4:
247
- return a.fn.call(a.context, s, c, w), !0;
247
+ return a.fn.call(a.context, o, c, w), !0;
248
248
  case 5:
249
- return a.fn.call(a.context, s, c, w, v), !0;
249
+ return a.fn.call(a.context, o, c, w, v), !0;
250
250
  case 6:
251
- return a.fn.call(a.context, s, c, w, v, l), !0;
251
+ return a.fn.call(a.context, o, c, w, v, l), !0;
252
252
  }
253
- for (d = 1, _ = new Array(y - 1); d < y; d++)
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), y) {
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, s);
264
+ a[d].fn.call(a[d].context, o);
265
265
  break;
266
266
  case 3:
267
- a[d].fn.call(a[d].context, s, c);
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, s, c, w);
270
+ a[d].fn.call(a[d].context, o, c, w);
271
271
  break;
272
272
  default:
273
- if (!_) for (b = 1, _ = new Array(y - 1); b < y; b++)
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, 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) {
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 (!s)
287
- return u(this, v), this;
286
+ if (!o)
287
+ return i(this, v), this;
288
288
  var l = this._events[v];
289
289
  if (l.fn)
290
- l.fn === s && (!w || l.once) && (!c || l.context === c) && u(this, v);
290
+ l.fn === o && (!w || l.once) && (!c || l.context === c) && i(this, v);
291
291
  else {
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]);
294
- a.length ? this._events[v] = a.length === 1 ? a[0] : a : u(this, v);
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 s;
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;
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: o,
306
- localUserId: i,
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, u] = D({}), [r, e, t] = D({});
311
- return g(() => {
312
- const s = (v) => {
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 !== i && (t.current[l] || L.emit("new_peer", v), p({
315
- ...u.current,
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 = 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);
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
- o.on("ephemeral-message", s);
330
+ s.on("ephemeral-message", o);
328
331
  const w = setInterval(
329
332
  c,
330
333
  n
331
334
  );
332
335
  return () => {
333
- o.removeListener("ephemeral-message", s), clearInterval(w);
336
+ s.removeListener("ephemeral-message", o), clearInterval(w);
334
337
  };
335
- }, [o, i, n, f]), [h, r];
338
+ }, [s, u, n, f]), [h, r];
336
339
  }, U = ({
337
- handle: o,
338
- userId: i,
340
+ handle: s,
341
+ userId: u,
339
342
  initialState: n,
340
343
  heartbeatTime: f = 15e3
341
344
  }) => {
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]);
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 g(() => {
347
- if (!i)
349
+ return y(() => {
350
+ if (!u || !s)
348
351
  return;
349
- const e = () => void o.broadcast([i, u.current]);
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
- }, [o, i, f]), g(() => {
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
- () => o.broadcast([i, u.current]),
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
- }, [o, i, L]), [h, r];
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: DocHandle<unknown>;
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;IAC1B,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,UAmDxB,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: DocHandle<T>;
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;IACpB,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,CAiDtD,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.4.0",
3
+ "version": "2.5.0-alpha.1",
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.4.0",
18
+ "@automerge/automerge-repo": "2.5.0-alpha.1",
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": "586290a804e12206362cf41c87e309bd0fb22d69"
49
+ "gitHead": "6865d2fca3f7fb1ebaf155aa858f4be3b65bbbef"
50
50
  }
@@ -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: DocHandle<unknown>
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.broadcast([userId, state])
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: DocHandle<T>
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
- setPeerStates(peerStates)
79
- setHeartbeats(heartbeats)
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
+ })