@graffiti-garden/implementation-local 0.6.0 → 0.6.2
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 +4 -1
- package/dist/browser/index.js +2 -20
- package/dist/browser/index.js.map +4 -4
- package/dist/cjs/database.js +166 -129
- package/dist/cjs/database.js.map +3 -3
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/utilities.js.map +2 -2
- package/dist/database.d.ts +10 -4
- package/dist/database.d.ts.map +1 -1
- package/dist/esm/database.js +166 -129
- package/dist/esm/database.js.map +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/utilities.js +0 -1
- package/dist/esm/utilities.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/utilities.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/database.ts +249 -191
- package/src/index.ts +3 -3
- package/src/utilities.ts +0 -1
package/dist/esm/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { Graffiti, type GraffitiSession } from \"@graffiti-garden/api\";\nimport { GraffitiLocalSessionManager } from \"./session-manager.js\";\nimport {\n GraffitiLocalDatabase,\n type GraffitiLocalOptions,\n} from \"./database.js\";\n\nexport type { GraffitiLocalOptions };\n\n/**\n * A local implementation of the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * based on [PouchDB](https://pouchdb.com/). PouchDb will automatically persist data in a local\n * database, either in the browser or in Node.js.\n * It can also be configured to work with an external [CouchDB](https://couchdb.apache.org/) server,\n * although using it with a remote server will not be secure.\n */\nexport class GraffitiLocal extends Graffiti {\n protected sessionManagerLocal = new GraffitiLocalSessionManager();\n login = this.sessionManagerLocal.login.bind(this.sessionManagerLocal);\n logout = this.sessionManagerLocal.logout.bind(this.sessionManagerLocal);\n sessionEvents = this.sessionManagerLocal.sessionEvents;\n\n put: Graffiti[\"put\"];\n get: Graffiti[\"get\"];\n patch: Graffiti[\"patch\"];\n delete: Graffiti[\"delete\"];\n discover: Graffiti[\"discover\"];\n recoverOrphans: Graffiti[\"recoverOrphans\"];\n channelStats: Graffiti[\"channelStats\"];\n
|
|
5
|
-
"mappings": "AAAA,SAAS,gBAAsC;AAC/C,SAAS,mCAAmC;AAC5C;AAAA,EACE;AAAA,OAEK;AAWA,MAAM,sBAAsB,SAAS;AAAA,EAChC,sBAAsB,IAAI,4BAA4B;AAAA,EAChE,QAAQ,KAAK,oBAAoB,MAAM,KAAK,KAAK,mBAAmB;AAAA,EACpE,SAAS,KAAK,oBAAoB,OAAO,KAAK,KAAK,mBAAmB;AAAA,EACtE,gBAAgB,KAAK,oBAAoB;AAAA,EAEzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,SAAgC;AAC1C,UAAM;AAEN,UAAM,sBAAsB,IAAI,sBAAsB,OAAO;AAE7D,SAAK,MAAM,oBAAoB,IAAI,KAAK,mBAAmB;AAC3D,SAAK,MAAM,oBAAoB,IAAI,KAAK,mBAAmB;AAC3D,SAAK,QAAQ,oBAAoB,MAAM,KAAK,mBAAmB;AAC/D,SAAK,SAAS,oBAAoB,OAAO,KAAK,mBAAmB;AACjE,SAAK,WAAW,oBAAoB,SAAS,KAAK,mBAAmB;AACrE,SAAK,iBACH,oBAAoB,eAAe,KAAK,mBAAmB;AAC7D,SAAK,eACH,oBAAoB,aAAa,KAAK,mBAAmB;AAC3D,SAAK,
|
|
4
|
+
"sourcesContent": ["import { Graffiti, type GraffitiSession } from \"@graffiti-garden/api\";\nimport { GraffitiLocalSessionManager } from \"./session-manager.js\";\nimport {\n GraffitiLocalDatabase,\n type GraffitiLocalOptions,\n} from \"./database.js\";\n\nexport type { GraffitiLocalOptions };\n\n/**\n * A local implementation of the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * based on [PouchDB](https://pouchdb.com/). PouchDb will automatically persist data in a local\n * database, either in the browser or in Node.js.\n * It can also be configured to work with an external [CouchDB](https://couchdb.apache.org/) server,\n * although using it with a remote server will not be secure.\n */\nexport class GraffitiLocal extends Graffiti {\n protected sessionManagerLocal = new GraffitiLocalSessionManager();\n login = this.sessionManagerLocal.login.bind(this.sessionManagerLocal);\n logout = this.sessionManagerLocal.logout.bind(this.sessionManagerLocal);\n sessionEvents = this.sessionManagerLocal.sessionEvents;\n\n put: Graffiti[\"put\"];\n get: Graffiti[\"get\"];\n patch: Graffiti[\"patch\"];\n delete: Graffiti[\"delete\"];\n discover: Graffiti[\"discover\"];\n recoverOrphans: Graffiti[\"recoverOrphans\"];\n channelStats: Graffiti[\"channelStats\"];\n continueObjectStream: Graffiti[\"continueObjectStream\"];\n\n constructor(options?: GraffitiLocalOptions) {\n super();\n\n const graffitiPouchDbBase = new GraffitiLocalDatabase(options);\n\n this.put = graffitiPouchDbBase.put.bind(graffitiPouchDbBase);\n this.get = graffitiPouchDbBase.get.bind(graffitiPouchDbBase);\n this.patch = graffitiPouchDbBase.patch.bind(graffitiPouchDbBase);\n this.delete = graffitiPouchDbBase.delete.bind(graffitiPouchDbBase);\n this.discover = graffitiPouchDbBase.discover.bind(graffitiPouchDbBase);\n this.recoverOrphans =\n graffitiPouchDbBase.recoverOrphans.bind(graffitiPouchDbBase);\n this.channelStats =\n graffitiPouchDbBase.channelStats.bind(graffitiPouchDbBase);\n this.continueObjectStream =\n graffitiPouchDbBase.continueObjectStream.bind(graffitiPouchDbBase);\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gBAAsC;AAC/C,SAAS,mCAAmC;AAC5C;AAAA,EACE;AAAA,OAEK;AAWA,MAAM,sBAAsB,SAAS;AAAA,EAChC,sBAAsB,IAAI,4BAA4B;AAAA,EAChE,QAAQ,KAAK,oBAAoB,MAAM,KAAK,KAAK,mBAAmB;AAAA,EACpE,SAAS,KAAK,oBAAoB,OAAO,KAAK,KAAK,mBAAmB;AAAA,EACtE,gBAAgB,KAAK,oBAAoB;AAAA,EAEzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,SAAgC;AAC1C,UAAM;AAEN,UAAM,sBAAsB,IAAI,sBAAsB,OAAO;AAE7D,SAAK,MAAM,oBAAoB,IAAI,KAAK,mBAAmB;AAC3D,SAAK,MAAM,oBAAoB,IAAI,KAAK,mBAAmB;AAC3D,SAAK,QAAQ,oBAAoB,MAAM,KAAK,mBAAmB;AAC/D,SAAK,SAAS,oBAAoB,OAAO,KAAK,mBAAmB;AACjE,SAAK,WAAW,oBAAoB,SAAS,KAAK,mBAAmB;AACrE,SAAK,iBACH,oBAAoB,eAAe,KAAK,mBAAmB;AAC7D,SAAK,eACH,oBAAoB,aAAa,KAAK,mBAAmB;AAC3D,SAAK,uBACH,oBAAoB,qBAAqB,KAAK,mBAAmB;AAAA,EACrE;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/esm/utilities.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utilities.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n GraffitiErrorInvalidSchema,\n
|
|
5
|
-
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["import {\n GraffitiErrorInvalidSchema,\n GraffitiErrorPatchError,\n GraffitiErrorPatchTestFailed,\n} from \"@graffiti-garden/api\";\nimport type {\n GraffitiObject,\n GraffitiObjectBase,\n GraffitiPatch,\n JSONSchema,\n GraffitiSession,\n GraffitiObjectUrl,\n} from \"@graffiti-garden/api\";\nimport type { Ajv } from \"ajv\";\nimport type { applyPatch } from \"fast-json-patch\";\n\nexport function unpackObjectUrl(url: string | GraffitiObjectUrl) {\n return typeof url === \"string\" ? url : url.url;\n}\n\nexport function randomBase64(numBytes: number = 24) {\n const bytes = new Uint8Array(numBytes);\n crypto.getRandomValues(bytes);\n // Convert it to base64\n const base64 = btoa(String.fromCodePoint(...bytes));\n // Make sure it is url safe\n return base64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/\\=+$/, \"\");\n}\n\nexport function applyGraffitiPatch<Prop extends keyof GraffitiPatch>(\n apply: typeof applyPatch,\n prop: Prop,\n patch: GraffitiPatch,\n object: GraffitiObjectBase,\n): void {\n const ops = patch[prop];\n if (!ops || !ops.length) return;\n try {\n object[prop] = apply(object[prop], ops, true, false).newDocument;\n } catch (e) {\n if (\n typeof e === \"object\" &&\n e &&\n \"name\" in e &&\n typeof e.name === \"string\" &&\n \"message\" in e &&\n typeof e.message === \"string\"\n ) {\n if (e.name === \"TEST_OPERATION_FAILED\") {\n throw new GraffitiErrorPatchTestFailed(e.message);\n } else {\n throw new GraffitiErrorPatchError(e.name + \": \" + e.message);\n }\n } else {\n throw e;\n }\n }\n}\n\nexport function compileGraffitiObjectSchema<Schema extends JSONSchema>(\n ajv: Ajv,\n schema: Schema,\n) {\n try {\n // Force the validation guard because\n // it is too big for the type checker.\n // Fortunately json-schema-to-ts is\n // well tested against ajv.\n return ajv.compile(schema) as (\n data: GraffitiObjectBase,\n ) => data is GraffitiObject<Schema>;\n } catch (error) {\n throw new GraffitiErrorInvalidSchema(\n error instanceof Error ? error.message : undefined,\n );\n }\n}\n\nexport function maskGraffitiObject(\n object: GraffitiObjectBase,\n channels: string[],\n session?: GraffitiSession | null,\n): void {\n if (object.actor !== session?.actor) {\n object.allowed = object.allowed && session ? [session.actor] : undefined;\n object.channels = object.channels.filter((channel) =>\n channels.includes(channel),\n );\n }\n}\nexport function isActorAllowedGraffitiObject(\n object: GraffitiObjectBase,\n session?: GraffitiSession | null,\n) {\n return (\n object.allowed === undefined ||\n object.allowed === null ||\n (!!session?.actor &&\n (object.actor === session.actor ||\n object.allowed.includes(session.actor)))\n );\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAYA,SAAS,gBAAgB,KAAiC;AAC/D,SAAO,OAAO,QAAQ,WAAW,MAAM,IAAI;AAC7C;AAEO,SAAS,aAAa,WAAmB,IAAI;AAClD,QAAM,QAAQ,IAAI,WAAW,QAAQ;AACrC,SAAO,gBAAgB,KAAK;AAE5B,QAAM,SAAS,KAAK,OAAO,cAAc,GAAG,KAAK,CAAC;AAElD,SAAO,OAAO,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,QAAQ,EAAE;AAC1E;AAEO,SAAS,mBACd,OACA,MACA,OACA,QACM;AACN,QAAM,MAAM,MAAM,IAAI;AACtB,MAAI,CAAC,OAAO,CAAC,IAAI,OAAQ;AACzB,MAAI;AACF,WAAO,IAAI,IAAI,MAAM,OAAO,IAAI,GAAG,KAAK,MAAM,KAAK,EAAE;AAAA,EACvD,SAAS,GAAG;AACV,QACE,OAAO,MAAM,YACb,KACA,UAAU,KACV,OAAO,EAAE,SAAS,YAClB,aAAa,KACb,OAAO,EAAE,YAAY,UACrB;AACA,UAAI,EAAE,SAAS,yBAAyB;AACtC,cAAM,IAAI,6BAA6B,EAAE,OAAO;AAAA,MAClD,OAAO;AACL,cAAM,IAAI,wBAAwB,EAAE,OAAO,OAAO,EAAE,OAAO;AAAA,MAC7D;AAAA,IACF,OAAO;AACL,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,SAAS,4BACd,KACA,QACA;AACA,MAAI;AAKF,WAAO,IAAI,QAAQ,MAAM;AAAA,EAG3B,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,SAAS,mBACd,QACA,UACA,SACM;AACN,MAAI,OAAO,UAAU,SAAS,OAAO;AACnC,WAAO,UAAU,OAAO,WAAW,UAAU,CAAC,QAAQ,KAAK,IAAI;AAC/D,WAAO,WAAW,OAAO,SAAS;AAAA,MAAO,CAAC,YACxC,SAAS,SAAS,OAAO;AAAA,IAC3B;AAAA,EACF;AACF;AACO,SAAS,6BACd,QACA,SACA;AACA,SACE,OAAO,YAAY,UACnB,OAAO,YAAY,QAClB,CAAC,CAAC,SAAS,UACT,OAAO,UAAU,QAAQ,SACxB,OAAO,QAAQ,SAAS,QAAQ,KAAK;AAE7C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare class GraffitiLocal extends Graffiti {
|
|
|
24
24
|
discover: Graffiti["discover"];
|
|
25
25
|
recoverOrphans: Graffiti["recoverOrphans"];
|
|
26
26
|
channelStats: Graffiti["channelStats"];
|
|
27
|
-
|
|
27
|
+
continueObjectStream: Graffiti["continueObjectStream"];
|
|
28
28
|
constructor(options?: GraffitiLocalOptions);
|
|
29
29
|
}
|
|
30
30
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,oBAAoB,EAAE,CAAC;AAErC;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,QAAQ;IACzC,SAAS,CAAC,mBAAmB,8BAAqC;IAClE,KAAK;aA+
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,oBAAoB,EAAE,CAAC;AAErC;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,QAAQ;IACzC,SAAS,CAAC,mBAAmB,8BAAqC;IAClE,KAAK;aA+Bq7tB,CAAC;aAA6iB,CAAC;wBA/Bn6uB;IACtE,MAAM,8CAAkE;IACxE,aAAa,cAA0C;IAEvD,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACrB,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACrB,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IACzB,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3B,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC/B,cAAc,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC3C,YAAY,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,oBAAoB,EAAE,QAAQ,CAAC,sBAAsB,CAAC,CAAC;gBAE3C,OAAO,CAAC,EAAE,oBAAoB;CAiB3C"}
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,eAAe,EACf,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,UAE9D;AAED,wBAAgB,YAAY,CAAC,QAAQ,GAAE,MAAW,UAOjD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,aAAa,EACjE,KAAK,EAAE,OAAO,UAAU,EACxB,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,kBAAkB,GACzB,IAAI,CAuBN;AAED,wBAAgB,2BAA2B,CAAC,MAAM,SAAS,UAAU,EACnE,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,UAQJ,kBAAkB,KACrB,IAAI,IAAI,cAAc,CAAC,MAAM,CAAC,CAMtC;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAC/B,IAAI,CAON;AACD,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,kBAAkB,EAC1B,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,WASjC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graffiti-garden/implementation-local",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "A local implementation of the Graffiti API using PouchDB",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -82,7 +82,6 @@
|
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"@graffiti-garden/api": "^0.6.0",
|
|
85
|
-
"@repeaterjs/repeater": "^3.0.6",
|
|
86
85
|
"@types/pouchdb": "^6.4.2",
|
|
87
86
|
"ajv": "^8.17.1",
|
|
88
87
|
"fast-json-patch": "^3.1.1",
|
package/src/database.ts
CHANGED
|
@@ -4,9 +4,8 @@ import type {
|
|
|
4
4
|
GraffitiObjectUrl,
|
|
5
5
|
JSONSchema,
|
|
6
6
|
GraffitiSession,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
ChannelStats,
|
|
7
|
+
GraffitiObjectStreamContinue,
|
|
8
|
+
GraffitiObjectStreamContinueEntry,
|
|
10
9
|
} from "@graffiti-garden/api";
|
|
11
10
|
import {
|
|
12
11
|
GraffitiErrorNotFound,
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
compileGraffitiObjectSchema,
|
|
23
22
|
unpackObjectUrl,
|
|
24
23
|
} from "./utilities.js";
|
|
25
|
-
import { Repeater } from "@repeaterjs/repeater";
|
|
26
24
|
import type Ajv from "ajv";
|
|
27
25
|
import type { applyPatch } from "fast-json-patch";
|
|
28
26
|
|
|
@@ -68,6 +66,7 @@ export interface GraffitiLocalOptions {
|
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
const DEFAULT_ORIGIN = "graffiti:local:";
|
|
69
|
+
const LAST_MODIFIED_BUFFER = 60000;
|
|
71
70
|
|
|
72
71
|
type GraffitiObjectWithTombstone = GraffitiObjectBase & { tombstone: boolean };
|
|
73
72
|
|
|
@@ -170,8 +169,8 @@ export class GraffitiLocalDatabase
|
|
|
170
169
|
protected get applyPatch() {
|
|
171
170
|
if (!this.applyPatch_) {
|
|
172
171
|
this.applyPatch_ = (async () => {
|
|
173
|
-
const
|
|
174
|
-
return applyPatch;
|
|
172
|
+
const imported = await import("fast-json-patch");
|
|
173
|
+
return imported.applyPatch || imported.default.applyPatch;
|
|
175
174
|
})();
|
|
176
175
|
}
|
|
177
176
|
return this.applyPatch_;
|
|
@@ -587,103 +586,105 @@ export class GraffitiLocalDatabase
|
|
|
587
586
|
};
|
|
588
587
|
}
|
|
589
588
|
|
|
590
|
-
protected
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
// Don't return tombstones on the first pass
|
|
589
|
+
protected async *streamObjects<Schema extends JSONSchema>(
|
|
590
|
+
index: string,
|
|
591
|
+
startkey: string,
|
|
592
|
+
endkey: string,
|
|
593
|
+
validate: ReturnType<typeof compileGraffitiObjectSchema<Schema>>,
|
|
594
|
+
session: GraffitiSession | undefined | null,
|
|
595
|
+
ifModifiedSince: number | undefined,
|
|
596
|
+
channels?: string[],
|
|
597
|
+
processedIds?: Set<string>,
|
|
598
|
+
): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {
|
|
602
599
|
const showTombstones = ifModifiedSince !== undefined;
|
|
603
600
|
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
601
|
+
const result = await (
|
|
602
|
+
await this.db
|
|
603
|
+
).query<GraffitiObjectWithTombstone>(index, {
|
|
604
|
+
startkey,
|
|
605
|
+
endkey,
|
|
606
|
+
include_docs: true,
|
|
607
|
+
});
|
|
608
608
|
|
|
609
|
-
|
|
609
|
+
for (const row of result.rows) {
|
|
610
|
+
const doc = row.doc;
|
|
611
|
+
if (!doc) continue;
|
|
610
612
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const startkey = keyPrefix + startKeySuffix;
|
|
614
|
-
const endkey = keyPrefix + endKeySuffix;
|
|
613
|
+
if (processedIds?.has(doc._id)) continue;
|
|
614
|
+
processedIds?.add(doc._id);
|
|
615
615
|
|
|
616
|
-
|
|
617
|
-
await this.db
|
|
618
|
-
).query<GraffitiObjectWithTombstone>(
|
|
619
|
-
"indexes/objectsPerChannelAndLastModified",
|
|
620
|
-
{ startkey, endkey, include_docs: true },
|
|
621
|
-
);
|
|
616
|
+
if (!showTombstones && doc.tombstone) continue;
|
|
622
617
|
|
|
623
|
-
|
|
624
|
-
const doc = row.doc;
|
|
625
|
-
if (!doc) continue;
|
|
618
|
+
const object = this.extractGraffitiObject(doc);
|
|
626
619
|
|
|
627
|
-
|
|
620
|
+
if (channels) {
|
|
621
|
+
if (!isActorAllowedGraffitiObject(object, session)) continue;
|
|
622
|
+
maskGraffitiObject(object, channels, session);
|
|
623
|
+
}
|
|
628
624
|
|
|
629
|
-
|
|
625
|
+
if (!validate(object)) continue;
|
|
630
626
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
627
|
+
yield doc.tombstone
|
|
628
|
+
? {
|
|
629
|
+
tombstone: true,
|
|
630
|
+
object: {
|
|
631
|
+
url: object.url,
|
|
632
|
+
lastModified: object.lastModified,
|
|
633
|
+
},
|
|
634
|
+
}
|
|
635
|
+
: { object };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
634
638
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
+
protected async *discoverMeta<Schema extends JSONSchema>(
|
|
640
|
+
args: Parameters<typeof Graffiti.prototype.discover<Schema>>,
|
|
641
|
+
ifModifiedSince?: number,
|
|
642
|
+
): AsyncGenerator<
|
|
643
|
+
GraffitiObjectStreamContinueEntry<Schema>,
|
|
644
|
+
number | undefined
|
|
645
|
+
> {
|
|
646
|
+
const [channels, schema, session] = args;
|
|
647
|
+
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
648
|
+
const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(
|
|
649
|
+
schema,
|
|
650
|
+
ifModifiedSince,
|
|
651
|
+
);
|
|
639
652
|
|
|
640
|
-
|
|
641
|
-
if (!isActorAllowedGraffitiObject(doc, session)) continue;
|
|
653
|
+
const processedIds = new Set<string>();
|
|
642
654
|
|
|
643
|
-
|
|
644
|
-
// if the user is not the owner
|
|
645
|
-
maskGraffitiObject(object, channels, session);
|
|
655
|
+
const startTime = new Date().getTime();
|
|
646
656
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
...(doc.tombstone ? { tombstone: true } : {}),
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
stop();
|
|
657
|
-
|
|
658
|
-
const cursor: string =
|
|
659
|
-
"discover:" +
|
|
660
|
-
JSON.stringify({
|
|
661
|
-
channels,
|
|
662
|
-
schema,
|
|
663
|
-
ifModifiedSince,
|
|
664
|
-
actor: session?.actor,
|
|
665
|
-
});
|
|
666
|
-
return {
|
|
667
|
-
cursor,
|
|
668
|
-
continue: () =>
|
|
669
|
-
this.continueStream(cursor, session) as GraffitiStream<
|
|
670
|
-
GraffitiObject<Schema>
|
|
671
|
-
>,
|
|
672
|
-
};
|
|
673
|
-
});
|
|
657
|
+
for (const channel of channels) {
|
|
658
|
+
const keyPrefix = encodeURIComponent(channel) + "/";
|
|
659
|
+
const startkey = keyPrefix + startKeySuffix;
|
|
660
|
+
const endkey = keyPrefix + endKeySuffix;
|
|
674
661
|
|
|
675
|
-
|
|
676
|
-
|
|
662
|
+
const iterator = this.streamObjects<Schema>(
|
|
663
|
+
"indexes/objectsPerChannelAndLastModified",
|
|
664
|
+
startkey,
|
|
665
|
+
endkey,
|
|
666
|
+
validate,
|
|
667
|
+
session,
|
|
668
|
+
ifModifiedSince,
|
|
669
|
+
channels,
|
|
670
|
+
processedIds,
|
|
671
|
+
);
|
|
677
672
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
673
|
+
for await (const result of iterator) yield result;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Subtract a minute to make sure we don't miss any objects
|
|
677
|
+
return startTime - LAST_MODIFIED_BUFFER;
|
|
678
|
+
}
|
|
681
679
|
|
|
682
|
-
protected recoverOrphansMeta<Schema extends JSONSchema>(
|
|
683
|
-
|
|
684
|
-
session: GraffitiSession,
|
|
680
|
+
protected async *recoverOrphansMeta<Schema extends JSONSchema>(
|
|
681
|
+
args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>,
|
|
685
682
|
ifModifiedSince?: number,
|
|
686
|
-
):
|
|
683
|
+
): AsyncGenerator<
|
|
684
|
+
GraffitiObjectStreamContinueEntry<Schema>,
|
|
685
|
+
number | undefined
|
|
686
|
+
> {
|
|
687
|
+
const [schema, session] = args;
|
|
687
688
|
const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(
|
|
688
689
|
schema,
|
|
689
690
|
ifModifiedSince,
|
|
@@ -692,139 +693,196 @@ export class GraffitiLocalDatabase
|
|
|
692
693
|
const startkey = keyPrefix + startKeySuffix;
|
|
693
694
|
const endkey = keyPrefix + endKeySuffix;
|
|
694
695
|
|
|
695
|
-
|
|
696
|
-
const showTombstones = ifModifiedSince !== undefined;
|
|
696
|
+
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
697
697
|
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
startkey,
|
|
709
|
-
endkey,
|
|
710
|
-
include_docs: true,
|
|
711
|
-
},
|
|
712
|
-
);
|
|
698
|
+
const startTime = new Date().getTime();
|
|
699
|
+
|
|
700
|
+
const iterator = this.streamObjects<Schema>(
|
|
701
|
+
"indexes/orphansPerActorAndLastModified",
|
|
702
|
+
startkey,
|
|
703
|
+
endkey,
|
|
704
|
+
validate,
|
|
705
|
+
session,
|
|
706
|
+
ifModifiedSince,
|
|
707
|
+
);
|
|
713
708
|
|
|
714
|
-
|
|
715
|
-
const doc = row.doc;
|
|
716
|
-
if (!doc) continue;
|
|
709
|
+
for await (const result of iterator) yield result;
|
|
717
710
|
|
|
718
|
-
|
|
711
|
+
return startTime - LAST_MODIFIED_BUFFER;
|
|
712
|
+
}
|
|
719
713
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
714
|
+
protected discoverCursor(
|
|
715
|
+
args: Parameters<typeof Graffiti.prototype.discover<{}>>,
|
|
716
|
+
ifModifiedSince?: number,
|
|
717
|
+
): string {
|
|
718
|
+
return (
|
|
719
|
+
"discover:" +
|
|
720
|
+
JSON.stringify({
|
|
721
|
+
channels: args[0],
|
|
722
|
+
schema: args[1],
|
|
723
|
+
actor: args[2]?.actor,
|
|
724
|
+
ifModifiedSince: ifModifiedSince,
|
|
725
|
+
})
|
|
726
|
+
);
|
|
727
|
+
}
|
|
723
728
|
|
|
724
|
-
|
|
725
|
-
|
|
729
|
+
protected async *discoverContinue<Schema extends JSONSchema>(
|
|
730
|
+
args: Parameters<typeof Graffiti.prototype.discover<Schema>>,
|
|
731
|
+
ifModifiedSince?: number,
|
|
732
|
+
): GraffitiObjectStreamContinue<Schema> {
|
|
733
|
+
const iterator = this.discoverMeta(args, ifModifiedSince);
|
|
726
734
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
735
|
+
while (true) {
|
|
736
|
+
const result = await iterator.next();
|
|
737
|
+
if (result.done) {
|
|
738
|
+
const ifModifiedSince = result.value;
|
|
739
|
+
return {
|
|
740
|
+
continue: () => this.discoverContinue<Schema>(args, ifModifiedSince),
|
|
741
|
+
cursor: this.discoverCursor(args, ifModifiedSince),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
yield result.value;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
discover: Graffiti["discover"] = (...args) => {
|
|
749
|
+
const iterator = this.discoverMeta(args);
|
|
750
|
+
|
|
751
|
+
const this_ = this;
|
|
752
|
+
return (async function* () {
|
|
753
|
+
while (true) {
|
|
754
|
+
const result = await iterator.next();
|
|
755
|
+
if (result.done) {
|
|
756
|
+
return {
|
|
757
|
+
continue: () =>
|
|
758
|
+
this_.discoverContinue<(typeof args)[1]>(args, result.value),
|
|
759
|
+
cursor: this_.discoverCursor(args, result.value),
|
|
760
|
+
};
|
|
734
761
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
762
|
+
// Make sure to filter out tombstones
|
|
763
|
+
if (result.value.tombstone) continue;
|
|
764
|
+
yield result.value;
|
|
765
|
+
}
|
|
766
|
+
})();
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
protected recoverOrphansCursor(
|
|
770
|
+
args: Parameters<typeof Graffiti.prototype.recoverOrphans<{}>>,
|
|
771
|
+
ifModifiedSince?: number,
|
|
772
|
+
): string {
|
|
773
|
+
return (
|
|
774
|
+
"orphans:" +
|
|
775
|
+
JSON.stringify({
|
|
776
|
+
schema: args[0],
|
|
777
|
+
actor: args[1]?.actor,
|
|
778
|
+
ifModifiedSince,
|
|
779
|
+
})
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
protected async *recoverOrphansContinue<Schema extends JSONSchema>(
|
|
784
|
+
args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>,
|
|
785
|
+
ifModifiedSince?: number,
|
|
786
|
+
): GraffitiObjectStreamContinue<Schema> {
|
|
787
|
+
const iterator = this.recoverOrphansMeta(args, ifModifiedSince);
|
|
788
|
+
|
|
789
|
+
while (true) {
|
|
790
|
+
const result = await iterator.next();
|
|
791
|
+
if (result.done) {
|
|
792
|
+
const ifModifiedSince = result.value;
|
|
743
793
|
return {
|
|
744
|
-
cursor,
|
|
745
794
|
continue: () =>
|
|
746
|
-
this.
|
|
747
|
-
|
|
748
|
-
>,
|
|
795
|
+
this.recoverOrphansContinue<Schema>(args, ifModifiedSince),
|
|
796
|
+
cursor: this.recoverOrphansCursor(args, ifModifiedSince),
|
|
749
797
|
};
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
|
|
798
|
+
}
|
|
799
|
+
yield result.value;
|
|
800
|
+
}
|
|
753
801
|
}
|
|
754
802
|
|
|
755
803
|
recoverOrphans: Graffiti["recoverOrphans"] = (...args) => {
|
|
756
|
-
|
|
804
|
+
const iterator = this.recoverOrphansMeta(args);
|
|
805
|
+
|
|
806
|
+
const this_ = this;
|
|
807
|
+
return (async function* () {
|
|
808
|
+
while (true) {
|
|
809
|
+
const result = await iterator.next();
|
|
810
|
+
if (result.done) {
|
|
811
|
+
return {
|
|
812
|
+
continue: () =>
|
|
813
|
+
this_.recoverOrphansContinue<(typeof args)[0]>(
|
|
814
|
+
args,
|
|
815
|
+
result.value,
|
|
816
|
+
),
|
|
817
|
+
cursor: this_.recoverOrphansCursor(args, result.value),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
// Make sure to filter out tombstones
|
|
821
|
+
if (result.value.tombstone) continue;
|
|
822
|
+
yield result.value;
|
|
823
|
+
}
|
|
824
|
+
})();
|
|
757
825
|
};
|
|
758
826
|
|
|
759
827
|
channelStats: Graffiti["channelStats"] = (session) => {
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
});
|
|
771
|
-
for (const row of result.rows) {
|
|
772
|
-
const channelEncoded = row.key.split("/")[1];
|
|
773
|
-
if (typeof channelEncoded !== "string") continue;
|
|
774
|
-
const { count, max: lastModified } = row.value;
|
|
775
|
-
if (typeof count !== "number" || typeof lastModified !== "number")
|
|
776
|
-
continue;
|
|
777
|
-
await push({
|
|
778
|
-
value: {
|
|
779
|
-
channel: decodeURIComponent(channelEncoded),
|
|
780
|
-
count,
|
|
781
|
-
lastModified,
|
|
782
|
-
},
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
stop();
|
|
786
|
-
const cursor = "channel-stats";
|
|
787
|
-
return {
|
|
788
|
-
cursor,
|
|
789
|
-
continue: () =>
|
|
790
|
-
this.continueStream(
|
|
791
|
-
cursor,
|
|
792
|
-
session,
|
|
793
|
-
) as GraffitiStream<ChannelStats>,
|
|
794
|
-
};
|
|
828
|
+
const this_ = this;
|
|
829
|
+
return (async function* () {
|
|
830
|
+
const keyPrefix = encodeURIComponent(session.actor) + "/";
|
|
831
|
+
const result = await (
|
|
832
|
+
await this_.db
|
|
833
|
+
).query("indexes/channelStatsPerActor", {
|
|
834
|
+
startkey: keyPrefix,
|
|
835
|
+
endkey: keyPrefix + "\uffff",
|
|
836
|
+
reduce: true,
|
|
837
|
+
group: true,
|
|
795
838
|
});
|
|
796
|
-
|
|
797
|
-
|
|
839
|
+
for (const row of result.rows) {
|
|
840
|
+
const channelEncoded = row.key.split("/")[1];
|
|
841
|
+
if (typeof channelEncoded !== "string") continue;
|
|
842
|
+
const { count, max: lastModified } = row.value;
|
|
843
|
+
if (typeof count !== "number" || typeof lastModified !== "number")
|
|
844
|
+
continue;
|
|
845
|
+
yield {
|
|
846
|
+
value: {
|
|
847
|
+
channel: decodeURIComponent(channelEncoded),
|
|
848
|
+
count,
|
|
849
|
+
lastModified,
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
})();
|
|
798
854
|
};
|
|
799
855
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
856
|
+
continueObjectStream: Graffiti["continueObjectStream"] = (
|
|
857
|
+
cursor,
|
|
858
|
+
session,
|
|
859
|
+
) => {
|
|
860
|
+
if (cursor.startsWith("discover:")) {
|
|
861
|
+
const { channels, schema, actor, ifModifiedSince } = JSON.parse(
|
|
862
|
+
cursor.slice("discover:".length),
|
|
863
|
+
);
|
|
864
|
+
if (actor && actor !== session?.actor) {
|
|
803
865
|
throw new GraffitiErrorForbidden(
|
|
804
|
-
"
|
|
866
|
+
"Cannot continue a cursor for another actor",
|
|
805
867
|
);
|
|
806
868
|
}
|
|
807
|
-
return this.
|
|
808
|
-
|
|
869
|
+
return this.discoverContinue<{}>(
|
|
870
|
+
[channels, schema, session],
|
|
871
|
+
ifModifiedSince,
|
|
872
|
+
);
|
|
873
|
+
} else if (cursor.startsWith("orphans:")) {
|
|
809
874
|
const { schema, actor, ifModifiedSince } = JSON.parse(
|
|
810
|
-
cursor.slice("
|
|
875
|
+
cursor.slice("orphans:".length),
|
|
811
876
|
);
|
|
812
|
-
if (!session ||
|
|
877
|
+
if (!session || actor !== session?.actor) {
|
|
813
878
|
throw new GraffitiErrorForbidden(
|
|
814
|
-
"
|
|
879
|
+
"Cannot continue a cursor for another actor",
|
|
815
880
|
);
|
|
816
881
|
}
|
|
817
|
-
return this.
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
cursor.slice("discover:".length),
|
|
882
|
+
return this.recoverOrphansContinue<{}>(
|
|
883
|
+
[schema, session],
|
|
884
|
+
ifModifiedSince,
|
|
821
885
|
);
|
|
822
|
-
if (session?.actor !== actor) {
|
|
823
|
-
throw new GraffitiErrorForbidden(
|
|
824
|
-
"You must be logged in as the actor same actor who started the stream",
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
return this.discoverMeta(channels, schema, session, ifModifiedSince);
|
|
828
886
|
} else {
|
|
829
887
|
throw new GraffitiErrorNotFound("Cursor not found");
|
|
830
888
|
}
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ export class GraffitiLocal extends Graffiti {
|
|
|
27
27
|
discover: Graffiti["discover"];
|
|
28
28
|
recoverOrphans: Graffiti["recoverOrphans"];
|
|
29
29
|
channelStats: Graffiti["channelStats"];
|
|
30
|
-
|
|
30
|
+
continueObjectStream: Graffiti["continueObjectStream"];
|
|
31
31
|
|
|
32
32
|
constructor(options?: GraffitiLocalOptions) {
|
|
33
33
|
super();
|
|
@@ -43,7 +43,7 @@ export class GraffitiLocal extends Graffiti {
|
|
|
43
43
|
graffitiPouchDbBase.recoverOrphans.bind(graffitiPouchDbBase);
|
|
44
44
|
this.channelStats =
|
|
45
45
|
graffitiPouchDbBase.channelStats.bind(graffitiPouchDbBase);
|
|
46
|
-
this.
|
|
47
|
-
graffitiPouchDbBase.
|
|
46
|
+
this.continueObjectStream =
|
|
47
|
+
graffitiPouchDbBase.continueObjectStream.bind(graffitiPouchDbBase);
|
|
48
48
|
}
|
|
49
49
|
}
|