@graffiti-garden/implementation-local 0.6.3 → 1.0.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.
Files changed (67) hide show
  1. package/README.md +0 -1
  2. package/dist/browser/ajv-IY2ZY7VT.js +9 -0
  3. package/dist/browser/ajv-IY2ZY7VT.js.map +7 -0
  4. package/dist/browser/{chunk-KNUPPOQC.js → chunk-GE6AZATH.js} +2 -2
  5. package/dist/browser/{chunk-KNUPPOQC.js.map → chunk-GE6AZATH.js.map} +1 -1
  6. package/dist/browser/{index-browser.es-G37SKL53.js → index-browser.es-UXYPGJ2M.js} +2 -2
  7. package/dist/browser/{index-browser.es-G37SKL53.js.map → index-browser.es-UXYPGJ2M.js.map} +1 -1
  8. package/dist/browser/index.js +11 -2
  9. package/dist/browser/index.js.map +4 -4
  10. package/dist/cjs/identity.js +112 -0
  11. package/dist/cjs/identity.js.map +7 -0
  12. package/dist/cjs/index.js +43 -22
  13. package/dist/cjs/index.js.map +2 -2
  14. package/dist/cjs/media.js +111 -0
  15. package/dist/cjs/media.js.map +7 -0
  16. package/dist/cjs/objects.js +307 -0
  17. package/dist/cjs/objects.js.map +7 -0
  18. package/dist/cjs/tests.spec.js +1 -2
  19. package/dist/cjs/tests.spec.js.map +2 -2
  20. package/dist/cjs/utilities.js +68 -43
  21. package/dist/cjs/utilities.js.map +2 -2
  22. package/dist/esm/identity.js +92 -0
  23. package/dist/esm/identity.js.map +7 -0
  24. package/dist/esm/index.js +43 -24
  25. package/dist/esm/index.js.map +2 -2
  26. package/dist/esm/media.js +91 -0
  27. package/dist/esm/media.js.map +7 -0
  28. package/dist/esm/objects.js +285 -0
  29. package/dist/esm/objects.js.map +7 -0
  30. package/dist/esm/tests.spec.js +2 -4
  31. package/dist/esm/tests.spec.js.map +2 -2
  32. package/dist/esm/utilities.js +69 -48
  33. package/dist/esm/utilities.js.map +2 -2
  34. package/dist/{session-manager.d.ts → identity.d.ts} +7 -5
  35. package/dist/identity.d.ts.map +1 -0
  36. package/dist/index.d.ts +15 -13
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/media.d.ts +9 -0
  39. package/dist/media.d.ts.map +1 -0
  40. package/dist/objects.d.ts +63 -0
  41. package/dist/objects.d.ts.map +1 -0
  42. package/dist/utilities.d.ts +19 -8
  43. package/dist/utilities.d.ts.map +1 -1
  44. package/package.json +31 -19
  45. package/src/identity.ts +131 -0
  46. package/src/index.ts +44 -29
  47. package/src/media.ts +106 -0
  48. package/src/objects.ts +431 -0
  49. package/src/tests.spec.ts +2 -4
  50. package/src/utilities.ts +67 -87
  51. package/dist/browser/ajv-6AI3HK2A.js +0 -9
  52. package/dist/browser/ajv-6AI3HK2A.js.map +0 -7
  53. package/dist/browser/fast-json-patch-ZE7SZEYK.js +0 -19
  54. package/dist/browser/fast-json-patch-ZE7SZEYK.js.map +0 -7
  55. package/dist/cjs/database.js +0 -621
  56. package/dist/cjs/database.js.map +0 -7
  57. package/dist/cjs/session-manager.js +0 -107
  58. package/dist/cjs/session-manager.js.map +0 -7
  59. package/dist/database.d.ts +0 -105
  60. package/dist/database.d.ts.map +0 -1
  61. package/dist/esm/database.js +0 -603
  62. package/dist/esm/database.js.map +0 -7
  63. package/dist/esm/session-manager.js +0 -87
  64. package/dist/esm/session-manager.js.map +0 -7
  65. package/dist/session-manager.d.ts.map +0 -1
  66. package/src/database.ts +0 -911
  67. package/src/session-manager.ts +0 -123
