@automerge/automerge-repo-react-hooks 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,24 +3,29 @@
3
3
  These hooks are provided as helpers for using Automerge in your React project.
4
4
 
5
5
  #### [useBootstrap](./src/useBootstrap.ts)
6
+
6
7
  This hook is used to load a document based on the URL hash, for example `//myapp/#documentId=[document ID]`.
7
8
  It can also load the document ID from localStorage, or create a new document if none is specified.
8
9
 
9
10
  #### [useLocalAwareness](./src/useLocalAwareness.ts) & [useRemoteAwareness](./src/useRemoteAwareness.ts)
11
+
10
12
  These hooks implement ephemeral awareness/presence, similar to [Yjs Awareness](https://docs.yjs.dev/getting-started/adding-awareness).
11
- They allow temporary state to be shared, such as cursor positions or peer online/offline status.
13
+ They allow temporary state to be shared, such as cursor positions or peer online/offline status.
12
14
 
13
15
  Ephemeral messages are replicated between peers, but not saved to the Automerge doc, and are used for temporary updates that will be discarded.
14
16
 
15
17
  #### [useRepo/RepoContext](./src/useRepo.ts)
18
+
16
19
  Use RepoContext to set up react context for an Automerge repo.
17
20
  Use useRepo to lookup the repo from context.
18
21
  Most hooks depend on RepoContext being available.
19
22
 
20
23
  #### [useDocument](./src/useDocument.ts)
24
+
21
25
  Return a document & updater fn, by ID.
22
26
 
23
27
  #### [useHandle](./src/useHandle.ts)
28
+
24
29
  Return a handle, by ID.
25
30
 
26
31
  ## Example usage
@@ -46,9 +51,7 @@ const sharedWorker = new SharedWorker(
46
51
 
47
52
  async function getRepo(): Promise<DocCollection> {
48
53
  return await Repo({
49
- network: [
50
- new BroadcastChannelNetworkAdapter(),
51
- ],
54
+ network: [new BroadcastChannelNetworkAdapter()],
52
55
  sharePolicy: peerId => peerId.includes("shared-worker"),
53
56
  })
54
57
  }
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import T,{createContext as k,useContext as A,useState as x,useRef as U,useEffect as g,useMemo as D}from"react";const O=k(null);function b(){const t=A(O);if(!t)throw new Error("Repo was not found on RepoContext.");return t}function P(t){const[s,o]=x(),i=b(),f=t?i.find(t):null,d=U(null);return g(()=>{if(o(void 0),!f)return;d.current=f,f.doc().then(a=>{d.current===f&&o(a)}).catch(a=>console.error(a));const u=a=>o(a.doc);return f.on("change",u),()=>{f.removeListener("change",u)}},[f]),[s,(u,a)=>{f&&f.change(u,a)}]}const H=t=>{const[s,o]=x({}),[i,f]=x({}),d=b();return g(()=>{const u=(e,r)=>{r&&o(l=>({...l,[e]:r}))},a=e=>{const r=e.documentId,l=({doc:v})=>u(r,v);e.on("change",l),f(v=>({...v,[r]:l}))},n=e=>{d.find(e).off("change",i[e]),o(r=>{const{[e]:l,...v}=r;return v})};return t&&(t.filter(e=>!s[e]).forEach(e=>{const r=d.find(e);r.doc().then(l=>{u(e,l),a(r)}).catch(l=>{console.error(`Error loading document ${e} in useDocuments: `,l)})}),Object.keys(s).map(e=>e).filter(e=>!t.includes(e)).forEach(n)),()=>{Object.entries(i).forEach(([e,r])=>{d.find(e).off("change",r)})}},[t]),s},M=(t,s=!1)=>{history[s?"pushState":"replaceState"]("","","#"+t),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+t,oldURL:window.location.href}))},N=()=>{const[t,s]=x(window.location.hash);return g(()=>{const o=()=>void s(window.location.hash);return window.addEventListener("hashchange",o),()=>void window.removeEventListener("hashchange",o)},[]),t},S=(t,s)=>new URLSearchParams(s.slice(1)).get(t),B=(t,s,o)=>{const i=new URLSearchParams(o.slice(1));return i.set(t,s),i.toString()},$=(t,s)=>t&&(S(t,s)||localStorage.getItem(t)),q=(t,s)=>{t&&s!==S(t,window.location.hash)&&M(B(t,s,window.location.hash)),t&&localStorage.setItem(t,s)},z=({key:t="automergeUrl",onNoDocument:s=i=>i.create(),onInvalidAutomergeUrl:o}={})=>{const i=b(),f=N(),d=D(()=>{const u=$(t,f);try{return u?i.find(u):s(i)}catch(a){if(u&&o)return o(i,a);throw a}},[f,i,s,o]);return g(()=>{d&&q(t,d.url)},[f,d]),d};function F(t){const s=b(),[o,i]=x(t?s.find(t):void 0);return g(()=>{i(t?s.find(t):void 0)},[t]),o}function R(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var L=T,J=function(t){return typeof t=="function"},K=function(t){var s=L.useState(t),o=s[0],i=s[1],f=L.useRef(o),d=L.useCallback(function(u){f.current=J(u)?u(f.current):u,i(f.current)},[]);return[o,d,f]},Q=K;const E=R(Q);var I={exports:{}};(function(t){var s=Object.prototype.hasOwnProperty,o="~";function i(){}Object.create&&(i.prototype=Object.create(null),new i().__proto__||(o=!1));function f(n,e,r){this.fn=n,this.context=e,this.once=r||!1}function d(n,e,r,l,v){if(typeof r!="function")throw new TypeError("The listener must be a function");var p=new f(r,l||n,v),h=o?o+e:e;return n._events[h]?n._events[h].fn?n._events[h]=[n._events[h],p]:n._events[h].push(p):(n._events[h]=p,n._eventsCount++),n}function u(n,e){--n._eventsCount===0?n._events=new i:delete n._events[e]}function a(){this._events=new i,this._eventsCount=0}a.prototype.eventNames=function(){var n=[],e,r;if(this._eventsCount===0)return n;for(r in e=this._events)s.call(e,r)&&n.push(o?r.slice(1):r);return Object.getOwnPropertySymbols?n.concat(Object.getOwnPropertySymbols(e)):n},a.prototype.listeners=function(n){var e=o?o+n:n,r=this._events[e];if(!r)return[];if(r.fn)return[r.fn];for(var l=0,v=r.length,p=new Array(v);l<v;l++)p[l]=r[l].fn;return p},a.prototype.listenerCount=function(n){var e=o?o+n:n,r=this._events[e];return r?r.fn?1:r.length:0},a.prototype.emit=function(n,e,r,l,v,p){var h=o?o+n:n;if(!this._events[h])return!1;var c=this._events[h],m=arguments.length,y,w;if(c.fn){switch(c.once&&this.removeListener(n,c.fn,void 0,!0),m){case 1:return c.fn.call(c.context),!0;case 2:return c.fn.call(c.context,e),!0;case 3:return c.fn.call(c.context,e,r),!0;case 4:return c.fn.call(c.context,e,r,l),!0;case 5:return c.fn.call(c.context,e,r,l,v),!0;case 6:return c.fn.call(c.context,e,r,l,v,p),!0}for(w=1,y=new Array(m-1);w<m;w++)y[w-1]=arguments[w];c.fn.apply(c.context,y)}else{var j=c.length,_;for(w=0;w<j;w++)switch(c[w].once&&this.removeListener(n,c[w].fn,void 0,!0),m){case 1:c[w].fn.call(c[w].context);break;case 2:c[w].fn.call(c[w].context,e);break;case 3:c[w].fn.call(c[w].context,e,r);break;case 4:c[w].fn.call(c[w].context,e,r,l);break;default:if(!y)for(_=1,y=new Array(m-1);_<m;_++)y[_-1]=arguments[_];c[w].fn.apply(c[w].context,y)}}return!0},a.prototype.on=function(n,e,r){return d(this,n,e,r,!1)},a.prototype.once=function(n,e,r){return d(this,n,e,r,!0)},a.prototype.removeListener=function(n,e,r,l){var v=o?o+n:n;if(!this._events[v])return this;if(!e)return u(this,v),this;var p=this._events[v];if(p.fn)p.fn===e&&(!l||p.once)&&(!r||p.context===r)&&u(this,v);else{for(var h=0,c=[],m=p.length;h<m;h++)(p[h].fn!==e||l&&!p[h].once||r&&p[h].context!==r)&&c.push(p[h]);c.length?this._events[v]=c.length===1?c[0]:c:u(this,v)}return this},a.prototype.removeAllListeners=function(n){var e;return n?(e=o?o+n:n,this._events[e]&&u(this,e)):(this._events=new i,this._eventsCount=0),this},a.prototype.off=a.prototype.removeListener,a.prototype.addListener=a.prototype.on,a.prefixed=o,a.EventEmitter=a,t.exports=a})(I);var V=I.exports;const W=R(V),C=new W,X=({handle:t,localUserId:s,offlineTimeout:o=3e4,getTime:i=()=>new Date().getTime()})=>{const[f,d,u]=E({}),[a,n,e]=E({});return g(()=>{const r=p=>{const[h,c]=p.message;h!==s&&(e.current[h]||C.emit("new_peer",p),d({...u.current,[h]:c}),n({...e.current,[h]:i()}))},l=()=>{const p=u.current,h=e.current,c=i();for(const m in h)c-h[m]>o&&(delete p[m],delete h[m]);d(p),n(h)};t.on("ephemeral-message",r);const v=setInterval(l,o);return()=>{t.removeListener("ephemeral-message",r),clearInterval(v)}},[t,s,o,i]),[f,a]},Y=({handle:t,userId:s,initialState:o,heartbeatTime:i=15e3})=>{const[f,d,u]=E(o),a=n=>{const e=typeof n=="function"?n(u.current):n;d(e),t.broadcast([s,e])};return g(()=>{if(!s)return;const n=()=>void t.broadcast([s,u.current]);n();const e=setInterval(n,i);return()=>void clearInterval(e)},[t,s,i]),g(()=>{let n;const e=C.on("new_peer",()=>{n=setTimeout(()=>t.broadcast([s,u.current]),500)});return()=>{e.off("new_peer"),n&&clearTimeout(n)}},[t,s,C]),[f,a]};export{O as RepoContext,z as useBootstrap,P as useDocument,H as useDocuments,F as useHandle,Y as useLocalAwareness,X as useRemoteAwareness,b as useRepo};
1
+ import T,{createContext as k,useContext as A,useRef as U,useState as x,useEffect as g,useMemo as D}from"react";const C=k(null);function L(){const n=A(C);if(!n)throw new Error("Repo was not found on RepoContext.");return n}function P(n){const s=L(),r=n?s.find(n):null,i=U(null),[m,v]=x(()=>r==null?void 0:r.docSync());return g(()=>{if(v(r==null?void 0:r.docSync()),!r)return;i.current=r,r.doc().then(e=>{i.current===r&&v(e)}).catch(e=>console.error(e));const p=e=>v(e.doc);r.on("change",p);const l=()=>v(void 0);return r.on("delete",l),()=>{r.removeListener("change",p),r.removeListener("delete",l)}},[r]),[m,(p,l)=>{r&&r.change(p,l)}]}const H=n=>{const[s,r]=x({}),[i,m]=x({}),v=L();return g(()=>{const p=(t,a)=>{a&&r(f=>({...f,[t]:a}))},l=t=>{r(a=>{const{[t]:f,...u}=a;return u})},e=t=>{const a=t.documentId,f={change:({doc:u})=>p(a,u),delete:()=>l(a)};t.on("change",f.change),t.on("delete",f.delete),m(u=>({...u,[a]:u}))},o=t=>{const a=v.find(t);a.off("change",i[t].change),a.off("delete",i[t].delete),r(f=>{const{[t]:u,...h}=f;return h})};return n&&(n.filter(t=>!s[t]).forEach(t=>{const a=v.find(t);a.doc().then(f=>{p(t,f),e(a)}).catch(f=>{console.error(`Error loading document ${t} in useDocuments: `,f)})}),Object.keys(s).map(t=>t).filter(t=>!n.includes(t)).forEach(o)),()=>{Object.entries(i).forEach(([t,a])=>{const f=v.find(t);f.off("change",a.change),f.off("delete",a.delete)})}},[n]),s},M=(n,s=!1)=>{history[s?"pushState":"replaceState"]("","","#"+n),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+n,oldURL:window.location.href}))},N=()=>{const[n,s]=x(window.location.hash);return g(()=>{const r=()=>void s(window.location.hash);return window.addEventListener("hashchange",r),()=>void window.removeEventListener("hashchange",r)},[]),n},O=(n,s)=>new URLSearchParams(s.slice(1)).get(n),B=(n,s,r)=>{const i=new URLSearchParams(r.slice(1));return i.set(n,s),i.toString()},$=(n,s)=>n&&(O(n,s)||localStorage.getItem(n)),q=(n,s)=>{n&&s!==O(n,window.location.hash)&&M(B(n,s,window.location.hash)),n&&localStorage.setItem(n,s)},z=({key:n="automergeUrl",onNoDocument:s=i=>i.create(),onInvalidAutomergeUrl:r}={})=>{const i=L(),m=N(),v=D(()=>{const p=$(n,m);try{return p?i.find(p):s(i)}catch(l){if(p&&r)return r(i,l);throw l}},[m,i,s,r]);return g(()=>{v&&q(n,v.url)},[m,v]),v};function F(n){const s=L(),[r,i]=x(n?s.find(n):void 0);return g(()=>{i(n?s.find(n):void 0)},[n]),r}function R(n){return n&&n.__esModule&&Object.prototype.hasOwnProperty.call(n,"default")?n.default:n}var b=T,J=function(n){return typeof n=="function"},K=function(n){var s=b.useState(n),r=s[0],i=s[1],m=b.useRef(r),v=b.useCallback(function(p){m.current=J(p)?p(m.current):p,i(m.current)},[]);return[r,v,m]},Q=K;const S=R(Q);var I={exports:{}};(function(n){var s=Object.prototype.hasOwnProperty,r="~";function i(){}Object.create&&(i.prototype=Object.create(null),new i().__proto__||(r=!1));function m(e,o,t){this.fn=e,this.context=o,this.once=t||!1}function v(e,o,t,a,f){if(typeof t!="function")throw new TypeError("The listener must be a function");var u=new m(t,a||e,f),h=r?r+o:o;return e._events[h]?e._events[h].fn?e._events[h]=[e._events[h],u]:e._events[h].push(u):(e._events[h]=u,e._eventsCount++),e}function p(e,o){--e._eventsCount===0?e._events=new i:delete e._events[o]}function l(){this._events=new i,this._eventsCount=0}l.prototype.eventNames=function(){var e=[],o,t;if(this._eventsCount===0)return e;for(t in o=this._events)s.call(o,t)&&e.push(r?t.slice(1):t);return Object.getOwnPropertySymbols?e.concat(Object.getOwnPropertySymbols(o)):e},l.prototype.listeners=function(e){var o=r?r+e:e,t=this._events[o];if(!t)return[];if(t.fn)return[t.fn];for(var a=0,f=t.length,u=new Array(f);a<f;a++)u[a]=t[a].fn;return u},l.prototype.listenerCount=function(e){var o=r?r+e:e,t=this._events[o];return t?t.fn?1:t.length:0},l.prototype.emit=function(e,o,t,a,f,u){var h=r?r+e:e;if(!this._events[h])return!1;var c=this._events[h],w=arguments.length,y,d;if(c.fn){switch(c.once&&this.removeListener(e,c.fn,void 0,!0),w){case 1:return c.fn.call(c.context),!0;case 2:return c.fn.call(c.context,o),!0;case 3:return c.fn.call(c.context,o,t),!0;case 4:return c.fn.call(c.context,o,t,a),!0;case 5:return c.fn.call(c.context,o,t,a,f),!0;case 6:return c.fn.call(c.context,o,t,a,f,u),!0}for(d=1,y=new Array(w-1);d<w;d++)y[d-1]=arguments[d];c.fn.apply(c.context,y)}else{var j=c.length,_;for(d=0;d<j;d++)switch(c[d].once&&this.removeListener(e,c[d].fn,void 0,!0),w){case 1:c[d].fn.call(c[d].context);break;case 2:c[d].fn.call(c[d].context,o);break;case 3:c[d].fn.call(c[d].context,o,t);break;case 4:c[d].fn.call(c[d].context,o,t,a);break;default:if(!y)for(_=1,y=new Array(w-1);_<w;_++)y[_-1]=arguments[_];c[d].fn.apply(c[d].context,y)}}return!0},l.prototype.on=function(e,o,t){return v(this,e,o,t,!1)},l.prototype.once=function(e,o,t){return v(this,e,o,t,!0)},l.prototype.removeListener=function(e,o,t,a){var f=r?r+e:e;if(!this._events[f])return this;if(!o)return p(this,f),this;var u=this._events[f];if(u.fn)u.fn===o&&(!a||u.once)&&(!t||u.context===t)&&p(this,f);else{for(var h=0,c=[],w=u.length;h<w;h++)(u[h].fn!==o||a&&!u[h].once||t&&u[h].context!==t)&&c.push(u[h]);c.length?this._events[f]=c.length===1?c[0]:c:p(this,f)}return this},l.prototype.removeAllListeners=function(e){var o;return e?(o=r?r+e:e,this._events[o]&&p(this,o)):(this._events=new i,this._eventsCount=0),this},l.prototype.off=l.prototype.removeListener,l.prototype.addListener=l.prototype.on,l.prefixed=r,l.EventEmitter=l,n.exports=l})(I);var V=I.exports;const W=R(V),E=new W,X=({handle:n,localUserId:s,offlineTimeout:r=3e4,getTime:i=()=>new Date().getTime()})=>{const[m,v,p]=S({}),[l,e,o]=S({});return g(()=>{const t=u=>{const[h,c]=u.message;h!==s&&(o.current[h]||E.emit("new_peer",u),v({...p.current,[h]:c}),e({...o.current,[h]:i()}))},a=()=>{const u=p.current,h=o.current,c=i();for(const w in h)c-h[w]>r&&(delete u[w],delete h[w]);v(u),e(h)};n.on("ephemeral-message",t);const f=setInterval(a,r);return()=>{n.removeListener("ephemeral-message",t),clearInterval(f)}},[n,s,r,i]),[m,l]},Y=({handle:n,userId:s,initialState:r,heartbeatTime:i=15e3})=>{const[m,v,p]=S(r),l=e=>{const o=typeof e=="function"?e(p.current):e;v(o),n.broadcast([s,o])};return g(()=>{if(!s)return;const e=()=>void n.broadcast([s,p.current]);e();const o=setInterval(e,i);return()=>void clearInterval(o)},[n,s,i]),g(()=>{let e;const o=E.on("new_peer",()=>{e=setTimeout(()=>n.broadcast([s,p.current]),500)});return()=>{o.off("new_peer"),e&&clearTimeout(e)}},[n,s,E]),[m,l]};export{C as RepoContext,z as useBootstrap,P as useDocument,H as useDocuments,F as useHandle,Y as useLocalAwareness,X as useRemoteAwareness,L as useRepo};
@@ -1 +1 @@
1
- {"version":3,"file":"useDocument.d.ts","sourceRoot":"","sources":["../src/useDocument.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAGd,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAA;AAIxE;;;;;;;KAOK;AACL,wBAAgB,WAAW,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,aAAa,4CAqCnC,SAAS,CAAC,CAAC,YACX,cAAc,CAAC,CAAC,GAAG,SAAS,WAOzC"}
1
+ {"version":3,"file":"useDocument.d.ts","sourceRoot":"","sources":["../src/useDocument.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAGd,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAA;AAIxE;;;;;;;KAOK;AACL,wBAAgB,WAAW,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,aAAa,4CA2CnC,SAAS,CAAC,CAAC,YACX,cAAc,CAAC,CAAC,GAAG,SAAS,WAOzC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useDocuments.d.ts","sourceRoot":"","sources":["../src/useDocuments.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAGZ,UAAU,EACX,MAAM,2BAA2B,CAAA;AAIlC;;;GAGG;AACH,eAAO,MAAM,YAAY,YAAa,KAAK,EAAE,qBAqE5C,CAAA;AAED,KAAK,KAAK,GAAG,UAAU,GAAG,YAAY,CAAA"}
1
+ {"version":3,"file":"useDocuments.d.ts","sourceRoot":"","sources":["../src/useDocuments.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAIZ,UAAU,EACX,MAAM,2BAA2B,CAAA;AAIlC;;;GAGG;AACH,eAAO,MAAM,YAAY,YAAa,KAAK,EAAE,qBAyF5C,CAAA;AAED,KAAK,KAAK,GAAG,UAAU,GAAG,YAAY,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo-react-hooks",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Hooks to access an Automerge Repo from your react app.",
5
5
  "repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo-react-hooks",
6
6
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@automerge/automerge": "^2.1.9",
18
- "@automerge/automerge-repo": "1.1.3",
18
+ "@automerge/automerge-repo": "1.1.5",
19
19
  "eventemitter3": "^5.0.1",
20
20
  "react": "^18.2.0",
21
21
  "react-dom": "^18.2.0",
@@ -42,5 +42,5 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "gitHead": "f59ba3612d1110003e2365fe0fe5a799539f1b59"
45
+ "gitHead": "64edfaea5e53e77cd9158fc1df52fea85801db71"
46
46
  }
@@ -16,17 +16,20 @@ import { useRepo } from "./useRepo.js"
16
16
  * This requires a {@link RepoContext} to be provided by a parent component.
17
17
  * */
18
18
  export function useDocument<T>(id?: AnyDocumentId) {
19
- const [doc, setDoc] = useState<Doc<T>>()
20
19
  const repo = useRepo()
21
20
 
22
21
  const handle = id ? repo.find<T>(id) : null
23
22
  const handleRef = useRef<DocHandle<T> | null>(null)
24
23
 
24
+ const [doc, setDoc] = useState<Doc<T> | undefined>(() => handle?.docSync())
25
+
25
26
  useEffect(() => {
26
- // When the handle has changed, reset the doc to an empty state.
27
+ // When the handle has changed, reset the doc to the current value of docSync().
28
+ // For already-loaded documents this will be the last known value, for unloaded documents
29
+ // this will be undefined.
27
30
  // This ensures that if loading the doc takes a long time, the UI
28
31
  // shows a loading state during that time rather than a stale doc.
29
- setDoc(undefined)
32
+ setDoc(handle?.docSync())
30
33
 
31
34
  if (!handle) return
32
35
 
@@ -44,8 +47,11 @@ export function useDocument<T>(id?: AnyDocumentId) {
44
47
 
45
48
  const onChange = (h: DocHandleChangePayload<T>) => setDoc(h.doc)
46
49
  handle.on("change", onChange)
50
+ const onDelete = () => setDoc(undefined)
51
+ handle.on("delete", onDelete)
47
52
  const cleanup = () => {
48
53
  handle.removeListener("change", onChange)
54
+ handle.removeListener("delete", onDelete)
49
55
  }
50
56
 
51
57
  return cleanup
@@ -2,6 +2,7 @@ import {
2
2
  AutomergeUrl,
3
3
  DocHandle,
4
4
  DocHandleChangePayload,
5
+ DocHandleDeletePayload,
5
6
  DocumentId,
6
7
  } from "@automerge/automerge-repo"
7
8
  import { useEffect, useState } from "react"
@@ -13,7 +14,7 @@ import { useRepo } from "./useRepo.js"
13
14
  */
14
15
  export const useDocuments = <T>(ids?: DocId[]) => {
15
16
  const [documents, setDocuments] = useState({} as Record<DocId, T>)
16
- const [listeners, setListeners] = useState({} as Record<DocId, Listener<T>>)
17
+ const [listeners, setListeners] = useState({} as Record<DocId, Listeners<T>>)
17
18
  const repo = useRepo()
18
19
 
19
20
  useEffect(
@@ -21,22 +22,35 @@ export const useDocuments = <T>(ids?: DocId[]) => {
21
22
  const updateDocument = (id: DocId, doc?: T) => {
22
23
  if (doc) setDocuments(docs => ({ ...docs, [id]: doc }))
23
24
  }
25
+ const updateDocumentDeleted = (id: DocId) => {
26
+ // (don't remove listeners)
27
+ // remove the document from the document map
28
+ setDocuments(docs => {
29
+ const { [id]: _removedDoc, ...remainingDocs } = docs
30
+ return remainingDocs
31
+ })
32
+ }
24
33
 
25
34
  const addListener = (handle: DocHandle<T>) => {
26
35
  const id = handle.documentId
27
36
 
28
37
  // whenever a document changes, update our map
29
- const listener: Listener<T> = ({ doc }) => updateDocument(id, doc)
30
- handle.on("change", listener)
38
+ const listeners: Listeners<T> = {
39
+ change: ({ doc }) => updateDocument(id, doc),
40
+ delete: () => updateDocumentDeleted(id),
41
+ }
42
+ handle.on("change", listeners.change)
43
+ handle.on("delete", listeners.delete)
31
44
 
32
45
  // store the listener so we can remove it later
33
- setListeners(listeners => ({ ...listeners, [id]: listener }))
46
+ setListeners(listeners => ({ ...listeners, [id]: listeners }))
34
47
  }
35
48
 
36
49
  const removeDocument = (id: DocId) => {
37
50
  // remove the listener
38
51
  const handle = repo.find<T>(id)
39
- handle.off("change", listeners[id])
52
+ handle.off("change", listeners[id].change)
53
+ handle.off("delete", listeners[id].delete)
40
54
 
41
55
  // remove the document from the document map
42
56
  setDocuments(docs => {
@@ -51,12 +65,18 @@ export const useDocuments = <T>(ids?: DocId[]) => {
51
65
  newIds.forEach(id => {
52
66
  const handle = repo.find<T>(id)
53
67
  // As each document loads, update our map
54
- handle.doc().then(doc => {
55
- updateDocument(id, doc)
56
- addListener(handle)
57
- }).catch(err => {
58
- console.error(`Error loading document ${id} in useDocuments: `, err)
59
- })
68
+ handle
69
+ .doc()
70
+ .then(doc => {
71
+ updateDocument(id, doc)
72
+ addListener(handle)
73
+ })
74
+ .catch(err => {
75
+ console.error(
76
+ `Error loading document ${id} in useDocuments: `,
77
+ err
78
+ )
79
+ })
60
80
  })
61
81
 
62
82
  // remove any documents that are no longer in the list
@@ -68,9 +88,10 @@ export const useDocuments = <T>(ids?: DocId[]) => {
68
88
 
69
89
  // on unmount, remove all listeners
70
90
  const teardown = () => {
71
- Object.entries(listeners).forEach(([id, listener]) => {
91
+ Object.entries(listeners).forEach(([id, listeners]) => {
72
92
  const handle = repo.find<T>(id as DocId)
73
- handle.off("change", listener)
93
+ handle.off("change", listeners.change)
94
+ handle.off("delete", listeners.delete)
74
95
  })
75
96
  }
76
97
 
@@ -83,4 +104,6 @@ export const useDocuments = <T>(ids?: DocId[]) => {
83
104
  }
84
105
 
85
106
  type DocId = DocumentId | AutomergeUrl
86
- type Listener<T> = (p: DocHandleChangePayload<T>) => void
107
+ type ChangeListener<T> = (p: DocHandleChangePayload<T>) => void
108
+ type DeleteListener<T> = (p: DocHandleDeletePayload<T>) => void
109
+ type Listeners<T> = { change: ChangeListener<T>, delete: DeleteListener<T> }
@@ -1,87 +1,87 @@
1
- import { useEffect } from "react"
2
- import useStateRef from "react-usestateref"
3
- import { peerEvents } from "./useRemoteAwareness.js"
4
- import { DocHandle } from "@automerge/automerge-repo"
5
-
6
- export interface UseLocalAwarenessProps {
7
- /** The document handle to send ephemeral state on */
8
- handle: DocHandle<unknown>
9
- /** Our user ID **/
10
- userId: string
11
- /** The initial state object/primitive we should advertise */
12
- initialState: any
13
- /** How frequently to send heartbeats */
14
- heartbeatTime?: number
15
- }
16
- /**
17
- * This hook maintains state for the local client.
18
- * Like React.useState, it returns a [state, setState] array.
19
- * It is intended to be used alongside useRemoteAwareness.
20
- *
21
- * When state is changed it is broadcast to all clients.
22
- * It also broadcasts a heartbeat to let other clients know it is online.
23
- *
24
- * Note that userIds aren't secure (yet). Any client can lie about theirs.
25
- *
26
- * @param {string} props.userId Unique user ID. Clients can lie about this.
27
- * @param {any} props.initialState Initial state object/primitive
28
- * @param {number?1500} props.heartbeatTime How often to send a heartbeat (in ms)
29
- * @returns [state, setState]
30
- */
31
- export const useLocalAwareness = ({
32
- handle,
33
- userId,
34
- initialState,
35
- heartbeatTime = 15000,
36
- }: UseLocalAwarenessProps) => {
37
- const [localState, setLocalState, localStateRef] = useStateRef(initialState)
38
-
39
- const setState = (stateOrUpdater: any) => {
40
- const state =
41
- typeof stateOrUpdater === "function"
42
- ? stateOrUpdater(localStateRef.current)
43
- : stateOrUpdater
44
- setLocalState(state)
45
- // TODO: Send deltas instead of entire state
46
- handle.broadcast([userId, state])
47
- }
48
-
49
- 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) {
53
- return
54
- }
55
-
56
- // Send periodic heartbeats
57
- const heartbeat = () =>
58
- void handle.broadcast([userId, localStateRef.current])
59
- heartbeat() // Initial heartbeat
60
- // TODO: we don't need to send a heartbeat if we've changed state recently; use recursive setTimeout instead of setInterval
61
- const heartbeatIntervalId = setInterval(heartbeat, heartbeatTime)
62
- return () => void clearInterval(heartbeatIntervalId)
63
- }, [handle, userId, heartbeatTime])
64
-
65
- useEffect(() => {
66
- // Send entire state to new peers
67
- let broadcastTimeoutId: ReturnType<typeof setTimeout>
68
- const newPeerEvents = peerEvents.on("new_peer", () => {
69
- broadcastTimeoutId = setTimeout(
70
- () => handle.broadcast([userId, localStateRef.current]),
71
- 500 // Wait for the peer to be ready
72
- )
73
- })
74
- return () => {
75
- newPeerEvents.off("new_peer")
76
- broadcastTimeoutId && clearTimeout(broadcastTimeoutId)
77
- }
78
- }, [handle, userId, peerEvents])
79
-
80
- // TODO: Send an "offline" message on unmount
81
- // useEffect(
82
- // () => () => void handle.broadcast(null), // Same as Yjs awareness
83
- // []
84
- // );
85
-
86
- return [localState, setState]
87
- }
1
+ import { useEffect } from "react"
2
+ import useStateRef from "react-usestateref"
3
+ import { peerEvents } from "./useRemoteAwareness.js"
4
+ import { DocHandle } from "@automerge/automerge-repo"
5
+
6
+ export interface UseLocalAwarenessProps {
7
+ /** The document handle to send ephemeral state on */
8
+ handle: DocHandle<unknown>
9
+ /** Our user ID **/
10
+ userId: string
11
+ /** The initial state object/primitive we should advertise */
12
+ initialState: any
13
+ /** How frequently to send heartbeats */
14
+ heartbeatTime?: number
15
+ }
16
+ /**
17
+ * This hook maintains state for the local client.
18
+ * Like React.useState, it returns a [state, setState] array.
19
+ * It is intended to be used alongside useRemoteAwareness.
20
+ *
21
+ * When state is changed it is broadcast to all clients.
22
+ * It also broadcasts a heartbeat to let other clients know it is online.
23
+ *
24
+ * Note that userIds aren't secure (yet). Any client can lie about theirs.
25
+ *
26
+ * @param {string} props.userId Unique user ID. Clients can lie about this.
27
+ * @param {any} props.initialState Initial state object/primitive
28
+ * @param {number?1500} props.heartbeatTime How often to send a heartbeat (in ms)
29
+ * @returns [state, setState]
30
+ */
31
+ export const useLocalAwareness = ({
32
+ handle,
33
+ userId,
34
+ initialState,
35
+ heartbeatTime = 15000,
36
+ }: UseLocalAwarenessProps) => {
37
+ const [localState, setLocalState, localStateRef] = useStateRef(initialState)
38
+
39
+ const setState = (stateOrUpdater: any) => {
40
+ const state =
41
+ typeof stateOrUpdater === "function"
42
+ ? stateOrUpdater(localStateRef.current)
43
+ : stateOrUpdater
44
+ setLocalState(state)
45
+ // TODO: Send deltas instead of entire state
46
+ handle.broadcast([userId, state])
47
+ }
48
+
49
+ 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) {
53
+ return
54
+ }
55
+
56
+ // Send periodic heartbeats
57
+ const heartbeat = () =>
58
+ void handle.broadcast([userId, localStateRef.current])
59
+ heartbeat() // Initial heartbeat
60
+ // TODO: we don't need to send a heartbeat if we've changed state recently; use recursive setTimeout instead of setInterval
61
+ const heartbeatIntervalId = setInterval(heartbeat, heartbeatTime)
62
+ return () => void clearInterval(heartbeatIntervalId)
63
+ }, [handle, userId, heartbeatTime])
64
+
65
+ useEffect(() => {
66
+ // Send entire state to new peers
67
+ let broadcastTimeoutId: ReturnType<typeof setTimeout>
68
+ const newPeerEvents = peerEvents.on("new_peer", () => {
69
+ broadcastTimeoutId = setTimeout(
70
+ () => handle.broadcast([userId, localStateRef.current]),
71
+ 500 // Wait for the peer to be ready
72
+ )
73
+ })
74
+ return () => {
75
+ newPeerEvents.off("new_peer")
76
+ broadcastTimeoutId && clearTimeout(broadcastTimeoutId)
77
+ }
78
+ }, [handle, userId, peerEvents])
79
+
80
+ // TODO: Send an "offline" message on unmount
81
+ // useEffect(
82
+ // () => () => void handle.broadcast(null), // Same as Yjs awareness
83
+ // []
84
+ // );
85
+
86
+ return [localState, setState]
87
+ }
@@ -2,6 +2,7 @@ import { AutomergeUrl, PeerId, Repo } from "@automerge/automerge-repo"
2
2
  import { DummyStorageAdapter } from "@automerge/automerge-repo/test/helpers/DummyStorageAdapter"
3
3
  import { render, waitFor } from "@testing-library/react"
4
4
  import React from "react"
5
+ import { act } from "react-dom/test-utils"
5
6
  import { describe, expect, it, vi } from "vitest"
6
7
  import { useDocument } from "../src/useDocument"
7
8
  import { RepoContext } from "../src/useRepo"
@@ -34,9 +35,14 @@ describe("useDocument", () => {
34
35
  const result = await oldDoc()
35
36
  return result
36
37
  }
38
+ handleSlow.docSync = () => {
39
+ return undefined
40
+ }
37
41
 
38
42
  const wrapper = ({ children }) => {
39
- return <RepoContext.Provider value={repo}>{children}</RepoContext.Provider>
43
+ return (
44
+ <RepoContext.Provider value={repo}>{children}</RepoContext.Provider>
45
+ )
40
46
  }
41
47
 
42
48
  return {
@@ -48,9 +54,12 @@ describe("useDocument", () => {
48
54
  }
49
55
  }
50
56
 
51
- const Component = ({ url, onDoc }: {
52
- url: AutomergeUrl,
53
- onDoc: (doc: ExampleDoc) => void,
57
+ const Component = ({
58
+ url,
59
+ onDoc,
60
+ }: {
61
+ url: AutomergeUrl
62
+ onDoc: (doc: ExampleDoc) => void
54
63
  }) => {
55
64
  const [doc] = useDocument(url)
56
65
  onDoc(doc)
@@ -61,15 +70,47 @@ describe("useDocument", () => {
61
70
  const { handleA, wrapper } = setup()
62
71
  const onDoc = vi.fn()
63
72
 
64
- render(<Component url={handleA.url} onDoc={onDoc} />, {wrapper})
73
+ render(<Component url={handleA.url} onDoc={onDoc} />, { wrapper })
65
74
  await waitFor(() => expect(onDoc).toHaveBeenLastCalledWith({ foo: "A" }))
66
75
  })
67
76
 
77
+ it("should immediately return a document if it has already been loaded", async () => {
78
+ const { handleA, wrapper } = setup()
79
+ const onDoc = vi.fn()
80
+
81
+ render(<Component url={handleA.url} onDoc={onDoc} />, { wrapper })
82
+ await waitFor(() => expect(onDoc).not.toHaveBeenCalledWith(undefined))
83
+ })
84
+
85
+ it("should update if the doc changes", async () => {
86
+ const { wrapper, handleA } = setup()
87
+ const onDoc = vi.fn()
88
+
89
+ render(<Component url={handleA.url} onDoc={onDoc} />, {wrapper})
90
+ await waitFor(() => expect(onDoc).toHaveBeenLastCalledWith({ foo: "A" }))
91
+
92
+ act(() => handleA.change(doc => (doc.foo = "new value")))
93
+ await waitFor(() => expect(onDoc).toHaveBeenLastCalledWith({ foo: "new value" }))
94
+ });
95
+
96
+ it("should update if the doc is deleted", async () => {
97
+ const { wrapper, handleA } = setup()
98
+ const onDoc = vi.fn()
99
+
100
+ render(<Component url={handleA.url} onDoc={onDoc} />, {wrapper})
101
+ await waitFor(() => expect(onDoc).toHaveBeenLastCalledWith({ foo: "A" }))
102
+
103
+ act(() => handleA.delete())
104
+ await waitFor(() => expect(onDoc).toHaveBeenLastCalledWith(undefined))
105
+ });
106
+
68
107
  it("should update if the url changes", async () => {
69
108
  const { handleA, handleB, wrapper } = setup()
70
109
  const onDoc = vi.fn()
71
110
 
72
- const { rerender } = render(<Component url={undefined} onDoc={onDoc} />, {wrapper})
111
+ const { rerender } = render(<Component url={undefined} onDoc={onDoc} />, {
112
+ wrapper,
113
+ })
73
114
  await waitFor(() => expect(onDoc).toHaveBeenLastCalledWith(undefined))
74
115
 
75
116
  // set url to doc A
@@ -89,7 +130,9 @@ describe("useDocument", () => {
89
130
  const { handleA, handleSlow, wrapper } = setup()
90
131
  const onDoc = vi.fn()
91
132
 
92
- const { rerender } = render(<Component url={undefined} onDoc={onDoc} />, {wrapper})
133
+ const { rerender } = render(<Component url={undefined} onDoc={onDoc} />, {
134
+ wrapper,
135
+ })
93
136
  await waitFor(() => expect(onDoc).toHaveBeenLastCalledWith(undefined))
94
137
 
95
138
  // start by setting url to doc A
@@ -107,7 +150,9 @@ describe("useDocument", () => {
107
150
  const { handleA, handleSlow, wrapper } = setup()
108
151
  const onDoc = vi.fn()
109
152
 
110
- const { rerender } = render(<Component url={undefined} onDoc={onDoc} />, {wrapper})
153
+ const { rerender } = render(<Component url={undefined} onDoc={onDoc} />, {
154
+ wrapper,
155
+ })
111
156
  await waitFor(() => expect(onDoc).toHaveBeenLastCalledWith(undefined))
112
157
 
113
158
  // Set the URL to a slow doc and then a fast doc.
@@ -15,20 +15,29 @@ describe("useDocuments", () => {
15
15
  })
16
16
 
17
17
  const wrapper = ({ children }) => {
18
- return <RepoContext.Provider value={repo}>{children}</RepoContext.Provider>
18
+ return (
19
+ <RepoContext.Provider value={repo}>{children}</RepoContext.Provider>
20
+ )
19
21
  }
20
22
 
23
+ let documentValues: Record<string, any> = {}
24
+
21
25
  const documentIds = range(10).map(i => {
22
- const handle = repo.create({ foo: i })
26
+ const value = { foo: i }
27
+ const handle = repo.create(value)
28
+ documentValues[handle.documentId] = value
23
29
  return handle.documentId
24
30
  })
25
31
 
26
- return { repo, wrapper, documentIds }
32
+ return { repo, wrapper, documentIds, documentValues }
27
33
  }
28
34
 
29
- const Component = ({ ids, onDocs }: {
30
- ids: DocumentId[],
31
- onDocs: (documents: Record<DocumentId, unknown>) => void,
35
+ const Component = ({
36
+ ids,
37
+ onDocs,
38
+ }: {
39
+ ids: DocumentId[]
40
+ onDocs: (documents: Record<DocumentId, unknown>) => void
32
41
  }) => {
33
42
  const documents = useDocuments(ids)
34
43
  onDocs(documents)
@@ -40,9 +49,11 @@ describe("useDocuments", () => {
40
49
  const onDocs = vi.fn()
41
50
 
42
51
  render(<Component ids={documentIds} onDocs={onDocs} />, { wrapper })
43
- await waitFor(() => expect(onDocs).toHaveBeenCalledWith(
44
- Object.fromEntries(documentIds.map((id, i) => [id, { foo: i }]))
45
- ))
52
+ await waitFor(() =>
53
+ expect(onDocs).toHaveBeenCalledWith(
54
+ Object.fromEntries(documentIds.map((id, i) => [id, { foo: i }]))
55
+ )
56
+ )
46
57
  })
47
58
 
48
59
  it("updates documents when they change", async () => {
@@ -50,9 +61,11 @@ describe("useDocuments", () => {
50
61
  const onDocs = vi.fn()
51
62
 
52
63
  render(<Component ids={documentIds} onDocs={onDocs} />, { wrapper })
53
- await waitFor(() => expect(onDocs).toHaveBeenCalledWith(
54
- Object.fromEntries(documentIds.map((id, i) => [id, { foo: i }]))
55
- ))
64
+ await waitFor(() =>
65
+ expect(onDocs).toHaveBeenCalledWith(
66
+ Object.fromEntries(documentIds.map((id, i) => [id, { foo: i }]))
67
+ )
68
+ )
56
69
 
57
70
  act(() => {
58
71
  // multiply the value of foo in each document by 10
@@ -61,28 +74,39 @@ describe("useDocuments", () => {
61
74
  handle.change(s => (s.foo *= 10))
62
75
  })
63
76
  })
64
- await waitFor(() => expect(onDocs).toHaveBeenCalledWith(
65
- Object.fromEntries(documentIds.map((id, i) => [id, { foo: i * 10 }]))
66
- ))
77
+ await waitFor(() =>
78
+ expect(onDocs).toHaveBeenCalledWith(
79
+ Object.fromEntries(documentIds.map((id, i) => [id, { foo: i * 10 }]))
80
+ )
81
+ )
67
82
  })
68
83
 
69
84
  it(`removes documents when they're removed from the list of ids`, async () => {
70
85
  const { documentIds, wrapper } = setup()
71
86
  const onDocs = vi.fn()
72
87
 
73
- const { rerender } = render(<Component ids={documentIds} onDocs={onDocs} />, { wrapper })
74
- await waitFor(() => expect(onDocs).toHaveBeenCalledWith(
75
- Object.fromEntries(documentIds.map((id, i) => [id, { foo: i }]))
76
- ))
88
+ const { rerender } = render(
89
+ <Component ids={documentIds} onDocs={onDocs} />,
90
+ { wrapper }
91
+ )
92
+ await waitFor(() =>
93
+ expect(onDocs).toHaveBeenCalledWith(
94
+ Object.fromEntries(documentIds.map((id, i) => [id, { foo: i }]))
95
+ )
96
+ )
77
97
 
78
98
  // remove the first document
79
99
  rerender(<Component ids={documentIds.slice(1)} onDocs={onDocs} />)
80
100
  // 👆 Note that this only works because documentIds.slice(1) is a different
81
101
  // object from documentIds. If we modified documentIds directly, the hook
82
102
  // wouldn't re-run.
83
- await waitFor(() => expect(onDocs).toHaveBeenCalledWith(
84
- Object.fromEntries(documentIds.map((id, i) => [id, { foo: i }]).slice(1))
85
- ))
103
+ await waitFor(() =>
104
+ expect(onDocs).toHaveBeenCalledWith(
105
+ Object.fromEntries(
106
+ documentIds.map((id, i) => [id, { foo: i }]).slice(1)
107
+ )
108
+ )
109
+ )
86
110
  })
87
111
  })
88
112
 
@@ -1,4 +1,9 @@
1
- import { AutomergeUrl, DocHandle, PeerId, Repo } from "@automerge/automerge-repo"
1
+ import {
2
+ AutomergeUrl,
3
+ DocHandle,
4
+ PeerId,
5
+ Repo,
6
+ } from "@automerge/automerge-repo"
2
7
  import { DummyStorageAdapter } from "@automerge/automerge-repo/test/helpers/DummyStorageAdapter"
3
8
  import { render, waitFor } from "@testing-library/react"
4
9
  import React from "react"
@@ -38,9 +43,12 @@ describe("useHandle", () => {
38
43
  }
39
44
  }
40
45
 
41
- const Component = ({ url, onHandle }: {
42
- url: AutomergeUrl,
43
- onHandle: (handle: DocHandle<unknown> | undefined) => void,
46
+ const Component = ({
47
+ url,
48
+ onHandle,
49
+ }: {
50
+ url: AutomergeUrl
51
+ onHandle: (handle: DocHandle<unknown> | undefined) => void
44
52
  }) => {
45
53
  const handle = useHandle(url)
46
54
  onHandle(handle)
@@ -51,7 +59,7 @@ describe("useHandle", () => {
51
59
  const { handleA, wrapper } = setup()
52
60
  const onHandle = vi.fn()
53
61
 
54
- render(<Component url={handleA.url} onHandle={onHandle} />, {wrapper})
62
+ render(<Component url={handleA.url} onHandle={onHandle} />, { wrapper })
55
63
  await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(handleA))
56
64
  })
57
65
 
@@ -59,7 +67,7 @@ describe("useHandle", () => {
59
67
  const { wrapper } = setup()
60
68
  const onHandle = vi.fn()
61
69
 
62
- render(<Component url={undefined} onHandle={onHandle} />, {wrapper})
70
+ render(<Component url={undefined} onHandle={onHandle} />, { wrapper })
63
71
  await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(undefined))
64
72
  })
65
73
 
@@ -67,7 +75,10 @@ describe("useHandle", () => {
67
75
  const { wrapper, handleA, handleB } = setup()
68
76
  const onHandle = vi.fn()
69
77
 
70
- const { rerender } = render(<Component url={undefined} onHandle={onHandle} />, {wrapper})
78
+ const { rerender } = render(
79
+ <Component url={undefined} onHandle={onHandle} />,
80
+ { wrapper }
81
+ )
71
82
  await waitFor(() => expect(onHandle).toHaveBeenLastCalledWith(undefined))
72
83
 
73
84
  // set url to doc A
@@ -5,9 +5,7 @@ import { describe, expect, test, vi } from "vitest"
5
5
  import { RepoContext, useRepo } from "../src/useRepo.js"
6
6
 
7
7
  describe("useRepo", () => {
8
- const Component = ({ onRepo }: {
9
- onRepo: (repo: Repo) => void,
10
- }) => {
8
+ const Component = ({ onRepo }: { onRepo: (repo: Repo) => void }) => {
11
9
  const repo = useRepo()
12
10
  onRepo(repo)
13
11
  return null
@@ -18,7 +16,7 @@ describe("useRepo", () => {
18
16
  // Prevent console spam by swallowing console.error "uncaught error" message
19
17
  const spy = vi.spyOn(console, "error")
20
18
  spy.mockImplementation(() => {})
21
- expect(() => render(<Component onRepo={() => {}}/>)).toThrow(
19
+ expect(() => render(<Component onRepo={() => {}} />)).toThrow(
22
20
  /Repo was not found on RepoContext/
23
21
  )
24
22
  spy.mockRestore()
package/tsconfig.json CHANGED
@@ -12,7 +12,5 @@
12
12
  "strict": true,
13
13
  "skipLibCheck": true
14
14
  },
15
- "include": [
16
- "src/**/*.ts"
17
- ]
18
- }
15
+ "include": ["src/**/*.ts"]
16
+ }
package/vite.config.ts CHANGED
@@ -18,22 +18,22 @@ export default defineConfig({
18
18
  ],
19
19
  build: {
20
20
  lib: {
21
- entry: resolve(__dirname, 'src/index.ts'),
22
- formats: ['es'],
23
- fileName: 'index',
21
+ entry: resolve(__dirname, "src/index.ts"),
22
+ formats: ["es"],
23
+ fileName: "index",
24
24
  },
25
25
  rollupOptions: {
26
- external: ['react', 'react/jsx-runtime', 'react-dom', 'tailwindcss'],
26
+ external: ["react", "react/jsx-runtime", "react-dom", "tailwindcss"],
27
27
  output: {
28
28
  globals: {
29
- react: 'React',
30
- 'react/jsx-runtime': 'react/jsx-runtime',
31
- 'react-dom': 'ReactDOM',
32
- }
33
- }
29
+ react: "React",
30
+ "react/jsx-runtime": "react/jsx-runtime",
31
+ "react-dom": "ReactDOM",
32
+ },
33
+ },
34
34
  },
35
35
  },
36
36
  worker: {
37
37
  plugins: [wasm(), topLevelAwait()],
38
- }
38
+ },
39
39
  })