@graffiti-garden/implementation-local 0.6.4 → 1.0.1

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 +432 -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 -626
  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 -106
  60. package/dist/database.d.ts.map +0 -1
  61. package/dist/esm/database.js +0 -608
  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 -921
  67. package/src/session-manager.ts +0 -123
package/src/objects.ts ADDED
@@ -0,0 +1,432 @@
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
+
62
+ get db() {
63
+ if (!this.db_) {
64
+ this.db_ = (async () => {
65
+ const { default: PouchDB } = await import("pouchdb");
66
+ const pouchDbOptions = {
67
+ name: "graffitiDb",
68
+ ...this.options.pouchDBOptions,
69
+ };
70
+ const db = new PouchDB<GraffitiObjectData>(
71
+ pouchDbOptions.name,
72
+ pouchDbOptions,
73
+ );
74
+ await db
75
+ //@ts-ignore
76
+ .put({
77
+ _id: "_design/indexes",
78
+ views: {
79
+ objectsPerChannelAndLastModified: {
80
+ map: function (object: GraffitiObjectData) {
81
+ const paddedLastModified = object.lastModified
82
+ .toString()
83
+ .padStart(15, "0");
84
+ object.channels.forEach(function (channel) {
85
+ const id =
86
+ encodeURIComponent(channel) + "/" + paddedLastModified;
87
+ //@ts-ignore
88
+ emit(id);
89
+ });
90
+ }.toString(),
91
+ },
92
+ },
93
+ })
94
+ //@ts-ignore
95
+ .catch((error) => {
96
+ if (
97
+ error &&
98
+ typeof error === "object" &&
99
+ "name" in error &&
100
+ error.name === "conflict"
101
+ ) {
102
+ // Design document already exists
103
+ return;
104
+ } else {
105
+ throw error;
106
+ }
107
+ });
108
+ return db;
109
+ })();
110
+ }
111
+ return this.db_;
112
+ }
113
+
114
+ protected get ajv() {
115
+ if (!this.ajv_) {
116
+ this.ajv_ = (async () => {
117
+ const { default: Ajv } = await import("ajv");
118
+ return new Ajv({ strict: false });
119
+ })();
120
+ }
121
+ return this.ajv_;
122
+ }
123
+
124
+ protected async getOperationClock() {
125
+ return Number((await (await this.db).info()).update_seq);
126
+ }
127
+
128
+ constructor(options?: GraffitiLocalOptions) {
129
+ this.options = options ?? {};
130
+ }
131
+
132
+ get: Graffiti["get"] = async (...args) => {
133
+ const [urlObject, schema, session] = args;
134
+ const url = unpackObjectUrl(urlObject);
135
+
136
+ let doc: GraffitiObjectData;
137
+ try {
138
+ doc = await (await this.db).get(url);
139
+ } catch (error) {
140
+ throw new GraffitiErrorNotFound(
141
+ "The object you are trying to get either does not exist or you are not allowed to see it",
142
+ );
143
+ }
144
+
145
+ if (doc.tombstone) {
146
+ throw new GraffitiErrorNotFound(
147
+ "The object you are trying to get either does not exist or you are not allowed to see it",
148
+ );
149
+ }
150
+
151
+ const { actor } = decodeObjectUrl(url);
152
+ const { value, channels, allowed } = doc;
153
+ const object: GraffitiObjectBase = {
154
+ value,
155
+ channels,
156
+ allowed,
157
+ url,
158
+ actor,
159
+ };
160
+
161
+ if (!isActorAllowedGraffitiObject(object, session)) {
162
+ throw new GraffitiErrorNotFound(
163
+ "The object you are trying to get either does not exist or you are not allowed to see it",
164
+ );
165
+ }
166
+
167
+ // Mask out the allowed list and channels
168
+ // if the user is not the owner
169
+ maskGraffitiObject(object, [], session);
170
+
171
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
172
+ if (!validate(object)) {
173
+ throw new GraffitiErrorSchemaMismatch();
174
+ }
175
+ return object;
176
+ };
177
+
178
+ delete: Graffiti["delete"] = async (...args) => {
179
+ const [urlObject, session] = args;
180
+
181
+ const url = unpackObjectUrl(urlObject);
182
+ const { actor } = decodeObjectUrl(url);
183
+ if (actor !== session.actor) {
184
+ throw new GraffitiErrorForbidden(
185
+ "You cannot delete an object that you did not create.",
186
+ );
187
+ }
188
+
189
+ let doc: GraffitiObjectData & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta;
190
+ try {
191
+ doc = await (await this.db).get(url);
192
+ } catch {
193
+ throw new GraffitiErrorNotFound("Object not found.");
194
+ }
195
+
196
+ if (doc.tombstone) {
197
+ throw new GraffitiErrorNotFound("Object not found.");
198
+ }
199
+
200
+ // Set the tombstone and update lastModified
201
+ doc.tombstone = true;
202
+ doc.lastModified = await this.getOperationClock();
203
+ try {
204
+ await (await this.db).put(doc);
205
+ } catch {
206
+ throw new GraffitiErrorNotFound("Object not found.");
207
+ }
208
+
209
+ return;
210
+ };
211
+
212
+ post: Graffiti["post"] = async (...args) => {
213
+ const [objectPartial, session] = args;
214
+
215
+ const actor = session.actor;
216
+ const id = randomBase64();
217
+ const url = encodeObjectUrl(actor, id);
218
+
219
+ const { value, channels, allowed } = objectPartial;
220
+ const object: GraffitiObjectData = {
221
+ value,
222
+ channels,
223
+ allowed,
224
+ lastModified: await this.getOperationClock(),
225
+ tombstone: false,
226
+ };
227
+
228
+ await (
229
+ await this.db
230
+ ).put({
231
+ _id: url,
232
+ ...object,
233
+ });
234
+
235
+ return {
236
+ ...objectPartial,
237
+ actor,
238
+ url,
239
+ };
240
+ };
241
+
242
+ protected async *discoverMeta<Schema extends JSONSchema>(
243
+ args: Parameters<typeof Graffiti.prototype.discover<Schema>>,
244
+ continueParams?: {
245
+ lastDiscovered: number;
246
+ ifModifiedSince: number;
247
+ },
248
+ ): AsyncGenerator<
249
+ GraffitiObjectStreamContinueEntry<Schema>,
250
+ ContinueDiscoverParams
251
+ > {
252
+ // If we are continuing a discover, make sure to wait at
253
+ // least 2 seconds since the last poll to start a new one.
254
+ if (continueParams) {
255
+ const continueBuffer = this.options.continueBuffer ?? 2000;
256
+ const timeElapsedSinceLastDiscover =
257
+ Date.now() - continueParams.lastDiscovered;
258
+ if (timeElapsedSinceLastDiscover < continueBuffer) {
259
+ // Continue was called too soon,
260
+ // wait a bit before continuing
261
+ await new Promise((resolve) =>
262
+ setTimeout(resolve, continueBuffer - timeElapsedSinceLastDiscover),
263
+ );
264
+ }
265
+ }
266
+
267
+ const [discoverChannels, schema, session] = args;
268
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
269
+ const startKeySuffix = continueParams
270
+ ? continueParams.ifModifiedSince.toString().padStart(15, "0")
271
+ : "";
272
+ const endKeySuffix = "\uffff";
273
+
274
+ const processedUrls = new Set<string>();
275
+
276
+ const startTime = await this.getOperationClock();
277
+
278
+ for (const channel of discoverChannels) {
279
+ const keyPrefix = encodeURIComponent(channel) + "/";
280
+ const startkey = keyPrefix + startKeySuffix;
281
+ const endkey = keyPrefix + endKeySuffix;
282
+
283
+ const result = await (
284
+ await this.db
285
+ ).query<GraffitiObjectData>("indexes/objectsPerChannelAndLastModified", {
286
+ startkey,
287
+ endkey,
288
+ include_docs: true,
289
+ });
290
+
291
+ for (const row of result.rows) {
292
+ const doc = row.doc;
293
+ if (!doc) continue;
294
+
295
+ const url = doc._id;
296
+
297
+ if (processedUrls.has(url)) continue;
298
+ processedUrls.add(url);
299
+
300
+ // If this is not a continuation, skip tombstones
301
+ if (!continueParams && doc.tombstone) continue;
302
+
303
+ const { tombstone, value, channels, allowed } = doc;
304
+ const { actor } = decodeObjectUrl(url);
305
+
306
+ const object: GraffitiObjectBase = {
307
+ url,
308
+ value,
309
+ allowed,
310
+ channels,
311
+ actor,
312
+ };
313
+
314
+ if (!isActorAllowedGraffitiObject(object, session)) continue;
315
+
316
+ maskGraffitiObject(object, discoverChannels, session);
317
+
318
+ if (!validate(object)) continue;
319
+
320
+ yield tombstone
321
+ ? {
322
+ tombstone: true,
323
+ object: { url },
324
+ }
325
+ : { object };
326
+ }
327
+ }
328
+
329
+ return {
330
+ lastDiscovered: Date.now(),
331
+ ifModifiedSince: startTime,
332
+ };
333
+ }
334
+
335
+ protected discoverCursor(
336
+ args: Parameters<typeof Graffiti.prototype.discover<{}>>,
337
+ continueParams: {
338
+ lastDiscovered: number;
339
+ ifModifiedSince: number;
340
+ },
341
+ ): string {
342
+ const [channels, schema, session] = args;
343
+ return (
344
+ "discover:" +
345
+ JSON.stringify({
346
+ channels,
347
+ schema,
348
+ continueParams,
349
+ actor: session?.actor,
350
+ })
351
+ );
352
+ }
353
+
354
+ protected async *discoverContinue<Schema extends JSONSchema>(
355
+ args: Parameters<typeof Graffiti.prototype.discover<Schema>>,
356
+ continueParams: {
357
+ lastDiscovered: number;
358
+ ifModifiedSince: number;
359
+ },
360
+ session?: GraffitiSession | null,
361
+ ): GraffitiObjectStreamContinue<Schema> {
362
+ if (session?.actor !== args[2]?.actor) {
363
+ throw new GraffitiErrorForbidden(
364
+ "Cannot continue a cursor started by another actor",
365
+ );
366
+ }
367
+ const iterator = this.discoverMeta<Schema>(args, continueParams);
368
+
369
+ while (true) {
370
+ const result = await iterator.next();
371
+ if (result.done) {
372
+ return {
373
+ continue: (session) =>
374
+ this.discoverContinue<Schema>(args, result.value, session),
375
+ cursor: this.discoverCursor(args, result.value),
376
+ };
377
+ }
378
+ yield result.value;
379
+ }
380
+ }
381
+
382
+ discover: Graffiti["discover"] = (...args) => {
383
+ const [channels, schema, session] = args;
384
+ const iterator = this.discoverMeta<(typeof args)[1]>([
385
+ channels,
386
+ schema,
387
+ session,
388
+ ]);
389
+
390
+ const this_ = this;
391
+ return (async function* () {
392
+ while (true) {
393
+ const result = await iterator.next();
394
+ if (result.done) {
395
+ return {
396
+ continue: (session) =>
397
+ this_.discoverContinue<(typeof args)[1]>(
398
+ args,
399
+ result.value,
400
+ session,
401
+ ),
402
+ cursor: this_.discoverCursor(args, result.value),
403
+ };
404
+ }
405
+ // Make sure to filter out tombstones
406
+ if (result.value.tombstone) continue;
407
+ yield result.value;
408
+ }
409
+ })();
410
+ };
411
+
412
+ continueDiscover: Graffiti["continueDiscover"] = (...args) => {
413
+ const [cursor, session] = args;
414
+ if (cursor.startsWith("discover:")) {
415
+ // TODO: use AJV here
416
+ const { channels, schema, actor, continueParams } = JSON.parse(
417
+ cursor.slice("discover:".length),
418
+ );
419
+ if (actor && actor !== session?.actor) {
420
+ throw new GraffitiErrorForbidden(
421
+ "Cannot continue a cursor started by another actor",
422
+ );
423
+ }
424
+ return this.discoverContinue<{}>(
425
+ [channels, schema, session],
426
+ continueParams,
427
+ );
428
+ } else {
429
+ throw new GraffitiErrorNotFound("Cursor not found");
430
+ }
431
+ };
432
+ }
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
  }