package/src/objects.ts ADDED
@@ -0,0 +1,431 @@
1
+ import type {
2
+ Graffiti,
3
+ GraffitiObjectBase,
4
+ JSONSchema,
5
+ GraffitiSession,
6
+ GraffitiObjectStreamContinue,
7
+ GraffitiObjectStreamContinueEntry,
8
+ } from "@graffiti-garden/api";
9
+ import {
10
+ GraffitiErrorNotFound,
11
+ GraffitiErrorSchemaMismatch,
12
+ GraffitiErrorForbidden,
13
+ unpackObjectUrl,
14
+ maskGraffitiObject,
15
+ isActorAllowedGraffitiObject,
16
+ compileGraffitiObjectSchema,
17
+ } from "@graffiti-garden/api";
18
+ import { randomBase64, decodeObjectUrl, encodeObjectUrl } from "./utilities.js";
19
+ import type Ajv from "ajv";
20
+
21
+ /**
22
+ * Constructor options for the GraffitiPoubchDB class.
23
+ */
24
+ export interface GraffitiLocalOptions {
25
+ /**
26
+ * Options to pass to the PouchDB constructor.
27
+ * Defaults to `{ name: "graffitiDb" }`.
28
+ *
29
+ * See the [PouchDB documentation](https://pouchdb.com/api.html#create_database)
30
+ * for available options.
31
+ */
32
+ pouchDBOptions?: PouchDB.Configuration.DatabaseConfiguration;
33
+ /**
34
+ * Wait at least this long (in milliseconds) before continuing a stream.
35
+ * A basic form of rate limiting. Defaults to 2 seconds.
36
+ */
37
+ continueBuffer?: number;
38
+ }
39
+
40
+ type GraffitiObjectData = {
41
+ tombstone: boolean;
42
+ value: {};
43
+ channels: string[];
44
+ allowed?: string[] | null;
45
+ lastModified: number;
46
+ };
47
+
48
+ type ContinueDiscoverParams = {
49
+ lastDiscovered: number;
50
+ ifModifiedSince: number;
51
+ };
52
+
53
+ /**
54
+ * An implementation of only the database operations of the
55
+ * GraffitiAPI without synchronization or session management.
56
+ */
57
+ export class GraffitiLocalObjects {
58
+ protected db_: Promise<PouchDB.Database<GraffitiObjectData>> | undefined;
59
+ protected ajv_: Promise<Ajv> | undefined;
60
+ protected readonly options: GraffitiLocalOptions;
61
+ protected operationClock: number = 0;
62
+
63
+ get db() {
64
+ if (!this.db_) {
65
+ this.db_ = (async () => {
66
+ const { default: PouchDB } = await import("pouchdb");
67
+ const pouchDbOptions = {
68
+ name: "graffitiDb",
69
+ ...this.options.pouchDBOptions,
70
+ };
71
+ const db = new PouchDB<GraffitiObjectData>(
72
+ pouchDbOptions.name,
73
+ pouchDbOptions,
74
+ );
75
+ await db
76
+ //@ts-ignore
77
+ .put({
78
+ _id: "_design/indexes",
79
+ views: {
80
+ objectsPerChannelAndLastModified: {
81
+ map: function (object: GraffitiObjectData) {
82
+ const paddedLastModified = object.lastModified
83
+ .toString()
84
+ .padStart(15, "0");
85
+ object.channels.forEach(function (channel) {
86
+ const id =
87
+ encodeURIComponent(channel) + "/" + paddedLastModified;
88
+ //@ts-ignore
89
+ emit(id);
90
+ });
91
+ }.toString(),
92
+ },
93
+ },
94
+ })
95
+ //@ts-ignore
96
+ .catch((error) => {
97
+ if (
98
+ error &&
99
+ typeof error === "object" &&
100
+ "name" in error &&
101
+ error.name === "conflict"
102
+ ) {
103
+ // Design document already exists
104
+ return;
105
+ } else {
106
+ throw error;
107
+ }
108
+ });
109
+ return db;
110
+ })();
111
+ }
112
+ return this.db_;
113
+ }
114
+
115
+ protected get ajv() {
116
+ if (!this.ajv_) {
117
+ this.ajv_ = (async () => {
118
+ const { default: Ajv } = await import("ajv");
119
+ return new Ajv({ strict: false });
120
+ })();
121
+ }
122
+ return this.ajv_;
123
+ }
124
+
125
+ constructor(options?: GraffitiLocalOptions) {
126
+ this.options = options ?? {};
127
+ }
128
+
129
+ get: Graffiti["get"] = async (...args) => {
130
+ const [urlObject, schema, session] = args;
131
+ const url = unpackObjectUrl(urlObject);
132
+
133
+ let doc: GraffitiObjectData;
134
+ try {
135
+ doc = await (await this.db).get(url);
136
+ } catch (error) {
137
+ throw new GraffitiErrorNotFound(
138
+ "The object you are trying to get either does not exist or you are not allowed to see it",
139
+ );
140
+ }
141
+
142
+ if (doc.tombstone) {
143
+ throw new GraffitiErrorNotFound(
144
+ "The object you are trying to get either does not exist or you are not allowed to see it",
145
+ );
146
+ }
147
+
148
+ const { actor } = decodeObjectUrl(url);
149
+ const { value, channels, allowed } = doc;
150
+ const object: GraffitiObjectBase = {
151
+ value,
152
+ channels,
153
+ allowed,
154
+ url,
155
+ actor,
156
+ };
157
+
158
+ if (!isActorAllowedGraffitiObject(object, session)) {
159
+ throw new GraffitiErrorNotFound(
160
+ "The object you are trying to get either does not exist or you are not allowed to see it",
161
+ );
162
+ }
163
+
164
+ // Mask out the allowed list and channels
165
+ // if the user is not the owner
166
+ maskGraffitiObject(object, [], session);
167
+
168
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
169
+ if (!validate(object)) {
170
+ throw new GraffitiErrorSchemaMismatch();
171
+ }
172
+ return object;
173
+ };
174
+
175
+ delete: Graffiti["delete"] = async (...args) => {
176
+ const [urlObject, session] = args;
177
+
178
+ const url = unpackObjectUrl(urlObject);
179
+ const { actor } = decodeObjectUrl(url);
180
+ if (actor !== session.actor) {
181
+ throw new GraffitiErrorForbidden(
182
+ "You cannot delete an object that you did not create.",
183
+ );
184
+ }
185
+
186
+ let doc: GraffitiObjectData & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;
187
+ try {
188
+ doc = await (await this.db).get(url);
189
+ } catch {
190
+ throw new GraffitiErrorNotFound("Object not found.");
191
+ }
192
+
193
+ if (doc.tombstone) {
194
+ throw new GraffitiErrorNotFound("Object not found.");
195
+ }
196
+
197
+ // Set the tombstone and update lastModified
198
+ doc.tombstone = true;
199
+ doc.lastModified = this.operationClock;
200
+ try {
201
+ await (await this.db).put(doc);
202
+ } catch {
203
+ throw new GraffitiErrorNotFound("Object not found.");
204
+ }
205
+ this.operationClock++;
206
+
207
+ return;
208
+ };
209
+
210
+ post: Graffiti["post"] = async (...args) => {
211
+ const [objectPartial, session] = args;
212
+
213
+ const actor = session.actor;
214
+ const id = randomBase64();
215
+ const url = encodeObjectUrl(actor, id);
216
+
217
+ const { value, channels, allowed } = objectPartial;
218
+ const object: GraffitiObjectData = {
219
+ value,
220
+ channels,
221
+ allowed,
222
+ lastModified: this.operationClock,
223
+ tombstone: false,
224
+ };
225
+
226
+ await (
227
+ await this.db
228
+ ).put({
229
+ _id: url,
230
+ ...object,
231
+ });
232
+ this.operationClock++;
233
+
234
+ return {
235
+ ...objectPartial,
236
+ actor,
237
+ url,
238
+ };
239
+ };
240
+
241
+ protected async *discoverMeta<Schema extends JSONSchema>(
242
+ args: Parameters<typeof Graffiti.prototype.discover<Schema>>,
243
+ continueParams?: {
244
+ lastDiscovered: number;
245
+ ifModifiedSince: number;
246
+ },
247
+ ): AsyncGenerator<
248
+ GraffitiObjectStreamContinueEntry<Schema>,
249
+ ContinueDiscoverParams
250
+ > {
251
+ // If we are continuing a discover, make sure to wait at
252
+ // least 2 seconds since the last poll to start a new one.
253
+ if (continueParams) {
254
+ const continueBuffer = this.options.continueBuffer ?? 2000;
255
+ const timeElapsedSinceLastDiscover =
256
+ Date.now() - continueParams.lastDiscovered;
257
+ if (timeElapsedSinceLastDiscover < continueBuffer) {
258
+ // Continue was called too soon,
259
+ // wait a bit before continuing
260
+ await new Promise((resolve) =>
261
+ setTimeout(resolve, continueBuffer - timeElapsedSinceLastDiscover),
262
+ );
263
+ }
264
+ }
265
+
266
+ const [discoverChannels, schema, session] = args;
267
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
268
+ const startKeySuffix = continueParams
269
+ ? continueParams.ifModifiedSince.toString().padStart(15, "0")
270
+ : "";
271
+ const endKeySuffix = "\uffff";
272
+
273
+ const processedUrls = new Set<string>();
274
+
275
+ const startTime = this.operationClock;
276
+
277
+ for (const channel of discoverChannels) {
278
+ const keyPrefix = encodeURIComponent(channel) + "/";
279
+ const startkey = keyPrefix + startKeySuffix;
280
+ const endkey = keyPrefix + endKeySuffix;
281
+
282
+ const result = await (
283
+ await this.db
284
+ ).query<GraffitiObjectData>("indexes/objectsPerChannelAndLastModified", {
285
+ startkey,
286
+ endkey,
287
+ include_docs: true,
288
+ });
289
+
290
+ for (const row of result.rows) {
291
+ const doc = row.doc;
292
+ if (!doc) continue;
293
+
294
+ const url = doc._id;
295
+
296
+ if (processedUrls.has(url)) continue;
297
+ processedUrls.add(url);
298
+
299
+ // If this is not a continuation, skip tombstones
300
+ if (!continueParams && doc.tombstone) continue;
301
+
302
+ const { tombstone, value, channels, allowed } = doc;
303
+ const { actor } = decodeObjectUrl(url);
304
+
305
+ const object: GraffitiObjectBase = {
306
+ url,
307
+ value,
308
+ allowed,
309
+ channels,
310
+ actor,
311
+ };
312
+
313
+ if (!isActorAllowedGraffitiObject(object, session)) continue;
314
+
315
+ maskGraffitiObject(object, discoverChannels, session);
316
+
317
+ if (!validate(object)) continue;
318
+
319
+ yield tombstone
320
+ ? {
321
+ tombstone: true,
322
+ object: { url },
323
+ }
324
+ : { object };
325
+ }
326
+ }
327
+
328
+ return {
329
+ lastDiscovered: Date.now(),
330
+ ifModifiedSince: startTime,
331
+ };
332
+ }
333
+
334
+ protected discoverCursor(
335
+ args: Parameters<typeof Graffiti.prototype.discover<{}>>,
336
+ continueParams: {
337
+ lastDiscovered: number;
338
+ ifModifiedSince: number;
339
+ },
340
+ ): string {
341
+ const [channels, schema, session] = args;
342
+ return (
343
+ "discover:" +
344
+ JSON.stringify({
345
+ channels,
346
+ schema,
347
+ continueParams,
348
+ actor: session?.actor,
349
+ })
350
+ );
351
+ }
352
+
353
+ protected async *discoverContinue<Schema extends JSONSchema>(
354
+ args: Parameters<typeof Graffiti.prototype.discover<Schema>>,
355
+ continueParams: {
356
+ lastDiscovered: number;
357
+ ifModifiedSince: number;
358
+ },
359
+ session?: GraffitiSession | null,
360
+ ): GraffitiObjectStreamContinue<Schema> {
361
+ if (session?.actor !== args[2]?.actor) {
362
+ throw new GraffitiErrorForbidden(
363
+ "Cannot continue a cursor started by another actor",
364
+ );
365
+ }
366
+ const iterator = this.discoverMeta<Schema>(args, continueParams);
367
+
368
+ while (true) {
369
+ const result = await iterator.next();
370
+ if (result.done) {
371
+ return {
372
+ continue: (session) =>
373
+ this.discoverContinue<Schema>(args, result.value, session),
374
+ cursor: this.discoverCursor(args, result.value),
375
+ };
376
+ }
377
+ yield result.value;
378
+ }
379
+ }
380
+
381
+ discover: Graffiti["discover"] = (...args) => {
382
+ const [channels, schema, session] = args;
383
+ const iterator = this.discoverMeta<(typeof args)[1]>([
384
+ channels,
385
+ schema,
386
+ session,
387
+ ]);
388
+
389
+ const this_ = this;
390
+ return (async function* () {
391
+ while (true) {
392
+ const result = await iterator.next();
393
+ if (result.done) {
394
+ return {
395
+ continue: (session) =>
396
+ this_.discoverContinue<(typeof args)[1]>(
397
+ args,
398
+ result.value,
399
+ session,
400
+ ),
401
+ cursor: this_.discoverCursor(args, result.value),
402
+ };
403
+ }
404
+ // Make sure to filter out tombstones
405
+ if (result.value.tombstone) continue;
406
+ yield result.value;
407
+ }
408
+ })();
409
+ };
410
+
411
+ continueDiscover: Graffiti["continueDiscover"] = (...args) => {
412
+ const [cursor, session] = args;
413
+ if (cursor.startsWith("discover:")) {
414
+ // TODO: use AJV here
415
+ const { channels, schema, actor, continueParams } = JSON.parse(
416
+ cursor.slice("discover:".length),
417
+ );
418
+ if (actor && actor !== session?.actor) {
419
+ throw new GraffitiErrorForbidden(
420
+ "Cannot continue a cursor started by another actor",
421
+ );
422
+ }
423
+ return this.discoverContinue<{}>(
424
+ [channels, schema, session],
425
+ continueParams,
426
+ );
427
+ } else {
428
+ throw new GraffitiErrorNotFound("Cursor not found");
429
+ }
430
+ };
431
+ }
package/src/tests.spec.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import {
2
2
  graffitiCRUDTests,
3
3
  graffitiDiscoverTests,
4
- graffitiOrphanTests,
5
- graffitiChannelStatsTests,
4
+ graffitiMediaTests,
6
5
  } from "@graffiti-garden/api/tests";
