@automerge/automerge-repo-react-hooks 2.3.0-alpha.0 → 2.3.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.
@@ -0,0 +1,2 @@
1
+ export declare function useSet<T>(items: T[]): Set<T>;
2
+ //# sourceMappingURL=useSet.d.ts.map
@@ -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 b, useEffect as y, useCallback as S } from "react";
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 l = "pending", r, i;
3
+ let i = "pending", n, f;
4
4
  const h = o.then(
5
5
  (p) => {
6
- l = "success", r = p;
6
+ i = "success", n = p;
7
7
  },
8
8
  (p) => {
9
- l = "error", i = p;
9
+ i = "error", f = p;
10
10
  }
11
11
  );
12
12
  return {
13
13
  promise: o,
14
14
  read() {
15
- switch (l) {
15
+ switch (i) {
16
16
  case "pending":
17
17
  throw h;
18
18
  case "error":
19
- throw i;
19
+ throw f;
20
20
  case "success":
21
- return r;
21
+ return n;
22
22
  }
23
23
  }
24
24
  };
25
25
  }
26
- const F = I(null);
26
+ const z = I(null);
27
27
  function k() {
28
- const o = j(F);
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 N(o, { suspense: l } = { suspense: !1 }) {
34
- const r = k(), i = $(), [h, p] = b();
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 t = r.findWithProgress(o);
38
- t.state === "ready" && (u = t.handle);
37
+ const e = n.findWithProgress(o);
38
+ e.state === "ready" && (u = e.handle);
39
39
  }
40
- let n = o ? x.get(o) : void 0;
41
- if (!n && o) {
42
- i.current?.abort(), i.current = new AbortController();
43
- const t = r.find(o, { signal: i.current.signal });
44
- n = O(t), x.set(o, n);
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 y(() => {
47
- l || !n || n.promise.then((t) => {
48
- p(t);
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
- }, [l, n]), u || !l || !n ? u : n.read();
52
+ }, [u, i, r]), u || !i || !r ? u : r.read();
53
53
  }
54
- function Q(o, l = { suspense: !1 }) {
55
- const r = N(o, l), [i, h] = b(() => r?.doc()), [p, u] = b();
56
- y(() => {
57
- h(r?.doc());
58
- }, [r]), y(() => {
59
- if (!r)
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 t = () => h(r.doc()), e = () => {
61
+ const e = () => h(n.doc()), t = () => {
62
62
  u(new Error(`Document ${o} was deleted`));
63
63
  };
64
- return r.on("change", t), r.on("delete", e), () => {
65
- r.removeListener("change", t), r.removeListener("delete", e);
64
+ return n.on("change", e), n.on("delete", t), () => {
65
+ n.removeListener("change", e), n.removeListener("delete", t);
66
66
  };
67
- }, [r, o]);
68
- const n = S(
69
- (t, e) => {
70
- r.change(t, e);
67
+ }, [n, o]);
68
+ const r = R(
69
+ (e, t) => {
70
+ n.change(e, t);
71
71
  },
72
- [r]
72
+ [n]
73
73
  );
74
74
  if (p)
75
75
  throw p;
76
- return i ? [i, n] : [void 0, () => {
76
+ return f ? [f, r] : [void 0, () => {
77
77
  }];
78
78
  }
79
- function W(o, { suspense: l = !1 } = {}) {
80
- const r = k(), [i, h] = b(() => {
81
- const n = /* @__PURE__ */ new Map();
82
- for (const t of o) {
83
- let e;
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
- e = r.findWithProgress(t);
95
+ s = f.findWithProgress(t);
86
96
  } catch {
87
97
  continue;
88
98
  }
89
- e.state === "ready" && n.set(t, e.handle);
99
+ s.state === "ready" && e.set(t, s.handle);
90
100
  }
91
- return n;
92
- }), p = [], u = /* @__PURE__ */ new Map();
93
- for (const n of o) {
94
- let t = i.get(n), e = x.get(n);
95
- if (!e)
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 s = r.find(n);
98
- e = O(s), x.set(n, e);
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 ??= e.read(), u.set(n, t);
104
- } catch (s) {
105
- s instanceof Promise ? p.push(e) : u.set(n, void 0);
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 (y(() => {
109
- p.length > 0 ? Promise.allSettled(p.map((n) => n.promise)).then(
110
- (n) => {
111
- n.forEach((t) => {
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 e = t.value;
114
- u.set(e.url, e);
123
+ const s = t.value;
124
+ r.set(s.url, s);
115
125
  }
116
- }), h(u);
126
+ }), p(r);
117
127
  }
118
- ) : h(u);
119
- }, [l, o]), l && p.length > 0)
120
- throw Promise.all(p.map((n) => n.promise));
121
- return i;
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 V(o, { suspense: l = !0 } = {}) {
124
- const r = W(o, { suspense: l }), [i, h] = b(() => {
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 r.forEach((n) => {
127
- const t = n?.url;
128
- t && u.set(t, n?.doc());
136
+ return n.forEach((r) => {
137
+ const e = r?.url;
138
+ e && u.set(e, r?.doc());
129
139
  }), u;
130
140
  });
131
- y(() => {
141
+ g(() => {
132
142
  const u = /* @__PURE__ */ new Map();
133
- return r.forEach((n, t) => {
134
- if (n) {
135
- const e = () => {
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(t, n.doc()), c;
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(t, n.doc()), c;
144
- }), n.on("change", e), u.set(t, e);
153
+ return c.set(e, r.doc()), c;
154
+ }), r.on("change", t), u.set(e, t);
145
155
  }
146
- }), h((n) => {
147
- const t = new Map(n);
148
- for (const [e] of t)
149
- r.has(e) || t.delete(e);
150
- return t;
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
- r.forEach((n, t) => {
153
- const e = u.get(t);
154
- n && e && n.removeListener("change", e);
162
+ n.forEach((r, e) => {
163
+ const t = u.get(e);
164
+ r && t && r.removeListener("change", t);
155
165
  });
156
166
  };
157
- }, [r]);
158
- const p = S(
159
- (u, n, t) => {
160
- const e = r.get(u);
161
- e && e.change(n, t);
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
- [r]
173
+ [n]
164
174
  );
165
- return [i, p];
175
+ return [f, p];
166
176
  }
167
- function H(o) {
177
+ function A(o) {
168
178
  return o && o.__esModule && Object.prototype.hasOwnProperty.call(o, "default") ? o.default : o;
169
179
  }
170
- var C, P;
171
- function T() {
172
- if (P) return C;
173
- P = 1;
174
- var o = q, l = function(i) {
175
- return typeof i == "function";
176
- }, r = function(i) {
177
- var h = o.useState(i), p = h[0], u = h[1], n = o.useRef(p), t = o.useCallback(function(e) {
178
- n.current = l(e) ? e(n.current) : e, u(n.current);
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, t, n];
190
+ return [p, e, r];
181
191
  };
182
- return C = r, C;
192
+ return S = n, S;
183
193
  }
184
- var z = T();
185
- const L = /* @__PURE__ */ H(z);
186
- var D = { exports: {} }, R;
187
- function B() {
188
- return R || (R = 1, function(o) {
189
- var l = Object.prototype.hasOwnProperty, r = "~";
190
- function i() {
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 && (i.prototype = /* @__PURE__ */ Object.create(null), new i().__proto__ || (r = !1));
193
- function h(t, e, s) {
194
- this.fn = t, this.context = e, this.once = s || !1;
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(t, e, s, c, w) {
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 || t, w), f = r ? r + e : e;
200
- return t._events[f] ? t._events[f].fn ? t._events[f] = [t._events[f], v] : t._events[f].push(v) : (t._events[f] = v, t._eventsCount++), t;
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(t, e) {
203
- --t._eventsCount === 0 ? t._events = new i() : delete t._events[e];
212
+ function u(e, t) {
213
+ --e._eventsCount === 0 ? e._events = new f() : delete e._events[t];
204
214
  }
205
- function n() {
206
- this._events = new i(), this._eventsCount = 0;
215
+ function r() {
216
+ this._events = new f(), this._eventsCount = 0;
207
217
  }
208
- n.prototype.eventNames = function() {
209
- var e = [], s, c;
210
- if (this._eventsCount === 0) return e;
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
- l.call(s, c) && e.push(r ? c.slice(1) : c);
213
- return Object.getOwnPropertySymbols ? e.concat(Object.getOwnPropertySymbols(s)) : e;
214
- }, n.prototype.listeners = function(e) {
215
- var s = r ? r + e : e, c = this._events[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, f = new Array(v); w < v; w++)
219
- f[w] = c[w].fn;
220
- return f;
221
- }, n.prototype.listenerCount = function(e) {
222
- var s = r ? r + e : e, c = this._events[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
- }, n.prototype.emit = function(e, s, c, w, v, f) {
225
- var m = r ? r + e : e;
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], g = arguments.length, _, d;
237
+ var a = this._events[m], y = arguments.length, _, d;
228
238
  if (a.fn) {
229
- switch (a.once && this.removeListener(e, a.fn, void 0, !0), g) {
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, f), !0;
251
+ return a.fn.call(a.context, s, c, w, v, l), !0;
242
252
  }
243
- for (d = 1, _ = new Array(g - 1); d < g; d++)
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 A = a.length, E;
248
- for (d = 0; d < A; d++)
249
- switch (a[d].once && this.removeListener(e, a[d].fn, void 0, !0), g) {
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 (E = 1, _ = new Array(g - 1); E < g; E++)
264
- _[E - 1] = arguments[E];
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
- }, n.prototype.on = function(e, s, c) {
270
- return p(this, e, s, c, !1);
271
- }, n.prototype.once = function(e, s, c) {
272
- return p(this, e, s, c, !0);
273
- }, n.prototype.removeListener = function(e, s, c, w) {
274
- var v = r ? r + e : e;
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 f = this._events[v];
279
- if (f.fn)
280
- f.fn === s && (!w || f.once) && (!c || f.context === c) && u(this, v);
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 = [], g = f.length; m < g; m++)
283
- (f[m].fn !== s || w && !f[m].once || c && f[m].context !== c) && a.push(f[m]);
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
- }, n.prototype.removeAllListeners = function(e) {
297
+ }, r.prototype.removeAllListeners = function(t) {
288
298
  var s;
289
- return e ? (s = r ? r + e : e, this._events[s] && u(this, s)) : (this._events = new i(), this._eventsCount = 0), this;
290
- }, n.prototype.off = n.prototype.removeListener, n.prototype.addListener = n.prototype.on, n.prefixed = r, n.EventEmitter = n, o.exports = n;
291
- }(D)), D.exports;
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 G = B();
294
- const J = /* @__PURE__ */ H(G), M = new J(), X = ({
303
+ var K = J();
304
+ const Q = /* @__PURE__ */ A(K), L = new Q(), Z = ({
295
305
  handle: o,
296
- localUserId: l,
297
- offlineTimeout: r = 3e4,
298
- getTime: i = () => (/* @__PURE__ */ new Date()).getTime()
306
+ localUserId: i,
307
+ offlineTimeout: n = 3e4,
308
+ getTime: f = () => (/* @__PURE__ */ new Date()).getTime()
299
309
  }) => {
300
- const [h, p, u] = L({}), [n, t, e] = L({});
301
- return y(() => {
310
+ const [h, p, u] = D({}), [r, e, t] = D({});
311
+ return g(() => {
302
312
  const s = (v) => {
303
- const [f, m] = v.message;
304
- f !== l && (e.current[f] || M.emit("new_peer", v), p({
313
+ const [l, m] = v.message;
314
+ l !== i && (t.current[l] || L.emit("new_peer", v), p({
305
315
  ...u.current,
306
- [f]: m
307
- }), t({
308
- ...e.current,
309
- [f]: i()
316
+ [l]: m
317
+ }), e({
318
+ ...t.current,
319
+ [l]: f()
310
320
  }));
311
321
  }, c = () => {
312
- const v = u.current, f = e.current, m = i();
313
- for (const a in f)
314
- m - f[a] > r && (delete v[a], delete f[a]);
315
- p(v), t(f);
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
- r
330
+ n
321
331
  );
322
332
  return () => {
323
333
  o.removeListener("ephemeral-message", s), clearInterval(w);
324
334
  };
325
- }, [o, l, r, i]), [h, n];
326
- }, Y = ({
335
+ }, [o, i, n, f]), [h, r];
336
+ }, U = ({
327
337
  handle: o,
328
- userId: l,
329
- initialState: r,
330
- heartbeatTime: i = 15e3
338
+ userId: i,
339
+ initialState: n,
340
+ heartbeatTime: f = 15e3
331
341
  }) => {
332
- const [h, p, u] = L(r), n = (t) => {
333
- const e = typeof t == "function" ? t(u.current) : t;
334
- p(e), o.broadcast([l, e]);
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 y(() => {
337
- if (!l)
346
+ return g(() => {
347
+ if (!i)
338
348
  return;
339
- const t = () => void o.broadcast([l, u.current]);
340
- t();
341
- const e = setInterval(t, i);
342
- return () => void clearInterval(e);
343
- }, [o, l, i]), y(() => {
344
- let t;
345
- const e = M.on("new_peer", () => {
346
- t = setTimeout(
347
- () => o.broadcast([l, u.current]),
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
- e.off("new_peer"), t && clearTimeout(t);
363
+ t.off("new_peer"), e && clearTimeout(e);
354
364
  };
355
- }, [o, l, M]), [h, n];
365
+ }, [o, i, L]), [h, r];
356
366
  };
357
367
  export {
358
- F as RepoContext,
359
- N as useDocHandle,
360
- W as useDocHandles,
361
- Q as useDocument,
362
- V as useDocuments,
363
- Y as useLocalAwareness,
364
- X as useRemoteAwareness,
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;AAMxE,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,CAqFjB"}
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-alpha.0",
3
+ "version": "2.3.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.3.0-alpha.0",
18
+ "@automerge/automerge-repo": "2.3.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": "4e54d3585f0f8f80fb558cac01ce2652e7cbf985"
49
+ "gitHead": "b4b18a23f7a2806fd8b259520b5444bea0ed8153"
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
+ }
@@ -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
@@ -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 ids) {
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 ids) {
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, ids])
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 } = await setupPairedRepos()
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 } = await setupPairedRepos()
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("useDocHandle with suspense: false", () => {
191
+ describe("with suspense: false", () => {
191
192
  it("returns undefined while loading then resolves to handle", async () => {
192
- const { repoCreator, wrapper } = await setupPairedRepos()
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
+ })