@graffiti-garden/wrapper-synchronize 0.2.3 → 1.0.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/src/index.ts CHANGED
@@ -1,28 +1,26 @@
1
1
  import type Ajv from "ajv";
2
- import { Graffiti } from "@graffiti-garden/api";
3
2
  import type {
3
+ Graffiti,
4
4
  GraffitiSession,
5
5
  JSONSchema,
6
+ GraffitiObjectBase,
6
7
  GraffitiObjectStream,
7
8
  GraffitiObjectStreamContinueEntry,
8
9
  GraffitiObjectStreamContinue,
9
- GraffitiObject,
10
+ GraffitiObjectUrl,
10
11
  } from "@graffiti-garden/api";
11
- import type { GraffitiObjectBase } from "@graffiti-garden/api";
12
- import { Repeater } from "@repeaterjs/repeater";
13
- import type { applyPatch } from "fast-json-patch";
14
12
  import {
15
- applyGraffitiPatch,
13
+ GraffitiErrorNotFound,
16
14
  compileGraffitiObjectSchema,
17
15
  isActorAllowedGraffitiObject,
18
16
  maskGraffitiObject,
19
17
  unpackObjectUrl,
20
- } from "@graffiti-garden/implementation-local/utilities";
18
+ } from "@graffiti-garden/api";
19
+ import { Repeater } from "@repeaterjs/repeater";
21
20
  export type * from "@graffiti-garden/api";
22
21
 
23
22
  export type GraffitiSynchronizeCallback = (
24
- oldObject: GraffitiObjectStreamContinueEntry<{}>,
25
- newObject?: GraffitiObjectStreamContinueEntry<{}>,
23
+ object: GraffitiObjectStreamContinueEntry<{}>,
26
24
  ) => void;
27
25
 