7
6
  import { GraffitiLocal } from "./index";
8
7
 
@@ -12,5 +11,4 @@ const useSession2 = () => ({ actor: "someoneelse" });
12
11
 
13
12
  graffitiCRUDTests(useGraffiti, useSession1, useSession2);
14
13
  graffitiDiscoverTests(useGraffiti, useSession1, useSession2);
15
- graffitiOrphanTests(useGraffiti, useSession1, useSession2);
16
- graffitiChannelStatsTests(useGraffiti, useSession1, useSession2);
14
+ graffitiMediaTests(useGraffiti, useSession1, useSession2);
package/src/utilities.ts CHANGED
@@ -1,102 +1,82 @@
1
- import {
2
- GraffitiErrorInvalidSchema,
3
- GraffitiErrorPatchError,
4
- GraffitiErrorPatchTestFailed,
5
- } from "@graffiti-garden/api";
6
- import type {
7
- GraffitiObject,
8
- GraffitiObjectBase,
9
- GraffitiPatch,
10
- JSONSchema,
11
- GraffitiSession,
12
- GraffitiObjectUrl,
13
- } from "@graffiti-garden/api";
14
- import type { Ajv } from "ajv";
15
- import type { applyPatch } from "fast-json-patch";
1
+ export function encodeBase64(bytes: Uint8Array): string {
2
+ // Convert it to base64
3
+ const base64 = btoa(String.fromCodePoint(...bytes));
4
+ // Make sure it is url safe
5
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=+$/, "");
6
+ }
16
7
 
