@graffiti-garden/implementation-local 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/database.browser.js +27 -0
- package/dist/database.browser.js.map +1 -0
- package/dist/database.cjs.js +2 -0
- package/dist/database.cjs.js.map +1 -0
- package/dist/database.js +2 -0
- package/dist/database.js.map +1 -0
- package/dist/index.browser.js +32 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/session-manager.browser.js +2 -0
- package/dist/session-manager.browser.js.map +1 -0
- package/dist/session-manager.cjs.js +2 -0
- package/dist/session-manager.cjs.js.map +1 -0
- package/dist/session-manager.js +2 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/src/database.d.ts +57 -0
- package/dist/src/database.d.ts.map +1 -0
- package/dist/src/index.d.ts +26 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/session-manager.d.ts +22 -0
- package/dist/src/session-manager.d.ts.map +1 -0
- package/dist/src/synchronize.d.ts +25 -0
- package/dist/src/synchronize.d.ts.map +1 -0
- package/dist/src/tests.spec.d.ts +2 -0
- package/dist/src/tests.spec.d.ts.map +1 -0
- package/dist/src/utilities.d.ts +15 -0
- package/dist/src/utilities.d.ts.map +1 -0
- package/dist/synchronize.browser.js +18 -0
- package/dist/synchronize.browser.js.map +1 -0
- package/dist/synchronize.cjs.js +2 -0
- package/dist/synchronize.cjs.js.map +1 -0
- package/dist/synchronize.js +2 -0
- package/dist/synchronize.js.map +1 -0
- package/dist/utilities.browser.js +2 -0
- package/dist/utilities.browser.js.map +1 -0
- package/dist/utilities.cjs.js +2 -0
- package/dist/utilities.cjs.js.map +1 -0
- package/dist/utilities.js +2 -0
- package/dist/utilities.js.map +1 -0
- package/package.json +110 -0
- package/src/database.ts +450 -0
- package/src/index.ts +58 -0
- package/src/session-manager.ts +122 -0
- package/src/synchronize.ts +154 -0
- package/src/tests.spec.ts +16 -0
- package/src/utilities.ts +128 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var e=require("ajv-draft-04"),t=require("@repeaterjs/repeater"),n=require("fast-json-patch"),a=require("@graffiti-garden/api");function i(e,t,n,i){const r=n[t];if(r&&r.length)try{i[t]=e(i[t],r,!0,!1).newDocument}catch(e){throw"object"==typeof e&&e&&"name"in e&&"string"==typeof e.name&&"message"in e&&"string"==typeof e.message?"TEST_OPERATION_FAILED"===e.name?new a.GraffitiErrorPatchTestFailed(e.message):new a.GraffitiErrorPatchError(e.name+": "+e.message):e}}function r(e,t,n){e.actor!==n?.actor&&(e.allowed=e.allowed&&n?[n.actor]:void 0,e.channels=e.channels.filter((e=>t.includes(e))))}function s(e,t){return void 0===e.allowed||!!t?.actor&&(e.actor===t.actor||e.allowed.includes(t.actor))}exports.GraffitiSynchronize=class{synchronizeEvents=new EventTarget;ajv;graffiti;constructor(t,n){this.ajv=n??new e({strict:!1}),this.graffiti=t}synchronizeDispatch(e,t){const n=new CustomEvent("change",{detail:{oldObject:e,newObject:t}});this.synchronizeEvents.dispatchEvent(n)}get=async(...e)=>{const t=await this.graffiti.get(...e);return this.synchronizeDispatch(t),t};put=async(...e)=>{const t=await this.graffiti.put(...e),n=e[0],a={...t,value:n.value,channels:n.channels,allowed:n.allowed,tombstone:!1};return this.synchronizeDispatch(t,a),t};patch=async(...e)=>{const t=await this.graffiti.patch(...e),a={...t};a.tombstone=!1;for(const t of["value","channels","allowed"])i(n.applyPatch,t,e[0],a);return this.synchronizeDispatch(t,a),t};delete=async(...e)=>{const t=await this.graffiti.delete(...e);return this.synchronizeDispatch(t),t};discover=(...e)=>{const t=this.graffiti.discover(...e),n=this.synchronizeDispatch.bind(this);return async function*(){let e=await t.next();for(;!e.done;)e.value.error||n(e.value.value),yield e.value,e=await t.next();return e.value}()};synchronize=(...e)=>{const[n,i,c]=e,o=function(e,t){try{return e.compile(t)}catch(e){throw new a.GraffitiErrorInvalidSchema(e instanceof Error?e.message:void 0)}}(this.ajv,i);return new t.Repeater((async(e,t)=>{const a=t=>{const{oldObject:a,newObject:i}=t.detail;for(const t of[i,a])if(t&&t.channels.some((e=>n.includes(e)))&&s(t,c)){const a={...t};if(r(a,n,c),o(a)){e({value:a});break}}};this.synchronizeEvents.addEventListener("change",a),await t,this.synchronizeEvents.removeEventListener("change",a)}))}};
|
|
2
|
+
//# sourceMappingURL=synchronize.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synchronize.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import t from"ajv-draft-04";import{Repeater as e}from"@repeaterjs/repeater";import{applyPatch as n}from"fast-json-patch";import{GraffitiErrorPatchTestFailed as a,GraffitiErrorPatchError as s,GraffitiErrorInvalidSchema as i}from"@graffiti-garden/api";function o(t,e,n,i){const o=n[e];if(o&&o.length)try{i[e]=t(i[e],o,!0,!1).newDocument}catch(t){throw"object"==typeof t&&t&&"name"in t&&"string"==typeof t.name&&"message"in t&&"string"==typeof t.message?"TEST_OPERATION_FAILED"===t.name?new a(t.message):new s(t.name+": "+t.message):t}}function r(t,e,n){t.actor!==n?.actor&&(t.allowed=t.allowed&&n?[n.actor]:void 0,t.channels=t.channels.filter((t=>e.includes(t))))}function c(t,e){return void 0===t.allowed||!!e?.actor&&(t.actor===e.actor||t.allowed.includes(e.actor))}class h{synchronizeEvents=new EventTarget;ajv;graffiti;constructor(e,n){this.ajv=n??new t({strict:!1}),this.graffiti=e}synchronizeDispatch(t,e){const n=new CustomEvent("change",{detail:{oldObject:t,newObject:e}});this.synchronizeEvents.dispatchEvent(n)}get=async(...t)=>{const e=await this.graffiti.get(...t);return this.synchronizeDispatch(e),e};put=async(...t)=>{const e=await this.graffiti.put(...t),n=t[0],a={...e,value:n.value,channels:n.channels,allowed:n.allowed,tombstone:!1};return this.synchronizeDispatch(e,a),e};patch=async(...t)=>{const e=await this.graffiti.patch(...t),a={...e};a.tombstone=!1;for(const e of["value","channels","allowed"])o(n,e,t[0],a);return this.synchronizeDispatch(e,a),e};delete=async(...t)=>{const e=await this.graffiti.delete(...t);return this.synchronizeDispatch(e),e};discover=(...t)=>{const e=this.graffiti.discover(...t),n=this.synchronizeDispatch.bind(this);return async function*(){let t=await e.next();for(;!t.done;)t.value.error||n(t.value.value),yield t.value,t=await e.next();return t.value}()};synchronize=(...t)=>{const[n,a,s]=t,o=function(t,e){try{return t.compile(e)}catch(t){throw new i(t instanceof Error?t.message:void 0)}}(this.ajv,a);return new e((async(t,e)=>{const a=e=>{const{oldObject:a,newObject:i}=e.detail;for(const e of[i,a])if(e&&e.channels.some((t=>n.includes(t)))&&c(e,s)){const a={...e};if(r(a,n,s),o(a)){t({value:a});break}}};this.synchronizeEvents.addEventListener("change",a),await e,this.synchronizeEvents.removeEventListener("change",a)}))}}export{h as GraffitiSynchronize};
|
|
2
|
+
//# sourceMappingURL=synchronize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synchronize.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
class t extends Error{constructor(e){super(e),this.name="GraffitiErrorInvalidSchema",Object.setPrototypeOf(this,t.prototype)}}class e extends Error{constructor(t){super(t),this.name="GraffitiErrorPatchTestFailed",Object.setPrototypeOf(this,e.prototype)}}class o extends Error{constructor(t){super(t),this.name="GraffitiErrorPatchError",Object.setPrototypeOf(this,o.prototype)}}class r extends Error{constructor(t){super(t),this.name="GraffitiErrorInvalidUri",Object.setPrototypeOf(this,r.prototype)}}const n=t=>`${t.source}/${encodeURIComponent(t.actor)}/${encodeURIComponent(t.name)}`,c=t=>{const e=t.split("/"),o=e.pop(),n=e.pop();if(!o||!n||!e.length)throw new r;return{name:decodeURIComponent(o),actor:decodeURIComponent(n),source:e.join("/")}};function s(t=16){const e=new Uint8Array(t);crypto.getRandomValues(e);return btoa(String.fromCodePoint(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/\=+$/,"")}function a(t){return"string"==typeof t?{location:c(t),uri:t}:{location:{name:t.name,actor:t.actor,source:t.source},uri:n(t)}}function i(t,r,n,c){const s=n[r];if(s&&s.length)try{c[r]=t(c[r],s,!0,!1).newDocument}catch(t){throw"object"==typeof t&&t&&"name"in t&&"string"==typeof t.name&&"message"in t&&"string"==typeof t.message?"TEST_OPERATION_FAILED"===t.name?new e(t.message):new o(t.name+": "+t.message):t}}function p(e,o){try{return e.compile(o)}catch(e){throw new t(e instanceof Error?e.message:void 0)}}function l(t,e,o){t.actor!==o?.actor&&(t.allowed=t.allowed&&o?[o.actor]:void 0,t.channels=t.channels.filter((t=>e.includes(t))))}function u(t,e){return void 0===t.allowed||!!e?.actor&&(t.actor===e.actor||t.allowed.includes(e.actor))}export{i as applyGraffitiPatch,p as attemptAjvCompile,u as isActorAllowedGraffitiObject,n as locationToUri,l as maskGraffitiObject,s as randomBase64,a as unpackLocationOrUri,c as uriToLocation};
|
|
2
|
+
//# sourceMappingURL=utilities.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utilities.browser.js","sources":["../node_modules/@graffiti-garden/api/dist/index.js"],"sourcesContent":["class r{objectToUri(r){return this.locationToUri(r)}}class t extends Error{constructor(r){super(r),this.name=\"GraffitiErrorUnauthorized\",Object.setPrototypeOf(this,t.prototype)}}class o extends Error{constructor(r){super(r),this.name=\"GraffitiErrorForbidden\",Object.setPrototypeOf(this,o.prototype)}}class e extends Error{constructor(r){super(r),this.name=\"GraffitiErrorNotFound\",Object.setPrototypeOf(this,e.prototype)}}class s extends Error{constructor(r){super(r),this.name=\"GraffitiErrorInvalidSchema\",Object.setPrototypeOf(this,s.prototype)}}class i extends Error{constructor(r){super(r),this.name=\"GraffitiErrorSchemaMismatch\",Object.setPrototypeOf(this,i.prototype)}}class c extends Error{constructor(r){super(r),this.name=\"GraffitiErrorPatchTestFailed\",Object.setPrototypeOf(this,c.prototype)}}class a extends Error{constructor(r){super(r),this.name=\"GraffitiErrorPatchError\",Object.setPrototypeOf(this,a.prototype)}}class p extends Error{constructor(r){super(r),this.name=\"GraffitiErrorInvalidUri\",Object.setPrototypeOf(this,p.prototype)}}export{r as Graffiti,o as GraffitiErrorForbidden,s as GraffitiErrorInvalidSchema,p as GraffitiErrorInvalidUri,e as GraffitiErrorNotFound,a as GraffitiErrorPatchError,c as GraffitiErrorPatchTestFailed,i as GraffitiErrorSchemaMismatch,t as GraffitiErrorUnauthorized};\n//# sourceMappingURL=index.js.map\n"],"names":["s","Error","constructor","r","super","this","name","Object","setPrototypeOf","prototype","c","a","p"],"mappings":"AAAqa,MAAMA,UAAUC,MAAM,WAAAC,CAAYC,GAAGC,MAAMD,GAAGE,KAAKC,KAAK,6BAA6BC,OAAOC,eAAeH,KAAKL,EAAES,UAAU,EAAiI,MAAMC,UAAUT,MAAM,WAAAC,CAAYC,GAAGC,MAAMD,GAAGE,KAAKC,KAAK,+BAA+BC,OAAOC,eAAeH,KAAKK,EAAED,UAAU,EAAE,MAAME,UAAUV,MAAM,WAAAC,CAAYC,GAAGC,MAAMD,GAAGE,KAAKC,KAAK,0BAA0BC,OAAOC,eAAeH,KAAKM,EAAEF,UAAU,EAAE,MAAMG,UAAUX,MAAM,WAAAC,CAAYC,GAAGC,MAAMD,GAAGE,KAAKC,KAAK,0BAA0BC,OAAOC,eAAeH,KAAKO,EAAEH,UAAU","x_google_ignoreList":[0]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var e=require("@graffiti-garden/api");const o=e=>`${e.source}/${encodeURIComponent(e.actor)}/${encodeURIComponent(e.name)}`,r=o=>{const r=o.split("/"),t=r.pop(),n=r.pop();if(!t||!n||!r.length)throw new e.GraffitiErrorInvalidUri;return{name:decodeURIComponent(t),actor:decodeURIComponent(n),source:r.join("/")}};exports.applyGraffitiPatch=function(o,r,t,n){const a=t[r];if(a&&a.length)try{n[r]=o(n[r],a,!0,!1).newDocument}catch(o){throw"object"==typeof o&&o&&"name"in o&&"string"==typeof o.name&&"message"in o&&"string"==typeof o.message?"TEST_OPERATION_FAILED"===o.name?new e.GraffitiErrorPatchTestFailed(o.message):new e.GraffitiErrorPatchError(o.name+": "+o.message):o}},exports.attemptAjvCompile=function(o,r){try{return o.compile(r)}catch(o){throw new e.GraffitiErrorInvalidSchema(o instanceof Error?o.message:void 0)}},exports.isActorAllowedGraffitiObject=function(e,o){return void 0===e.allowed||!!o?.actor&&(e.actor===o.actor||e.allowed.includes(o.actor))},exports.locationToUri=o,exports.maskGraffitiObject=function(e,o,r){e.actor!==r?.actor&&(e.allowed=e.allowed&&r?[r.actor]:void 0,e.channels=e.channels.filter((e=>o.includes(e))))},exports.randomBase64=function(e=16){const o=new Uint8Array(e);return crypto.getRandomValues(o),btoa(String.fromCodePoint(...o)).replace(/\+/g,"-").replace(/\//g,"_").replace(/\=+$/,"")},exports.unpackLocationOrUri=function(e){return"string"==typeof e?{location:r(e),uri:e}:{location:{name:e.name,actor:e.actor,source:e.source},uri:o(e)}},exports.uriToLocation=r;
|
|
2
|
+
//# sourceMappingURL=utilities.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utilities.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{GraffitiErrorInvalidUri as e,GraffitiErrorPatchTestFailed as o,GraffitiErrorPatchError as n,GraffitiErrorInvalidSchema as t}from"@graffiti-garden/api";const r=e=>`${e.source}/${encodeURIComponent(e.actor)}/${encodeURIComponent(e.name)}`,c=o=>{const n=o.split("/"),t=n.pop(),r=n.pop();if(!t||!r||!n.length)throw new e;return{name:decodeURIComponent(t),actor:decodeURIComponent(r),source:n.join("/")}};function a(e=16){const o=new Uint8Array(e);crypto.getRandomValues(o);return btoa(String.fromCodePoint(...o)).replace(/\+/g,"-").replace(/\//g,"_").replace(/\=+$/,"")}function i(e){return"string"==typeof e?{location:c(e),uri:e}:{location:{name:e.name,actor:e.actor,source:e.source},uri:r(e)}}function s(e,t,r,c){const a=r[t];if(a&&a.length)try{c[t]=e(c[t],a,!0,!1).newDocument}catch(e){throw"object"==typeof e&&e&&"name"in e&&"string"==typeof e.name&&"message"in e&&"string"==typeof e.message?"TEST_OPERATION_FAILED"===e.name?new o(e.message):new n(e.name+": "+e.message):e}}function l(e,o){try{return e.compile(o)}catch(e){throw new t(e instanceof Error?e.message:void 0)}}function m(e,o,n){e.actor!==n?.actor&&(e.allowed=e.allowed&&n?[n.actor]:void 0,e.channels=e.channels.filter((e=>o.includes(e))))}function p(e,o){return void 0===e.allowed||!!o?.actor&&(e.actor===o.actor||e.allowed.includes(o.actor))}export{s as applyGraffitiPatch,l as attemptAjvCompile,p as isActorAllowedGraffitiObject,r as locationToUri,m as maskGraffitiObject,a as randomBase64,i as unpackLocationOrUri,c as uriToLocation};
|
|
2
|
+
//# sourceMappingURL=utilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utilities.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graffiti-garden/implementation-local",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A local-only implementation of the Graffiti API using PouchDB",
|
|
5
|
+
"types": "./dist/src/index.d.ts",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"browser": "./dist/index.browser.js",
|
|
8
|
+
"main": "./dist/index.cjs.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/src/index.d.ts",
|
|
13
|
+
"node": "./dist/index.cjs.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/src/index.d.ts",
|
|
18
|
+
"default": "./dist/index.cjs.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./database": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./dist/src/database.d.ts",
|
|
24
|
+
"node": "./dist/database.cjs.js",
|
|
25
|
+
"default": "./dist/database.js"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/src/database.d.ts",
|
|
29
|
+
"default": "./dist/database.cjs.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"./session-manager": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/src/session-manager-local.d.ts",
|
|
35
|
+
"node": "./dist/session-manager-local.cjs.js",
|
|
36
|
+
"default": "./dist/session-manager-local.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/src/session-manager-local.d.ts",
|
|
40
|
+
"default": "./dist/session-manager-local.cjs.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"./synchronize": {
|
|
44
|
+
"import": {
|
|
45
|
+
"types": "./dist/src/synchronize.d.ts",
|
|
46
|
+
"node": "./dist/synchronize.cjs.js",
|
|
47
|
+
"default": "./dist/synchronize.js"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"types": "./dist/src/synchronize.d.ts",
|
|
51
|
+
"default": "./dist/synchronize.cjs.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"./utilities": {
|
|
55
|
+
"import": {
|
|
56
|
+
"types": "./dist/src/utilities.d.ts",
|
|
57
|
+
"node": "./dist/utilities.cjs.js",
|
|
58
|
+
"default": "./dist/utilities.js"
|
|
59
|
+
},
|
|
60
|
+
"require": {
|
|
61
|
+
"types": "./dist/src/utilities.d.ts",
|
|
62
|
+
"default": "./dist/utilities.cjs.js"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"test:watch": "vitest",
|
|
69
|
+
"test:coverage": "vitest --coverage",
|
|
70
|
+
"build": "rollup -c rollup.config.ts --configPlugin rollup-plugin-typescript2",
|
|
71
|
+
"prepublishOnly": "npm update && npm test && npm run build"
|
|
72
|
+
},
|
|
73
|
+
"files": [
|
|
74
|
+
"src",
|
|
75
|
+
"dist",
|
|
76
|
+
"package.json",
|
|
77
|
+
"README.md"
|
|
78
|
+
],
|
|
79
|
+
"author": "Theia Henderson",
|
|
80
|
+
"license": "GPL-3.0-or-later",
|
|
81
|
+
"repository": {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "git+https://github.com/graffiti-garden/implementation-pouchdb.git"
|
|
84
|
+
},
|
|
85
|
+
"bugs": {
|
|
86
|
+
"url": "https://github.com/graffiti-garden/implementation-pouchdb/issues"
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@rollup/plugin-commonjs": "^28.0.2",
|
|
90
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
91
|
+
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
92
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
93
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
94
|
+
"rollup": "^4.30.1",
|
|
95
|
+
"rollup-plugin-node-polyfills": "^0.2.1",
|
|
96
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
97
|
+
"rollup-plugin-visualizer": "^5.14.0",
|
|
98
|
+
"tslib": "^2.8.1",
|
|
99
|
+
"vitest": "^2.1.8"
|
|
100
|
+
},
|
|
101
|
+
"dependencies": {
|
|
102
|
+
"@graffiti-garden/api": "^0.1.10",
|
|
103
|
+
"@repeaterjs/repeater": "^3.0.6",
|
|
104
|
+
"@types/pouchdb": "^6.4.2",
|
|
105
|
+
"ajv": "^8.17.1",
|
|
106
|
+
"ajv-draft-04": "^1.0.0",
|
|
107
|
+
"fast-json-patch": "^3.1.1",
|
|
108
|
+
"pouchdb": "^9.0.0"
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/database.ts
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Graffiti,
|
|
3
|
+
GraffitiObjectBase,
|
|
4
|
+
GraffitiLocation,
|
|
5
|
+
} from "@graffiti-garden/api";
|
|
6
|
+
import {
|
|
7
|
+
GraffitiErrorNotFound,
|
|
8
|
+
GraffitiErrorSchemaMismatch,
|
|
9
|
+
GraffitiErrorForbidden,
|
|
10
|
+
GraffitiErrorPatchError,
|
|
11
|
+
} from "@graffiti-garden/api";
|
|
12
|
+
import PouchDB from "pouchdb";
|
|
13
|
+
import {
|
|
14
|
+
locationToUri,
|
|
15
|
+
unpackLocationOrUri,
|
|
16
|
+
randomBase64,
|
|
17
|
+
applyGraffitiPatch,
|
|
18
|
+
attemptAjvCompile,
|
|
19
|
+
maskGraffitiObject,
|
|
20
|
+
isActorAllowedGraffitiObject,
|
|
21
|
+
} from "./utilities";
|
|
22
|
+
import { Repeater } from "@repeaterjs/repeater";
|
|
23
|
+
import Ajv from "ajv-draft-04";
|
|
24
|
+
import { applyPatch } from "fast-json-patch";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Constructor options for the GraffitiPoubchDB class.
|
|
28
|
+
*/
|
|
29
|
+
export interface GraffitiLocalOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Options to pass to the PouchDB constructor.
|
|
32
|
+
* Defaults to `{ name: "graffitiDb" }`.
|
|
33
|
+
*
|
|
34
|
+
* See the [PouchDB documentation](https://pouchdb.com/api.html#create_database)
|
|
35
|
+
* for available options.
|
|
36
|
+
*/
|
|
37
|
+
pouchDBOptions?: PouchDB.Configuration.DatabaseConfiguration;
|
|
38
|
+
/**
|
|
39
|
+
* Defines the name of the {@link https://api.graffiti.garden/interfaces/GraffitiObjectBase.html#source | `source` }
|
|
40
|
+
* under which to store objects.
|
|
41
|
+
* Defaults to `"local"`.
|
|
42
|
+
*/
|
|
43
|
+
sourceName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The time in milliseconds to keep tombstones before deleting them.
|
|
46
|
+
* See the {@link https://api.graffiti.garden/classes/Graffiti.html#discover | `discover` }
|
|
47
|
+
* documentation for more information.
|
|
48
|
+
*/
|
|
49
|
+
tombstoneRetention?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An implementation of only the database operations of the
|
|
54
|
+
* GraffitiAPI without synchronization or session management.
|
|
55
|
+
*/
|
|
56
|
+
export class GraffitiLocalDatabase
|
|
57
|
+
implements
|
|
58
|
+
Pick<
|
|
59
|
+
Graffiti,
|
|
60
|
+
| "get"
|
|
61
|
+
| "put"
|
|
62
|
+
| "patch"
|
|
63
|
+
| "delete"
|
|
64
|
+
| "discover"
|
|
65
|
+
| "listChannels"
|
|
66
|
+
| "listOrphans"
|
|
67
|
+
>
|
|
68
|
+
{
|
|
69
|
+
protected readonly db: PouchDB.Database<GraffitiObjectBase>;
|
|
70
|
+
protected readonly source: string = "local";
|
|
71
|
+
protected readonly tombstoneRetention: number = 86400000; // 1 day in ms
|
|
72
|
+
protected readonly ajv: Ajv;
|
|
73
|
+
|
|
74
|
+
constructor(options?: GraffitiLocalOptions, ajv?: Ajv) {
|
|
75
|
+
this.ajv = ajv ?? new Ajv({ strict: false });
|
|
76
|
+
this.source = options?.sourceName ?? this.source;
|
|
77
|
+
this.tombstoneRetention =
|
|
78
|
+
options?.tombstoneRetention ?? this.tombstoneRetention;
|
|
79
|
+
const pouchDbOptions = {
|
|
80
|
+
name: "graffitiDb",
|
|
81
|
+
...options?.pouchDBOptions,
|
|
82
|
+
};
|
|
83
|
+
this.db = new PouchDB<GraffitiObjectBase>(
|
|
84
|
+
pouchDbOptions.name,
|
|
85
|
+
pouchDbOptions,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
this.db
|
|
89
|
+
//@ts-ignore
|
|
90
|
+
.put({
|
|
91
|
+
_id: "_design/index3",
|
|
92
|
+
views: {
|
|
93
|
+
byChannelAndLastModified: {
|
|
94
|
+
map: function (object: GraffitiObjectBase) {
|
|
95
|
+
const paddedLastModified = object.lastModified
|
|
96
|
+
.toString()
|
|
97
|
+
.padStart(15, "0");
|
|
98
|
+
object.channels.forEach(function (channel) {
|
|
99
|
+
const id =
|
|
100
|
+
encodeURIComponent(channel) + "/" + paddedLastModified;
|
|
101
|
+
//@ts-ignore
|
|
102
|
+
emit(id);
|
|
103
|
+
});
|
|
104
|
+
}.toString(),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
//@ts-ignore
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
if (
|
|
111
|
+
error &&
|
|
112
|
+
typeof error === "object" &&
|
|
113
|
+
"name" in error &&
|
|
114
|
+
error.name === "conflict"
|
|
115
|
+
) {
|
|
116
|
+
// Design document already exists
|
|
117
|
+
return;
|
|
118
|
+
} else {
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected async queryByLocation(location: GraffitiLocation) {
|
|
125
|
+
const uri = locationToUri(location) + "/";
|
|
126
|
+
const results = await this.db.allDocs({
|
|
127
|
+
startkey: uri,
|
|
128
|
+
endkey: uri + "\uffff", // \uffff is the last unicode character
|
|
129
|
+
include_docs: true,
|
|
130
|
+
});
|
|
131
|
+
const docs = results.rows
|
|
132
|
+
.map((row) => row.doc)
|
|
133
|
+
// Remove undefined docs
|
|
134
|
+
.reduce<
|
|
135
|
+
PouchDB.Core.ExistingDocument<
|
|
136
|
+
GraffitiObjectBase & PouchDB.Core.AllDocsMeta
|
|
137
|
+
>[]
|
|
138
|
+
>((acc, doc) => {
|
|
139
|
+
if (doc) acc.push(doc);
|
|
140
|
+
return acc;
|
|
141
|
+
}, [])
|
|
142
|
+
// Remove tombstones
|
|
143
|
+
.filter((doc) => !doc.tombstone);
|
|
144
|
+
return docs;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
protected docId(location: GraffitiLocation) {
|
|
148
|
+
return locationToUri(location) + "/" + randomBase64();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get: Graffiti["get"] = async (...args) => {
|
|
152
|
+
const [locationOrUri, schema, session] = args;
|
|
153
|
+
const { location } = unpackLocationOrUri(locationOrUri);
|
|
154
|
+
|
|
155
|
+
const docs = await this.queryByLocation(location);
|
|
156
|
+
if (!docs.length) throw new GraffitiErrorNotFound();
|
|
157
|
+
|
|
158
|
+
// Get the most recent document
|
|
159
|
+
const doc = docs.reduce((a, b) =>
|
|
160
|
+
a.lastModified > b.lastModified ? a : b,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Strip out the _id and _rev
|
|
164
|
+
const { _id, _rev, ...object } = doc;
|
|
165
|
+
|
|
166
|
+
// Make sure the user is allowed to see it
|
|
167
|
+
if (!isActorAllowedGraffitiObject(doc, session))
|
|
168
|
+
throw new GraffitiErrorNotFound();
|
|
169
|
+
|
|
170
|
+
// Mask out the allowed list and channels
|
|
171
|
+
// if the user is not the owner
|
|
172
|
+
maskGraffitiObject(object, [], session);
|
|
173
|
+
|
|
174
|
+
const validate = attemptAjvCompile(this.ajv, schema);
|
|
175
|
+
if (!validate(object)) {
|
|
176
|
+
throw new GraffitiErrorSchemaMismatch();
|
|
177
|
+
}
|
|
178
|
+
return object;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Deletes all docs at a particular location.
|
|
183
|
+
* If the `keepLatest` flag is set to true,
|
|
184
|
+
* the doc with the most recent timestamp will be
|
|
185
|
+
* spared. If there are multiple docs with the same
|
|
186
|
+
* timestamp, the one with the highest `_id` will be
|
|
187
|
+
* spared.
|
|
188
|
+
*/
|
|
189
|
+
protected async deleteAtLocation(
|
|
190
|
+
location: GraffitiLocation,
|
|
191
|
+
keepLatest: boolean = false,
|
|
192
|
+
) {
|
|
193
|
+
const docsAtLocation = await this.queryByLocation(location);
|
|
194
|
+
if (!docsAtLocation.length) return undefined;
|
|
195
|
+
|
|
196
|
+
// Get the most recent lastModified timestamp.
|
|
197
|
+
const latestModified = docsAtLocation
|
|
198
|
+
.map((doc) => doc.lastModified)
|
|
199
|
+
.reduce((a, b) => (a > b ? a : b));
|
|
200
|
+
|
|
201
|
+
// Delete all old docs
|
|
202
|
+
const docsToDelete = docsAtLocation.filter(
|
|
203
|
+
(doc) => !keepLatest || doc.lastModified < latestModified,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// For docs with the same timestamp,
|
|
207
|
+
// keep the one with the highest _id
|
|
208
|
+
// to break concurrency ties
|
|
209
|
+
const concurrentDocsAll = docsAtLocation.filter(
|
|
210
|
+
(doc) => keepLatest && doc.lastModified === latestModified,
|
|
211
|
+
);
|
|
212
|
+
if (concurrentDocsAll.length) {
|
|
213
|
+
const keepDocId = concurrentDocsAll
|
|
214
|
+
.map((doc) => doc._id)
|
|
215
|
+
.reduce((a, b) => (a > b ? a : b));
|
|
216
|
+
const concurrentDocsToDelete = concurrentDocsAll.filter(
|
|
217
|
+
(doc) => doc._id !== keepDocId,
|
|
218
|
+
);
|
|
219
|
+
docsToDelete.push(...concurrentDocsToDelete);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lastModified = keepLatest ? latestModified : new Date().getTime();
|
|
223
|
+
|
|
224
|
+
let deletedObject: GraffitiObjectBase | undefined = undefined;
|
|
225
|
+
// Go through documents oldest to newest
|
|
226
|
+
for (const doc of docsToDelete.sort(
|
|
227
|
+
(a, b) => a.lastModified - b.lastModified,
|
|
228
|
+
)) {
|
|
229
|
+
// Change it's tombstone to true
|
|
230
|
+
// and update it's timestamp
|
|
231
|
+
const deletedDoc = {
|
|
232
|
+
...doc,
|
|
233
|
+
tombstone: true,
|
|
234
|
+
lastModified,
|
|
235
|
+
};
|
|
236
|
+
try {
|
|
237
|
+
await this.db.put(deletedDoc);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (
|
|
240
|
+
error &&
|
|
241
|
+
typeof error === "object" &&
|
|
242
|
+
"name" in error &&
|
|
243
|
+
error.name === "conflict"
|
|
244
|
+
) {
|
|
245
|
+
// Document was already deleted
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const { _id, _rev, ...object } = deletedDoc;
|
|
250
|
+
deletedObject = object;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return deletedObject;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
delete: Graffiti["delete"] = async (...args) => {
|
|
257
|
+
const [locationOrUri, session] = args;
|
|
258
|
+
const { location } = unpackLocationOrUri(locationOrUri);
|
|
259
|
+
if (location.actor !== session.actor) {
|
|
260
|
+
throw new GraffitiErrorForbidden();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const deletedObject = await this.deleteAtLocation(location);
|
|
264
|
+
if (!deletedObject) {
|
|
265
|
+
throw new GraffitiErrorNotFound();
|
|
266
|
+
}
|
|
267
|
+
return deletedObject;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
put: Graffiti["put"] = async (...args) => {
|
|
271
|
+
const [objectPartial, session] = args;
|
|
272
|
+
if (objectPartial.actor && objectPartial.actor !== session.actor) {
|
|
273
|
+
throw new GraffitiErrorForbidden();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const object: GraffitiObjectBase = {
|
|
277
|
+
value: objectPartial.value,
|
|
278
|
+
channels: objectPartial.channels,
|
|
279
|
+
allowed: objectPartial.allowed,
|
|
280
|
+
name: objectPartial.name ?? randomBase64(),
|
|
281
|
+
source: objectPartial.source ?? this.source,
|
|
282
|
+
actor: session.actor,
|
|
283
|
+
tombstone: false,
|
|
284
|
+
lastModified: new Date().getTime(),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
await this.db.put({
|
|
288
|
+
_id: this.docId(object),
|
|
289
|
+
...object,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Delete the old object
|
|
293
|
+
const previousObject = await this.deleteAtLocation(object, true);
|
|
294
|
+
if (previousObject) {
|
|
295
|
+
return previousObject;
|
|
296
|
+
} else {
|
|
297
|
+
return {
|
|
298
|
+
...object,
|
|
299
|
+
value: {},
|
|
300
|
+
channels: [],
|
|
301
|
+
allowed: undefined,
|
|
302
|
+
tombstone: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
patch: Graffiti["patch"] = async (...args) => {
|
|
308
|
+
const [patch, locationOrUri, session] = args;
|
|
309
|
+
const { location } = unpackLocationOrUri(locationOrUri);
|
|
310
|
+
if (location.actor !== session.actor) {
|
|
311
|
+
throw new GraffitiErrorForbidden();
|
|
312
|
+
}
|
|
313
|
+
const originalObject = await this.get(locationOrUri, {}, session);
|
|
314
|
+
|
|
315
|
+
// Patch it outside of the database
|
|
316
|
+
const patchObject: GraffitiObjectBase = { ...originalObject };
|
|
317
|
+
for (const prop of ["value", "channels", "allowed"] as const) {
|
|
318
|
+
applyGraffitiPatch(applyPatch, prop, patch, patchObject);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Make sure the value is an object
|
|
322
|
+
if (
|
|
323
|
+
typeof patchObject.value !== "object" ||
|
|
324
|
+
Array.isArray(patchObject.value) ||
|
|
325
|
+
!patchObject.value
|
|
326
|
+
) {
|
|
327
|
+
throw new GraffitiErrorPatchError("value is no longer an object");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Make sure the channels are an array of strings
|
|
331
|
+
if (
|
|
332
|
+
!Array.isArray(patchObject.channels) ||
|
|
333
|
+
!patchObject.channels.every((channel) => typeof channel === "string")
|
|
334
|
+
) {
|
|
335
|
+
throw new GraffitiErrorPatchError(
|
|
336
|
+
"channels are no longer an array of strings",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Make sure the allowed list is an array of strings or undefined
|
|
341
|
+
if (
|
|
342
|
+
patchObject.allowed &&
|
|
343
|
+
(!Array.isArray(patchObject.allowed) ||
|
|
344
|
+
!patchObject.allowed.every((allowed) => typeof allowed === "string"))
|
|
345
|
+
) {
|
|
346
|
+
throw new GraffitiErrorPatchError(
|
|
347
|
+
"allowed list is not an array of strings",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
patchObject.lastModified = new Date().getTime();
|
|
352
|
+
await this.db.put({
|
|
353
|
+
...patchObject,
|
|
354
|
+
_id: this.docId(patchObject),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Delete the old object
|
|
358
|
+
await this.deleteAtLocation(patchObject, true);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
...originalObject,
|
|
362
|
+
tombstone: true,
|
|
363
|
+
lastModified: patchObject.lastModified,
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
discover: Graffiti["discover"] = (...args) => {
|
|
368
|
+
const [channels, schema, session] = args;
|
|
369
|
+
|
|
370
|
+
const validate = attemptAjvCompile(this.ajv, schema);
|
|
371
|
+
|
|
372
|
+
// Use the index for queries over ranges of lastModified
|
|
373
|
+
let startKeyAppend = "";
|
|
374
|
+
let endKeyAppend = "\uffff";
|
|
375
|
+
const lastModifiedSchema = schema.properties?.lastModified;
|
|
376
|
+
if (lastModifiedSchema?.minimum) {
|
|
377
|
+
let minimum = Math.ceil(lastModifiedSchema.minimum);
|
|
378
|
+
minimum === lastModifiedSchema.minimum &&
|
|
379
|
+
lastModifiedSchema.exclusiveMinimum &&
|
|
380
|
+
minimum++;
|
|
381
|
+
startKeyAppend = minimum.toString().padStart(15, "0");
|
|
382
|
+
}
|
|
383
|
+
if (lastModifiedSchema?.maximum) {
|
|
384
|
+
let maximum = Math.floor(lastModifiedSchema.maximum);
|
|
385
|
+
maximum === lastModifiedSchema.maximum &&
|
|
386
|
+
lastModifiedSchema.exclusiveMaximum &&
|
|
387
|
+
maximum--;
|
|
388
|
+
endKeyAppend = maximum.toString().padStart(15, "0");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const repeater: ReturnType<
|
|
392
|
+
typeof Graffiti.prototype.discover<typeof schema>
|
|
393
|
+
> = new Repeater(async (push, stop) => {
|
|
394
|
+
const processedIds = new Set<string>();
|
|
395
|
+
|
|
396
|
+
for (const channel of channels) {
|
|
397
|
+
const encodedChannel = encodeURIComponent(channel);
|
|
398
|
+
const startkey = encodedChannel + "/" + startKeyAppend;
|
|
399
|
+
const endkey = encodedChannel + "/" + endKeyAppend;
|
|
400
|
+
|
|
401
|
+
const result = await this.db.query<GraffitiObjectBase>(
|
|
402
|
+
"index3/byChannelAndLastModified",
|
|
403
|
+
{ startkey, endkey, include_docs: true },
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
for (const row of result.rows) {
|
|
407
|
+
const doc = row.doc;
|
|
408
|
+
if (!doc) continue;
|
|
409
|
+
|
|
410
|
+
const { _id, _rev, ...object } = doc;
|
|
411
|
+
|
|
412
|
+
// Don't double return the same object
|
|
413
|
+
// (which can happen if it's in multiple channels)
|
|
414
|
+
if (processedIds.has(_id)) continue;
|
|
415
|
+
processedIds.add(_id);
|
|
416
|
+
|
|
417
|
+
// Make sure the user is allowed to see it
|
|
418
|
+
if (!isActorAllowedGraffitiObject(doc, session)) continue;
|
|
419
|
+
|
|
420
|
+
// Mask out the allowed list and channels
|
|
421
|
+
// if the user is not the owner
|
|
422
|
+
maskGraffitiObject(object, channels, session);
|
|
423
|
+
|
|
424
|
+
// Check that it matches the schema
|
|
425
|
+
if (validate(object)) {
|
|
426
|
+
push({
|
|
427
|
+
value: object,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
stop();
|
|
433
|
+
return {
|
|
434
|
+
tombstoneRetention: this.tombstoneRetention,
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return repeater;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
listChannels: Graffiti["listChannels"] = (...args) => {
|
|
442
|
+
// TODO
|
|
443
|
+
return (async function* () {})();
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
listOrphans: Graffiti["listOrphans"] = (...args) => {
|
|
447
|
+
// TODO
|
|
448
|
+
return (async function* () {})();
|
|
449
|
+
};
|
|
450
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Graffiti } from "@graffiti-garden/api";
|
|
2
|
+
import Ajv from "ajv-draft-04";
|
|
3
|
+
import { GraffitiLocalSessionManager } from "./session-manager";
|
|
4
|
+
import { GraffitiLocalDatabase, type GraffitiLocalOptions } from "./database";
|
|
5
|
+
import { GraffitiSynchronize } from "./synchronize";
|
|
6
|
+
import { locationToUri, uriToLocation } from "./utilities";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A local implementation of the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
|
|
10
|
+
* based on [PouchDB](https://pouchdb.com/). PouchDb will automatically persist data in a local
|
|
11
|
+
* database, either in the browser or in Node.js.
|
|
12
|
+
* It can also be configured to work with an external [CouchDB](https://couchdb.apache.org/) server,
|
|
13
|
+
* although using it with a remote server will not be secure.
|
|
14
|
+
*/
|
|
15
|
+
export class GraffitiLocal extends Graffiti {
|
|
16
|
+
locationToUri = locationToUri;
|
|
17
|
+
uriToLocation = uriToLocation;
|
|
18
|
+
|
|
19
|
+
login: Graffiti["login"];
|
|
20
|
+
logout: Graffiti["logout"];
|
|
21
|
+
sessionEvents: Graffiti["sessionEvents"];
|
|
22
|
+
put: Graffiti["put"];
|
|
23
|
+
get: Graffiti["get"];
|
|
24
|
+
patch: Graffiti["patch"];
|
|
25
|
+
delete: Graffiti["delete"];
|
|
26
|
+
discover: Graffiti["discover"];
|
|
27
|
+
synchronize: Graffiti["synchronize"];
|
|
28
|
+
listChannels: Graffiti["listChannels"];
|
|
29
|
+
listOrphans: Graffiti["listOrphans"];
|
|
30
|
+
|
|
31
|
+
constructor(options?: GraffitiLocalOptions) {
|
|
32
|
+
super();
|
|
33
|
+
|
|
34
|
+
const sessionManagerLocal = new GraffitiLocalSessionManager();
|
|
35
|
+
this.login = sessionManagerLocal.login.bind(sessionManagerLocal);
|
|
36
|
+
this.logout = sessionManagerLocal.logout.bind(sessionManagerLocal);
|
|
37
|
+
this.sessionEvents = sessionManagerLocal.sessionEvents;
|
|
38
|
+
|
|
39
|
+
const ajv = new Ajv({ strict: false });
|
|
40
|
+
const graffitiPouchDbBase = new GraffitiLocalDatabase(options, ajv);
|
|
41
|
+
const graffitiSynchronize = new GraffitiSynchronize(
|
|
42
|
+
graffitiPouchDbBase,
|
|
43
|
+
ajv,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
this.put = graffitiSynchronize.put.bind(graffitiSynchronize);
|
|
47
|
+
this.get = graffitiSynchronize.get.bind(graffitiSynchronize);
|
|
48
|
+
this.patch = graffitiSynchronize.patch.bind(graffitiSynchronize);
|
|
49
|
+
this.delete = graffitiSynchronize.delete.bind(graffitiSynchronize);
|
|
50
|
+
this.discover = graffitiSynchronize.discover.bind(graffitiSynchronize);
|
|
51
|
+
this.synchronize =
|
|
52
|
+
graffitiSynchronize.synchronize.bind(graffitiSynchronize);
|
|
53
|
+
this.listChannels =
|
|
54
|
+
graffitiPouchDbBase.listChannels.bind(graffitiPouchDbBase);
|
|
55
|
+
this.listOrphans =
|
|
56
|
+
graffitiPouchDbBase.listOrphans.bind(graffitiPouchDbBase);
|
|
57
|
+
}
|
|
58
|
+
}
|