@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 +7 -4
- package/dist/index.js +1 -1
- package/dist/useDocument.d.ts.map +1 -1
- package/dist/useDocuments.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/useDocument.ts +9 -3
- package/src/useDocuments.ts +37 -14
- package/src/useLocalAwareness.ts +87 -87
- package/test/useDocument.test.tsx +53 -8
- package/test/useDocuments.test.tsx +46 -22
- package/test/useHandle.test.tsx +18 -7
- package/test/useRepo.test.tsx +2 -4
- package/tsconfig.json +2 -4
- package/vite.config.ts +10 -10
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,
|
|
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,
|
|
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,
|
|
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
|
+
"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.
|
|
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": "
|
|
45
|
+
"gitHead": "64edfaea5e53e77cd9158fc1df52fea85801db71"
|
|
46
46
|
}
|
package/src/useDocument.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
package/src/useDocuments.ts
CHANGED
|
@@ -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,
|
|
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
|
|
30
|
-
|
|
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]:
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
91
|
+
Object.entries(listeners).forEach(([id, listeners]) => {
|
|
72
92
|
const handle = repo.find<T>(id as DocId)
|
|
73
|
-
handle.off("change",
|
|
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
|
|
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> }
|
package/src/useLocalAwareness.ts
CHANGED
|
@@ -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
|
|
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 = ({
|
|
52
|
-
url
|
|
53
|
-
onDoc
|
|
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} />, {
|
|
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} />, {
|
|
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} />, {
|
|
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
|
|
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
|
|
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 = ({
|
|
30
|
-
ids
|
|
31
|
-
onDocs
|
|
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(() =>
|
|
44
|
-
|
|
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(() =>
|
|
54
|
-
|
|
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(() =>
|
|
65
|
-
|
|
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(
|
|
74
|
-
|
|
75
|
-
|
|
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(() =>
|
|
84
|
-
|
|
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
|
|
package/test/useHandle.test.tsx
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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 = ({
|
|
42
|
-
url
|
|
43
|
-
onHandle
|
|
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(
|
|
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
|
package/test/useRepo.test.tsx
CHANGED
|
@@ -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
package/vite.config.ts
CHANGED
|
@@ -18,22 +18,22 @@ export default defineConfig({
|
|
|
18
18
|
],
|
|
19
19
|
build: {
|
|
20
20
|
lib: {
|
|
21
|
-
entry: resolve(__dirname,
|
|
22
|
-
formats: [
|
|
23
|
-
fileName:
|
|
21
|
+
entry: resolve(__dirname, "src/index.ts"),
|
|
22
|
+
formats: ["es"],
|
|
23
|
+
fileName: "index",
|
|
24
24
|
},
|
|
25
25
|
rollupOptions: {
|
|
26
|
-
external: [
|
|
26
|
+
external: ["react", "react/jsx-runtime", "react-dom", "tailwindcss"],
|
|
27
27
|
output: {
|
|
28
28
|
globals: {
|
|
29
|
-
react:
|
|
30
|
-
|
|
31
|
-
|
|
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
|
})
|