17
- export function unpackObjectUrl(url: string | GraffitiObjectUrl) {
18
- return typeof url === "string" ? url : url.url;
8
+ export function decodeBase64(base64Url: string): Uint8Array {
9
+ // Undo url-safe base64
10
+ let base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
11
+ // Add padding if necessary
12
+ while (base64.length % 4 !== 0) base64 += "=";
13
+ // Decode
14
+ return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
19
15
  }
20
16
 
21
- export function randomBase64(numBytes: number = 24) {
17
+ export function randomBase64(numBytes: number = 32): string {
18
+ // Generate random bytes
22
19
  const bytes = new Uint8Array(numBytes);
23
20
  crypto.getRandomValues(bytes);
24
- // Convert it to base64
25
- const base64 = btoa(String.fromCodePoint(...bytes));
26
- // Make sure it is url safe
27
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=+$/, "");
21
+ return encodeBase64(bytes);
28
22
  }
29
23
 
30
- export function applyGraffitiPatch<Prop extends keyof GraffitiPatch>(
31
- apply: typeof applyPatch,
32
- prop: Prop,
33
- patch: GraffitiPatch,
34
- object: GraffitiObjectBase,
35
- ): void {
36
- const ops = patch[prop];
37
- if (!ops || !ops.length) return;
38
- try {
39
- object[prop] = apply(object[prop], ops, true, false).newDocument;
40
- } catch (e) {
41
- if (
42
- typeof e === "object" &&
43
- e &&
44
- "name" in e &&
45
- typeof e.name === "string" &&
46
- "message" in e &&
47
- typeof e.message === "string"
48
- ) {
49
- if (e.name === "TEST_OPERATION_FAILED") {
50
- throw new GraffitiErrorPatchTestFailed(e.message);
51
- } else {
52
- throw new GraffitiErrorPatchError(e.name + ": " + e.message);
53
- }
54
- } else {
55
- throw e;
56
- }
57
- }
24
+ const OBJECT_URL_PREFIX = "graffiti:object:";
25
+ const MEDIA_URL_PREFIX = "graffiti:media:";
26
+
27
+ export function encodeGraffitiUrl(actor: string, id: string, prefix: string) {
28
+ return `${prefix}${encodeURIComponent(actor)}:${encodeURIComponent(id)}`;
29
+ }
30
+ export function encodeObjectUrl(actor: string, id: string) {
31
+ return encodeGraffitiUrl(actor, id, OBJECT_URL_PREFIX);
32
+ }
33
+ export function encodeMediaUrl(actor: string, id: string) {
34
+ return encodeGraffitiUrl(actor, id, MEDIA_URL_PREFIX);
58
35
  }
59
36
 
60
- export function compileGraffitiObjectSchema<Schema extends JSONSchema>(
61
- ajv: Ajv,
62
- schema: Schema,
63
- ) {
64
- try {
65
- // Force the validation guard because
66
- // it is too big for the type checker.
67
- // Fortunately json-schema-to-ts is
68
- // well tested against ajv.
69
- return ajv.compile(schema) as (
70
- data: GraffitiObjectBase,
71
- ) => data is GraffitiObject<Schema>;
72
- } catch (error) {
73
- throw new GraffitiErrorInvalidSchema(
74
- error instanceof Error ? error.message : undefined,
75
- );
37
+ export function decodeGraffitiUrl(url: string, prefix: string) {
38
+ if (!url.startsWith(prefix)) {
39
+ throw new Error(`URL does not start with ${prefix}`);
76
40
  }
41
+ const slices = url.slice(prefix.length).split(":");
42
+ if (slices.length !== 2) {
43
+ throw new Error("URL has too many colon-seperated parts");
44
+ }
45
+ const [actor, id] = slices.map(decodeURIComponent);
46
+ return { actor, id };
47
+ }
48
+ export function decodeObjectUrl(url: string) {
49
+ return decodeGraffitiUrl(url, OBJECT_URL_PREFIX);
77
50
  }
51
+ export function decodeMediaUrl(url: string) {
52
+ return decodeGraffitiUrl(url, MEDIA_URL_PREFIX);
53
+ }
54
+
55
+ export async function blobToBase64(blob: Blob): Promise<string> {
56
+ if (typeof FileReader !== "undefined") {
57
+ return new Promise((resolve, reject) => {
58
+ const r = new FileReader();
59
+ r.onload = () => {
60
+ if (typeof r.result === "string") {
61
+ resolve(r.result);
62
+ } else {
63
+ reject(new Error("Unexpected result type"));
64
+ }
65
+ };
66
+ r.onerror = reject;
67
+ r.readAsDataURL(blob);
68
+ });
69
+ }
78
70
 
79
- export function maskGraffitiObject(
80
- object: GraffitiObjectBase,
81
- channels: string[],
82
- session?: GraffitiSession | null,
83
- ): void {
84
- if (object.actor !== session?.actor) {
85
- object.allowed = object.allowed && session ? [session.actor] : undefined;
86
- object.channels = object.channels.filter((channel) =>
87
- channels.includes(channel),
88
- );
71
+ if (typeof Buffer !== "undefined") {
72
+ const ab = await blob.arrayBuffer();
73
+ return `data:${blob.type};base64,${Buffer.from(ab).toString("base64")}`;
89
74
  }
75
+
76
+ throw new Error("Unsupported environment");
90
77
  }
91
- export function isActorAllowedGraffitiObject(
92
- object: GraffitiObjectBase,
93
- session?: GraffitiSession | null,
94
- ) {
95
- return (
96
- object.allowed === undefined ||
97
- object.allowed === null ||
98
- (!!session?.actor &&
99
- (object.actor === session.actor ||
100
- object.allowed.includes(session.actor)))
101
- );
78
+
79
+ export async function base64ToBlob(dataUrl: string) {
80
+ const response = await fetch(dataUrl);
81
+ return await response.blob();
102
82
  }