@graffiti-garden/api 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ "use strict";var e=require("vitest");class t extends Error{constructor(e){super(e),this.name="GraffitiErrorForbidden",Object.setPrototypeOf(this,t.prototype)}}class a extends Error{constructor(e){super(e),this.name="GraffitiErrorNotFound",Object.setPrototypeOf(this,a.prototype)}}class o extends Error{constructor(e){super(e),this.name="GraffitiErrorInvalidSchema",Object.setPrototypeOf(this,o.prototype)}}class n extends Error{constructor(e){super(e),this.name="GraffitiErrorSchemaMismatch",Object.setPrototypeOf(this,n.prototype)}}class c extends Error{constructor(e){super(e),this.name="GraffitiErrorPatchTestFailed",Object.setPrototypeOf(this,c.prototype)}}class l extends Error{constructor(e){super(e),this.name="GraffitiErrorPatchError",Object.setPrototypeOf(this,l.prototype)}}class s extends Error{constructor(e){super(e),this.name="GraffitiErrorInvalidUri",Object.setPrototypeOf(this,s.prototype)}}function r(){const e=new Uint8Array(16);return crypto.getRandomValues(e),Array.from(e).map((e=>e.toString(16).padStart(2,"0"))).join("")}function i(){return{value:{[r()]:r()},channels:[r(),r()]}}async function p(t){const a=await t.next();return e.assert(!a.done&&!a.value.error,"result has no value"),a.value.value}exports.graffitiCRUDTests=(s,p,u)=>{e.describe("CRUD",(()=>{e.it("put, get, delete",(async()=>{const t=s(),o=p(),n={something:"hello, world~ c:"},c=[r(),r()],l=await t.put({value:n,channels:c},o);e.expect(l.value).toEqual({}),e.expect(l.channels).toEqual([]),e.expect(l.allowed).toBeUndefined(),e.expect(l.actor).toEqual(o.actor);const i=await t.get(l,{});e.expect(i.value).toEqual(n),e.expect(i.channels).toEqual([]),e.expect(i.allowed).toBeUndefined(),e.expect(i.name).toEqual(l.name),e.expect(i.actor).toEqual(l.actor),e.expect(i.source).toEqual(l.source),e.expect(i.lastModified).toEqual(l.lastModified);const u={something:"goodbye, world~ :c"},d=await t.put({...l,value:u,channels:[]},o);e.expect(d.value).toEqual(n),e.expect(d.tombstone).toEqual(!0),e.expect(d.name).toEqual(l.name),e.expect(d.actor).toEqual(l.actor),e.expect(d.source).toEqual(l.source),e.expect(d.lastModified).toBeGreaterThan(i.lastModified);const v=await t.get(l,{});e.expect(v.value).toEqual(u),e.expect(v.lastModified).toEqual(d.lastModified),e.expect(v.tombstone).toEqual(!1);const h=await t.delete(v,o);e.expect(h.tombstone).toEqual(!0),e.expect(h.value).toEqual(u),e.expect(h.lastModified).toBeGreaterThan(d.lastModified),await e.expect(t.get(v,{})).rejects.toThrow(a)})),e.it("put, get, delete with wrong actor",(async()=>{const a=s(),o=p(),n=u();await e.expect(a.put({value:{},channels:[],actor:n.actor},o)).rejects.toThrow(t),await e.expect(a.delete({name:"asdf",source:"asdf",actor:n.actor},o)).rejects.toThrow(t),await e.expect(a.patch({},{name:"asdf",source:"asdf",actor:n.actor},o)).rejects.toThrow(t)})),e.it("put and get with schema",(async()=>{const t=s(),a=p(),o={something:"hello",another:42},n=await t.put({value:o,channels:[]},a),c=await t.get(n,{properties:{value:{properties:{something:{type:"string"},another:{type:"integer"}}}}});e.expect(c.value.something).toEqual(o.something),e.expect(c.value.another).toEqual(o.another)})),e.it("put and get with invalid schema",(async()=>{const t=s(),a=p(),n=await t.put({value:{},channels:[]},a);await e.expect(t.get(n,{properties:{value:{type:"asdf"}}})).rejects.toThrow(o)})),e.it("put and get with wrong schema",(async()=>{const t=s(),a=p(),o=await t.put({value:{hello:"world"},channels:[]},a);await e.expect(t.get(o,{properties:{value:{properties:{hello:{type:"number"}}}}})).rejects.toThrow(n)})),e.it("put and get with empty access control",(async()=>{const t=s(),a=p(),o=u(),n={um:"hi"},c=[r()],l=[r()],i=await t.put({value:n,allowed:c,channels:l},a),d=await t.get(i,{},a);e.expect(d.value).toEqual(n),e.expect(d.allowed).toEqual(c),e.expect(d.channels).toEqual(l),await e.expect(t.get(i,{})).rejects.toThrow(),await e.expect(t.get(i,{},o)).rejects.toThrow()})),e.it("put and get with specific access control",(async()=>{const t=s(),a=p(),o=u(),n={um:"hi"},c=[r(),o.actor,r()],l=[r()],i=await t.put({value:n,allowed:c,channels:l},a),d=await t.get(i,{},a);e.expect(d.value).toEqual(n),e.expect(d.allowed).toEqual(c),e.expect(d.channels).toEqual(l),await e.expect(t.get(i,{})).rejects.toThrow();const v=await t.get(i,{},o);e.expect(v.value).toEqual(n),e.expect(v.allowed).toEqual([o.actor]),e.expect(v.channels).toEqual([])})),e.it("patch value",(async()=>{const t=s(),a=p(),o={something:"hello, world~ c:"},n=await t.put({value:o,channels:[]},a),c=await t.patch({value:[{op:"replace",path:"/something",value:"goodbye, world~ :c"}]},n,a);e.expect(c.value).toEqual(o),e.expect(c.tombstone).toBe(!0);const l=await t.get(n,{});e.expect(l.value).toEqual({something:"goodbye, world~ :c"}),e.expect(c.lastModified).toBe(l.lastModified),await t.delete(n,a)})),e.it("deep patch",(async()=>{const t=s(),a=p(),o={something:{another:{somethingElse:"hello"}}},n=await t.put({value:o,channels:[]},a),c=await t.patch({value:[{op:"replace",path:"/something/another/somethingElse",value:"goodbye"}]},n,a),l=await t.get(n,{});e.expect(c.value).toEqual(o),e.expect(l.value).toEqual({something:{another:{somethingElse:"goodbye"}}})})),e.it("patch channels",(async()=>{const t=s(),a=p(),o=[r()],n=[r()],c=await t.put({value:{},channels:o},a),l={channels:[{op:"replace",path:"/0",value:n[0]}]},i=await t.patch(l,c,a);e.expect(i.channels).toEqual(o);const u=await t.get(c,{},a);e.expect(u.channels).toEqual(n),await t.delete(c,a)})),e.it("patch 'increment' with test",(async()=>{const t=s(),a=p(),o=await t.put({value:{counter:1},channels:[]},a),n=await t.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:2}]},o,a);e.expect(n.value).toEqual({counter:1});const l=await t.get(n,{properties:{value:{properties:{counter:{type:"integer"}}}}});e.expect(l.value.counter).toEqual(2),await e.expect(t.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:3}]},o,a)).rejects.toThrow(c)})),e.it("invalid patch",(async()=>{const t=s(),a=p(),o=i(),n=await t.put(o,a);await e.expect(t.patch({value:[{op:"add",path:"/root",value:[]},{op:"add",path:"/root/2",value:2}]},n,a)).rejects.toThrow(l)})),e.it("patch channels to be wrong",(async()=>{const t=s(),a=p(),o=i();o.allowed=[r()];const n=await t.put(o,a),c=[{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(const o of c)await e.expect(t.patch(o,n,a)).rejects.toThrow(l);const u=await t.get(n,{},a);e.expect(u.value).toEqual(o.value),e.expect(u.channels).toEqual(o.channels),e.expect(u.allowed).toEqual(o.allowed),e.expect(u.lastModified).toEqual(n.lastModified)}))}))},exports.graffitiDiscoverTests=(t,a,o)=>{e.describe("discover",(()=>{e.it("discover nothing",(async()=>{const a=t().discover([],{});e.expect(await a.next()).toHaveProperty("done",!0)})),e.it("discover single",(async()=>{const o=t(),n=a(),c=i(),l=await o.put(c,n),s=[r(),c.channels[0]],u=o.discover(s,{}),d=await p(u);e.expect(d.value).toEqual(c.value),e.expect(d.channels).toEqual([c.channels[0]]),e.expect(d.allowed).toBeUndefined(),e.expect(d.actor).toEqual(n.actor),e.expect(d.tombstone).toBe(!1),e.expect(d.lastModified).toEqual(l.lastModified);const v=await u.next();e.expect(v.done).toBe(!0)})),e.it("discover wrong channel",(async()=>{const o=t(),n=a(),c=i();await o.put(c,n);const l=o.discover([r()],{});await e.expect(l.next()).resolves.toHaveProperty("done",!0)})),e.it("discover not allowed",(async()=>{const n=t(),c=a(),l=o(),s=i();s.allowed=[r(),r()];const u=await n.put(s,c),d=n.discover(s.channels,{},c),v=await p(d);e.expect(v.value).toEqual(s.value),e.expect(v.channels).toEqual(s.channels),e.expect(v.allowed).toEqual(s.allowed),e.expect(v.actor).toEqual(c.actor),e.expect(v.tombstone).toBe(!1),e.expect(v.lastModified).toEqual(u.lastModified);const h=n.discover(s.channels,{},l);e.expect(await h.next()).toHaveProperty("done",!0);const x=n.discover(s.channels,{});e.expect(await x.next()).toHaveProperty("done",!0)})),e.it("discover allowed",(async()=>{const n=t(),c=a(),l=o(),s=i();s.allowed=[r(),l.actor,r()];const u=await n.put(s,c),d=n.discover(s.channels,{},l),v=await p(d);e.expect(v.value).toEqual(s.value),e.expect(v.allowed).toEqual([l.actor]),e.expect(v.channels).toEqual(s.channels),e.expect(v.actor).toEqual(c.actor),e.expect(v.tombstone).toBe(!1),e.expect(v.lastModified).toEqual(u.lastModified)}));for(const n of["name","actor","lastModified"])e.it(`discover for ${n}`,(async()=>{const c=t(),l=a(),s=o(),r=i(),u=await c.put(r,l),d=i();d.channels=r.channels,await new Promise((e=>setTimeout(e,20)));const v=await c.put(d,s),h=c.discover(r.channels,{properties:{[n]:{enum:[u[n]]}}}),x=await p(h);e.expect(x.name).toEqual(u.name),e.expect(x.name).not.toEqual(v.name),e.expect(x.value).toEqual(r.value),await e.expect(h.next()).resolves.toHaveProperty("done",!0)}));e.it("discover with lastModified range",(async()=>{const o=t(),n=a(),c=i(),l=await o.put(c,n);await new Promise((e=>setTimeout(e,20)));const s=await o.put(c,n);e.expect(l.name).not.toEqual(s.name),e.expect(l.lastModified).toBeLessThan(s.lastModified);const r=o.discover([c.channels[0]],{properties:{lastModified:{minimum:s.lastModified,exclusiveMinimum:!0}}});e.expect(await r.next()).toHaveProperty("done",!0);const u=o.discover([c.channels[0]],{properties:{lastModified:{minimum:s.lastModified-.1,exclusiveMinimum:!0}}}),d=await p(u);e.expect(d.name).toEqual(s.name),e.expect(await u.next()).toHaveProperty("done",!0);const v=o.discover(c.channels,{properties:{value:{},lastModified:{minimum:s.lastModified}}}),h=await p(v);e.expect(h.name).toEqual(s.name),e.expect(await v.next()).toHaveProperty("done",!0);const x=o.discover(c.channels,{properties:{lastModified:{minimum:s.lastModified+.1}}});e.expect(await x.next()).toHaveProperty("done",!0);const w=o.discover(c.channels,{properties:{lastModified:{maximum:l.lastModified,exclusiveMaximum:!0}}});e.expect(await w.next()).toHaveProperty("done",!0);const m=o.discover(c.channels,{properties:{lastModified:{maximum:l.lastModified+.1,exclusiveMaximum:!0}}}),E=await p(m);e.expect(E.name).toEqual(l.name),e.expect(await m.next()).toHaveProperty("done",!0);const f=o.discover(c.channels,{properties:{lastModified:{maximum:l.lastModified}}}),y=await p(f);e.expect(y.name).toEqual(l.name),e.expect(await f.next()).toHaveProperty("done",!0);const q=o.discover(c.channels,{properties:{lastModified:{maximum:l.lastModified-.1}}});e.expect(await q.next()).toHaveProperty("done",!0)})),e.it("discover schema allowed, as and not as owner",(async()=>{const n=t(),c=a(),l=o(),s=i();s.allowed=[r(),l.actor,r()],await n.put(s,c);const u=n.discover(s.channels,{properties:{allowed:{minItems:3,not:{items:{not:{enum:[l.actor]}}}}}},c),d=await p(u);e.expect(d.value).toEqual(s.value),await e.expect(u.next()).resolves.toHaveProperty("done",!0);const v=n.discover(s.channels,{properties:{allowed:{minItems:3}}},l);await e.expect(v.next()).resolves.toHaveProperty("done",!0);const h=n.discover(s.channels,{properties:{allowed:{not:{items:{not:{enum:[s.channels[0]]}}}}}},l);await e.expect(h.next()).resolves.toHaveProperty("done",!0);const x=n.discover(s.channels,{properties:{allowed:{maxItems:1,not:{items:{not:{enum:[l.actor]}}}}}},l),w=await p(x);e.expect(w.value).toEqual(s.value),await e.expect(x.next()).resolves.toHaveProperty("done",!0)})),e.it("discover schema channels, as and not as owner",(async()=>{const n=t(),c=a(),l=o(),s=i();s.channels=[r(),r(),r()],await n.put(s,c);const u=n.discover([s.channels[0],s.channels[2]],{properties:{channels:{minItems:3,not:{items:{not:{enum:[s.channels[1]]}}}}}},c),d=await p(u);e.expect(d.value).toEqual(s.value),await e.expect(u.next()).resolves.toHaveProperty("done",!0);const v=n.discover([s.channels[0],s.channels[2]],{properties:{channels:{minItems:3}}},l);await e.expect(v.next()).resolves.toHaveProperty("done",!0);const h=n.discover([s.channels[0],s.channels[2]],{properties:{channels:{not:{items:{not:{enum:[s.channels[1]]}}}}}},l);await e.expect(h.next()).resolves.toHaveProperty("done",!0);const x=n.discover([s.channels[0],s.channels[2]],{properties:{allowed:{maxItems:2,not:{items:{not:{enum:[s.channels[2]]}}}}}},l),w=await p(x);e.expect(w.value).toEqual(s.value),await e.expect(x.next()).resolves.toHaveProperty("done",!0)})),e.it("discover query for empty allowed",(async()=>{const o=t(),n=a(),c=i(),l={not:{required:["allowed"]}};await o.put(c,n);const s=o.discover(c.channels,l,n),r=await p(s);e.expect(r.value).toEqual(c.value),e.expect(r.allowed).toBeUndefined(),await e.expect(s.next()).resolves.toHaveProperty("done",!0);const u=i();u.allowed=[],await o.put(u,n);const d=o.discover(u.channels,l,n);await e.expect(d.next()).resolves.toHaveProperty("done",!0)})),e.it("discover query for values",(async()=>{const o=t(),n=a(),c=i();c.value={test:r()},await o.put(c,n);const l=i();l.channels=c.channels,l.value={test:r(),something:r()},await o.put(l,n);const s=i();s.channels=c.channels,s.value={other:r(),something:r()},await o.put(s,n);const p=new Map;for(const t of["test","something","other"]){let a=0;for await(const n of o.discover(c.channels,{properties:{value:{required:[t]}}}))e.assert(!n.error,"result has error"),t in n.value.value&&a++;p.set(t,a)}e.expect(p.get("test")).toBe(2),e.expect(p.get("something")).toBe(2),e.expect(p.get("other")).toBe(1)})),e.it("discover for deleted content",(async()=>{const o=t(),n=a(),c=i(),l=await o.put(c,n),s=await o.delete(l,n),r=o.discover(c.channels,{}),u=await p(r);e.expect(u.tombstone).toBe(!0),e.expect(u.value).toEqual(c.value),e.expect(u.channels).toEqual(c.channels),e.expect(u.actor).toEqual(n.actor),e.expect(u.lastModified).toEqual(s.lastModified),await e.expect(r.next()).resolves.toHaveProperty("done",!0)})),e.it("discover for replaced channels",(async()=>{for(let o=0;o<10;o++){const o=t(),n=a(),c=i(),l=await o.put(c,n),s=i(),r=await o.put({...l,...s},n),u=o.discover(c.channels,{}),d=await p(u);await e.expect(u.next()).resolves.toHaveProperty("done",!0);const v=o.discover(s.channels,{}),h=await p(v);await e.expect(v.next()).resolves.toHaveProperty("done",!0),l.lastModified===r.lastModified?(e.expect(d.tombstone||h.tombstone).toBe(!0),e.expect(d.tombstone&&h.tombstone).toBe(!1)):(e.expect(d.tombstone).toBe(!0),e.expect(d.value).toEqual(c.value),e.expect(d.channels).toEqual(c.channels),e.expect(d.lastModified).toEqual(r.lastModified),e.expect(h.tombstone).toBe(!1),e.expect(h.value).toEqual(s.value),e.expect(h.channels).toEqual(s.channels),e.expect(h.lastModified).toEqual(r.lastModified))}})),e.it("discover for patched allowed",(async()=>{const o=t(),n=a(),c=i(),l=await o.put(c,n);await o.patch({allowed:[{op:"add",path:"",value:[]}]},l,n);const s=o.discover(c.channels,{}),r=await p(s);e.expect(r.tombstone).toBe(!0),e.expect(r.value).toEqual(c.value),e.expect(r.channels).toEqual(c.channels),e.expect(r.allowed).toBeUndefined(),await e.expect(s.next()).resolves.toHaveProperty("done",!0)})),e.it("put concurrently and discover one",(async()=>{const o=t(),n=a(),c=i();c.name=r();const l=Array(100).fill(0).map((()=>o.put(c,n)));await Promise.all(l);const s=o.discover(c.channels,{});let p=0,u=0;for await(const t of s)e.assert(!t.error,"result has error"),t.value.tombstone?p++:u++;e.expect(p).toBe(99),e.expect(u).toBe(1)}))}))},exports.graffitiLocationTests=t=>{e.describe("URI and location conversion",(()=>{e.it("location to uri and back",(async()=>{const a=t(),o={name:r(),actor:r(),source:r()},n=a.locationToUri(o),c=a.uriToLocation(n);e.expect(o).toEqual(c)})),e.it("collision resistance",(async()=>{const a=t(),o={name:r(),actor:r(),source:r()};for(const t of["name","actor","source"]){const n={...o,[t]:r()},c=a.locationToUri(o),l=a.locationToUri(n);e.expect(c).not.toEqual(l)}})),e.it("random URI should not be a valid location",(async()=>{const a=t();e.expect((()=>a.uriToLocation(""))).toThrow(s)}))}))},exports.graffitiSynchronizeTests=(t,a,o)=>{e.describe("synchronize",(()=>{e.it("get",(async()=>{const o=t(),n=a(),c=i(),l=c.channels.slice(1),s=await o.put(c,n),r=t(),p=r.synchronize(l,{}).next(),u=await r.get(s,{},n),d=(await p).value;if(!d||d.error)throw new Error("Error in synchronize");e.expect(d.value.value).toEqual(c.value),e.expect(d.value.channels).toEqual(l),e.expect(d.value.tombstone).toBe(!1),e.expect(d.value.lastModified).toEqual(u.lastModified)})),e.it("put",(async()=>{const o=t(),n=a(),c=r(),l=r(),s=r(),i={hello:"world"},p=[c,s],u=await o.put({value:i,channels:p},n),d=o.synchronize([c],{}).next(),v=o.synchronize([l],{}).next(),h=o.synchronize([s],{}).next(),x={goodbye:"world"},w=[l,s];await o.put({...u,value:x,channels:w},n);const m=(await d).value,E=(await v).value,f=(await h).value;if(!m||m.error||!E||E.error||!f||f.error)throw new Error("Error in synchronize");e.expect(m.value.value).toEqual(i),e.expect(m.value.channels).toEqual([c]),e.expect(m.value.tombstone).toBe(!0),e.expect(E.value.value).toEqual(x),e.expect(E.value.channels).toEqual([l]),e.expect(E.value.tombstone).toBe(!1),e.expect(f.value.value).toEqual(x),e.expect(f.value.channels).toEqual([s]),e.expect(f.value.tombstone).toBe(!1),e.expect(m.value.lastModified).toEqual(E.value.lastModified),e.expect(f.value.lastModified).toEqual(E.value.lastModified)})),e.it("patch",(async()=>{const o=t(),n=a(),c=r(),l=r(),s=r(),i={hello:"world"},p=[c,s],u=await o.put({value:i,channels:p},n),d=o.synchronize([c],{}).next(),v=o.synchronize([l],{}).next(),h=o.synchronize([s],{}).next();await o.patch({value:[{op:"add",path:"/something",value:"new value"}],channels:[{op:"add",path:"/-",value:l},{op:"remove",path:`/${p.indexOf(c)}`}]},u,n);const x=(await d).value,w=(await v).value,m=(await h).value;if(!x||x.error||!w||w.error||!m||m.error)throw new Error("Error in synchronize");const E={...i,something:"new value"};e.expect(x.value.value).toEqual(i),e.expect(x.value.channels).toEqual([c]),e.expect(x.value.tombstone).toBe(!0),e.expect(w.value.value).toEqual(E),e.expect(w.value.channels).toEqual([l]),e.expect(w.value.tombstone).toBe(!1),e.expect(m.value.value).toEqual(E),e.expect(m.value.channels).toEqual([s]),e.expect(m.value.tombstone).toBe(!1),e.expect(x.value.lastModified).toEqual(w.value.lastModified),e.expect(m.value.lastModified).toEqual(w.value.lastModified)})),e.it("delete",(async()=>{const o=t(),n=a(),c=[r(),r(),r()],l={hello:"world"},s=[r(),...c.slice(1)],i=await o.put({value:l,channels:s},n),p=o.synchronize(c,{}).next();o.delete(i,n);const u=(await p).value;if(!u||u.error)throw new Error("Error in synchronize");e.expect(u.value.tombstone).toBe(!0),e.expect(u.value.value).toEqual(l),e.expect(u.value.channels).toEqual(c.filter((e=>s.includes(e))))})),e.it("not allowed",(async()=>{const n=t(),c=a(),l=o(),s=[r(),r(),r()],i=s.slice(1),p=n.synchronize(i,{},c).next(),u=n.synchronize(i,{},l).next(),d=n.synchronize(i,{}).next(),v={hello:"world"},h=[r(),l.actor];await n.put({value:v,channels:s,allowed:h},c),await e.expect(Promise.race([d,new Promise(((e,t)=>setTimeout(t,100,"Timeout")))])).rejects.toThrow("Timeout");const x=(await p).value,w=(await u).value;if(!x||x.error||!w||w.error)throw new Error("Error in synchronize");e.expect(x.value.value).toEqual(v),e.expect(x.value.allowed).toEqual(h),e.expect(x.value.channels).toEqual(s),e.expect(w.value.value).toEqual(v),e.expect(w.value.allowed).toEqual([l.actor]),e.expect(w.value.channels).toEqual(i)}))}))};
2
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ import{assert as e,describe as a,it as t,expect as o}from"vitest";class n extends Error{constructor(e){super(e),this.name="GraffitiErrorForbidden",Object.setPrototypeOf(this,n.prototype)}}class l extends Error{constructor(e){super(e),this.name="GraffitiErrorNotFound",Object.setPrototypeOf(this,l.prototype)}}class s extends Error{constructor(e){super(e),this.name="GraffitiErrorInvalidSchema",Object.setPrototypeOf(this,s.prototype)}}class r extends Error{constructor(e){super(e),this.name="GraffitiErrorSchemaMismatch",Object.setPrototypeOf(this,r.prototype)}}class i extends Error{constructor(e){super(e),this.name="GraffitiErrorPatchTestFailed",Object.setPrototypeOf(this,i.prototype)}}class c extends Error{constructor(e){super(e),this.name="GraffitiErrorPatchError",Object.setPrototypeOf(this,c.prototype)}}class u extends Error{constructor(e){super(e),this.name="GraffitiErrorInvalidUri",Object.setPrototypeOf(this,u.prototype)}}function d(){const e=new Uint8Array(16);return crypto.getRandomValues(e),Array.from(e).map((e=>e.toString(16).padStart(2,"0"))).join("")}function v(){return{value:{[d()]:d()},channels:[d(),d()]}}async function h(a){const t=await a.next();return e(!t.done&&!t.value.error,"result has no value"),t.value.value}const p=e=>{a("URI and location conversion",(()=>{t("location to uri and back",(async()=>{const a=e(),t={name:d(),actor:d(),source:d()},n=a.locationToUri(t),l=a.uriToLocation(n);o(t).toEqual(l)})),t("collision resistance",(async()=>{const a=e(),t={name:d(),actor:d(),source:d()};for(const e of["name","actor","source"]){const n={...t,[e]:d()},l=a.locationToUri(t),s=a.locationToUri(n);o(l).not.toEqual(s)}})),t("random URI should not be a valid location",(async()=>{const a=e();o((()=>a.uriToLocation(""))).toThrow(u)}))}))},w=(e,u,h)=>{a("CRUD",(()=>{t("put, get, delete",(async()=>{const a=e(),t=u(),n={something:"hello, world~ c:"},s=[d(),d()],r=await a.put({value:n,channels:s},t);o(r.value).toEqual({}),o(r.channels).toEqual([]),o(r.allowed).toBeUndefined(),o(r.actor).toEqual(t.actor);const i=await a.get(r,{});o(i.value).toEqual(n),o(i.channels).toEqual([]),o(i.allowed).toBeUndefined(),o(i.name).toEqual(r.name),o(i.actor).toEqual(r.actor),o(i.source).toEqual(r.source),o(i.lastModified).toEqual(r.lastModified);const c={something:"goodbye, world~ :c"},v=await a.put({...r,value:c,channels:[]},t);o(v.value).toEqual(n),o(v.tombstone).toEqual(!0),o(v.name).toEqual(r.name),o(v.actor).toEqual(r.actor),o(v.source).toEqual(r.source),o(v.lastModified).toBeGreaterThan(i.lastModified);const h=await a.get(r,{});o(h.value).toEqual(c),o(h.lastModified).toEqual(v.lastModified),o(h.tombstone).toEqual(!1);const p=await a.delete(h,t);o(p.tombstone).toEqual(!0),o(p.value).toEqual(c),o(p.lastModified).toBeGreaterThan(v.lastModified),await o(a.get(h,{})).rejects.toThrow(l)})),t("put, get, delete with wrong actor",(async()=>{const a=e(),t=u(),l=h();await o(a.put({value:{},channels:[],actor:l.actor},t)).rejects.toThrow(n),await o(a.delete({name:"asdf",source:"asdf",actor:l.actor},t)).rejects.toThrow(n),await o(a.patch({},{name:"asdf",source:"asdf",actor:l.actor},t)).rejects.toThrow(n)})),t("put and get with schema",(async()=>{const a=e(),t=u(),n={something:"hello",another:42},l=await a.put({value:n,channels:[]},t),s=await a.get(l,{properties:{value:{properties:{something:{type:"string"},another:{type:"integer"}}}}});o(s.value.something).toEqual(n.something),o(s.value.another).toEqual(n.another)})),t("put and get with invalid schema",(async()=>{const a=e(),t=u(),n=await a.put({value:{},channels:[]},t);await o(a.get(n,{properties:{value:{type:"asdf"}}})).rejects.toThrow(s)})),t("put and get with wrong schema",(async()=>{const a=e(),t=u(),n=await a.put({value:{hello:"world"},channels:[]},t);await o(a.get(n,{properties:{value:{properties:{hello:{type:"number"}}}}})).rejects.toThrow(r)})),t("put and get with empty access control",(async()=>{const a=e(),t=u(),n=h(),l={um:"hi"},s=[d()],r=[d()],i=await a.put({value:l,allowed:s,channels:r},t),c=await a.get(i,{},t);o(c.value).toEqual(l),o(c.allowed).toEqual(s),o(c.channels).toEqual(r),await o(a.get(i,{})).rejects.toThrow(),await o(a.get(i,{},n)).rejects.toThrow()})),t("put and get with specific access control",(async()=>{const a=e(),t=u(),n=h(),l={um:"hi"},s=[d(),n.actor,d()],r=[d()],i=await a.put({value:l,allowed:s,channels:r},t),c=await a.get(i,{},t);o(c.value).toEqual(l),o(c.allowed).toEqual(s),o(c.channels).toEqual(r),await o(a.get(i,{})).rejects.toThrow();const v=await a.get(i,{},n);o(v.value).toEqual(l),o(v.allowed).toEqual([n.actor]),o(v.channels).toEqual([])})),t("patch value",(async()=>{const a=e(),t=u(),n={something:"hello, world~ c:"},l=await a.put({value:n,channels:[]},t),s=await a.patch({value:[{op:"replace",path:"/something",value:"goodbye, world~ :c"}]},l,t);o(s.value).toEqual(n),o(s.tombstone).toBe(!0);const r=await a.get(l,{});o(r.value).toEqual({something:"goodbye, world~ :c"}),o(s.lastModified).toBe(r.lastModified),await a.delete(l,t)})),t("deep patch",(async()=>{const a=e(),t=u(),n={something:{another:{somethingElse:"hello"}}},l=await a.put({value:n,channels:[]},t),s=await a.patch({value:[{op:"replace",path:"/something/another/somethingElse",value:"goodbye"}]},l,t),r=await a.get(l,{});o(s.value).toEqual(n),o(r.value).toEqual({something:{another:{somethingElse:"goodbye"}}})})),t("patch channels",(async()=>{const a=e(),t=u(),n=[d()],l=[d()],s=await a.put({value:{},channels:n},t),r={channels:[{op:"replace",path:"/0",value:l[0]}]},i=await a.patch(r,s,t);o(i.channels).toEqual(n);const c=await a.get(s,{},t);o(c.channels).toEqual(l),await a.delete(s,t)})),t("patch 'increment' with test",(async()=>{const a=e(),t=u(),n=await a.put({value:{counter:1},channels:[]},t),l=await a.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:2}]},n,t);o(l.value).toEqual({counter:1});const s=await a.get(l,{properties:{value:{properties:{counter:{type:"integer"}}}}});o(s.value.counter).toEqual(2),await o(a.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:3}]},n,t)).rejects.toThrow(i)})),t("invalid patch",(async()=>{const a=e(),t=u(),n=v(),l=await a.put(n,t);await o(a.patch({value:[{op:"add",path:"/root",value:[]},{op:"add",path:"/root/2",value:2}]},l,t)).rejects.toThrow(c)})),t("patch channels to be wrong",(async()=>{const a=e(),t=u(),n=v();n.allowed=[d()];const l=await a.put(n,t),s=[{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(const e of s)await o(a.patch(e,l,t)).rejects.toThrow(c);const r=await a.get(l,{},t);o(r.value).toEqual(n.value),o(r.channels).toEqual(n.channels),o(r.allowed).toEqual(n.allowed),o(r.lastModified).toEqual(l.lastModified)}))}))},m=(e,n,l)=>{a("synchronize",(()=>{t("get",(async()=>{const a=e(),t=n(),l=v(),s=l.channels.slice(1),r=await a.put(l,t),i=e(),c=i.synchronize(s,{}).next(),u=await i.get(r,{},t),d=(await c).value;if(!d||d.error)throw new Error("Error in synchronize");o(d.value.value).toEqual(l.value),o(d.value.channels).toEqual(s),o(d.value.tombstone).toBe(!1),o(d.value.lastModified).toEqual(u.lastModified)})),t("put",(async()=>{const a=e(),t=n(),l=d(),s=d(),r=d(),i={hello:"world"},c=[l,r],u=await a.put({value:i,channels:c},t),v=a.synchronize([l],{}).next(),h=a.synchronize([s],{}).next(),p=a.synchronize([r],{}).next(),w={goodbye:"world"},m=[s,r];await a.put({...u,value:w,channels:m},t);const E=(await v).value,f=(await h).value,y=(await p).value;if(!E||E.error||!f||f.error||!y||y.error)throw new Error("Error in synchronize");o(E.value.value).toEqual(i),o(E.value.channels).toEqual([l]),o(E.value.tombstone).toBe(!0),o(f.value.value).toEqual(w),o(f.value.channels).toEqual([s]),o(f.value.tombstone).toBe(!1),o(y.value.value).toEqual(w),o(y.value.channels).toEqual([r]),o(y.value.tombstone).toBe(!1),o(E.value.lastModified).toEqual(f.value.lastModified),o(y.value.lastModified).toEqual(f.value.lastModified)})),t("patch",(async()=>{const a=e(),t=n(),l=d(),s=d(),r=d(),i={hello:"world"},c=[l,r],u=await a.put({value:i,channels:c},t),v=a.synchronize([l],{}).next(),h=a.synchronize([s],{}).next(),p=a.synchronize([r],{}).next();await a.patch({value:[{op:"add",path:"/something",value:"new value"}],channels:[{op:"add",path:"/-",value:s},{op:"remove",path:`/${c.indexOf(l)}`}]},u,t);const w=(await v).value,m=(await h).value,E=(await p).value;if(!w||w.error||!m||m.error||!E||E.error)throw new Error("Error in synchronize");const f={...i,something:"new value"};o(w.value.value).toEqual(i),o(w.value.channels).toEqual([l]),o(w.value.tombstone).toBe(!0),o(m.value.value).toEqual(f),o(m.value.channels).toEqual([s]),o(m.value.tombstone).toBe(!1),o(E.value.value).toEqual(f),o(E.value.channels).toEqual([r]),o(E.value.tombstone).toBe(!1),o(w.value.lastModified).toEqual(m.value.lastModified),o(E.value.lastModified).toEqual(m.value.lastModified)})),t("delete",(async()=>{const a=e(),t=n(),l=[d(),d(),d()],s={hello:"world"},r=[d(),...l.slice(1)],i=await a.put({value:s,channels:r},t),c=a.synchronize(l,{}).next();a.delete(i,t);const u=(await c).value;if(!u||u.error)throw new Error("Error in synchronize");o(u.value.tombstone).toBe(!0),o(u.value.value).toEqual(s),o(u.value.channels).toEqual(l.filter((e=>r.includes(e))))})),t("not allowed",(async()=>{const a=e(),t=n(),s=l(),r=[d(),d(),d()],i=r.slice(1),c=a.synchronize(i,{},t).next(),u=a.synchronize(i,{},s).next(),v=a.synchronize(i,{}).next(),h={hello:"world"},p=[d(),s.actor];await a.put({value:h,channels:r,allowed:p},t),await o(Promise.race([v,new Promise(((e,a)=>setTimeout(a,100,"Timeout")))])).rejects.toThrow("Timeout");const w=(await c).value,m=(await u).value;if(!w||w.error||!m||m.error)throw new Error("Error in synchronize");o(w.value.value).toEqual(h),o(w.value.allowed).toEqual(p),o(w.value.channels).toEqual(r),o(m.value.value).toEqual(h),o(m.value.allowed).toEqual([s.actor]),o(m.value.channels).toEqual(i)}))}))},E=(n,l,s)=>{a("discover",(()=>{t("discover nothing",(async()=>{const e=n().discover([],{});o(await e.next()).toHaveProperty("done",!0)})),t("discover single",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a),r=[d(),t.channels[0]],i=e.discover(r,{}),c=await h(i);o(c.value).toEqual(t.value),o(c.channels).toEqual([t.channels[0]]),o(c.allowed).toBeUndefined(),o(c.actor).toEqual(a.actor),o(c.tombstone).toBe(!1),o(c.lastModified).toEqual(s.lastModified);const u=await i.next();o(u.done).toBe(!0)})),t("discover wrong channel",(async()=>{const e=n(),a=l(),t=v();await e.put(t,a);const s=e.discover([d()],{});await o(s.next()).resolves.toHaveProperty("done",!0)})),t("discover not allowed",(async()=>{const e=n(),a=l(),t=s(),r=v();r.allowed=[d(),d()];const i=await e.put(r,a),c=e.discover(r.channels,{},a),u=await h(c);o(u.value).toEqual(r.value),o(u.channels).toEqual(r.channels),o(u.allowed).toEqual(r.allowed),o(u.actor).toEqual(a.actor),o(u.tombstone).toBe(!1),o(u.lastModified).toEqual(i.lastModified);const p=e.discover(r.channels,{},t);o(await p.next()).toHaveProperty("done",!0);const w=e.discover(r.channels,{});o(await w.next()).toHaveProperty("done",!0)})),t("discover allowed",(async()=>{const e=n(),a=l(),t=s(),r=v();r.allowed=[d(),t.actor,d()];const i=await e.put(r,a),c=e.discover(r.channels,{},t),u=await h(c);o(u.value).toEqual(r.value),o(u.allowed).toEqual([t.actor]),o(u.channels).toEqual(r.channels),o(u.actor).toEqual(a.actor),o(u.tombstone).toBe(!1),o(u.lastModified).toEqual(i.lastModified)}));for(const e of["name","actor","lastModified"])t(`discover for ${e}`,(async()=>{const a=n(),t=l(),r=s(),i=v(),c=await a.put(i,t),u=v();u.channels=i.channels,await new Promise((e=>setTimeout(e,20)));const d=await a.put(u,r),p=a.discover(i.channels,{properties:{[e]:{enum:[c[e]]}}}),w=await h(p);o(w.name).toEqual(c.name),o(w.name).not.toEqual(d.name),o(w.value).toEqual(i.value),await o(p.next()).resolves.toHaveProperty("done",!0)}));t("discover with lastModified range",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a);await new Promise((e=>setTimeout(e,20)));const r=await e.put(t,a);o(s.name).not.toEqual(r.name),o(s.lastModified).toBeLessThan(r.lastModified);const i=e.discover([t.channels[0]],{properties:{lastModified:{minimum:r.lastModified,exclusiveMinimum:!0}}});o(await i.next()).toHaveProperty("done",!0);const c=e.discover([t.channels[0]],{properties:{lastModified:{minimum:r.lastModified-.1,exclusiveMinimum:!0}}}),u=await h(c);o(u.name).toEqual(r.name),o(await c.next()).toHaveProperty("done",!0);const d=e.discover(t.channels,{properties:{value:{},lastModified:{minimum:r.lastModified}}}),p=await h(d);o(p.name).toEqual(r.name),o(await d.next()).toHaveProperty("done",!0);const w=e.discover(t.channels,{properties:{lastModified:{minimum:r.lastModified+.1}}});o(await w.next()).toHaveProperty("done",!0);const m=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified,exclusiveMaximum:!0}}});o(await m.next()).toHaveProperty("done",!0);const E=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified+.1,exclusiveMaximum:!0}}}),f=await h(E);o(f.name).toEqual(s.name),o(await E.next()).toHaveProperty("done",!0);const y=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified}}}),q=await h(y);o(q.name).toEqual(s.name),o(await y.next()).toHaveProperty("done",!0);const g=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified-.1}}});o(await g.next()).toHaveProperty("done",!0)})),t("discover schema allowed, as and not as owner",(async()=>{const e=n(),a=l(),t=s(),r=v();r.allowed=[d(),t.actor,d()],await e.put(r,a);const i=e.discover(r.channels,{properties:{allowed:{minItems:3,not:{items:{not:{enum:[t.actor]}}}}}},a),c=await h(i);o(c.value).toEqual(r.value),await o(i.next()).resolves.toHaveProperty("done",!0);const u=e.discover(r.channels,{properties:{allowed:{minItems:3}}},t);await o(u.next()).resolves.toHaveProperty("done",!0);const p=e.discover(r.channels,{properties:{allowed:{not:{items:{not:{enum:[r.channels[0]]}}}}}},t);await o(p.next()).resolves.toHaveProperty("done",!0);const w=e.discover(r.channels,{properties:{allowed:{maxItems:1,not:{items:{not:{enum:[t.actor]}}}}}},t),m=await h(w);o(m.value).toEqual(r.value),await o(w.next()).resolves.toHaveProperty("done",!0)})),t("discover schema channels, as and not as owner",(async()=>{const e=n(),a=l(),t=s(),r=v();r.channels=[d(),d(),d()],await e.put(r,a);const i=e.discover([r.channels[0],r.channels[2]],{properties:{channels:{minItems:3,not:{items:{not:{enum:[r.channels[1]]}}}}}},a),c=await h(i);o(c.value).toEqual(r.value),await o(i.next()).resolves.toHaveProperty("done",!0);const u=e.discover([r.channels[0],r.channels[2]],{properties:{channels:{minItems:3}}},t);await o(u.next()).resolves.toHaveProperty("done",!0);const p=e.discover([r.channels[0],r.channels[2]],{properties:{channels:{not:{items:{not:{enum:[r.channels[1]]}}}}}},t);await o(p.next()).resolves.toHaveProperty("done",!0);const w=e.discover([r.channels[0],r.channels[2]],{properties:{allowed:{maxItems:2,not:{items:{not:{enum:[r.channels[2]]}}}}}},t),m=await h(w);o(m.value).toEqual(r.value),await o(w.next()).resolves.toHaveProperty("done",!0)})),t("discover query for empty allowed",(async()=>{const e=n(),a=l(),t=v(),s={not:{required:["allowed"]}};await e.put(t,a);const r=e.discover(t.channels,s,a),i=await h(r);o(i.value).toEqual(t.value),o(i.allowed).toBeUndefined(),await o(r.next()).resolves.toHaveProperty("done",!0);const c=v();c.allowed=[],await e.put(c,a);const u=e.discover(c.channels,s,a);await o(u.next()).resolves.toHaveProperty("done",!0)})),t("discover query for values",(async()=>{const a=n(),t=l(),s=v();s.value={test:d()},await a.put(s,t);const r=v();r.channels=s.channels,r.value={test:d(),something:d()},await a.put(r,t);const i=v();i.channels=s.channels,i.value={other:d(),something:d()},await a.put(i,t);const c=new Map;for(const t of["test","something","other"]){let o=0;for await(const n of a.discover(s.channels,{properties:{value:{required:[t]}}}))e(!n.error,"result has error"),t in n.value.value&&o++;c.set(t,o)}o(c.get("test")).toBe(2),o(c.get("something")).toBe(2),o(c.get("other")).toBe(1)})),t("discover for deleted content",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a),r=await e.delete(s,a),i=e.discover(t.channels,{}),c=await h(i);o(c.tombstone).toBe(!0),o(c.value).toEqual(t.value),o(c.channels).toEqual(t.channels),o(c.actor).toEqual(a.actor),o(c.lastModified).toEqual(r.lastModified),await o(i.next()).resolves.toHaveProperty("done",!0)})),t("discover for replaced channels",(async()=>{for(let e=0;e<10;e++){const e=n(),a=l(),t=v(),s=await e.put(t,a),r=v(),i=await e.put({...s,...r},a),c=e.discover(t.channels,{}),u=await h(c);await o(c.next()).resolves.toHaveProperty("done",!0);const d=e.discover(r.channels,{}),p=await h(d);await o(d.next()).resolves.toHaveProperty("done",!0),s.lastModified===i.lastModified?(o(u.tombstone||p.tombstone).toBe(!0),o(u.tombstone&&p.tombstone).toBe(!1)):(o(u.tombstone).toBe(!0),o(u.value).toEqual(t.value),o(u.channels).toEqual(t.channels),o(u.lastModified).toEqual(i.lastModified),o(p.tombstone).toBe(!1),o(p.value).toEqual(r.value),o(p.channels).toEqual(r.channels),o(p.lastModified).toEqual(i.lastModified))}})),t("discover for patched allowed",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a);await e.patch({allowed:[{op:"add",path:"",value:[]}]},s,a);const r=e.discover(t.channels,{}),i=await h(r);o(i.tombstone).toBe(!0),o(i.value).toEqual(t.value),o(i.channels).toEqual(t.channels),o(i.allowed).toBeUndefined(),await o(r.next()).resolves.toHaveProperty("done",!0)})),t("put concurrently and discover one",(async()=>{const a=n(),t=l(),s=v();s.name=d();const r=Array(100).fill(0).map((()=>a.put(s,t)));await Promise.all(r);const i=a.discover(s.channels,{});let c=0,u=0;for await(const a of i)e(!a.error,"result has error"),a.value.tombstone?c++:u++;o(c).toBe(99),o(u).toBe(1)}))}))};export{w as graffitiCRUDTests,E as graffitiDiscoverTests,p as graffitiLocationTests,m as graffitiSynchronizeTests};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,638 @@
1
+ import type { GraffitiLocation, GraffitiObject, GraffitiObjectBase, GraffitiPatch, GraffitiSession, GraffitiPutObject, GraffitiStream } from "./2-types";
2
+ import type { JSONSchema4 } from "json-schema";
3
+ /**
4
+ * This API describes a small but powerful set of methods that
5
+ * can be used to create many different kinds of social media applications,
6
+ * all of which can interoperate.
7
+ * These methods should satisfy all of an application's needs for
8
+ * the communication, storage, and access management of social data.
9
+ * The rest of the application can be built with standard client-side
10
+ * user interface tools to present and interact with the data —
11
+ * no server code necessary.
12
+ * The Typescript source for this API is available at
13
+ * [graffiti-garden/api](https://github.com/graffiti-garden/api).
14
+ *
15
+ * There are several different implementations of this Graffiti API available,
16
+ * including a [decentralized implementation](https://github.com/graffiti-garden/client-core),
17
+ * and a [local implementation](https://github.com/graffiti-garden/implementation-pouchdb)
18
+ * that can be used for testing. In our design of Graffiti, this API is our
19
+ * primary focus as it is the layer that shapes the experience
20
+ * of developing applications. While different implementations can provide tradeoffs between
21
+ * other important properties (e.g. privacy, security, scalability), those properties
22
+ * are useless if the system as a whole doesn't expose useful functionality to developers.
23
+ *
24
+ * On the other side of the stack, there is [Vue plugin](https://github.com/graffiti-garden/wrapper-vue/)
25
+ * that wraps around this API to provide reactivity. Other high-level libraries
26
+ * will be available in the future.
27
+ *
28
+ * ## Overview
29
+ *
30
+ * This API tries to draw from well-known concepts and standards wherever possible.
31
+ * JSON objects, representing social artifacts (e.g. posts, profiles) and activities
32
+ * (e.g. likes, follows) can be interacted with through standard CRUD operations:
33
+ * {@link put}, {@link get}, {@link patch}, and {@link delete}.
34
+ * Objects can be typed with [JSON Schema](https://json-schema.org/) and patches
35
+ * can be applied with [JSON Patch](https://jsonpatch.com).
36
+ * For interoperability between Graffiti applications, we recommend using established properties from the
37
+ * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) when available.
38
+ *
39
+ * The social aspect of Graffiti comes from the {@link discover} operation
40
+ * which allows applications to find objects that other users made.
41
+ * It is a lot like a traditional query operation, but it only
42
+ * returns objects that have been placed in particular
43
+ * {@link GraffitiObjectBase.channels | `channels`}
44
+ * specified by the discovering application.
45
+ *
46
+ * {@link GraffitiObjectBase.channels | `channels`} are one of the major concepts
47
+ * unique to Graffiti along with *interaction relativity*.
48
+ * Channels create boundaries between public spaces and work to prevent
49
+ * [context collapse](https://en.wikipedia.org/wiki/Context_collapse)
50
+ * even in a highly interoperable environment.
51
+ * Interaction relativity means that all interactions between users are
52
+ * actually atomic single-user operations that can be interpreted in different ways,
53
+ * which also supports interoperability and pluralism.
54
+ *
55
+ * ### Channels
56
+ *
57
+ * {@link GraffitiObjectBase.channels | `channels`}
58
+ * are a way for the creators of social data to express the intended audience of their
59
+ * data. When a user creates data using the {@link put} method, they
60
+ * can place their data in one or more channels.
61
+ * Content consumers using the {@link discover} method will only see data
62
+ * contained in one of the channels they specify.
63
+ *
64
+ * While many channels may be public, they partition
65
+ * the public into different "contexts", mitigating the
66
+ * phenomenon of [context collapse](https://en.wikipedia.org/wiki/Context_collapse) or the "flattening of multiple audiences."
67
+ * Any [URI](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) can be used as a channel, and so channels can represent people,
68
+ * comment threads, topics, places (real or virtual), pieces of media, and more.
69
+ *
70
+ * For example, consider a comment on a post. If we place that comment in the channel
71
+ * represented by the post's URI, then only people viewing the post will know to
72
+ * look in that channel, giving it visibility akin to a comment on a blog post
73
+ * or comment on Instagram ([since 2019](https://www.buzzfeednews.com/article/katienotopoulos/instagrams-following-activity-tab-is-going-away)).
74
+ * If we also place the comment in the channel represented by the commenter's URI (their
75
+ * {@link GraffitiObjectBase.actor | `actor` URI}), then people viewing the commenter's profile
76
+ * will also see the comment, giving it more visibility, like a reply on Twitter.
77
+ * If we *only* place the comment in the channel represented by the commenter's URI, then
78
+ * it becomes like a quote tweet ([prior to 2020](https://x.com/Support/status/1300555325750292480)),
79
+ * where the comment is only visible to the commenter's followers but not the audience
80
+ * of the original post.
81
+ *
82
+ * The channel model differs from other models of communication such as the
83
+ * [actor model](https://www.w3.org/TR/activitypub/#Overview) used by ActivityPub,
84
+ * the protocol underlying Mastodon, or the [firehose model](https://bsky.social/about/blog/5-5-2023-federation-architecture)
85
+ * used by the AT Protocol, the protocol underlying BlueSky.
86
+ * The actor model is a fusion of direct messaging (like Email) and broadcasting
87
+ * (like RSS) and works well for follow-based communication but struggles
88
+ * to pass information via other rendez-vous.
89
+ * In the actor model, even something as simple as comments can be
90
+ * [very tricky and require server "side effects"](https://seb.jambor.dev/posts/understanding-activitypub-part-3-the-state-of-mastodon/).
91
+ * The firehose model dumps all user data into one public database,
92
+ * which doesn't allow for the carving out of different contexts that we did in our comment
93
+ * example above. In the firehose model a comment will always be visible to *both* the original post's audience and
94
+ * the commenter's followers.
95
+ *
96
+ * In some sense, channels provide a sort of "social access control" by forming
97
+ * expectations about the audiences of different online spaces.
98
+ * As a real world analogy, oftentimes support groups, such as alcoholics
99
+ * anonymous, are open to the public but people in those spaces feel comfortable sharing intimate details
100
+ * because they have expectations about the other people attending.
101
+ * If someone malicious went to support groups just to spread people's secrets,
102
+ * they would be shamed for violating these norms.
103
+ * Similarly, in Graffiti, while you could spider public channels like a search engine
104
+ * to find content about a person, revealing that you've done such a thing
105
+ * would be shameful.
106
+ *
107
+ * Still, social access control is not perfect and so in situations where privacy is important,
108
+ * objects can also be given
109
+ * an {@link GraffitiObjectBase.allowed | `allowed`} list.
110
+ * For example, to send someone a direct message you should put an object representing
111
+ * that message in the channel that represents them (their {@link GraffitiObjectBase.actor | `actor` URI}),
112
+ * so they can find it, *and* set the `allowed` field to only include the recipient,
113
+ * so only they can read it.
114
+ *
115
+ * ### Interaction relativity
116
+ *
117
+ * Interaction relativity posits that "interaction between two individuals only
118
+ * exists relative to an observer," or equivalently, all interaction is [reified](https://en.wikipedia.org/wiki/Reification_(computer_science)).
119
+ * For example, if one user creates a post and another user wants to "like" that post,
120
+ * their like is not modifying the original post, it is simply another data object that points
121
+ * to the post being liked, via its {@link locationToUri | URI}.
122
+ *
123
+ * ```json
124
+ * {
125
+ * activity: 'like',
126
+ * target: 'uri-of-the-post-i-like',
127
+ * actor: 'my-user-id'
128
+ * }
129
+ * ```
130
+ *
131
+ * In Graffiti, all interactions including *moderation* and *collaboration* are relative.
132
+ * This means that applications can freely choose which interactions
133
+ * they want to express to their users and how.
134
+ * For example, one application could have a single fixed moderator,
135
+ * another could allow users to choose which moderators they would like filter their content
136
+ * like [Bluesky's stackable moderation](https://bsky.social/about/blog/03-12-2024-stackable-moderation),
137
+ * and another could implement a fully democratic system like [PolicyKit](https://policykit.org/).
138
+ * Each of these applications is one interpretation of the underlying refieid user interactions and
139
+ * users can freely switch between them.
140
+ *
141
+ * Interaction relativy also allows applications to introduce new sorts of interactions
142
+ * without having to coordinate with all the other existing applications,
143
+ * keeping the ecosystem flexible and interoperable.
144
+ * For example, an application could [add a "Trust" button to posts](https://social.cs.washington.edu/pub_details.html?id=trustnet)
145
+ * and use it assess the truthfulness of posts made on applications across Graffiti.
146
+ * New sorts of interactions like these can be smoothly absorbed by the broader ecosystem
147
+ * as a [folksonomy](https://en.wikipedia.org/wiki/Folksonomy).
148
+ *
149
+ * Interactivy relativity is realized in Graffiti through two design decisions:
150
+ * 1. The creators of objects can only modify their own objects. It is important for
151
+ * users to be able to change and delete their own content to respect their
152
+ * [right to be forgotten](https://en.wikipedia.org/wiki/Right_to_be_forgotten),
153
+ * but beyond self-correction and self-censorship all other interaction is reified.
154
+ * Many interactions can be reified via pointers, as in the "like" example above, and collaborative
155
+ * edits can be refieid via [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type).
156
+ * 2. No one owns channels. Unlike IRC/Slack channels or [Matrix rooms](https://matrix.org/docs/matrix-concepts/rooms_and_events/),
157
+ * anyone can post to any channel, so long as they know the URI of that channel.
158
+ * It is up to applications to hide content from channels either according to manual
159
+ * filters or in response to user action.
160
+ * For example, a user may create a post with the flag `disableReplies`.
161
+ * Applications could then filter out any content from the replies channel
162
+ * that the original poster has not specifically approved.
163
+ *
164
+ * ## Implementing the API
165
+ *
166
+ * To implement the API, first install it:
167
+ *
168
+ * ```bash
169
+ * npm install @graffiti-garden/api
170
+ * ```
171
+ *
172
+ * Then create a class that extends the `Graffiti` class and implement the abstract methods.
173
+ *
174
+ * ```typescript
175
+ * import { Graffiti } from "@graffiti-garden/api";
176
+ *
177
+ * class MyGraffitiImplementation extends Graffiti {
178
+ * // Implement the abstract methods here
179
+ * }
180
+ * ```
181
+ * ### Testing
182
+ *
183
+ * We have written a number of unit tests written with [vitest](https://vitest.dev/)
184
+ * that can be used to verify implementations of the API.
185
+ * To use them, create a test file in that ends in `*.spec.ts` and format it as follows:
186
+ *
187
+ * ```typescript
188
+ * import { graffitiCRUDTests } from "@graffiti-garden/api/tests";
189
+ *
190
+ * const useGraffiti = () => new MyGraffitiImplementation();
191
+ * // Fill in with implementation-specific information
192
+ * // to provide to valid actor sessions for the tests
193
+ * // to use as identities.
194
+ * const useSession1 = () => ({ actor: "someone" });
195
+ * const useSession2 = () => ({ actor: "someoneelse" });
196
+ *
197
+ * // Run the tests
198
+ * graffitiCRUDTests(useGraffiti, useSession1, useSession2);
199
+ * ```
200
+ *
201
+ * Then run the tests in the root of your directory with:
202
+ *
203
+ * ```bash
204
+ * npx vitest
205
+ * ```
206
+ *
207
+ * ## Building the Documentation
208
+ *
209
+ * To build the [TypeDoc](https://typedoc.org/) documentation, run the following commands:
210
+ *
211
+ * ```bash
212
+ * npm run install
213
+ * npm run docs
214
+ * ```
215
+ *
216
+ * Then run a local server to view the documentation:
217
+ *
218
+ * ```bash
219
+ * cd docs
220
+ * npx http-server
221
+ * ```
222
+ *
223
+ * ## TODO
224
+ *
225
+ * - Test for listChannels and listOrphans,
226
+ * - Implement scope.
227
+ *
228
+ * @groupDescription CRUD Methods
229
+ * Methods for {@link put | creating}, {@link get | reading}, {@link patch | updating},
230
+ * and {@link delete | deleting} {@link GraffitiObjectBase | Graffiti objects}.
231
+ * @groupDescription Query Methods
232
+ * Methods for retrieving multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
233
+ * @groupDescription Session Management
234
+ * Methods and properties for logging in and out of a Graffiti implementation.
235
+ * @groupDescription Utilities
236
+ * Methods for for converting Graffiti objects to and from URIs
237
+ * and for finding lost objects.
238
+ */
239
+ export declare abstract class Graffiti {
240
+ /**
241
+ * Converts a {@link GraffitiLocation} object containing a
242
+ * {@link GraffitiObjectBase.name | `name`}, {@link GraffitiObjectBase.actor | `actor`},
243
+ * and {@link GraffitiObjectBase.source | `source`} into a globally unique URI.
244
+ * The form of this URI is implementation dependent.
245
+ *
246
+ * Its exact inverse is {@link uriToLocation}.
247
+ *
248
+ * @group Utilities
249
+ */
250
+ abstract locationToUri(location: GraffitiLocation): string;
251
+ /**
252
+ * Parses a globally unique Graffiti URI into a {@link GraffitiLocation}
253
+ * object containing a {@link GraffitiObjectBase.name | `name`},
254
+ * {@link GraffitiObjectBase.actor | `actor`}, and {@link GraffitiObjectBase.source | `source`}.
255
+ *
256
+ * Its exact inverse is {@link locationToUri}.
257
+ *
258
+ * @group Utilities
259
+ */
260
+ abstract uriToLocation(uri: string): GraffitiLocation;
261
+ /**
262
+ * An alias of {@link locationToUri}
263
+ *
264
+ * @group Utilities
265
+ */
266
+ objectToUri(object: GraffitiObjectBase): string;
267
+ /**
268
+ * Creates a new {@link GraffitiObjectBase | object} or replaces an existing object.
269
+ * An object can only be replaced by the same {@link GraffitiObjectBase.actor | `actor`}
270
+ * that created it.
271
+ *
272
+ * Replacement occurs when the {@link GraffitiLocation} properties of the supplied object
273
+ * ({@link GraffitiObjectBase.name | `name`}, {@link GraffitiObjectBase.actor | `actor`},
274
+ * and {@link GraffitiObjectBase.source | `source`}) exactly match the location of an existing object.
275
+ *
276
+ * @returns The object that was replaced if one exists or an object with
277
+ * with a `null` {@link GraffitiObjectBase.value | `value`} if this method
278
+ * created a new object.
279
+ * The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
280
+ * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
281
+ * field updated to the time of replacement/creation.
282
+ *
283
+ * @group CRUD Methods
284
+ */
285
+ abstract put<Schema>(
286
+ /**
287
+ * The object to be put. This object is statically type-checked against the [JSON schema](https://json-schema.org/) that can be optionally provided
288
+ * as the generic type parameter. We highly recommend providing a schema to
289
+ * ensure that the PUT object matches subsequent {@link get} or {@link discover}
290
+ * methods.
291
+ */
292
+ object: GraffitiPutObject<Schema>,
293
+ /**
294
+ * An implementation-specific object with information to authenticate the
295
+ * {@link GraffitiObjectBase.actor | `actor`}.
296
+ */
297
+ session: GraffitiSession): Promise<GraffitiObjectBase>;
298
+ /**
299
+ * Retrieves an object from a given location.
300
+ * If no object exists at that location or if the retrieving
301
+ * {@link GraffitiObjectBase.actor | `actor`} is not the creator or included in
302
+ * the object's {@link GraffitiObjectBase.allowed | `allowed`} property,
303
+ * a {@link GraffitiErrorNotFound} is thrown.
304
+ *
305
+ * The retrieved object is also type-checked against the provided [JSON schema](https://json-schema.org/)
306
+ * otherwise a {@link GraffitiErrorSchemaMismatch} is thrown.
307
+ *
308
+ * @group CRUD Methods
309
+ */
310
+ abstract get<Schema extends JSONSchema4>(
311
+ /**
312
+ * The location of the object to get.
313
+ */
314
+ locationOrUri: GraffitiLocation | string,
315
+ /**
316
+ * The JSON schema to validate the retrieved object against.
317
+ */
318
+ schema: Schema,
319
+ /**
320
+ * An implementation-specific object with information to authenticate the
321
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
322
+ * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
323
+ * property must be `undefined`.
324
+ */
325
+ session?: GraffitiSession): Promise<GraffitiObject<Schema>>;
326
+ /**
327
+ * Patches an existing object at a given location.
328
+ * The patching {@link GraffitiObjectBase.actor | `actor`} must be the same as the
329
+ * `actor` that created the object.
330
+ *
331
+ * @returns The object that was deleted if one exists or an object with
332
+ * with a `null` {@link GraffitiObjectBase.value | `value`} otherwise.
333
+ * The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
334
+ * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
335
+ * field updated to the time of deletion.
336
+ *
337
+ * @group CRUD Methods
338
+ */
339
+ abstract patch(
340
+ /**
341
+ * A collection of [JSON Patch](https://jsonpatch.com) operations
342
+ * to apply to the object. See {@link GraffitiPatch} for more information.
343
+ */
344
+ patch: GraffitiPatch,
345
+ /**
346
+ * The location of the object to patch.
347
+ */
348
+ locationOrUri: GraffitiLocation | string,
349
+ /**
350
+ * An implementation-specific object with information to authenticate the
351
+ * {@link GraffitiObjectBase.actor | `actor`}.
352
+ */
353
+ session: GraffitiSession): Promise<GraffitiObjectBase>;
354
+ /**
355
+ * Deletes an object from a given location.
356
+ * The deleting {@link GraffitiObjectBase.actor | `actor`} must be the same as the
357
+ * `actor` that created the object.
358
+ *
359
+ * If the object does not exist or has already been deleted,
360
+ * {@link GraffitiErrorNotFound} is thrown.
361
+ *
362
+ * @returns The object that was deleted if one exists or an object with
363
+ * with a `null` {@link GraffitiObjectBase.value | `value`} otherwise.
364
+ * The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
365
+ * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
366
+ * field updated to the time of deletion.
367
+ *
368
+ * @group CRUD Methods
369
+ */
370
+ abstract delete(
371
+ /**
372
+ * The location of the object to delete.
373
+ */
374
+ locationOrUri: GraffitiLocation | string,
375
+ /**
376
+ * An implementation-specific object with information to authenticate the
377
+ * {@link GraffitiObjectBase.actor | `actor`}.
378
+ */
379
+ session: GraffitiSession): Promise<GraffitiObjectBase>;
380
+ /**
381
+ * Discovers objects created by any user that are contained
382
+ * in at least one of the given {@link GraffitiObjectBase.channels | `channels`}
383
+ * and match the given [JSON Schema](https://json-schema.org).
384
+ *
385
+ * Objects are returned asynchronously as they are discovered but the stream
386
+ * will end once all leads have been exhausted.
387
+ * The method must be polled again for new objects.
388
+ *
389
+ * `discover` will not return objects that the {@link GraffitiObjectBase.actor | `actor`}
390
+ * is not {@link GraffitiObjectBase.allowed | `allowed`} to access.
391
+ * If the actor is not the creator of a discovered object,
392
+ * the allowed list will be masked to only contain the querying actor if the
393
+ * allowed list is not `undefined` (public). Additionally, if the actor is not the
394
+ * creator of a discovered object, any {@link GraffitiObjectBase.channels | `channels`}
395
+ * not specified by the `discover` method will not be revealed. This masking happens
396
+ * before the supplied schema is applied.
397
+ *
398
+ * {@link discover} can be used in conjunction with {@link synchronize}
399
+ * to provide a responsive and consistent user experience.
400
+ *
401
+ * Since different implementations may fetch data from multiple sources there is
402
+ * no guarentee on the order that objects are returned in. Additionally, the method
403
+ * will return objects that have been deleted but with a
404
+ * {@link GraffitiObjectBase.tombstone | `tombstone`} field set to `true` for
405
+ * cache invalidation purposes.
406
+ * The final `return()` value of the stream includes a `tombstoneRetention`
407
+ * property that represents the minimum amount of time,
408
+ * in milliseconds, that an application will retain and return tombstones for objects that
409
+ * have been deleted.
410
+ *
411
+ * When repolling, the {@link GraffitiObjectBase.lastModified | `lastModified`}
412
+ * field can be queried via the schema to
413
+ * only fetch objects that have been modified since the last poll.
414
+ * Such queries should only be done if the time since the last poll
415
+ * is less than the `tombstoneRetention` value of that poll, otherwise the tombstones
416
+ * for objects that have been deleted may not be returned.
417
+ *
418
+ * ```json
419
+ * {
420
+ * "properties": {
421
+ * "lastModified": {
422
+ * "minimum": LAST_RETRIEVED_TIME
423
+ * }
424
+ * }
425
+ * }
426
+ * ```
427
+ *
428
+ * `discover` needs to be polled for new data because live updates to
429
+ * an application can be visually distracting or lead to toxic engagement.
430
+ * If and when an application wants real-time updates, such as in a chat
431
+ * application, application authors must be intentional about their polling.
432
+ *
433
+ * Implementers should be aware that some users may applications may try to poll
434
+ * {@link discover} repetitively. You can deal with this by rate limiting or
435
+ * preemptively fetching data via a bidirectional channel, like a WebSocket.
436
+ * Additionally, implementers should probably index the `lastModified` field
437
+ * to speed up responses to schemas like the one above.
438
+ *
439
+ * @returns A stream of objects that match the given {@link GraffitiObjectBase.channels | `channels`}
440
+ * and [JSON Schema](https://json-schema.org).
441
+ *
442
+ * @group Query Methods
443
+ */
444
+ abstract discover<Schema extends JSONSchema4>(
445
+ /**
446
+ * The {@link GraffitiObjectBase.channels | `channels`} that objects must be associated with.
447
+ */
448
+ channels: string[],
449
+ /**
450
+ * A [JSON Schema](https://json-schema.org) that objects must satisfy.
451
+ */
452
+ schema: Schema,
453
+ /**
454
+ * An implementation-specific object with information to authenticate the
455
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
456
+ * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
457
+ * property will be returned.
458
+ */
459
+ session?: GraffitiSession): GraffitiStream<GraffitiObject<Schema>, {
460
+ tombstoneRetention: number;
461
+ }>;
462
+ /**
463
+ * This method has the same signature as {@link discover} but listens for
464
+ * changes made via {@link put}, {@link patch}, and {@link delete} or
465
+ * fetched from {@link get} or {@link discover} and then streams appropriate
466
+ * changes to provide a responsive and consistent user experience.
467
+ *
468
+ * Unlike {@link discover}, this method continuously listens for changes
469
+ * and will not terminate unless the user calls the `return` method on the iterator
470
+ * or `break`s out of the loop.
471
+ *
472
+ * Example 1: Suppose a user publishes a post using {@link put}. If the feed
473
+ * displaying that user's posts is using {@link synchronize} to listen for changes,
474
+ * then the user's new post will instantly appear in their feed, giving the UI a
475
+ * responsive feel.
476
+ *
477
+ * Example 2: Suppose one of a user's friends changes their name. As soon as the
478
+ * user's application receives one notice of that change (using {@link get}
479
+ * or {@link discover}), then {@link synchronize} listeners can be used to update
480
+ * all instance's of that friend's name in the user's application instantly,
481
+ * providing a consistent user experience.
482
+ *
483
+ * @group Query Methods
484
+ */
485
+ abstract synchronize<Schema extends JSONSchema4>(
486
+ /**
487
+ * The {@link GraffitiObjectBase.channels | `channels`} that the objects must be associated with.
488
+ */
489
+ channels: string[],
490
+ /**
491
+ * A [JSON Schema](https://json-schema.org) that objects must satisfy.
492
+ */
493
+ schema: Schema,
494
+ /**
495
+ * An implementation-specific object with information to authenticate the
496
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
497
+ * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
498
+ * property will be returned.
499
+ */
500
+ session?: GraffitiSession): GraffitiStream<GraffitiObject<Schema>>;
501
+ /**
502
+ * Returns a list of all {@link GraffitiObjectBase.channels | `channels`}
503
+ * that an {@link GraffitiObjectBase.actor | `actor`} has posted to.
504
+ * This is not very useful for most applications, but
505
+ * necessary for certain applications where a user wants a
506
+ * global view of all their Graffiti data or to debug
507
+ * channel usage.
508
+ *
509
+ * @group Utilities
510
+ *
511
+ * @returns A stream the {@link GraffitiObjectBase.channels | `channel`}s
512
+ * that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
513
+ * The `lastModified` field is the time that the user last modified an
514
+ * object in that channel. The `count` field is the number of objects
515
+ * that the user has posted to that channel.
516
+ */
517
+ abstract listChannels(
518
+ /**
519
+ * An implementation-specific object with information to authenticate the
520
+ * {@link GraffitiObjectBase.actor | `actor`}.
521
+ */
522
+ session: GraffitiSession): GraffitiStream<{
523
+ channel: string;
524
+ lastModified: number;
525
+ count: number;
526
+ }>;
527
+ /**
528
+ * Returns a list of all {@link GraffitiObjectBase | objects} a user has posted that are
529
+ * not associated with any {@link GraffitiObjectBase.channels | `channel`}, i.e. orphaned objects.
530
+ * This is not very useful for most applications, but
531
+ * necessary for certain applications where a user wants a
532
+ * global view of all their Graffiti data or to debug
533
+ * channel usage.
534
+ *
535
+ * @group Utilities
536
+ *
537
+ * @returns A stream of the {@link GraffitiObjectBase.name | `name`}
538
+ * and {@link GraffitiObjectBase.source | `source`} of the orphaned objects
539
+ * that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
540
+ * The {@link GraffitiObjectBase.lastModified | lastModified} field is the
541
+ * time that the user last modified the orphan.
542
+ */
543
+ abstract listOrphans(session: GraffitiSession): GraffitiStream<{
544
+ name: string;
545
+ source: string;
546
+ lastModified: string;
547
+ }>;
548
+ /**
549
+ * The age at which a query for a session will be considered expired.
550
+ */
551
+ /**
552
+ * Begins the login process. Depending on the implementation, this may
553
+ * involve redirecting the user to a login page or opening a popup,
554
+ * so it should always be called in response to a user action.
555
+ *
556
+ * The {@link GraffitiSession | session} object is returned
557
+ * asynchronously via {@link Graffiti.sessionEvents | sessionEvents}
558
+ * as a {@link GraffitiLoginEvent} with event type `login`.
559
+ *
560
+ * @group Session Management
561
+ */
562
+ abstract login(
563
+ /**
564
+ * Suggestions for the permissions that the
565
+ * login process should grant. The login process may not
566
+ * provide the exact proposed permissions.
567
+ */
568
+ proposal?: {
569
+ /**
570
+ * A suggested actor to login as. For example, if a user tries to
571
+ * edit a post but are not logged in, the interface can infer that
572
+ * they might want to log in as the actor who created the post
573
+ * they are attempting to edit.
574
+ *
575
+ * Even if provided, the implementation should allow the user
576
+ * to log in as a different actor if they choose.
577
+ */
578
+ actor?: string;
579
+ /**
580
+ * A yet to be defined permissions scope. An application may use
581
+ * this to indicate the minimum necessary scope needed to
582
+ * operate. For example, it may need to be able read private
583
+ * messages from a certain set of channels, or write messages that
584
+ * follow a particular schema.
585
+ *
586
+ * The login process should make it clear what scope an application
587
+ * is requesting and allow the user to enhance or reduce that
588
+ * scope as necessary.
589
+ */
590
+ scope?: {};
591
+ },
592
+ /**
593
+ * An arbitrary string that will be returned with the
594
+ * {@link GraffitiSession | session} object
595
+ * when the login process is complete.
596
+ * See {@link GraffitiLoginEvent}.
597
+ */
598
+ state?: string): Promise<void>;
599
+ /**
600
+ * Begins the logout process. Depending on the implementation, this may
601
+ * involve redirecting the user to a logout page or opening a popup,
602
+ * so it should always be called in response to a user action.
603
+ *
604
+ * A confirmation will be returned asynchronously via
605
+ * {@link Graffiti.sessionEvents | sessionEvents}
606
+ * as a {@link GraffitiLogoutEvent} as event type `logout`.
607
+ *
608
+ * @group Session Management
609
+ */
610
+ abstract logout(
611
+ /**
612
+ * The {@link GraffitiSession | session} object to logout.
613
+ */
614
+ session: GraffitiSession,
615
+ /**
616
+ * An arbitrary string that will be returned with the
617
+ * when the logout process is complete.
618
+ * See {@link GraffitiLogoutEvent}.
619
+ */
620
+ state?: string): Promise<void>;
621
+ /**
622
+ * An event target that can be used to listen for `login`
623
+ * and `logout` events. They are custom events of types
624
+ * {@link GraffitiLoginEvent} and {@link GraffitiLogoutEvent }
625
+ * respectively.
626
+ *
627
+ * @group Session Management
628
+ */
629
+ abstract readonly sessionEvents: EventTarget;
630
+ }
631
+ /**
632
+ * This is a factory function that produces an instance of
633
+ * the {@link Graffiti} class. Since the Graffiti class is
634
+ * abstract, factory functions provide an easy way to
635
+ * swap out different implementations.
636
+ */
637
+ export type GraffitiFactory = () => Graffiti;
638
+ //# sourceMappingURL=1-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"1-api.d.ts","sourceRoot":"","sources":["../../../src/1-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,cAAc,EACf,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2OG;AACH,8BAAsB,QAAQ;IAC5B;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM;IAE1D;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB;IAErD;;;;OAIG;IACH,WAAW,CAAC,MAAM,EAAE,kBAAkB;IAItC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,GAAG,CAAC,MAAM;IACjB;;;;;OAKG;IACH,MAAM,EAAE,iBAAiB,CAAC,MAAM,CAAC;IACjC;;;OAGG;IACH,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,kBAAkB,CAAC;IAE9B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,GAAG,CAAC,MAAM,SAAS,WAAW;IACrC;;OAEG;IACH,aAAa,EAAE,gBAAgB,GAAG,MAAM;IACxC;;OAEG;IACH,MAAM,EAAE,MAAM;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAElC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,KAAK;IACZ;;;OAGG;IACH,KAAK,EAAE,aAAa;IACpB;;OAEG;IACH,aAAa,EAAE,gBAAgB,GAAG,MAAM;IACxC;;;OAGG;IACH,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,kBAAkB,CAAC;IAE9B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,MAAM;IACb;;OAEG;IACH,aAAa,EAAE,gBAAgB,GAAG,MAAM;IACxC;;;OAGG;IACH,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,kBAAkB,CAAC;IAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+DG;IACH,QAAQ,CAAC,QAAQ,CAAC,MAAM,SAAS,WAAW;IAC1C;;OAEG;IACH,QAAQ,EAAE,MAAM,EAAE;IAClB;;OAEG;IACH,MAAM,EAAE,MAAM;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,eAAe,GACxB,cAAc,CACf,cAAc,CAAC,MAAM,CAAC,EACtB;QACE,kBAAkB,EAAE,MAAM,CAAC;KAC5B,CACF;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,QAAQ,CAAC,WAAW,CAAC,MAAM,SAAS,WAAW;IAC7C;;OAEG;IACH,QAAQ,EAAE,MAAM,EAAE;IAClB;;OAEG;IACH,MAAM,EAAE,MAAM;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,eAAe,GACxB,cAAc,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAEzC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,YAAY;IACnB;;;OAGG;IACH,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IAEF;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,cAAc,CAAC;QAC7D,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IAEF;;OAEG;IAGH;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,KAAK;IACZ;;;;OAIG;IACH,QAAQ,CAAC,EAAE;QACT;;;;;;;;WAQG;QACH,KAAK,CAAC,EAAE,MAAM,CAAC;QACf;;;;;;;;;;WAUG;QACH,KAAK,CAAC,EAAE,EAAE,CAAC;KACZ;IACD;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC;IAEhB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM;IACb;;OAEG;IACH,OAAO,EAAE,eAAe;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC;IAEhB;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC;CAC9C;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,QAAQ,CAAC"}
@@ -0,0 +1,270 @@
1
+ import type { JTDDataType } from "ajv/dist/core";
2
+ import type { Operation as JSONPatchOperation } from "fast-json-patch";
3
+ /**
4
+ * Objects are the atomic unit in Graffiti that can represent both data (*e.g.* a social media post or profile)
5
+ * and activities (*e.g.* a like or follow).
6
+ * Objects are created and modified by a single {@link actor | `actor`}.
7
+ *
8
+ * Most of an object's content is stored in its {@link value | `value`} property, which can be any JSON
9
+ * object. However, we recommend using properties from the
10
+ * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/)
11
+ * or properties that emerge in the Graffiti [folksonomy](https://en.wikipedia.org/wiki/Folksonomy)
12
+ * to promote interoperability.
13
+ *
14
+ * The {@link name | `name`}, {@link actor | `actor`}, and {@link source | `source`}
15
+ * properties together uniquely describe the {@link GraffitiLocation | object's location}
16
+ * and can be {@link Graffiti.locationToUri | converted to a globally unique URI}.
17
+ *
18
+ * The {@link channels | `channels`} and {@link allowed | `allowed`} properties
19
+ * enable the object's creator to shape the visibility of and access to their object.
20
+ *
21
+ * The {@link tombstone | `tombstone`} and {@link lastModified | `lastModified`} properties are for
22
+ * caching and synchronization.
23
+ */
24
+ export interface GraffitiObjectBase {
25
+ /**
26
+ * The object's content as freeform JSON. We recommend using properties from the
27
+ * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/)
28
+ * or properties that emerge in the Graffiti [folksonomy](https://en.wikipedia.org/wiki/Folksonomy)
29
+ * to promote interoperability.
30
+ */
31
+ value: {};
32
+ /**
33
+ * An array of URIs the creator associates with the object. Objects can only be found by querying
34
+ * one of the object's channels using the
35
+ * {@link Graffiti.discover} method. This allows creators to express the intended audience of their object
36
+ * which helps to prevent [context collapse](https://en.wikipedia.org/wiki/Context_collapse) even
37
+ * in the highly interoperable ecosystem that Graffiti envisions. For example, channel URIs may be:
38
+ * - A user's own {@link actor | `actor`} URI. Putting an object in this channel is a way to broadcast
39
+ * the object to the user's followers, like posting a tweet.
40
+ * - The URI of a Graffiti post. Putting an object in this channel is a way to broadcast to anyone viewing
41
+ * the post, like commenting on a tweet.
42
+ * - A URI representing a topic. Putting an object in this channel is a way to broadcast to anyone interested
43
+ * in that topic, like posting in a subreddit.
44
+ */
45
+ channels: string[];
46
+ /**
47
+ * An optional array of {@link actor | `actor`} URIs that the creator allows to access the object.
48
+ * If no `allowed` array is provided, the object can be accessed by anyone (so long as they
49
+ * also know the right {@link channels | `channel` } to look in). An object can always be accessed by its creator, even if
50
+ * the `allowed` array is empty.
51
+ *
52
+ * The `allowed` array is not revealed to users other than the creator, like
53
+ * a BCC email. A user may choose to add a `to` property to the object's {@link value | `value`} to indicate
54
+ * other recipients, however this is not enforced by Graffiti and may not accurately reflect the actual `allowed` array.
55
+ *
56
+ * `allowed` can be combined with {@link channels | `channels`}. For example, to send someone a direct message
57
+ * the sender should put their object in the channel of the recipient's {@link actor | `actor`} URI to notify them of the message and also add
58
+ * the recipient's {@link actor | `actor`} URI to the `allowed` array to prevent others from seeing the message.
59
+ */
60
+ allowed?: string[];
61
+ /**
62
+ * The URI of the `actor` that {@link Graffiti.put | created } the object. This `actor` also has the unique permission to
63
+ * {@link Graffiti.patch | modify} or {@link Graffiti.delete | delete} the object.
64
+ *
65
+ * We borrow the term actor from the ActivityPub because
66
+ * [like in ActivityPub](https://www.w3.org/TR/activitypub/#h-note-0)
67
+ * there is not necessarily a one-to-one mapping between actors and people/users.
68
+ * Multiple people can share the same actor or one person can have multiple actors.
69
+ * Actors can also be bots.
70
+ *
71
+ * In Graffiti, actors are always globally unique URIs which
72
+ * allows them to also function as {@link channels | `channels`}.
73
+ */
74
+ actor: string;
75
+ /**
76
+ * A name for the object. This name is not globally unique but it is unique when
77
+ * combined with the {@link actor | `actor`} and {@link source | `source`}.
78
+ * Often times it is not specified by the user and randomly generated during {@link Graffiti.put | creation}.
79
+ * If an object is created with the same `name`, `actor`, and `source` as an existing object,
80
+ * the existing object will be replaced with the new object.
81
+ */
82
+ name: string;
83
+ /**
84
+ * The URI of the source that stores the object. In some decentralized implementations,
85
+ * it can represent the server or [pod](https://en.wikipedia.org/wiki/Solid_(web_decentralization_project)#Design)
86
+ * that a user has delegated to store their objects. In others it may represent the distributed
87
+ * storage network that the object is stored on.
88
+ */
89
+ source: string;
90
+ /**
91
+ * The time the object was last modified, measured in milliseconds since January 1, 1970.
92
+ * This is used for caching and synchronization.
93
+ * A number, rather than an ISO string or Date object, is used for easy comparison, sorting,
94
+ * and JSON Schema [range queries](https://json-schema.org/understanding-json-schema/reference/numeric#range).
95
+ *
96
+ * It is possible to use this value to sort objects in a user's interface but in many cases it would be better to
97
+ * use a `createdAt` property in the object's {@link value | `value`} to indicate when the object was created
98
+ * rather than when it was modified.
99
+ */
100
+ lastModified: number;
101
+ /**
102
+ * A boolean indicating whether the object has been deleted.
103
+ * Depending on implementation, objects stay available for some time after deletion to allow for synchronization.
104
+ */
105
+ tombstone: boolean;
106
+ }
107
+ /**
108
+ * This type constrains the {@link GraffitiObjectBase} type to adhere to a
109
+ * particular [JSON schema](https://json-schema.org/).
110
+ * This allows for static type-checking of an object's {@link GraffitiObjectBase.value | `value`}
111
+ * which is otherwise a freeform JSON object.
112
+ *
113
+ * Schema-aware objects are returned by {@link Graffiti.get} and {@link Graffiti.discover}.
114
+ */
115
+ export type GraffitiObject<Schema> = GraffitiObjectBase & JTDDataType<Schema>;
116
+ /**
117
+ * This is a subset of properties from {@link GraffitiObjectBase} that uniquely
118
+ * identify an object's location: {@link GraffitiObjectBase.actor | `actor`},
119
+ * {@link GraffitiObjectBase.name | `name`}, and {@link GraffitiObjectBase.source | `source`}.
120
+ * Attempts to create an object with the same `actor`, `name`, and `source`
121
+ * as an existing object will replace the existing object (see {@link Graffiti.put}).
122
+ *
123
+ * This location can be converted to
124
+ * a globally unique URI using {@link Graffiti.locationToUri}.
125
+ */
126
+ export type GraffitiLocation = Pick<GraffitiObjectBase, "actor" | "name" | "source">;
127
+ /**
128
+ * This object is a subset of {@link GraffitiObjectBase} that a user must construct locally before calling {@link Graffiti.put}.
129
+ * This local copy does not require system-generated properties and may be statically typed with
130
+ * a [JSON schema](https://json-schema.org/) to prevent the accidental creation of erroneous objects.
131
+ *
132
+ * This local object must have a {@link GraffitiObjectBase.value | `value`} and {@link GraffitiObjectBase.channels | `channels`}
133
+ * and may optionally have an {@link GraffitiObjectBase.allowed | `allowed`} property.
134
+ *
135
+ * It may also contain any of the {@link GraffitiLocation } properties: {@link GraffitiObjectBase.actor | `actor`},
136
+ * {@link GraffitiObjectBase.name | `name`}, and {@link GraffitiObjectBase.source | `source`}.
137
+ * If the location provided exactly matches an existing object, the existing object will be replaced.
138
+ * If no `name` is provided, one will be randomly generated.
139
+ * If no `actor` is provided, the `actor` from the supplied {@link GraffitiSession | `session` } will be used.
140
+ * If no `source` is provided, one may be inferred by the depending on implementation.
141
+ *
142
+ * This object does not need a {@link GraffitiObjectBase.lastModified | `lastModified`} or {@link GraffitiObjectBase.tombstone | `tombstone`}
143
+ * property since these are automatically generated by the Graffiti system.
144
+ */
145
+ export type GraffitiPutObject<Schema> = Pick<GraffitiObjectBase, "value" | "channels" | "allowed"> & Partial<GraffitiLocation> & JTDDataType<Schema>;
146
+ /**
147
+ * This object contains information that
148
+ * {@link GraffitiObjectBase.source | `source`}s can
149
+ * use to verify that a user has permission to operate a
150
+ * particular {@link GraffitiObjectBase.actor | `actor`}.
151
+ * This object is required of all {@link Graffiti} methods
152
+ * that modify objects and is optional for methods that read objects.
153
+ *
154
+ * At a minimum the `session` object must contain the
155
+ * {@link GraffitiSession.actor | `actor`} URI the user wants to authenticate with.
156
+ * However it is likely that the `session` object must contain other
157
+ * implementation-specific properties.
158
+ * For example, a Solid implementation might include a
159
+ * [`fetch`](https://docs.inrupt.com/developer-tools/api/javascript/solid-client-authn-browser/functions.html#fetch)
160
+ * function. A distributed implementation may include
161
+ * a cryptographic signature.
162
+ *
163
+ * As to why the `session` object is passed as an argument to every method
164
+ * rather than being an internal property of the {@link Graffiti} instance,
165
+ * this is primarily for type-checking to catch bugs related to login state.
166
+ * Graffiti applications can expose some functionality to users who are not logged in
167
+ * with {@link Graffiti.get} and {@link Graffiti.discover} but without type-checking
168
+ * the `session` it can be easy to forget to hide buttons that trigger
169
+ * other methods that require login.
170
+ * In the future, `session` object may be updated to include scope information
171
+ * and passing the `session` to each method can type-check whether the session provides the
172
+ * necessary permissions.
173
+ *
174
+ * Passing the `session` object per-method also allows for multiple sessions
175
+ * to be used within the same application, like an Email client fetching from
176
+ * multiple accounts.
177
+ */
178
+ export interface GraffitiSession {
179
+ /**
180
+ * The {@link GraffitiObjectBase.actor | `actor`} a user wants to authenticate with.
181
+ */
182
+ actor: string;
183
+ /**
184
+ * A yet undefined property detailing what operations the session
185
+ * grants the user to perform. For example, to allow a user to
186
+ * read private messages from a particular set of channels or
187
+ * to allow the user to write object matching a particular schema.
188
+ */
189
+ scope?: {};
190
+ }
191
+ /**
192
+ * This is the format for patches that modify {@link GraffitiObjectBase} objects
193
+ * using the {@link Graffiti.patch} method. The patches must
194
+ * be an array of [JSON Patch](https://jsonpatch.com) operations.
195
+ * Patches can only be applied to the
196
+ * {@link GraffitiObjectBase.value | `value`}, {@link GraffitiObjectBase.channels | `channels`},
197
+ * and {@link GraffitiObjectBase.allowed | `allowed`} properties since the other
198
+ * properties either describe the object's location or are automatically generated.
199
+ * (See also {@link GraffitiPutObject}).
200
+ */
201
+ export interface GraffitiPatch {
202
+ /**
203
+ * An array of [JSON Patch](https://jsonpatch.com) operations to
204
+ * modify the object's {@link GraffitiObjectBase.value | `value`}. The resulting
205
+ * `value` must still be a JSON object.
206
+ */
207
+ value?: JSONPatchOperation[];
208
+ /**
209
+ * An array of [JSON Patch](https://jsonpatch.com) operations to
210
+ * modify the object's {@link GraffitiObjectBase.channels | `channels`}. The resulting
211
+ * `channels` must still be an array of strings.
212
+ */
213
+ channels?: JSONPatchOperation[];
214
+ /**
215
+ * An array of [JSON Patch](https://jsonpatch.com) operations to
216
+ * modify the object's {@link GraffitiObjectBase.allowed | `allowed`} property. The resulting
217
+ * `allowed` property must still be an array of strings or `undefined`.
218
+ */
219
+ allowed?: JSONPatchOperation[];
220
+ }
221
+ /**
222
+ * This type represents a stream of data that are
223
+ * returned by Graffiti's query-like operations such as
224
+ * {@link Graffiti.discover} and {@link Graffiti.listChannels}.
225
+ *
226
+ * Errors are returned within the stream rather than as
227
+ * exceptions that would halt the entire stream. This is because
228
+ * some implementations may pull data from multiple
229
+ * {@link GraffitiObjectBase.source | `source`}s
230
+ * including some that may be unreliable. In many cases,
231
+ * these errors can be safely ignored.
232
+ *
233
+ * The stream is an [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
234
+ * that can be iterated over using `for await` loops or calling `next` on the generator.
235
+ * The stream can be terminated by breaking out of a loop calling `return` on the generator.
236
+ */
237
+ export type GraffitiStream<TValue, TReturn = void> = AsyncGenerator<{
238
+ error?: undefined;
239
+ value: TValue;
240
+ } | {
241
+ error: Error;
242
+ source: string;
243
+ }, TReturn>;
244
+ /**
245
+ * The event type produced in {@link Graffiti.sessionEvents}
246
+ * when a user logs in manually from {@link Graffiti.login}
247
+ * or when their session is restored from a previous login.
248
+ * The event name to listen for is `login`.
249
+ */
250
+ export type GraffitiLoginEvent = CustomEvent<{
251
+ state?: string;
252
+ } & ({
253
+ error: Error;
254
+ session?: undefined;
255
+ } | {
256
+ error?: undefined;
257
+ session: GraffitiSession;
258
+ })>;
259
+ /**
260
+ * The event type produced in {@link Graffiti.sessionEvents}
261
+ * when a user logs out either manually with {@link Graffiti.logout}
262
+ * or when their session times out or otherwise becomes invalid.
263
+ * The event name to listen for is `logout`.
264
+ */
265
+ export type GraffitiLogoutEvent = CustomEvent<{
266
+ actor: string;
267
+ state?: string;
268
+ error?: Error;
269
+ }>;
270
+ //# sourceMappingURL=2-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"2-types.d.ts","sourceRoot":"","sources":["../../../src/2-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,IAAI,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,KAAK,EAAE,EAAE,CAAC;IAEV;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;IAEnB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;;;;;;;;;;;OAYG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;;;;;OASG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,CAAC,MAAM,IAAI,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;AAE9E;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,kBAAkB,EAClB,OAAO,GAAG,MAAM,GAAG,QAAQ,CAC5B,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,iBAAiB,CAAC,MAAM,IAAI,IAAI,CAC1C,kBAAkB,EAClB,OAAO,GAAG,UAAU,GAAG,SAAS,CACjC,GACC,OAAO,CAAC,gBAAgB,CAAC,GACzB,WAAW,CAAC,MAAM,CAAC,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,EAAE,CAAC;CACZ;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAEhC;;;;OAIG;IACH,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,IAAI,cAAc,CAC/D;IACE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IACE,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,EACH,OAAO,CACR,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,CAC1C;IACE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,CACA;IACE,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB,GACD;IACE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,eAAe,CAAC;CAC1B,CACJ,CACF,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ export declare class GraffitiErrorUnauthorized extends Error {
2
+ constructor(message?: string);
3
+ }
4
+ export declare class GraffitiErrorForbidden extends Error {
5
+ constructor(message?: string);
6
+ }
7
+ export declare class GraffitiErrorNotFound extends Error {
8
+ constructor(message?: string);
9
+ }
10
+ export declare class GraffitiErrorInvalidSchema extends Error {
11
+ constructor(message?: string);
12
+ }
13
+ export declare class GraffitiErrorSchemaMismatch extends Error {
14
+ constructor(message?: string);
15
+ }
16
+ export declare class GraffitiErrorPatchTestFailed extends Error {
17
+ constructor(message?: string);
18
+ }
19
+ export declare class GraffitiErrorPatchError extends Error {
20
+ constructor(message?: string);
21
+ }
22
+ export declare class GraffitiErrorInvalidUri extends Error {
23
+ constructor(message?: string);
24
+ }
25
+ //# sourceMappingURL=3-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"3-errors.d.ts","sourceRoot":"","sources":["../../../src/3-errors.ts"],"names":[],"mappings":"AAAA,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,CAAC,EAAE,MAAM;CAK7B;AAED,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,CAAC,EAAE,MAAM;CAK7B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,CAAC,EAAE,MAAM;CAK7B;AAED,qBAAa,0BAA2B,SAAQ,KAAK;gBACvC,OAAO,CAAC,EAAE,MAAM;CAK7B;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,CAAC,EAAE,MAAM;CAK7B;AAED,qBAAa,4BAA6B,SAAQ,KAAK;gBACzC,OAAO,CAAC,EAAE,MAAM;CAK7B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,CAAC,EAAE,MAAM;CAK7B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,CAAC,EAAE,MAAM;CAK7B"}
@@ -0,0 +1,5 @@
1
+ export * from "./1-api";
2
+ export * from "./2-types";
3
+ export * from "./3-errors";
4
+ export type { JSONSchema4 } from "json-schema";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type GraffitiFactory, type GraffitiSession } from "../src/index";
2
+ export declare const graffitiCRUDTests: (useGraffiti: GraffitiFactory, useSession1: () => GraffitiSession, useSession2: () => GraffitiSession) => void;
3
+ //# sourceMappingURL=crud.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../../../tests/crud.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,eAAe,EAQrB,MAAM,cAAc,CAAC;AAGtB,eAAO,MAAM,iBAAiB,gBACf,eAAe,eACf,MAAM,eAAe,eACrB,MAAM,eAAe,SAucnC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type GraffitiFactory, type GraffitiSession } from "../src/index";
2
+ export declare const graffitiDiscoverTests: (useGraffiti: GraffitiFactory, useSession1: () => GraffitiSession, useSession2: () => GraffitiSession) => void;
3
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../tests/discover.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,eAAe,EAGrB,MAAM,cAAc,CAAC;AAGtB,eAAO,MAAM,qBAAqB,gBACnB,eAAe,eACf,MAAM,eAAe,eACrB,MAAM,eAAe,SA0kBnC,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from "./location";
2
+ export * from "./crud";
3
+ export * from "./synchronize";
4
+ export * from "./discover";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../tests/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type GraffitiFactory } from "../src/index";
2
+ export declare const graffitiLocationTests: (useGraffiti: GraffitiFactory) => void;
3
+ //# sourceMappingURL=location.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"location.d.ts","sourceRoot":"","sources":["../../../tests/location.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAG7E,eAAO,MAAM,qBAAqB,gBAAiB,eAAe,SAkCjE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type GraffitiFactory, type GraffitiSession } from "../src/index";
2
+ export declare const graffitiSynchronizeTests: (useGraffiti: GraffitiFactory, useSession1: () => GraffitiSession, useSession2: () => GraffitiSession) => void;
3
+ //# sourceMappingURL=synchronize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"synchronize.d.ts","sourceRoot":"","sources":["../../../tests/synchronize.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAG1E,eAAO,MAAM,wBAAwB,gBACtB,eAAe,eACf,MAAM,eAAe,eACrB,MAAM,eAAe,SA2PnC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { GraffitiPutObject, GraffitiStream } from "../src";
2
+ export declare function randomString(): string;
3
+ export declare function randomValue(): {
4
+ [x: string]: string;
5
+ };
6
+ export declare function randomPutObject(): GraffitiPutObject<{}>;
7
+ export declare function nextStreamValue<S, T>(iterator: GraffitiStream<S, T>): Promise<S>;
8
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../tests/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAEhE,wBAAgB,YAAY,IAAI,MAAM,CAMrC;AAED,wBAAgB,WAAW;;EAI1B;AAED,wBAAgB,eAAe,IAAI,iBAAiB,CAAC,EAAE,CAAC,CAKvD;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,cAIzE"}
package/package.json CHANGED
@@ -1,23 +1,33 @@
1
1
  {
2
2
  "name": "@graffiti-garden/api",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "The heart of Graffiti",
5
- "types": "src/index.ts",
6
- "module": "dist/index.js",
7
- "main": "dist/index.cjs.js",
5
+ "types": "./dist/types/src/index.d.ts",
6
+ "module": "./dist/index.js",
7
+ "main": "./dist/index.cjs.js",
8
8
  "exports": {
9
9
  ".": {
10
10
  "import": {
11
- "types": "./src/index.ts",
11
+ "types": "./dist/types/src/index.d.ts",
12
12
  "node": "./dist/index.cjs.js",
13
13
  "default": "./dist/index.js"
14
14
  },
15
15
  "require": {
16
- "types": "./src/index.ts",
16
+ "types": "./dist/types/src/index.d.ts",
17
17
  "default": "./dist/index.cjs.js"
18
18
  }
19
19
  },
20
- "./tests": "./tests/index.ts"
20
+ "./tests": {
21
+ "import": {
22
+ "types": "./dist/types/tests/index.d.ts",
23
+ "node": "./dist/tests/index.cjs.js",
24
+ "default": "./dist/tests/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/types/tests/index.d.ts",
28
+ "default": "./dist/tests/index.cjs.js"
29
+ }
30
+ }
21
31
  },
22
32
  "files": [
23
33
  "src",