@graffiti-garden/api 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tests.mjs CHANGED
@@ -1,2 +1,1457 @@
1
- import{it as z,expect as C,describe as L}from"vitest";import{GraffitiErrorInvalidUri as J}from"@graffiti-garden/api";import{assert as N}from"vitest";function u(){let h=new Uint8Array(16);return crypto.getRandomValues(h),Array.from(h).map(q=>q.toString(16).padStart(2,"0")).join("")+"\u{1F469}\u{1F3FD}\u200D\u2764\uFE0F\u200D\u{1F48B}\u200D\u{1F469}\u{1F3FB}\u{1FAF1}\u{1F3FC}\u200D\u{1FAF2}\u{1F3FF}"}function F(){return{[u()]:u()}}function w(){return{value:F(),channels:[u(),u()]}}async function y(h){let p=await h.next();return N(!p.done&&!p.value.error,"result has no value"),p.value.value}var st=h=>{L("URI and location conversion",()=>{z("location to uri and back",async()=>{let p=h(),q={name:u(),actor:u(),source:u()},t=p.locationToUri(q),o=p.uriToLocation(t);C(q).toEqual(o)}),z("collision resistance",async()=>{let p=h(),q={name:u(),actor:u(),source:u()};for(let t of["name","actor","source"]){let o={...q,[t]:u()},e=p.locationToUri(q),a=p.locationToUri(o);C(e).not.toEqual(a)}}),z("random URI should not be a valid location",async()=>{let p=h();C(()=>p.uriToLocation("")).toThrow(J)})})};import{it as x,expect as d,describe as $}from"vitest";import{GraffitiErrorNotFound as O,GraffitiErrorSchemaMismatch as K,GraffitiErrorInvalidSchema as Q,GraffitiErrorForbidden as S,GraffitiErrorPatchTestFailed as W,GraffitiErrorPatchError as R}from"@graffiti-garden/api";var ut=(h,p,q)=>{$("CRUD",{timeout:2e4},()=>{x("put, get, delete",async()=>{let t=h(),o=p(),e={something:"hello, world~ c:"},a=[u(),u()],n=await t.put({value:e,channels:a},o);d(n.value).toEqual({}),d(n.channels).toEqual([]),d(n.allowed).toBeUndefined(),d(n.actor).toEqual(o.actor);let r=await t.get(n,{});d(r.value).toEqual(e),d(r.channels).toEqual([]),d(r.allowed).toBeUndefined(),d(r.name).toEqual(n.name),d(r.actor).toEqual(n.actor),d(r.source).toEqual(n.source),d(r.lastModified).toEqual(n.lastModified);let s={something:"goodbye, world~ :c"},l=await t.put({...n,value:s,channels:[]},o);d(l.value).toEqual(e),d(l.tombstone).toEqual(!0),d(l.name).toEqual(n.name),d(l.actor).toEqual(n.actor),d(l.source).toEqual(n.source),d(l.lastModified).toBeGreaterThan(r.lastModified);let i=await t.get(n,{});d(i.value).toEqual(s),d(i.lastModified).toEqual(l.lastModified),d(i.tombstone).toEqual(!1);let m=await t.delete(i,o);d(m.tombstone).toEqual(!0),d(m.value).toEqual(s),d(m.lastModified).toBeGreaterThan(l.lastModified);let f=await t.get(i,{});d(f).toEqual(m)}),x("get non-existant",async()=>{let t=h(),o=p(),e=await t.put(w(),o);await d(t.get({...e,name:u()},{})).rejects.toBeInstanceOf(O)}),x("put, get, delete with wrong actor",async()=>{let t=h(),o=p(),e=q();await d(t.put({value:{},channels:[],actor:e.actor},o)).rejects.toThrow(S);let a=await t.put({value:{},channels:[]},e);await d(t.delete(a,o)).rejects.toThrow(S),await d(t.patch({},a,o)).rejects.toThrow(S)}),x("put and get with schema",async()=>{let t=h(),o=p(),e={properties:{value:{properties:{something:{type:"string"},another:{type:"integer"}}}}},a={something:"hello",another:42},n=await t.put({value:a,channels:[]},o),r=await t.get(n,e);d(r.value.something).toEqual(a.something),d(r.value.another).toEqual(a.another)}),x("put and get with invalid schema",async()=>{let t=h(),o=p(),e=await t.put({value:{},channels:[]},o);await d(t.get(e,{properties:{value:{type:"asdf"}}})).rejects.toThrow(Q)}),x("put and get with wrong schema",async()=>{let t=h(),o=p(),e=await t.put({value:{hello:"world"},channels:[]},o);await d(t.get(e,{properties:{value:{properties:{hello:{type:"number"}}}}})).rejects.toThrow(K)}),x("put and get with empty access control",async()=>{let t=h(),o=p(),e=q(),a={um:"hi"},n=[u()],r=[u()],s=await t.put({value:a,allowed:n,channels:r},o),l=await t.get(s,{},o);d(l.value).toEqual(a),d(l.allowed).toEqual(n),d(l.channels).toEqual(r),await d(t.get(s,{})).rejects.toBeInstanceOf(O),await d(t.get(s,{},e)).rejects.toBeInstanceOf(O)}),x("put and get with specific access control",async()=>{let t=h(),o=p(),e=q(),a={um:"hi"},n=[u(),e.actor,u()],r=[u()],s=await t.put({value:a,allowed:n,channels:r},o),l=await t.get(s,{},o);d(l.value).toEqual(a),d(l.allowed).toEqual(n),d(l.channels).toEqual(r),await d(t.get(s,{})).rejects.toBeInstanceOf(O);let i=await t.get(s,{},e);d(i.value).toEqual(a),d(i.allowed).toEqual([e.actor]),d(i.channels).toEqual([])}),x("patch value",async()=>{let t=h(),o=p(),e={something:"hello, world~ c:"},a=await t.put({value:e,channels:[]},o),n={value:[{op:"replace",path:"/something",value:"goodbye, world~ :c"}]},r=await t.patch(n,a,o);d(r.value).toEqual(e),d(r.tombstone).toBe(!0);let s=await t.get(a,{});d(s.value).toEqual({something:"goodbye, world~ :c"}),d(r.lastModified).toBe(s.lastModified),await t.delete(a,o)}),x("patch deleted object",async()=>{let t=h(),o=p(),e=await t.put(w(),o),a=await t.delete(e,o);await d(t.patch({},e,o)).rejects.toBeInstanceOf(O)}),x("deep patch",async()=>{let t=h(),o=p(),e={something:{another:{somethingElse:"hello"}}},a=await t.put({value:e,channels:[]},o),n=await t.patch({value:[{op:"replace",path:"/something/another/somethingElse",value:"goodbye"}]},a,o),r=await t.get(a,{});d(n.value).toEqual(e),d(r.value).toEqual({something:{another:{somethingElse:"goodbye"}}})}),x("patch channels",async()=>{let t=h(),o=p(),e=[u()],a=[u()],n=await t.put({value:{},channels:e},o),r={channels:[{op:"replace",path:"/0",value:a[0]}]},s=await t.patch(r,n,o);d(s.channels).toEqual(e);let l=await t.get(n,{},o);d(l.channels).toEqual(a),await t.delete(n,o)}),x("patch 'increment' with test",async()=>{let t=h(),o=p(),e=await t.put({value:{counter:1},channels:[]},o),a=await t.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:2}]},e,o);d(a.value).toEqual({counter:1});let n=await t.get(a,{properties:{value:{properties:{counter:{type:"integer"}}}}});d(n.value.counter).toEqual(2),await d(t.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:3}]},e,o)).rejects.toThrow(W)}),x("invalid patch",async()=>{let t=h(),o=p(),e=w(),a=await t.put(e,o);await d(t.patch({value:[{op:"add",path:"/root",value:[]},{op:"add",path:"/root/2",value:2}]},a,o)).rejects.toThrow(R)}),x("patch channels to be wrong",async()=>{let t=h(),o=p(),e=w();e.allowed=[u()];let a=await t.put(e,o),n=[{channels:[{op:"replace",path:"",value:null}]},{channels:[{op:"replace",path:"",value:{}}]},{channels:[{op:"replace",path:"",value:["hello",["hi"]]}]},{channels:[{op:"add",path:"/0",value:1}]},{value:[{op:"replace",path:"",value:"not an object"}]},{value:[{op:"replace",path:"",value:null}]},{value:[{op:"replace",path:"",value:[]}]},{allowed:[{op:"replace",path:"",value:{}}]},{allowed:[{op:"replace",path:"",value:["hello",["hi"]]}]}];for(let s of n)await d(t.patch(s,a,o)).rejects.toThrow(R);let r=await t.get(a,{},o);d(r.value).toEqual(e.value),d(r.channels).toEqual(e.channels),d(r.allowed).toEqual(e.allowed),d(r.lastModified).toEqual(a.lastModified)})})};import{it as I,expect as v,describe as D,assert as H}from"vitest";var vt=(h,p,q)=>{D("synchronizeDiscover",()=>{I("get",async()=>{let t=h(),o=p(),e=w(),a=e.channels.slice(1),n=await t.put(e,o),r=h(),s=r.synchronizeDiscover(a,{}).next(),l=await r.get(n,{},o),i=(await s).value;if(!i||i.error)throw new Error("Error in synchronize");v(i.value.value).toEqual(e.value),v(i.value.channels).toEqual(a),v(i.value.tombstone).toBe(!1),v(i.value.lastModified).toEqual(l.lastModified)}),I("put",async()=>{let t=h(),o=p(),e=u(),a=u(),n=u(),r={hello:"world"},s=[e,n],l=await t.put({value:r,channels:s},o),i=t.synchronizeDiscover([e],{}).next(),m=t.synchronizeDiscover([a],{}).next(),f=t.synchronizeDiscover([n],{}).next(),g={goodbye:"world"},E=[a,n];await t.put({...l,value:g,channels:E},o);let b=(await i).value,B=(await m).value,j=(await f).value;if(!b||b.error||!B||B.error||!j||j.error)throw new Error("Error in synchronize");v(b.value.value).toEqual(r),v(b.value.channels).toEqual([e]),v(b.value.tombstone).toBe(!0),v(B.value.value).toEqual(g),v(B.value.channels).toEqual([a]),v(B.value.tombstone).toBe(!1),v(j.value.value).toEqual(g),v(j.value.channels).toEqual([n]),v(j.value.tombstone).toBe(!1),v(b.value.lastModified).toEqual(B.value.lastModified),v(j.value.lastModified).toEqual(B.value.lastModified)}),I("patch",async()=>{let t=h(),o=p(),e=u(),a=u(),n=u(),r={hello:"world"},s=[e,n],l=await t.put({value:r,channels:s},o),i=t.synchronizeDiscover([e],{}).next(),m=t.synchronizeDiscover([a],{}).next(),f=t.synchronizeDiscover([n],{}).next();await t.patch({value:[{op:"add",path:"/something",value:"new value"}],channels:[{op:"add",path:"/-",value:a},{op:"remove",path:`/${s.indexOf(e)}`}]},l,o);let g=(await i).value,E=(await m).value,b=(await f).value;if(!g||g.error||!E||E.error||!b||b.error)throw new Error("Error in synchronize");let B={...r,something:"new value"},j=[n,a];v(g.value.value).toEqual(r),v(g.value.channels).toEqual([e]),v(g.value.tombstone).toBe(!0),v(E.value.value).toEqual(B),v(E.value.channels).toEqual([a]),v(E.value.tombstone).toBe(!1),v(b.value.value).toEqual(B),v(b.value.channels).toEqual([n]),v(b.value.tombstone).toBe(!1),v(g.value.lastModified).toEqual(E.value.lastModified),v(b.value.lastModified).toEqual(E.value.lastModified)}),I("delete",async()=>{let t=h(),o=p(),e=[u(),u(),u()],a={hello:"world"},n=[u(),...e.slice(1)],r=await t.put({value:a,channels:n},o),s=t.synchronizeDiscover(e,{}).next();t.delete(r,o);let l=(await s).value;if(!l||l.error)throw new Error("Error in synchronize");v(l.value.tombstone).toBe(!0),v(l.value.value).toEqual(a),v(l.value.channels).toEqual(e.filter(i=>n.includes(i)))}),I("not allowed",async()=>{let t=h(),o=p(),e=q(),a=[u(),u(),u()],n=a.slice(1),r=t.synchronizeDiscover(n,{},o).next(),s=t.synchronizeDiscover(n,{},e).next(),l=t.synchronizeDiscover(n,{}).next(),i={hello:"world"},m=[u(),e.actor];await t.put({value:i,channels:a,allowed:m},o),await v(Promise.race([l,new Promise((E,b)=>setTimeout(b,100,"Timeout"))])).rejects.toThrow("Timeout");let f=(await r).value,g=(await s).value;if(!f||f.error||!g||g.error)throw new Error("Error in synchronize");v(f.value.value).toEqual(i),v(f.value.allowed).toEqual(m),v(f.value.channels).toEqual(a),v(g.value.value).toEqual(i),v(g.value.allowed).toEqual([e.actor]),v(g.value.channels).toEqual(n)})}),D("synchronizeGet",()=>{I("replace, delete",async()=>{let t=h(),o=p(),e=w(),a=await t.put(e,o),n=t.synchronizeGet(a,{}),r=n.next(),s={goodbye:"world"},l=await t.put({...a,value:s},o),i=(await r).value;H(i&&!i.error),v(i.value.value).toEqual(s),v(i.value.actor).toEqual(o.actor),v(i.value.channels).toEqual([]),v(i.value.tombstone).toBe(!1),v(i.value.lastModified).toEqual(l.lastModified),v(i.value.allowed).toBeUndefined();let m=await t.delete(l,o),f=(await n.next()).value;H(f&&!f.error),v(f.value.tombstone).toBe(!0),v(f.value.lastModified).toEqual(m.lastModified),await t.put(w(),o),await v(Promise.race([n.next(),new Promise((g,E)=>setTimeout(E,100,"Timeout"))])).rejects.toThrow("Timeout")}),I("not allowed",async()=>{let t=h(),o=p(),e=q(),a=w(),n=await t.put(a,o),r=t.synchronizeGet(n,{},o),s=t.synchronizeGet(n,{},e),l=r.next(),i=s.next(),m={goodbye:"world"},f=await t.put({...n,...a,allowed:[],value:m},o),g=(await l).value,E=(await i).value;H(g&&!g.error),H(E&&!E.error),v(g.value.value).toEqual(m),v(E.value.value).toEqual(a.value),v(g.value.actor).toEqual(o.actor),v(E.value.actor).toEqual(o.actor),v(g.value.channels).toEqual(a.channels),v(E.value.channels).toEqual([]),v(g.value.tombstone).toBe(!1),v(E.value.tombstone).toBe(!0),v(g.value.lastModified).toEqual(f.lastModified),v(E.value.lastModified).toEqual(f.lastModified)})})};import{it as M,expect as c,describe as X,assert as U}from"vitest";var gt=(h,p,q)=>{X("discover",{timeout:2e4},()=>{M("discover nothing",async()=>{let o=h().discover([],{});c(await o.next()).toHaveProperty("done",!0)}),M("discover single",async()=>{let t=h(),o=p(),e=w(),a=await t.put(e,o),n=[u(),e.channels[0]],r=t.discover(n,{}),s=await y(r);c(s.value).toEqual(e.value),c(s.channels).toEqual([e.channels[0]]),c(s.allowed).toBeUndefined(),c(s.actor).toEqual(o.actor),c(s.tombstone).toBe(!1),c(s.lastModified).toEqual(a.lastModified);let l=await r.next();c(l.done).toBe(!0)}),M("discover wrong channel",async()=>{let t=h(),o=p(),e=w();await t.put(e,o);let a=t.discover([u()],{});await c(a.next()).resolves.toHaveProperty("done",!0)}),M("discover not allowed",async()=>{let t=h(),o=p(),e=q(),a=w();a.allowed=[u(),u()];let n=await t.put(a,o),r=t.discover(a.channels,{},o),s=await y(r);c(s.value).toEqual(a.value),c(s.channels).toEqual(a.channels),c(s.allowed).toEqual(a.allowed),c(s.actor).toEqual(o.actor),c(s.tombstone).toBe(!1),c(s.lastModified).toEqual(n.lastModified);let l=t.discover(a.channels,{},e);c(await l.next()).toHaveProperty("done",!0);let i=t.discover(a.channels,{});c(await i.next()).toHaveProperty("done",!0)}),M("discover allowed",async()=>{let t=h(),o=p(),e=q(),a=w();a.allowed=[u(),e.actor,u()];let n=await t.put(a,o),r=t.discover(a.channels,{},e),s=await y(r);c(s.value).toEqual(a.value),c(s.allowed).toEqual([e.actor]),c(s.channels).toEqual(a.channels),c(s.actor).toEqual(o.actor),c(s.tombstone).toBe(!1),c(s.lastModified).toEqual(n.lastModified)});for(let t of["name","actor","lastModified"])M(`discover for ${t}`,async()=>{let o=h(),e=p(),a=q(),n=w(),r=await o.put(n,e),s=w();s.channels=n.channels,await new Promise(f=>setTimeout(f,20));let l=await o.put(s,a),i=o.discover(n.channels,{properties:{[t]:{enum:[r[t]]}}}),m=await y(i);c(m.name).toEqual(r.name),c(m.name).not.toEqual(l.name),c(m.value).toEqual(n.value),await c(i.next()).resolves.toHaveProperty("done",!0)});M("discover with lastModified range",async()=>{let t=h(),o=p(),e=w(),a=await t.put(e,o);await new Promise(G=>setTimeout(G,20));let n=await t.put(e,o);c(a.name).not.toEqual(n.name),c(a.lastModified).toBeLessThan(n.lastModified);let r=t.discover([e.channels[0]],{properties:{lastModified:{minimum:n.lastModified,exclusiveMinimum:!0}}});c(await r.next()).toHaveProperty("done",!0);let s=t.discover([e.channels[0]],{properties:{lastModified:{minimum:n.lastModified-.1,exclusiveMinimum:!0}}}),l=await y(s);c(l.name).toEqual(n.name),c(await s.next()).toHaveProperty("done",!0);let i=t.discover(e.channels,{properties:{value:{},lastModified:{minimum:n.lastModified}}}),m=await y(i);c(m.name).toEqual(n.name),c(await i.next()).toHaveProperty("done",!0);let f=t.discover(e.channels,{properties:{lastModified:{minimum:n.lastModified+.1}}});c(await f.next()).toHaveProperty("done",!0);let g=t.discover(e.channels,{properties:{lastModified:{maximum:a.lastModified,exclusiveMaximum:!0}}});c(await g.next()).toHaveProperty("done",!0);let E=t.discover(e.channels,{properties:{lastModified:{maximum:a.lastModified+.1,exclusiveMaximum:!0}}}),b=await y(E);c(b.name).toEqual(a.name),c(await E.next()).toHaveProperty("done",!0);let B=t.discover(e.channels,{properties:{lastModified:{maximum:a.lastModified}}}),j=await y(B);c(j.name).toEqual(a.name),c(await B.next()).toHaveProperty("done",!0);let A=t.discover(e.channels,{properties:{lastModified:{maximum:a.lastModified-.1}}});c(await A.next()).toHaveProperty("done",!0)}),M("discover schema allowed, as and not as owner",async()=>{let t=h(),o=p(),e=q(),a=w();a.allowed=[u(),e.actor,u()],await t.put(a,o);let n=t.discover(a.channels,{properties:{allowed:{minItems:3,not:{items:{not:{enum:[e.actor]}}}}}},o),r=await y(n);c(r.value).toEqual(a.value),await c(n.next()).resolves.toHaveProperty("done",!0);let s=t.discover(a.channels,{properties:{allowed:{minItems:3}}},e);await c(s.next()).resolves.toHaveProperty("done",!0);let l=t.discover(a.channels,{properties:{allowed:{not:{items:{not:{enum:[a.channels[0]]}}}}}},e);await c(l.next()).resolves.toHaveProperty("done",!0);let i=t.discover(a.channels,{properties:{allowed:{maxItems:1,not:{items:{not:{enum:[e.actor]}}}}}},e),m=await y(i);c(m.value).toEqual(a.value),await c(i.next()).resolves.toHaveProperty("done",!0)}),M("discover schema channels, as and not as owner",async()=>{let t=h(),o=p(),e=q(),a=w();a.channels=[u(),u(),u()],await t.put(a,o);let n=t.discover([a.channels[0],a.channels[2]],{properties:{channels:{minItems:3,not:{items:{not:{enum:[a.channels[1]]}}}}}},o),r=await y(n);c(r.value).toEqual(a.value),await c(n.next()).resolves.toHaveProperty("done",!0);let s=t.discover([a.channels[0],a.channels[2]],{properties:{channels:{minItems:3}}},e);await c(s.next()).resolves.toHaveProperty("done",!0);let l=t.discover([a.channels[0],a.channels[2]],{properties:{channels:{not:{items:{not:{enum:[a.channels[1]]}}}}}},e);await c(l.next()).resolves.toHaveProperty("done",!0);let i=t.discover([a.channels[0],a.channels[2]],{properties:{allowed:{maxItems:2,not:{items:{not:{enum:[a.channels[2]]}}}}}},e),m=await y(i);c(m.value).toEqual(a.value),await c(i.next()).resolves.toHaveProperty("done",!0)}),M("discover query for empty allowed",async()=>{let t=h(),o=p(),e=w(),a={not:{required:["allowed"]}};await t.put(e,o);let n=t.discover(e.channels,a,o),r=await y(n);c(r.value).toEqual(e.value),c(r.allowed).toBeUndefined(),await c(n.next()).resolves.toHaveProperty("done",!0);let s=w();s.allowed=[],await t.put(s,o);let l=t.discover(s.channels,a,o);await c(l.next()).resolves.toHaveProperty("done",!0)}),M("discover query for values",async()=>{let t=h(),o=p(),e=w();e.value={test:u()},await t.put(e,o);let a=w();a.channels=e.channels,a.value={test:u(),something:u()},await t.put(a,o);let n=w();n.channels=e.channels,n.value={other:u(),something:u()},await t.put(n,o);let r=new Map;for(let s of["test","something","other"]){let l=0;for await(let i of t.discover(e.channels,{properties:{value:{required:[s]}}}))U(!i.error,"result has error"),s in i.value.value&&l++;r.set(s,l)}c(r.get("test")).toBe(2),c(r.get("something")).toBe(2),c(r.get("other")).toBe(1)}),M("discover for deleted content",async()=>{let t=h(),o=p(),e=w(),a=await t.put(e,o),n=await t.delete(a,o),r=t.discover(e.channels,{}),s=await y(r);c(s.tombstone).toBe(!0),c(s.value).toEqual(e.value),c(s.channels).toEqual(e.channels),c(s.actor).toEqual(o.actor),c(s.lastModified).toEqual(n.lastModified),await c(r.next()).resolves.toHaveProperty("done",!0)}),M("discover for replaced channels",async()=>{for(let t=0;t<10;t++){let o=h(),e=p(),a=w(),n=await o.put(a,e),r=w(),s=await o.put({...n,...r},e),l=o.discover(a.channels,{}),i=await y(l);await c(l.next()).resolves.toHaveProperty("done",!0);let m=o.discover(r.channels,{}),f=await y(m);await c(m.next()).resolves.toHaveProperty("done",!0),n.lastModified===s.lastModified?(c(i.tombstone||f.tombstone).toBe(!0),c(i.tombstone&&f.tombstone).toBe(!1)):(c(i.tombstone).toBe(!0),c(i.value).toEqual(a.value),c(i.channels).toEqual(a.channels),c(i.lastModified).toEqual(s.lastModified),c(f.tombstone).toBe(!1),c(f.value).toEqual(r.value),c(f.channels).toEqual(r.channels),c(f.lastModified).toEqual(s.lastModified))}}),M("discover for patched allowed",async()=>{let t=h(),o=p(),e=w(),a=await t.put(e,o);await t.patch({allowed:[{op:"add",path:"",value:[]}]},a,o);let n=t.discover(e.channels,{}),r=await y(n);c(r.tombstone).toBe(!0),c(r.value).toEqual(e.value),c(r.channels).toEqual(e.channels),c(r.allowed).toBeUndefined(),await c(n.next()).resolves.toHaveProperty("done",!0)}),M("put concurrently and discover one",async()=>{let t=h(),o=p(),e=w();e.name=u();let a=Array(100).fill(0).map(()=>t.put(e,o));await Promise.all(a);let n=t.discover(e.channels,{}),r=0,s=0;for await(let l of n)U(!l.error,"result has error"),l.value.tombstone?r++:s++;c(r).toBe(99),c(s).toBe(1)})})};import{it as V,expect as T,describe as Y}from"vitest";var xt=(h,p,q)=>{Y("recoverOrphans",()=>{V("list orphans",async()=>{let t=h(),o=p(),e=[],a=t.recoverOrphans({},o);for await(let i of a)i.error||e.push(i.value.name);let n=w();n.channels=[];let r=await t.put(n,o),s=t.recoverOrphans({},o),l=0;for await(let i of s)i.error||i.value.name===r.name&&(l++,T(i.value.source).toBe(r.source),T(i.value.lastModified).toBe(r.lastModified));T(l).toBe(1)}),V("replaced orphan, no longer",async()=>{let t=h(),o=p(),e=w();e.channels=[];let a=await t.put(e,o),n=await t.put({...a,...e,channels:[u()]},o);T(n.name).toBe(a.name);let r=t.recoverOrphans({},o),s=0;for await(let l of r)l.error||l.value.name===a.name&&(s++,T(l.value.tombstone).toBe(!0),T(l.value.lastModified).toBe(n.lastModified),T(l.value.channels).toEqual([]));T(s).toBe(1)})})};import{it as k,expect as P,describe as Z,assert as _}from"vitest";var Tt=(h,p,q)=>{Z("channel stats",()=>{k("list channels",async()=>{let t=h(),o=p(),e=new Map,a=t.channelStats(o);for await(let l of a)l.error||e.set(l.value.channel,l.value.count);let n=[u(),u(),u()];for(let l=0;l<3;l++)for(let i=0;i<l+1;i++)await t.put({value:{index:i},channels:n.slice(0,l+1)},o);await t.put({value:{index:3},channels:[n[2]]},o);let r=t.channelStats(o),s=new Map;for await(let l of r)l.error||s.set(l.value.channel,l.value.count);s=new Map(Array.from(s).filter(([l,i])=>!e.has(l))),P(s.size).toBe(3),P(s.get(n[0])).toBe(6),P(s.get(n[1])).toBe(5),P(s.get(n[2])).toBe(4)}),k("list channels with deleted channel",async()=>{let t=h(),o=p(),e=[u(),u(),u()],a=await t.put({value:{index:2},channels:e.slice(1)},o),n=await t.put({value:{index:0},channels:e},o);await t.delete(n,o);let r=await t.put({value:{index:1},channels:e.slice(2)},o),s=t.channelStats(o),l=0,i=0;for await(let m of s){if(m.error)continue;let{channel:f,count:g,lastModified:E}=m.value;_(f!==e[0],"There should not be an object in channel[0]"),f===e[1]?(P(g).toBe(1),P(E).toBe(a.lastModified),l++):f===e[2]&&(P(g).toBe(2),P(E).toBe(r.lastModified),i++)}P(l).toBe(1),P(i).toBe(1)})})};export{ut as graffitiCRUDTests,Tt as graffitiChannelStatsTests,gt as graffitiDiscoverTests,st as graffitiLocationTests,xt as graffitiOrphanTests,vt as graffitiSynchronizeTests};
1
+ // tests/location.ts
2
+ import { it, expect, describe } from "vitest";
3
+ import { GraffitiErrorInvalidUri } from "@graffiti-garden/api";
4
+
5
+ // tests/utils.ts
6
+ import { assert } from "vitest";
7
+ function randomString() {
8
+ const array = new Uint8Array(16);
9
+ crypto.getRandomValues(array);
10
+ const str = Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
11
+ return str + "\u{1F469}\u{1F3FD}\u200D\u2764\uFE0F\u200D\u{1F48B}\u200D\u{1F469}\u{1F3FB}\u{1FAF1}\u{1F3FC}\u200D\u{1FAF2}\u{1F3FF}";
12
+ }
13
+ function randomValue() {
14
+ return {
15
+ [randomString()]: randomString()
16
+ };
17
+ }
18
+ function randomPutObject() {
19
+ return {
20
+ value: randomValue(),
21
+ channels: [randomString(), randomString()]
22
+ };
23
+ }
24
+ async function nextStreamValue(iterator) {
25
+ const result = await iterator.next();
26
+ assert(!result.done && !result.value.error, "result has no value");
27
+ return result.value.value;
28
+ }
29
+
30
+ // tests/location.ts
31
+ var graffitiLocationTests = (useGraffiti) => {
32
+ describe.concurrent("URI and location conversion", () => {
33
+ it("location to uri and back", async () => {
34
+ const graffiti = useGraffiti();
35
+ const location = {
36
+ name: randomString(),
37
+ actor: randomString(),
38
+ source: randomString()
39
+ };
40
+ const uri = graffiti.locationToUri(location);
41
+ const location2 = graffiti.uriToLocation(uri);
42
+ expect(location).toEqual(location2);
43
+ });
44
+ it("collision resistance", async () => {
45
+ const graffiti = useGraffiti();
46
+ const location1 = {
47
+ name: randomString(),
48
+ actor: randomString(),
49
+ source: randomString()
50
+ };
51
+ for (const prop of ["name", "actor", "source"]) {
52
+ const location2 = { ...location1, [prop]: randomString() };
53
+ const uri1 = graffiti.locationToUri(location1);
54
+ const uri2 = graffiti.locationToUri(location2);
55
+ expect(uri1).not.toEqual(uri2);
56
+ }
57
+ });
58
+ it("random URI should not be a valid location", async () => {
59
+ const graffiti = useGraffiti();
60
+ expect(() => graffiti.uriToLocation("")).toThrow(GraffitiErrorInvalidUri);
61
+ });
62
+ });
63
+ };
64
+
65
+ // tests/crud.ts
66
+ import { it as it2, expect as expect2, describe as describe2 } from "vitest";
67
+ import {
68
+ GraffitiErrorNotFound,
69
+ GraffitiErrorSchemaMismatch,
70
+ GraffitiErrorInvalidSchema,
71
+ GraffitiErrorForbidden,
72
+ GraffitiErrorPatchTestFailed,
73
+ GraffitiErrorPatchError
74
+ } from "@graffiti-garden/api";
75
+ var graffitiCRUDTests = (useGraffiti, useSession1, useSession2) => {
76
+ describe2.concurrent(
77
+ "CRUD",
78
+ {
79
+ timeout: 2e4
80
+ },
81
+ () => {
82
+ it2("put, get, delete", async () => {
83
+ const graffiti = useGraffiti();
84
+ const session = useSession1();
85
+ const value = {
86
+ something: "hello, world~ c:"
87
+ };
88
+ const channels = [randomString(), randomString()];
89
+ const previous = await graffiti.put({ value, channels }, session);
90
+ expect2(previous.value).toEqual({});
91
+ expect2(previous.channels).toEqual([]);
92
+ expect2(previous.allowed).toBeUndefined();
93
+ expect2(previous.actor).toEqual(session.actor);
94
+ const gotten = await graffiti.get(previous, {});
95
+ expect2(gotten.value).toEqual(value);
96
+ expect2(gotten.channels).toEqual([]);
97
+ expect2(gotten.allowed).toBeUndefined();
98
+ expect2(gotten.name).toEqual(previous.name);
99
+ expect2(gotten.actor).toEqual(previous.actor);
100
+ expect2(gotten.source).toEqual(previous.source);
101
+ expect2(gotten.lastModified).toEqual(previous.lastModified);
102
+ const newValue = {
103
+ something: "goodbye, world~ :c"
104
+ };
105
+ const beforeReplaced = await graffiti.put(
106
+ { ...previous, value: newValue, channels: [] },
107
+ session
108
+ );
109
+ expect2(beforeReplaced.value).toEqual(value);
110
+ expect2(beforeReplaced.tombstone).toEqual(true);
111
+ expect2(beforeReplaced.name).toEqual(previous.name);
112
+ expect2(beforeReplaced.actor).toEqual(previous.actor);
113
+ expect2(beforeReplaced.source).toEqual(previous.source);
114
+ expect2(beforeReplaced.lastModified).toBeGreaterThanOrEqual(
115
+ gotten.lastModified
116
+ );
117
+ const afterReplaced = await graffiti.get(previous, {});
118
+ expect2(afterReplaced.value).toEqual(newValue);
119
+ expect2(afterReplaced.lastModified).toEqual(beforeReplaced.lastModified);
120
+ expect2(afterReplaced.tombstone).toEqual(false);
121
+ const beforeDeleted = await graffiti.delete(afterReplaced, session);
122
+ expect2(beforeDeleted.tombstone).toEqual(true);
123
+ expect2(beforeDeleted.value).toEqual(newValue);
124
+ expect2(beforeDeleted.lastModified).toBeGreaterThanOrEqual(
125
+ beforeReplaced.lastModified
126
+ );
127
+ const final = await graffiti.get(afterReplaced, {});
128
+ expect2(final).toEqual(beforeDeleted);
129
+ });
130
+ it2("get non-existant", async () => {
131
+ const graffiti = useGraffiti();
132
+ const session = useSession1();
133
+ const putted = await graffiti.put(randomPutObject(), session);
134
+ await expect2(
135
+ graffiti.get(
136
+ {
137
+ ...putted,
138
+ name: randomString()
139
+ },
140
+ {}
141
+ )
142
+ ).rejects.toBeInstanceOf(GraffitiErrorNotFound);
143
+ });
144
+ it2("put, get, delete with wrong actor", async () => {
145
+ const graffiti = useGraffiti();
146
+ const session1 = useSession1();
147
+ const session2 = useSession2();
148
+ await expect2(
149
+ graffiti.put(
150
+ { value: {}, channels: [], actor: session2.actor },
151
+ session1
152
+ )
153
+ ).rejects.toThrow(GraffitiErrorForbidden);
154
+ const putted = await graffiti.put(
155
+ { value: {}, channels: [] },
156
+ session2
157
+ );
158
+ await expect2(graffiti.delete(putted, session1)).rejects.toThrow(
159
+ GraffitiErrorForbidden
160
+ );
161
+ await expect2(graffiti.patch({}, putted, session1)).rejects.toThrow(
162
+ GraffitiErrorForbidden
163
+ );
164
+ });
165
+ it2("put and get with schema", async () => {
166
+ const graffiti = useGraffiti();
167
+ const session = useSession1();
168
+ const schema = {
169
+ properties: {
170
+ value: {
171
+ properties: {
172
+ something: {
173
+ type: "string"
174
+ },
175
+ another: {
176
+ type: "integer"
177
+ }
178
+ }
179
+ }
180
+ }
181
+ };
182
+ const goodValue = {
183
+ something: "hello",
184
+ another: 42
185
+ };
186
+ const putted = await graffiti.put(
187
+ {
188
+ value: goodValue,
189
+ channels: []
190
+ },
191
+ session
192
+ );
193
+ const gotten = await graffiti.get(putted, schema);
194
+ expect2(gotten.value.something).toEqual(goodValue.something);
195
+ expect2(gotten.value.another).toEqual(goodValue.another);
196
+ });
197
+ it2("put and get with invalid schema", async () => {
198
+ const graffiti = useGraffiti();
199
+ const session = useSession1();
200
+ const putted = await graffiti.put({ value: {}, channels: [] }, session);
201
+ await expect2(
202
+ graffiti.get(putted, {
203
+ properties: {
204
+ value: {
205
+ //@ts-ignore
206
+ type: "asdf"
207
+ }
208
+ }
209
+ })
210
+ ).rejects.toThrow(GraffitiErrorInvalidSchema);
211
+ });
212
+ it2("put and get with wrong schema", async () => {
213
+ const graffiti = useGraffiti();
214
+ const session = useSession1();
215
+ const putted = await graffiti.put(
216
+ {
217
+ value: {
218
+ hello: "world"
219
+ },
220
+ channels: []
221
+ },
222
+ session
223
+ );
224
+ await expect2(
225
+ graffiti.get(putted, {
226
+ properties: {
227
+ value: {
228
+ properties: {
229
+ hello: {
230
+ type: "number"
231
+ }
232
+ }
233
+ }
234
+ }
235
+ })
236
+ ).rejects.toThrow(GraffitiErrorSchemaMismatch);
237
+ });
238
+ it2("put and get with empty access control", async () => {
239
+ const graffiti = useGraffiti();
240
+ const session1 = useSession1();
241
+ const session2 = useSession2();
242
+ const value = {
243
+ um: "hi"
244
+ };
245
+ const allowed = [randomString()];
246
+ const channels = [randomString()];
247
+ const putted = await graffiti.put(
248
+ { value, allowed, channels },
249
+ session1
250
+ );
251
+ const gotten = await graffiti.get(putted, {}, session1);
252
+ expect2(gotten.value).toEqual(value);
253
+ expect2(gotten.allowed).toEqual(allowed);
254
+ expect2(gotten.channels).toEqual(channels);
255
+ await expect2(graffiti.get(putted, {})).rejects.toBeInstanceOf(
256
+ GraffitiErrorNotFound
257
+ );
258
+ await expect2(graffiti.get(putted, {}, session2)).rejects.toBeInstanceOf(
259
+ GraffitiErrorNotFound
260
+ );
261
+ });
262
+ it2("put and get with specific access control", async () => {
263
+ const graffiti = useGraffiti();
264
+ const session1 = useSession1();
265
+ const session2 = useSession2();
266
+ const value = {
267
+ um: "hi"
268
+ };
269
+ const allowed = [randomString(), session2.actor, randomString()];
270
+ const channels = [randomString()];
271
+ const putted = await graffiti.put(
272
+ {
273
+ value,
274
+ allowed,
275
+ channels
276
+ },
277
+ session1
278
+ );
279
+ const gotten = await graffiti.get(putted, {}, session1);
280
+ expect2(gotten.value).toEqual(value);
281
+ expect2(gotten.allowed).toEqual(allowed);
282
+ expect2(gotten.channels).toEqual(channels);
283
+ await expect2(graffiti.get(putted, {})).rejects.toBeInstanceOf(
284
+ GraffitiErrorNotFound
285
+ );
286
+ const gotten2 = await graffiti.get(putted, {}, session2);
287
+ expect2(gotten2.value).toEqual(value);
288
+ expect2(gotten2.allowed).toEqual([session2.actor]);
289
+ expect2(gotten2.channels).toEqual([]);
290
+ });
291
+ it2("patch value", async () => {
292
+ const graffiti = useGraffiti();
293
+ const session = useSession1();
294
+ const value = {
295
+ something: "hello, world~ c:"
296
+ };
297
+ const putted = await graffiti.put({ value, channels: [] }, session);
298
+ await new Promise((resolve) => setTimeout(resolve, 10));
299
+ const patch = {
300
+ value: [
301
+ { op: "replace", path: "/something", value: "goodbye, world~ :c" }
302
+ ]
303
+ };
304
+ const beforePatched = await graffiti.patch(patch, putted, session);
305
+ expect2(beforePatched.value).toEqual(value);
306
+ expect2(beforePatched.tombstone).toBe(true);
307
+ expect2(beforePatched.lastModified).toBeGreaterThan(putted.lastModified);
308
+ const gotten = await graffiti.get(putted, {});
309
+ expect2(gotten.value).toEqual({
310
+ something: "goodbye, world~ :c"
311
+ });
312
+ expect2(beforePatched.lastModified).toBe(gotten.lastModified);
313
+ await graffiti.delete(putted, session);
314
+ });
315
+ it2("patch deleted object", async () => {
316
+ const graffiti = useGraffiti();
317
+ const session = useSession1();
318
+ const putted = await graffiti.put(randomPutObject(), session);
319
+ const deleted = await graffiti.delete(putted, session);
320
+ await expect2(
321
+ graffiti.patch({}, putted, session)
322
+ ).rejects.toBeInstanceOf(GraffitiErrorNotFound);
323
+ });
324
+ it2("deep patch", async () => {
325
+ const graffiti = useGraffiti();
326
+ const session = useSession1();
327
+ const value = {
328
+ something: {
329
+ another: {
330
+ somethingElse: "hello"
331
+ }
332
+ }
333
+ };
334
+ const putted = await graffiti.put(
335
+ { value, channels: [] },
336
+ session
337
+ );
338
+ const beforePatch = await graffiti.patch(
339
+ {
340
+ value: [
341
+ {
342
+ op: "replace",
343
+ path: "/something/another/somethingElse",
344
+ value: "goodbye"
345
+ }
346
+ ]
347
+ },
348
+ putted,
349
+ session
350
+ );
351
+ const gotten = await graffiti.get(putted, {});
352
+ expect2(beforePatch.value).toEqual(value);
353
+ expect2(gotten.value).toEqual({
354
+ something: {
355
+ another: {
356
+ somethingElse: "goodbye"
357
+ }
358
+ }
359
+ });
360
+ });
361
+ it2("patch channels", async () => {
362
+ const graffiti = useGraffiti();
363
+ const session = useSession1();
364
+ const channelsBefore = [randomString()];
365
+ const channelsAfter = [randomString()];
366
+ const putted = await graffiti.put(
367
+ { value: {}, channels: channelsBefore },
368
+ session
369
+ );
370
+ const patch = {
371
+ channels: [{ op: "replace", path: "/0", value: channelsAfter[0] }]
372
+ };
373
+ const patched = await graffiti.patch(patch, putted, session);
374
+ expect2(patched.channels).toEqual(channelsBefore);
375
+ const gotten = await graffiti.get(putted, {}, session);
376
+ expect2(gotten.channels).toEqual(channelsAfter);
377
+ await graffiti.delete(putted, session);
378
+ });
379
+ it2("patch 'increment' with test", async () => {
380
+ const graffiti = useGraffiti();
381
+ const session = useSession1();
382
+ const putted = await graffiti.put(
383
+ {
384
+ value: {
385
+ counter: 1
386
+ },
387
+ channels: []
388
+ },
389
+ session
390
+ );
391
+ const previous = await graffiti.patch(
392
+ {
393
+ value: [
394
+ { op: "test", path: "/counter", value: 1 },
395
+ { op: "replace", path: "/counter", value: 2 }
396
+ ]
397
+ },
398
+ putted,
399
+ session
400
+ );
401
+ expect2(previous.value).toEqual({ counter: 1 });
402
+ const result = await graffiti.get(previous, {
403
+ properties: {
404
+ value: {
405
+ properties: {
406
+ counter: {
407
+ type: "integer"
408
+ }
409
+ }
410
+ }
411
+ }
412
+ });
413
+ expect2(result.value.counter).toEqual(2);
414
+ await expect2(
415
+ graffiti.patch(
416
+ {
417
+ value: [
418
+ { op: "test", path: "/counter", value: 1 },
419
+ { op: "replace", path: "/counter", value: 3 }
420
+ ]
421
+ },
422
+ putted,
423
+ session
424
+ )
425
+ ).rejects.toThrow(GraffitiErrorPatchTestFailed);
426
+ });
427
+ it2("invalid patch", async () => {
428
+ const graffiti = useGraffiti();
429
+ const session = useSession1();
430
+ const object = randomPutObject();
431
+ const putted = await graffiti.put(object, session);
432
+ await expect2(
433
+ graffiti.patch(
434
+ {
435
+ value: [
436
+ { op: "add", path: "/root", value: [] },
437
+ { op: "add", path: "/root/2", value: 2 }
438
+ // out of bounds
439
+ ]
440
+ },
441
+ putted,
442
+ session
443
+ )
444
+ ).rejects.toThrow(GraffitiErrorPatchError);
445
+ });
446
+ it2("patch channels to be wrong", async () => {
447
+ const graffiti = useGraffiti();
448
+ const session = useSession1();
449
+ const object = randomPutObject();
450
+ object.allowed = [randomString()];
451
+ const putted = await graffiti.put(object, session);
452
+ const patches = [
453
+ {
454
+ channels: [{ op: "replace", path: "", value: null }]
455
+ },
456
+ {
457
+ channels: [{ op: "replace", path: "", value: {} }]
458
+ },
459
+ {
460
+ channels: [{ op: "replace", path: "", value: ["hello", ["hi"]] }]
461
+ },
462
+ {
463
+ channels: [{ op: "add", path: "/0", value: 1 }]
464
+ },
465
+ {
466
+ value: [{ op: "replace", path: "", value: "not an object" }]
467
+ },
468
+ {
469
+ value: [{ op: "replace", path: "", value: null }]
470
+ },
471
+ {
472
+ value: [{ op: "replace", path: "", value: [] }]
473
+ },
474
+ {
475
+ allowed: [{ op: "replace", path: "", value: {} }]
476
+ },
477
+ {
478
+ allowed: [{ op: "replace", path: "", value: ["hello", ["hi"]] }]
479
+ }
480
+ ];
481
+ for (const patch of patches) {
482
+ await expect2(graffiti.patch(patch, putted, session)).rejects.toThrow(
483
+ GraffitiErrorPatchError
484
+ );
485
+ }
486
+ const gotten = await graffiti.get(putted, {}, session);
487
+ expect2(gotten.value).toEqual(object.value);
488
+ expect2(gotten.channels).toEqual(object.channels);
489
+ expect2(gotten.allowed).toEqual(object.allowed);
490
+ expect2(gotten.lastModified).toEqual(putted.lastModified);
491
+ });
492
+ }
493
+ );
494
+ };
495
+
496
+ // tests/synchronize.ts
497
+ import { it as it3, expect as expect3, describe as describe3, assert as assert2 } from "vitest";
498
+ var graffitiSynchronizeTests = (useGraffiti, useSession1, useSession2) => {
499
+ describe3.concurrent("synchronizeDiscover", () => {
500
+ it3("get", async () => {
501
+ const graffiti1 = useGraffiti();
502
+ const session = useSession1();
503
+ const object = randomPutObject();
504
+ const channels = object.channels.slice(1);
505
+ const putted = await graffiti1.put(object, session);
506
+ const graffiti2 = useGraffiti();
507
+ const next = graffiti2.synchronizeDiscover(channels, {}).next();
508
+ const gotten = await graffiti2.get(putted, {}, session);
509
+ const result = (await next).value;
510
+ if (!result || result.error) {
511
+ throw new Error("Error in synchronize");
512
+ }
513
+ expect3(result.value.value).toEqual(object.value);
514
+ expect3(result.value.channels).toEqual(channels);
515
+ expect3(result.value.tombstone).toBe(false);
516
+ expect3(result.value.lastModified).toEqual(gotten.lastModified);
517
+ });
518
+ it3("put", async () => {
519
+ const graffiti = useGraffiti();
520
+ const session = useSession1();
521
+ const beforeChannel = randomString();
522
+ const afterChannel = randomString();
523
+ const sharedChannel = randomString();
524
+ const oldValue = { hello: "world" };
525
+ const oldChannels = [beforeChannel, sharedChannel];
526
+ const putted = await graffiti.put(
527
+ {
528
+ value: oldValue,
529
+ channels: oldChannels
530
+ },
531
+ session
532
+ );
533
+ const before = graffiti.synchronizeDiscover([beforeChannel], {}).next();
534
+ const after = graffiti.synchronizeDiscover([afterChannel], {}).next();
535
+ const shared = graffiti.synchronizeDiscover([sharedChannel], {}).next();
536
+ const newValue = { goodbye: "world" };
537
+ const newChannels = [afterChannel, sharedChannel];
538
+ await graffiti.put(
539
+ {
540
+ ...putted,
541
+ value: newValue,
542
+ channels: newChannels
543
+ },
544
+ session
545
+ );
546
+ const beforeResult = (await before).value;
547
+ const afterResult = (await after).value;
548
+ const sharedResult = (await shared).value;
549
+ if (!beforeResult || beforeResult.error || !afterResult || afterResult.error || !sharedResult || sharedResult.error) {
550
+ throw new Error("Error in synchronize");
551
+ }
552
+ expect3(beforeResult.value.value).toEqual(oldValue);
553
+ expect3(beforeResult.value.channels).toEqual([beforeChannel]);
554
+ expect3(beforeResult.value.tombstone).toBe(true);
555
+ expect3(afterResult.value.value).toEqual(newValue);
556
+ expect3(afterResult.value.channels).toEqual([afterChannel]);
557
+ expect3(afterResult.value.tombstone).toBe(false);
558
+ expect3(sharedResult.value.value).toEqual(newValue);
559
+ expect3(sharedResult.value.channels).toEqual([sharedChannel]);
560
+ expect3(sharedResult.value.tombstone).toBe(false);
561
+ expect3(beforeResult.value.lastModified).toEqual(
562
+ afterResult.value.lastModified
563
+ );
564
+ expect3(sharedResult.value.lastModified).toEqual(
565
+ afterResult.value.lastModified
566
+ );
567
+ });
568
+ it3("patch", async () => {
569
+ const graffiti = useGraffiti();
570
+ const session = useSession1();
571
+ const beforeChannel = randomString();
572
+ const afterChannel = randomString();
573
+ const sharedChannel = randomString();
574
+ const oldValue = { hello: "world" };
575
+ const oldChannels = [beforeChannel, sharedChannel];
576
+ const putted = await graffiti.put(
577
+ {
578
+ value: oldValue,
579
+ channels: oldChannels
580
+ },
581
+ session
582
+ );
583
+ const before = graffiti.synchronizeDiscover([beforeChannel], {}).next();
584
+ const after = graffiti.synchronizeDiscover([afterChannel], {}).next();
585
+ const shared = graffiti.synchronizeDiscover([sharedChannel], {}).next();
586
+ await graffiti.patch(
587
+ {
588
+ value: [
589
+ {
590
+ op: "add",
591
+ path: "/something",
592
+ value: "new value"
593
+ }
594
+ ],
595
+ channels: [
596
+ {
597
+ op: "add",
598
+ path: "/-",
599
+ value: afterChannel
600
+ },
601
+ {
602
+ op: "remove",
603
+ path: `/${oldChannels.indexOf(beforeChannel)}`
604
+ }
605
+ ]
606
+ },
607
+ putted,
608
+ session
609
+ );
610
+ const beforeResult = (await before).value;
611
+ const afterResult = (await after).value;
612
+ const sharedResult = (await shared).value;
613
+ if (!beforeResult || beforeResult.error || !afterResult || afterResult.error || !sharedResult || sharedResult.error) {
614
+ throw new Error("Error in synchronize");
615
+ }
616
+ const newValue = { ...oldValue, something: "new value" };
617
+ const newChannels = [sharedChannel, afterChannel];
618
+ expect3(beforeResult.value.value).toEqual(oldValue);
619
+ expect3(beforeResult.value.channels).toEqual([beforeChannel]);
620
+ expect3(beforeResult.value.tombstone).toBe(true);
621
+ expect3(afterResult.value.value).toEqual(newValue);
622
+ expect3(afterResult.value.channels).toEqual([afterChannel]);
623
+ expect3(afterResult.value.tombstone).toBe(false);
624
+ expect3(sharedResult.value.value).toEqual(newValue);
625
+ expect3(sharedResult.value.channels).toEqual([sharedChannel]);
626
+ expect3(sharedResult.value.tombstone).toBe(false);
627
+ expect3(beforeResult.value.lastModified).toEqual(
628
+ afterResult.value.lastModified
629
+ );
630
+ expect3(sharedResult.value.lastModified).toEqual(
631
+ afterResult.value.lastModified
632
+ );
633
+ });
634
+ it3("delete", async () => {
635
+ const graffiti = useGraffiti();
636
+ const session = useSession1();
637
+ const channels = [randomString(), randomString(), randomString()];
638
+ const oldValue = { hello: "world" };
639
+ const oldChannels = [randomString(), ...channels.slice(1)];
640
+ const putted = await graffiti.put(
641
+ {
642
+ value: oldValue,
643
+ channels: oldChannels
644
+ },
645
+ session
646
+ );
647
+ const next = graffiti.synchronizeDiscover(channels, {}).next();
648
+ graffiti.delete(putted, session);
649
+ const result = (await next).value;
650
+ if (!result || result.error) {
651
+ throw new Error("Error in synchronize");
652
+ }
653
+ expect3(result.value.tombstone).toBe(true);
654
+ expect3(result.value.value).toEqual(oldValue);
655
+ expect3(result.value.channels).toEqual(
656
+ channels.filter((c) => oldChannels.includes(c))
657
+ );
658
+ });
659
+ it3("not allowed", async () => {
660
+ const graffiti = useGraffiti();
661
+ const session1 = useSession1();
662
+ const session2 = useSession2();
663
+ const allChannels = [randomString(), randomString(), randomString()];
664
+ const channels = allChannels.slice(1);
665
+ const creatorNext = graffiti.synchronizeDiscover(channels, {}, session1).next();
666
+ const allowedNext = graffiti.synchronizeDiscover(channels, {}, session2).next();
667
+ const noSession = graffiti.synchronizeDiscover(channels, {}).next();
668
+ const value = {
669
+ hello: "world"
670
+ };
671
+ const allowed = [randomString(), session2.actor];
672
+ await graffiti.put({ value, channels: allChannels, allowed }, session1);
673
+ await expect3(
674
+ Promise.race([
675
+ noSession,
676
+ new Promise(
677
+ (resolve, rejects) => setTimeout(rejects, 100, "Timeout")
678
+ )
679
+ ])
680
+ ).rejects.toThrow("Timeout");
681
+ const creatorResult = (await creatorNext).value;
682
+ const allowedResult = (await allowedNext).value;
683
+ if (!creatorResult || creatorResult.error || !allowedResult || allowedResult.error) {
684
+ throw new Error("Error in synchronize");
685
+ }
686
+ expect3(creatorResult.value.value).toEqual(value);
687
+ expect3(creatorResult.value.allowed).toEqual(allowed);
688
+ expect3(creatorResult.value.channels).toEqual(allChannels);
689
+ expect3(allowedResult.value.value).toEqual(value);
690
+ expect3(allowedResult.value.allowed).toEqual([session2.actor]);
691
+ expect3(allowedResult.value.channels).toEqual(channels);
692
+ });
693
+ });
694
+ describe3("synchronizeGet", () => {
695
+ it3("replace, delete", async () => {
696
+ const graffiti = useGraffiti();
697
+ const session = useSession1();
698
+ const object = randomPutObject();
699
+ const putted = await graffiti.put(object, session);
700
+ const iterator = graffiti.synchronizeGet(putted, {});
701
+ const next = iterator.next();
702
+ const newValue = { goodbye: "world" };
703
+ const putted2 = await graffiti.put(
704
+ {
705
+ ...putted,
706
+ value: newValue
707
+ },
708
+ session
709
+ );
710
+ const result = (await next).value;
711
+ assert2(result && !result.error);
712
+ expect3(result.value.value).toEqual(newValue);
713
+ expect3(result.value.actor).toEqual(session.actor);
714
+ expect3(result.value.channels).toEqual([]);
715
+ expect3(result.value.tombstone).toBe(false);
716
+ expect3(result.value.lastModified).toEqual(putted2.lastModified);
717
+ expect3(result.value.allowed).toBeUndefined();
718
+ const deleted = await graffiti.delete(putted2, session);
719
+ const result2 = (await iterator.next()).value;
720
+ assert2(result2 && !result2.error);
721
+ expect3(result2.value.tombstone).toBe(true);
722
+ expect3(result2.value.lastModified).toEqual(deleted.lastModified);
723
+ await graffiti.put(randomPutObject(), session);
724
+ await expect3(
725
+ Promise.race([
726
+ iterator.next(),
727
+ new Promise((resolve, reject) => setTimeout(reject, 100, "Timeout"))
728
+ ])
729
+ ).rejects.toThrow("Timeout");
730
+ });
731
+ it3("not allowed", async () => {
732
+ const graffiti = useGraffiti();
733
+ const session1 = useSession1();
734
+ const session2 = useSession2();
735
+ const object = randomPutObject();
736
+ const putted = await graffiti.put(object, session1);
737
+ const iterator1 = graffiti.synchronizeGet(putted, {}, session1);
738
+ const iterator2 = graffiti.synchronizeGet(putted, {}, session2);
739
+ const next1 = iterator1.next();
740
+ const next2 = iterator2.next();
741
+ const newValue = { goodbye: "world" };
742
+ const putted2 = await graffiti.put(
743
+ {
744
+ ...putted,
745
+ ...object,
746
+ allowed: [],
747
+ value: newValue
748
+ },
749
+ session1
750
+ );
751
+ const result1 = (await next1).value;
752
+ const result2 = (await next2).value;
753
+ assert2(result1 && !result1.error);
754
+ assert2(result2 && !result2.error);
755
+ expect3(result1.value.value).toEqual(newValue);
756
+ expect3(result2.value.value).toEqual(object.value);
757
+ expect3(result1.value.actor).toEqual(session1.actor);
758
+ expect3(result2.value.actor).toEqual(session1.actor);
759
+ expect3(result1.value.channels).toEqual(object.channels);
760
+ expect3(result2.value.channels).toEqual([]);
761
+ expect3(result1.value.tombstone).toBe(false);
762
+ expect3(result2.value.tombstone).toBe(true);
763
+ expect3(result1.value.lastModified).toEqual(putted2.lastModified);
764
+ expect3(result2.value.lastModified).toEqual(putted2.lastModified);
765
+ });
766
+ });
767
+ };
768
+
769
+ // tests/discover.ts
770
+ import { it as it4, expect as expect4, describe as describe4, assert as assert3 } from "vitest";
771
+ var graffitiDiscoverTests = (useGraffiti, useSession1, useSession2) => {
772
+ describe4.concurrent("discover", { timeout: 2e4 }, () => {
773
+ it4("discover nothing", async () => {
774
+ const graffiti = useGraffiti();
775
+ const iterator = graffiti.discover([], {});
776
+ expect4(await iterator.next()).toHaveProperty("done", true);
777
+ });
778
+ it4("discover single", async () => {
779
+ const graffiti = useGraffiti();
780
+ const session = useSession1();
781
+ const object = randomPutObject();
782
+ const putted = await graffiti.put(object, session);
783
+ const queryChannels = [randomString(), object.channels[0]];
784
+ const iterator = graffiti.discover(queryChannels, {});
785
+ const value = await nextStreamValue(iterator);
786
+ expect4(value.value).toEqual(object.value);
787
+ expect4(value.channels).toEqual([object.channels[0]]);
788
+ expect4(value.allowed).toBeUndefined();
789
+ expect4(value.actor).toEqual(session.actor);
790
+ expect4(value.tombstone).toBe(false);
791
+ expect4(value.lastModified).toEqual(putted.lastModified);
792
+ const result2 = await iterator.next();
793
+ expect4(result2.done).toBe(true);
794
+ });
795
+ it4("discover wrong channel", async () => {
796
+ const graffiti = useGraffiti();
797
+ const session = useSession1();
798
+ const object = randomPutObject();
799
+ await graffiti.put(object, session);
800
+ const iterator = graffiti.discover([randomString()], {});
801
+ await expect4(iterator.next()).resolves.toHaveProperty("done", true);
802
+ });
803
+ it4("discover not allowed", async () => {
804
+ const graffiti = useGraffiti();
805
+ const session1 = useSession1();
806
+ const session2 = useSession2();
807
+ const object = randomPutObject();
808
+ object.allowed = [randomString(), randomString()];
809
+ const putted = await graffiti.put(object, session1);
810
+ const iteratorSession1 = graffiti.discover(object.channels, {}, session1);
811
+ const value = await nextStreamValue(iteratorSession1);
812
+ expect4(value.value).toEqual(object.value);
813
+ expect4(value.channels).toEqual(object.channels);
814
+ expect4(value.allowed).toEqual(object.allowed);
815
+ expect4(value.actor).toEqual(session1.actor);
816
+ expect4(value.tombstone).toBe(false);
817
+ expect4(value.lastModified).toEqual(putted.lastModified);
818
+ const iteratorSession2 = graffiti.discover(object.channels, {}, session2);
819
+ expect4(await iteratorSession2.next()).toHaveProperty("done", true);
820
+ const iteratorNoSession = graffiti.discover(object.channels, {});
821
+ expect4(await iteratorNoSession.next()).toHaveProperty("done", true);
822
+ });
823
+ it4("discover allowed", async () => {
824
+ const graffiti = useGraffiti();
825
+ const session1 = useSession1();
826
+ const session2 = useSession2();
827
+ const object = randomPutObject();
828
+ object.allowed = [randomString(), session2.actor, randomString()];
829
+ const putted = await graffiti.put(object, session1);
830
+ const iteratorSession2 = graffiti.discover(object.channels, {}, session2);
831
+ const value = await nextStreamValue(iteratorSession2);
832
+ expect4(value.value).toEqual(object.value);
833
+ expect4(value.allowed).toEqual([session2.actor]);
834
+ expect4(value.channels).toEqual(object.channels);
835
+ expect4(value.actor).toEqual(session1.actor);
836
+ expect4(value.tombstone).toBe(false);
837
+ expect4(value.lastModified).toEqual(putted.lastModified);
838
+ });
839
+ for (const prop of ["name", "actor", "lastModified"]) {
840
+ it4(`discover for ${prop}`, async () => {
841
+ const graffiti = useGraffiti();
842
+ const session1 = useSession1();
843
+ const session2 = useSession2();
844
+ const object1 = randomPutObject();
845
+ const putted1 = await graffiti.put(object1, session1);
846
+ const object2 = randomPutObject();
847
+ object2.channels = object1.channels;
848
+ await new Promise((r) => setTimeout(r, 20));
849
+ const putted2 = await graffiti.put(object2, session2);
850
+ const iterator = graffiti.discover(object1.channels, {
851
+ properties: {
852
+ [prop]: {
853
+ enum: [putted1[prop]]
854
+ }
855
+ }
856
+ });
857
+ const value = await nextStreamValue(iterator);
858
+ expect4(value.name).toEqual(putted1.name);
859
+ expect4(value.name).not.toEqual(putted2.name);
860
+ expect4(value.value).toEqual(object1.value);
861
+ await expect4(iterator.next()).resolves.toHaveProperty("done", true);
862
+ });
863
+ }
864
+ it4("discover with lastModified range", async () => {
865
+ const graffiti = useGraffiti();
866
+ const session = useSession1();
867
+ const object = randomPutObject();
868
+ const putted1 = await graffiti.put(object, session);
869
+ await new Promise((r) => setTimeout(r, 20));
870
+ const putted2 = await graffiti.put(object, session);
871
+ expect4(putted1.name).not.toEqual(putted2.name);
872
+ expect4(putted1.lastModified).toBeLessThan(putted2.lastModified);
873
+ const gtIterator = graffiti.discover([object.channels[0]], {
874
+ properties: {
875
+ lastModified: {
876
+ minimum: putted2.lastModified,
877
+ exclusiveMinimum: true
878
+ }
879
+ }
880
+ });
881
+ expect4(await gtIterator.next()).toHaveProperty("done", true);
882
+ const gtIteratorEpsilon = graffiti.discover([object.channels[0]], {
883
+ properties: {
884
+ lastModified: {
885
+ minimum: putted2.lastModified - 0.1,
886
+ exclusiveMinimum: true
887
+ }
888
+ }
889
+ });
890
+ const value1 = await nextStreamValue(gtIteratorEpsilon);
891
+ expect4(value1.name).toEqual(putted2.name);
892
+ expect4(await gtIteratorEpsilon.next()).toHaveProperty("done", true);
893
+ const gteIterator = graffiti.discover(object.channels, {
894
+ properties: {
895
+ value: {},
896
+ lastModified: {
897
+ minimum: putted2.lastModified
898
+ }
899
+ }
900
+ });
901
+ const value = await nextStreamValue(gteIterator);
902
+ expect4(value.name).toEqual(putted2.name);
903
+ expect4(await gteIterator.next()).toHaveProperty("done", true);
904
+ const gteIteratorEpsilon = graffiti.discover(object.channels, {
905
+ properties: {
906
+ lastModified: {
907
+ minimum: putted2.lastModified + 0.1
908
+ }
909
+ }
910
+ });
911
+ expect4(await gteIteratorEpsilon.next()).toHaveProperty("done", true);
912
+ const ltIterator = graffiti.discover(object.channels, {
913
+ properties: {
914
+ lastModified: {
915
+ maximum: putted1.lastModified,
916
+ exclusiveMaximum: true
917
+ }
918
+ }
919
+ });
920
+ expect4(await ltIterator.next()).toHaveProperty("done", true);
921
+ const ltIteratorEpsilon = graffiti.discover(object.channels, {
922
+ properties: {
923
+ lastModified: {
924
+ maximum: putted1.lastModified + 0.1,
925
+ exclusiveMaximum: true
926
+ }
927
+ }
928
+ });
929
+ const value3 = await nextStreamValue(ltIteratorEpsilon);
930
+ expect4(value3.name).toEqual(putted1.name);
931
+ expect4(await ltIteratorEpsilon.next()).toHaveProperty("done", true);
932
+ const lteIterator = graffiti.discover(object.channels, {
933
+ properties: {
934
+ lastModified: {
935
+ maximum: putted1.lastModified
936
+ }
937
+ }
938
+ });
939
+ const value2 = await nextStreamValue(lteIterator);
940
+ expect4(value2.name).toEqual(putted1.name);
941
+ expect4(await lteIterator.next()).toHaveProperty("done", true);
942
+ const lteIteratorEpsilon = graffiti.discover(object.channels, {
943
+ properties: {
944
+ lastModified: {
945
+ maximum: putted1.lastModified - 0.1
946
+ }
947
+ }
948
+ });
949
+ expect4(await lteIteratorEpsilon.next()).toHaveProperty("done", true);
950
+ });
951
+ it4("discover schema allowed, as and not as owner", async () => {
952
+ const graffiti = useGraffiti();
953
+ const session1 = useSession1();
954
+ const session2 = useSession2();
955
+ const object = randomPutObject();
956
+ object.allowed = [randomString(), session2.actor, randomString()];
957
+ await graffiti.put(object, session1);
958
+ const iteratorSession1 = graffiti.discover(
959
+ object.channels,
960
+ {
961
+ properties: {
962
+ allowed: {
963
+ minItems: 3,
964
+ // Make sure session2.actor is in the allow list
965
+ not: {
966
+ items: {
967
+ not: {
968
+ enum: [session2.actor]
969
+ }
970
+ }
971
+ }
972
+ }
973
+ }
974
+ },
975
+ session1
976
+ );
977
+ const value = await nextStreamValue(iteratorSession1);
978
+ expect4(value.value).toEqual(object.value);
979
+ await expect4(iteratorSession1.next()).resolves.toHaveProperty(
980
+ "done",
981
+ true
982
+ );
983
+ const iteratorSession2BigAllow = graffiti.discover(
984
+ object.channels,
985
+ {
986
+ properties: {
987
+ allowed: {
988
+ minItems: 3
989
+ }
990
+ }
991
+ },
992
+ session2
993
+ );
994
+ await expect4(iteratorSession2BigAllow.next()).resolves.toHaveProperty(
995
+ "done",
996
+ true
997
+ );
998
+ const iteratorSession2PeekOther = graffiti.discover(
999
+ object.channels,
1000
+ {
1001
+ properties: {
1002
+ allowed: {
1003
+ not: {
1004
+ items: {
1005
+ not: {
1006
+ enum: [object.channels[0]]
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ },
1013
+ session2
1014
+ );
1015
+ await expect4(iteratorSession2PeekOther.next()).resolves.toHaveProperty(
1016
+ "done",
1017
+ true
1018
+ );
1019
+ const iteratorSession2SmallAllowPeekSelf = graffiti.discover(
1020
+ object.channels,
1021
+ {
1022
+ properties: {
1023
+ allowed: {
1024
+ maxItems: 1,
1025
+ not: {
1026
+ items: {
1027
+ not: {
1028
+ enum: [session2.actor]
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ }
1034
+ },
1035
+ session2
1036
+ );
1037
+ const value2 = await nextStreamValue(iteratorSession2SmallAllowPeekSelf);
1038
+ expect4(value2.value).toEqual(object.value);
1039
+ await expect4(
1040
+ iteratorSession2SmallAllowPeekSelf.next()
1041
+ ).resolves.toHaveProperty("done", true);
1042
+ });
1043
+ it4("discover schema channels, as and not as owner", async () => {
1044
+ const graffiti = useGraffiti();
1045
+ const session1 = useSession1();
1046
+ const session2 = useSession2();
1047
+ const object = randomPutObject();
1048
+ object.channels = [randomString(), randomString(), randomString()];
1049
+ await graffiti.put(object, session1);
1050
+ const iteratorSession1 = graffiti.discover(
1051
+ [object.channels[0], object.channels[2]],
1052
+ {
1053
+ properties: {
1054
+ channels: {
1055
+ minItems: 3,
1056
+ // Make sure session2.actor is in the allow list
1057
+ not: {
1058
+ items: {
1059
+ not: {
1060
+ enum: [object.channels[1]]
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ },
1067
+ session1
1068
+ );
1069
+ const value = await nextStreamValue(iteratorSession1);
1070
+ expect4(value.value).toEqual(object.value);
1071
+ await expect4(iteratorSession1.next()).resolves.toHaveProperty(
1072
+ "done",
1073
+ true
1074
+ );
1075
+ const iteratorSession2BigAllow = graffiti.discover(
1076
+ [object.channels[0], object.channels[2]],
1077
+ {
1078
+ properties: {
1079
+ channels: {
1080
+ minItems: 3
1081
+ }
1082
+ }
1083
+ },
1084
+ session2
1085
+ );
1086
+ await expect4(iteratorSession2BigAllow.next()).resolves.toHaveProperty(
1087
+ "done",
1088
+ true
1089
+ );
1090
+ const iteratorSession2PeekOther = graffiti.discover(
1091
+ [object.channels[0], object.channels[2]],
1092
+ {
1093
+ properties: {
1094
+ channels: {
1095
+ not: {
1096
+ items: {
1097
+ not: {
1098
+ enum: [object.channels[1]]
1099
+ }
1100
+ }
1101
+ }
1102
+ }
1103
+ }
1104
+ },
1105
+ session2
1106
+ );
1107
+ await expect4(iteratorSession2PeekOther.next()).resolves.toHaveProperty(
1108
+ "done",
1109
+ true
1110
+ );
1111
+ const iteratorSession2SmallAllowPeekSelf = graffiti.discover(
1112
+ [object.channels[0], object.channels[2]],
1113
+ {
1114
+ properties: {
1115
+ allowed: {
1116
+ maxItems: 2,
1117
+ not: {
1118
+ items: {
1119
+ not: {
1120
+ enum: [object.channels[2]]
1121
+ }
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+ },
1127
+ session2
1128
+ );
1129
+ const value2 = await nextStreamValue(iteratorSession2SmallAllowPeekSelf);
1130
+ expect4(value2.value).toEqual(object.value);
1131
+ await expect4(
1132
+ iteratorSession2SmallAllowPeekSelf.next()
1133
+ ).resolves.toHaveProperty("done", true);
1134
+ });
1135
+ it4("discover query for empty allowed", async () => {
1136
+ const graffiti = useGraffiti();
1137
+ const session1 = useSession1();
1138
+ const publicO = randomPutObject();
1139
+ const publicSchema = {
1140
+ not: {
1141
+ required: ["allowed"]
1142
+ }
1143
+ };
1144
+ await graffiti.put(publicO, session1);
1145
+ const iterator = graffiti.discover(
1146
+ publicO.channels,
1147
+ publicSchema,
1148
+ session1
1149
+ );
1150
+ const value = await nextStreamValue(iterator);
1151
+ expect4(value.value).toEqual(publicO.value);
1152
+ expect4(value.allowed).toBeUndefined();
1153
+ await expect4(iterator.next()).resolves.toHaveProperty("done", true);
1154
+ const restricted = randomPutObject();
1155
+ restricted.allowed = [];
1156
+ await graffiti.put(restricted, session1);
1157
+ const iterator2 = graffiti.discover(
1158
+ restricted.channels,
1159
+ publicSchema,
1160
+ session1
1161
+ );
1162
+ await expect4(iterator2.next()).resolves.toHaveProperty("done", true);
1163
+ });
1164
+ it4("discover query for values", async () => {
1165
+ const graffiti = useGraffiti();
1166
+ const session = useSession1();
1167
+ const object1 = randomPutObject();
1168
+ object1.value = { test: randomString() };
1169
+ await graffiti.put(object1, session);
1170
+ const object2 = randomPutObject();
1171
+ object2.channels = object1.channels;
1172
+ object2.value = { test: randomString(), something: randomString() };
1173
+ await graffiti.put(object2, session);
1174
+ const object3 = randomPutObject();
1175
+ object3.channels = object1.channels;
1176
+ object3.value = { other: randomString(), something: randomString() };
1177
+ await graffiti.put(object3, session);
1178
+ const counts = /* @__PURE__ */ new Map();
1179
+ for (const property of ["test", "something", "other"]) {
1180
+ let count = 0;
1181
+ for await (const result of graffiti.discover(object1.channels, {
1182
+ properties: {
1183
+ value: {
1184
+ required: [property]
1185
+ }
1186
+ }
1187
+ })) {
1188
+ assert3(!result.error, "result has error");
1189
+ if (property in result.value.value) {
1190
+ count++;
1191
+ }
1192
+ }
1193
+ counts.set(property, count);
1194
+ }
1195
+ expect4(counts.get("test")).toBe(2);
1196
+ expect4(counts.get("something")).toBe(2);
1197
+ expect4(counts.get("other")).toBe(1);
1198
+ });
1199
+ it4("discover for deleted content", async () => {
1200
+ const graffiti = useGraffiti();
1201
+ const session = useSession1();
1202
+ const object = randomPutObject();
1203
+ const putted = await graffiti.put(object, session);
1204
+ const deleted = await graffiti.delete(putted, session);
1205
+ const iterator = graffiti.discover(object.channels, {});
1206
+ const value = await nextStreamValue(iterator);
1207
+ expect4(value.tombstone).toBe(true);
1208
+ expect4(value.value).toEqual(object.value);
1209
+ expect4(value.channels).toEqual(object.channels);
1210
+ expect4(value.actor).toEqual(session.actor);
1211
+ expect4(value.lastModified).toEqual(deleted.lastModified);
1212
+ await expect4(iterator.next()).resolves.toHaveProperty("done", true);
1213
+ });
1214
+ it4("discover for replaced channels", async () => {
1215
+ for (let i = 0; i < 10; i++) {
1216
+ const graffiti = useGraffiti();
1217
+ const session = useSession1();
1218
+ const object1 = randomPutObject();
1219
+ const putted = await graffiti.put(object1, session);
1220
+ const object2 = randomPutObject();
1221
+ const replaced = await graffiti.put(
1222
+ {
1223
+ ...putted,
1224
+ ...object2
1225
+ },
1226
+ session
1227
+ );
1228
+ const iterator1 = graffiti.discover(object1.channels, {});
1229
+ const value1 = await nextStreamValue(iterator1);
1230
+ await expect4(iterator1.next()).resolves.toHaveProperty("done", true);
1231
+ const iterator2 = graffiti.discover(object2.channels, {});
1232
+ const value2 = await nextStreamValue(iterator2);
1233
+ await expect4(iterator2.next()).resolves.toHaveProperty("done", true);
1234
+ if (putted.lastModified === replaced.lastModified) {
1235
+ expect4(value1.tombstone || value2.tombstone).toBe(true);
1236
+ expect4(value1.tombstone && value2.tombstone).toBe(false);
1237
+ } else {
1238
+ expect4(value1.tombstone).toBe(true);
1239
+ expect4(value1.value).toEqual(object1.value);
1240
+ expect4(value1.channels).toEqual(object1.channels);
1241
+ expect4(value1.lastModified).toEqual(replaced.lastModified);
1242
+ expect4(value2.tombstone).toBe(false);
1243
+ expect4(value2.value).toEqual(object2.value);
1244
+ expect4(value2.channels).toEqual(object2.channels);
1245
+ expect4(value2.lastModified).toEqual(replaced.lastModified);
1246
+ }
1247
+ }
1248
+ });
1249
+ it4("discover for patched allowed", async () => {
1250
+ const graffiti = useGraffiti();
1251
+ const session = useSession1();
1252
+ const object = randomPutObject();
1253
+ const putted = await graffiti.put(object, session);
1254
+ await graffiti.patch(
1255
+ {
1256
+ allowed: [{ op: "add", path: "", value: [] }]
1257
+ },
1258
+ putted,
1259
+ session
1260
+ );
1261
+ const iterator = graffiti.discover(object.channels, {});
1262
+ const value = await nextStreamValue(iterator);
1263
+ expect4(value.tombstone).toBe(true);
1264
+ expect4(value.value).toEqual(object.value);
1265
+ expect4(value.channels).toEqual(object.channels);
1266
+ expect4(value.allowed).toBeUndefined();
1267
+ await expect4(iterator.next()).resolves.toHaveProperty("done", true);
1268
+ });
1269
+ it4("put concurrently and discover one", async () => {
1270
+ const graffiti = useGraffiti();
1271
+ const session = useSession1();
1272
+ const object = randomPutObject();
1273
+ object.name = randomString();
1274
+ const putPromises = Array(100).fill(0).map(() => graffiti.put(object, session));
1275
+ await Promise.all(putPromises);
1276
+ const iterator = graffiti.discover(object.channels, {});
1277
+ let tombstoneCount = 0;
1278
+ let valueCount = 0;
1279
+ for await (const result of iterator) {
1280
+ assert3(!result.error, "result has error");
1281
+ if (result.value.tombstone) {
1282
+ tombstoneCount++;
1283
+ } else {
1284
+ valueCount++;
1285
+ }
1286
+ }
1287
+ expect4(tombstoneCount).toBe(99);
1288
+ expect4(valueCount).toBe(1);
1289
+ });
1290
+ });
1291
+ };
1292
+
1293
+ // tests/orphans.ts
1294
+ import { it as it5, expect as expect5, describe as describe5 } from "vitest";
1295
+ var graffitiOrphanTests = (useGraffiti, useSession1, useSession2) => {
1296
+ describe5("recoverOrphans", () => {
1297
+ it5("list orphans", async () => {
1298
+ const graffiti = useGraffiti();
1299
+ const session = useSession1();
1300
+ const existingOrphans = [];
1301
+ const orphanIterator1 = graffiti.recoverOrphans({}, session);
1302
+ for await (const orphan of orphanIterator1) {
1303
+ if (orphan.error) continue;
1304
+ existingOrphans.push(orphan.value.name);
1305
+ }
1306
+ const object = randomPutObject();
1307
+ object.channels = [];
1308
+ const putted = await graffiti.put(object, session);
1309
+ const orphanIterator2 = graffiti.recoverOrphans({}, session);
1310
+ let numResults = 0;
1311
+ for await (const orphan of orphanIterator2) {
1312
+ if (orphan.error) continue;
1313
+ if (orphan.value.name === putted.name) {
1314
+ numResults++;
1315
+ expect5(orphan.value.source).toBe(putted.source);
1316
+ expect5(orphan.value.lastModified).toBe(putted.lastModified);
1317
+ }
1318
+ }
1319
+ expect5(numResults).toBe(1);
1320
+ });
1321
+ it5("replaced orphan, no longer", async () => {
1322
+ const graffiti = useGraffiti();
1323
+ const session = useSession1();
1324
+ const object = randomPutObject();
1325
+ object.channels = [];
1326
+ const putOrphan = await graffiti.put(object, session);
1327
+ await new Promise((resolve) => setTimeout(resolve, 10));
1328
+ const putNotOrphan = await graffiti.put(
1329
+ {
1330
+ ...putOrphan,
1331
+ ...object,
1332
+ channels: [randomString()]
1333
+ },
1334
+ session
1335
+ );
1336
+ expect5(putNotOrphan.name).toBe(putOrphan.name);
1337
+ expect5(putNotOrphan.lastModified).toBeGreaterThan(putOrphan.lastModified);
1338
+ const orphanIterator = graffiti.recoverOrphans({}, session);
1339
+ let numResults = 0;
1340
+ for await (const orphan of orphanIterator) {
1341
+ if (orphan.error) continue;
1342
+ if (orphan.value.name === putOrphan.name) {
1343
+ numResults++;
1344
+ expect5(orphan.value.tombstone).toBe(true);
1345
+ expect5(orphan.value.lastModified).toBe(putNotOrphan.lastModified);
1346
+ expect5(orphan.value.channels).toEqual([]);
1347
+ }
1348
+ }
1349
+ expect5(numResults).toBe(1);
1350
+ });
1351
+ });
1352
+ };
1353
+
1354
+ // tests/channel-stats.ts
1355
+ import { it as it6, expect as expect6, describe as describe6, assert as assert5 } from "vitest";
1356
+ var graffitiChannelStatsTests = (useGraffiti, useSession1, useSession2) => {
1357
+ describe6("channel stats", () => {
1358
+ it6("list channels", async () => {
1359
+ const graffiti = useGraffiti();
1360
+ const session = useSession1();
1361
+ const existingChannels = /* @__PURE__ */ new Map();
1362
+ const channelIterator1 = graffiti.channelStats(session);
1363
+ for await (const channel of channelIterator1) {
1364
+ if (channel.error) continue;
1365
+ existingChannels.set(channel.value.channel, channel.value.count);
1366
+ }
1367
+ const channels = [randomString(), randomString(), randomString()];
1368
+ for (let i = 0; i < 3; i++) {
1369
+ for (let j = 0; j < i + 1; j++) {
1370
+ await graffiti.put(
1371
+ {
1372
+ value: {
1373
+ index: j
1374
+ },
1375
+ channels: channels.slice(0, i + 1)
1376
+ },
1377
+ session
1378
+ );
1379
+ }
1380
+ }
1381
+ await graffiti.put(
1382
+ { value: { index: 3 }, channels: [channels[2]] },
1383
+ session
1384
+ );
1385
+ const channelIterator2 = graffiti.channelStats(session);
1386
+ let newChannels = /* @__PURE__ */ new Map();
1387
+ for await (const channel of channelIterator2) {
1388
+ if (channel.error) continue;
1389
+ newChannels.set(channel.value.channel, channel.value.count);
1390
+ }
1391
+ newChannels = new Map(
1392
+ Array.from(newChannels).filter(
1393
+ ([channel, count]) => !existingChannels.has(channel)
1394
+ )
1395
+ );
1396
+ expect6(newChannels.size).toBe(3);
1397
+ expect6(newChannels.get(channels[0])).toBe(6);
1398
+ expect6(newChannels.get(channels[1])).toBe(5);
1399
+ expect6(newChannels.get(channels[2])).toBe(4);
1400
+ });
1401
+ it6("list channels with deleted channel", async () => {
1402
+ const graffiti = useGraffiti();
1403
+ const session = useSession1();
1404
+ const channels = [randomString(), randomString(), randomString()];
1405
+ const before = await graffiti.put(
1406
+ {
1407
+ value: { index: 2 },
1408
+ channels: channels.slice(1)
1409
+ },
1410
+ session
1411
+ );
1412
+ const first = await graffiti.put(
1413
+ { value: { index: 0 }, channels },
1414
+ session
1415
+ );
1416
+ await graffiti.delete(first, session);
1417
+ const second = await graffiti.put(
1418
+ {
1419
+ value: { index: 1 },
1420
+ channels: channels.slice(2)
1421
+ },
1422
+ session
1423
+ );
1424
+ const channelIterator = graffiti.channelStats(session);
1425
+ let got1 = 0;
1426
+ let got2 = 0;
1427
+ for await (const result of channelIterator) {
1428
+ if (result.error) continue;
1429
+ const { channel, count, lastModified } = result.value;
1430
+ assert5(
1431
+ channel !== channels[0],
1432
+ "There should not be an object in channel[0]"
1433
+ );
1434
+ if (channel === channels[1]) {
1435
+ expect6(count).toBe(1);
1436
+ expect6(lastModified).toBe(before.lastModified);
1437
+ got1++;
1438
+ } else if (channel === channels[2]) {
1439
+ expect6(count).toBe(2);
1440
+ expect6(lastModified).toBe(second.lastModified);
1441
+ got2++;
1442
+ }
1443
+ }
1444
+ expect6(got1).toBe(1);
1445
+ expect6(got2).toBe(1);
1446
+ });
1447
+ });
1448
+ };
1449
+ export {
1450
+ graffitiCRUDTests,
1451
+ graffitiChannelStatsTests,
1452
+ graffitiDiscoverTests,
1453
+ graffitiLocationTests,
1454
+ graffitiOrphanTests,
1455
+ graffitiSynchronizeTests
1456
+ };
2
1457
  //# sourceMappingURL=tests.mjs.map