28
26
  export interface GraffitiSynchronizeOptions {
@@ -48,23 +46,24 @@ export interface GraffitiSynchronizeOptions {
48
46
  * the [Graffiti Vue Plugin](https://vue.graffiti.garden/variables/GraffitiPlugin.html)
49
47
  * and possibly other front-end libraries in the future.
50
48
  *
51
- * Specifically, it provides the following *synchronize*
52
- * methods for each of the following API methods:
49
+ * [See a live example](/example).
50
+ *
51
+ * Specifically, this library provides the following *synchronize*
52
+ * methods to correspond with each of the following Graffiti API methods:
53
53
  *
54
54
  * | API Method | Synchronize Method |
55
55
  * |------------|--------------------|
56
56
  * | {@link get} | {@link synchronizeGet} |
57
57
  * | {@link discover} | {@link synchronizeDiscover} |
58
- * | {@link recoverOrphans} | {@link synchronizeRecoverOrphans} |
59
58
  *
60
- * Whenever a change is made via {@link put}, {@link patch}, and {@link delete} or
61
- * received from {@link get}, {@link discover}, and {@link recoverOrphans},
59
+ * Whenever a change is made via {@link post} and {@link delete} or
60
+ * received from {@link get}, {@link discover}, and {@link continueDiscover},
62
61
  * those changes are forwarded to the appropriate synchronize method.
63
62
  * Each synchronize method returns an iterator that streams these changes
64
63
  * continually until the user calls `return` on the iterator or `break`s out of the loop,
65
64
  * allowing for live updates without additional polling.
66
65
  *
67
- * Example 1: Suppose a user publishes a post using {@link put}. If the feed
66
+ * Example 1: Suppose a user publishes a post using {@link post}. If the feed
68
67
  * displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,
69
68
  * then the user's new post will instantly appear in their feed, giving the UI a
70
69
  * responsive feel.
@@ -75,25 +74,34 @@ export interface GraffitiSynchronizeOptions {
75
74
  * all instance's of that friend's name in the user's application instantly,
76
75
  * providing a consistent user experience.
77
76
  *
78
- * @groupDescription Synchronize Methods
77
+ * Additionally, the library supplies a {@link synchronizeAll} method that can be used
78
+ * to stream all the Graffiti changes that an application is aware of, which can be used
79
+ * for caching or history building.
80
+ *
81
+ * The source code for this library is [available on GitHub](https://github.com/graffiti-garden/wrapper-synchronize/).
82
+ *
83
+ * @groupDescription 0 - Synchronize Methods
79
84
  * This group contains methods that listen for changes made via
80
- * {@link put}, {@link patch}, and {@link delete} or fetched from
81
- * {@link get}, {@link discover}, and {@link recoverOrphans} and then
85
+ * {@link post}, and {@link delete} or fetched from
86
+ * {@link get}, {@link discover}, or {@link continueDiscover} and then
82
87
  * streams appropriate changes to provide a responsive and consistent user experience.
83
88
  */
84
- export class GraffitiSynchronize extends Graffiti {
89
+ export class GraffitiSynchronize implements Graffiti {
85
90
  protected ajv_: Promise<Ajv> | undefined;
86
- protected applyPatch_: Promise<typeof applyPatch> | undefined;
87
91
  protected readonly graffiti: Graffiti;
88
92
  protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();
89
93
  protected readonly options: GraffitiSynchronizeOptions;
90
94
 
91
- channelStats: Graffiti["channelStats"];
92
95
  login: Graffiti["login"];
93
96
  logout: Graffiti["logout"];
94
97
  sessionEvents: Graffiti["sessionEvents"];
98
+ postMedia: Graffiti["postMedia"];
99
+ getMedia: Graffiti["getMedia"];
100
+ deleteMedia: Graffiti["deleteMedia"];
101
+ actorToHandle: Graffiti["actorToHandle"];
102
+ handleToActor: Graffiti["handleToActor"];
95
103
 
96
- get ajv() {
104
+ protected get ajv() {
97
105
  if (!this.ajv_) {
98
106
  this.ajv_ = (async () => {
99
107
  const { default: Ajv } = await import("ajv");
@@ -103,16 +111,6 @@ export class GraffitiSynchronize extends Graffiti {
103
111
  return this.ajv_;
104
112
  }
105
113
 
106
- get applyPatch() {
107
- if (!this.applyPatch_) {
108
- this.applyPatch_ = (async () => {
109
- const { applyPatch } = await import("fast-json-patch");
110
- return applyPatch;
111
- })();
112
- }
113
- return this.applyPatch_;
114
- }
115
-
116
114
  /**
117
115
  * Wraps a Graffiti API instance to provide the synchronize methods.
118
116
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -126,13 +124,16 @@ export class GraffitiSynchronize extends Graffiti {
126
124
  graffiti: Graffiti,
127
125
  options?: GraffitiSynchronizeOptions,
128
126
  ) {
129
- super();
130
127
  this.options = options ?? {};
131
128
  this.graffiti = graffiti;
132
- this.channelStats = graffiti.channelStats.bind(graffiti);
133
129
  this.login = graffiti.login.bind(graffiti);
134
130
  this.logout = graffiti.logout.bind(graffiti);
135
131
  this.sessionEvents = graffiti.sessionEvents;
132
+ this.postMedia = graffiti.postMedia.bind(graffiti);
133
+ this.getMedia = graffiti.getMedia.bind(graffiti);
134
+ this.deleteMedia = graffiti.deleteMedia.bind(graffiti);
135
+ this.actorToHandle = graffiti.actorToHandle.bind(graffiti);
136
+ this.handleToActor = graffiti.handleToActor.bind(graffiti);
136
137
  }
137
138
 
138
139
  protected synchronize<Schema extends JSONSchema>(
@@ -140,39 +141,32 @@ export class GraffitiSynchronize extends Graffiti {
140
141
  channels: string[],
141
142
  schema: Schema,
142
143
  session?: GraffitiSession | null,
144
+ seenUrls: Set<string> = new Set<string>(),
143
145
  ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {
144
- const seenUrls = new Set<string>();
145
-
146
146
  const repeater = new Repeater<GraffitiObjectStreamContinueEntry<Schema>>(
147
147
  async (push, stop) => {
148
148
  const validate = compileGraffitiObjectSchema(await this.ajv, schema);
149
- const callback: GraffitiSynchronizeCallback = (
150
- oldObjectRaw,
151
- newObjectRaw,
152
- ) => {
153
- for (const objectRaw of [newObjectRaw, oldObjectRaw]) {
154
- if (objectRaw?.tombstone) {
155
- if (seenUrls.has(objectRaw.object.url)) {
156
- push(objectRaw);
157
- }
158
- } else if (
159
- objectRaw &&
160
- matchObject(objectRaw.object) &&
161
- (this.options.omniscient ||
162
- isActorAllowedGraffitiObject(objectRaw.object, session))
163
- ) {
164
- // Deep clone the object to prevent mutation
165
- const object = JSON.parse(
166
- JSON.stringify(objectRaw.object),
167
- ) as GraffitiObject<{}>;
168
- if (!this.options.omniscient) {
169
- maskGraffitiObject(object, channels, session);
170
- }
171
- if (validate(object)) {
172
- push({ object });
173
- seenUrls.add(object.url);
174
- break;
175
- }
149
+ const callback: GraffitiSynchronizeCallback = (objectUpdate) => {
150
+ if (objectUpdate?.tombstone) {
151
+ if (seenUrls.has(objectUpdate.object.url)) {
152
+ push(objectUpdate);
153
+ }
154
+ } else if (
155
+ objectUpdate &&
156
+ matchObject(objectUpdate.object) &&
157
+ (this.options.omniscient ||
158
+ isActorAllowedGraffitiObject(objectUpdate.object, session))
159
+ ) {
160
+ // Deep clone the object to prevent mutation
161
+ const object = JSON.parse(
162
+ JSON.stringify(objectUpdate.object),
163
+ ) as GraffitiObjectBase;
164
+ if (!this.options.omniscient) {
165
+ maskGraffitiObject(object, channels, session);
166
+ }
167
+ if (validate(object)) {
168
+ push({ object });
169
+ seenUrls.add(object.url);
176
170
  }
177
171
  }
178
172
  };
@@ -183,13 +177,15 @@ export class GraffitiSynchronize extends Graffiti {
183
177
  },
184
178
  );
185
179
 
186
- return repeater;
180
+ return (async function* () {
181
+ for await (const i of repeater) yield i;
182
+ })();
187
183
  }
188
184
 
189
185
  /**
190
186
  * This method has the same signature as {@link discover} but listens for
191
- * changes made via {@link put}, {@link patch}, and {@link delete} or
192
- * fetched from {@link get}, {@link discover}, and {@link recoverOrphans}
187
+ * changes made via {@link post} and {@link delete} or
188
+ * fetched from {@link get}, {@link discover}, and {@link continueDiscover}
193
189
  * and then streams appropriate changes to provide a responsive and
194
190
  * consistent user experience.
195
191
  *
@@ -197,12 +193,13 @@ export class GraffitiSynchronize extends Graffiti {
197
193
  * and will not terminate unless the user calls the `return` method on the iterator
198
194
  * or `break`s out of the loop.
199
195
  *
200
- * @group Synchronize Methods
196
+ * @group 0 - Synchronize Methods
201
197
  */
202
198
  synchronizeDiscover<Schema extends JSONSchema>(
203
- ...args: Parameters<typeof Graffiti.prototype.discover<Schema>>
199
+ channels: string[],
200
+ schema: Schema,
201
+ session?: GraffitiSession | null,
204
202
  ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {
205
- const [channels, schema, session] = args;
206
203
  function matchObject(object: GraffitiObjectBase) {
207
204
  return object.channels.some((channel) => channels.includes(channel));
208
205
  }
@@ -211,48 +208,32 @@ export class GraffitiSynchronize extends Graffiti {
211
208
 
212
209
  /**
213
210
  * This method has the same signature as {@link get} but
214
- * listens for changes made via {@link put}, {@link patch}, and {@link delete} or
215
- * fetched from {@link get}, {@link discover}, and {@link recoverOrphans} and then
211
+ * listens for changes made via {@link post}, and {@link delete} or
212
+ * fetched from {@link get}, {@link discover}, and {@link continueDiscover} and then
216
213
  * streams appropriate changes to provide a responsive and consistent user experience.
217
214
  *
218
215
  * Unlike {@link get}, which returns a single result, this method continuously
219
216
  * listens for changes which are output as an asynchronous stream, similar
220
217
  * to {@link discover}.
221
218
  *
222
- * @group Synchronize Methods
219
+ * @group 0 - Synchronize Methods
223
220
  */
224
221
  synchronizeGet<Schema extends JSONSchema>(
225
- ...args: Parameters<typeof Graffiti.prototype.get<Schema>>
222
+ objectUrl: string | GraffitiObjectUrl,
223
+ schema: Schema,
224
+ session?: GraffitiSession | null | undefined,
226
225
  ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {
227
- const [objectUrl, schema, session] = args;
228
226
  const url = unpackObjectUrl(objectUrl);
229
227
  function matchObject(object: GraffitiObjectBase) {
230
228
  return object.url === url;
231
229
  }
232
- return this.synchronize<Schema>(matchObject, [], schema, session);
233
- }
234
-
235
- /**
236
- * This method has the same signature as {@link recoverOrphans} but
237
- * listens for changes made via
238
- * {@link put}, {@link patch}, and {@link delete} or fetched from
239
- * {@link get}, {@link discover}, and {@link recoverOrphans} and then
240
- * streams appropriate changes to provide a responsive and consistent user experience.
241
- *
242
- * Unlike {@link recoverOrphans}, this method continuously listens for changes
243
- * and will not terminate unless the user calls the `return` method on the iterator
244
- * or `break`s out of the loop.
245
- *
246
- * @group Synchronize Methods
247
- */
248
- synchronizeRecoverOrphans<Schema extends JSONSchema>(
249
- ...args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>
250
- ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {
251
- const [schema, session] = args;
252
- function matchObject(object: GraffitiObjectBase) {
253
- return object.actor === session.actor && object.channels.length === 0;
254
- }
255
- return this.synchronize<Schema>(matchObject, [], schema, session);
230
+ return this.synchronize<Schema>(
231
+ matchObject,
232
+ [],
233
+ schema,
234
+ session,
235
+ new Set<string>([url]),
236
+ );
256
237
  }
257
238
 
258
239
  /**
@@ -264,6 +245,8 @@ export class GraffitiSynchronize extends Graffiti {
264
245
  *
265
246
  * Be careful using this method. Without additional filters it can
266
247
  * expose the user to content out of context.
248
+ *
249
+ * @group 0 - Synchronize Methods
267
250
  */
268
251
  synchronizeAll<Schema extends JSONSchema>(
269
252
  schema: Schema,
@@ -273,12 +256,11 @@ export class GraffitiSynchronize extends Graffiti {
273
256
  }
274
257
 
275
258
  protected async synchronizeDispatch(
276
- oldObject: GraffitiObjectStreamContinueEntry<{}>,
277
- newObject?: GraffitiObjectStreamContinueEntry<{}>,
259
+ objectUpdate: GraffitiObjectStreamContinueEntry<{}>,
278
260
  waitForListeners = false,
279
261
  ) {
280
262
  for (const callback of this.callbacks) {
281
- callback(oldObject, newObject);
263
+ callback(objectUpdate);
282
264
  }
283
265
  if (waitForListeners) {
284
266
  // Wait for the listeners to receive
@@ -306,63 +288,43 @@ export class GraffitiSynchronize extends Graffiti {
306
288
  }
307
289
 
308
290
  get: Graffiti["get"] = async (...args) => {
309
- const object = await this.graffiti.get(...args);
310
- this.synchronizeDispatch({ object });
311
- return object;
312
- };
313
-
314
- put: Graffiti["put"] = async (...args) => {
315
- const oldObject = await this.graffiti.put<{}>(...args);
316
- const partialObject = args[0];
317
- const newObject: GraffitiObjectBase = {
318
- ...oldObject,
319
- value: partialObject.value,
320
- channels: partialObject.channels,
321
- allowed: partialObject.allowed,
322
- };
323
- await this.synchronizeDispatch(
324
- {
325
- tombstone: true,
326
- object: oldObject,
327
- },
328
- {
329
- object: newObject,
330
- },
331
- true,
332
- );
333
- return oldObject;
291
+ try {
292
+ const object = await this.graffiti.get(...args);
293
+ this.synchronizeDispatch({ object });
294
+ return object;
295
+ } catch (error) {
296
+ if (error instanceof GraffitiErrorNotFound) {
297
+ this.synchronizeDispatch({
298
+ tombstone: true,
299
+ object: { url: unpackObjectUrl(args[0]) },
300
+ });
301
+ }
302
+ throw error;
303
+ }
334
304
  };
335
305
 
336
- patch: Graffiti["patch"] = async (...args) => {
337
- const oldObject = await this.graffiti.patch(...args);
338
- const newObject: GraffitiObjectBase = { ...oldObject };
339
- for (const prop of ["value", "channels", "allowed"] as const) {
340
- applyGraffitiPatch(await this.applyPatch, prop, args[0], newObject);
341
- }
342
- await this.synchronizeDispatch(
343
- {
344
- tombstone: true,
345
- object: oldObject,
346
- },
347
- {
348
- object: newObject,
349
- },
350
- true,
351
- );
352
- return oldObject;
306
+ post: Graffiti["post"] = async (...args) => {
307
+ // @ts-ignore
308
+ const object = await this.graffiti.post(...args);
309
+ await this.synchronizeDispatch({ object }, true);
310
+ return object;
353
311
  };
354
312
 
355
313
  delete: Graffiti["delete"] = async (...args) => {
356
- const oldObject = await this.graffiti.delete(...args);
357
- await this.synchronizeDispatch(
358
- {
359
- tombstone: true,
360
- object: oldObject,
361
- },
362
- undefined,
363
- true,
364
- );
365
- return oldObject;
314
+ const update = {
315
+ tombstone: true,
316
+ object: { url: unpackObjectUrl(args[0]) },
317
+ } as const;
318
+ try {
319
+ const oldObject = await this.graffiti.delete(...args);
320
+ await this.synchronizeDispatch(update, true);
321
+ return oldObject;
322
+ } catch (error) {
323
+ if (error instanceof GraffitiErrorNotFound) {
324
+ await this.synchronizeDispatch(update, true);
325
+ }
326
+ throw error;
327
+ }
366
328
  };
367
329
 
368
330
  protected objectStreamContinue<Schema extends JSONSchema>(
@@ -375,14 +337,13 @@ export class GraffitiSynchronize extends Graffiti {
375
337
  if (result.done) {
376
338
  const { continue: continue_, cursor } = result.value;
377
339
  return {
378
- continue: () => this_.objectStreamContinue<Schema>(continue_()),
340
+ continue: (session?: GraffitiSession | null) =>
341
+ this_.objectStreamContinue<Schema>(continue_(session)),
379
342
  cursor,
380
343
  };
381
344
  }
382
345
  if (!result.value.error) {
383
- this_.synchronizeDispatch(
384
- result.value as GraffitiObjectStreamContinueEntry<{}>,
385
- );
346
+ this_.synchronizeDispatch(result.value);
386
347
  }
387
348
  yield result.value;
388
349
  }
@@ -408,13 +369,8 @@ export class GraffitiSynchronize extends Graffiti {
408
369
  return this.objectStream<(typeof args)[1]>(iterator);
409
370
  };
410
371
 
411
- recoverOrphans: Graffiti["recoverOrphans"] = (...args) => {
412
- const iterator = this.graffiti.recoverOrphans(...args);
413
- return this.objectStream<(typeof args)[0]>(iterator);
414
- };
415
-
416
- continueObjectStream: Graffiti["continueObjectStream"] = (...args) => {
417
- // TODO!!
418
- return this.graffiti.continueObjectStream(...args);
372
+ continueDiscover: Graffiti["continueDiscover"] = (...args) => {
373
+ const iterator = this.graffiti.continueDiscover(...args);
374
+ return this.objectStreamContinue<{}>(iterator);
419
375
  };
420
376
  }