@graffiti-garden/wrapper-synchronize 1.0.4 → 1.0.8

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/dist/cjs/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
  var index_exports = {};
30
20
  __export(index_exports, {
@@ -34,7 +24,6 @@ module.exports = __toCommonJS(index_exports);
34
24
  var import_api = require("@graffiti-garden/api");
35
25
  var import_repeater = require("@repeaterjs/repeater");
36
26
  class GraffitiSynchronize {
37
- ajv_;
38
27
  graffiti;
39
28
  callbacks = /* @__PURE__ */ new Set();
40
29
  options;
@@ -46,15 +35,6 @@ class GraffitiSynchronize {
46
35
  deleteMedia;
47
36
  actorToHandle;
48
37
  handleToActor;
49
- get ajv() {
50
- if (!this.ajv_) {
51
- this.ajv_ = (async () => {
52
- const { default: Ajv } = await import("ajv");
53
- return new Ajv({ strict: false });
54
- })();
55
- }
56
- return this.ajv_;
57
- }
58
38
  /**
59
39
  * Wraps a Graffiti API instance to provide the synchronize methods.
60
40
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -75,7 +55,7 @@ class GraffitiSynchronize {
75
55
  synchronize(matchObject, channels, schema, session, seenUrls = /* @__PURE__ */ new Set()) {
76
56
  const repeater = new import_repeater.Repeater(
77
57
  async (push, stop) => {
78
- const validate = compileGraffitiObjectSchema(await this.ajv, schema);
58
+ const validate = await (0, import_api.compileGraffitiObjectSchema)(schema);
79
59
  const callback = (objectUpdate) => {
80
60
  if (objectUpdate?.tombstone) {
81
61
  if (seenUrls.has(objectUpdate.object.url)) {
@@ -244,13 +224,4 @@ class GraffitiSynchronize {
244
224
  return this.objectStreamContinue(iterator);
245
225
  };
246
226
  }
247
- function compileGraffitiObjectSchema(ajv, schema) {
248
- try {
249
- return ajv.compile(schema);
250
- } catch (error) {
251
- throw new import_api.GraffitiErrorInvalidSchema(
252
- error instanceof Error ? error.message : String(error)
253
- );
254
- }
255
- }
256
227
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts"],
4
- "sourcesContent": ["import type Ajv from \"ajv\";\nimport type {\n Graffiti,\n GraffitiSession,\n JSONSchema,\n GraffitiObjectBase,\n GraffitiObjectStream,\n GraffitiObjectStreamContinueEntry,\n GraffitiObjectStreamContinue,\n GraffitiObjectUrl,\n GraffitiObject,\n} from \"@graffiti-garden/api\";\nimport {\n GraffitiErrorInvalidSchema,\n GraffitiErrorNotFound,\n isActorAllowedGraffitiObject,\n maskGraffitiObject,\n unpackObjectUrl,\n} from \"@graffiti-garden/api\";\nimport { Repeater } from \"@repeaterjs/repeater\";\nexport type * from \"@graffiti-garden/api\";\n\nexport type GraffitiSynchronizeCallback = (\n object: GraffitiObjectStreamContinueEntry<{}>,\n) => void;\n\nexport interface GraffitiSynchronizeOptions {\n /**\n * Allows synchronize to listen to all objects, not just those\n * that the session is allowed to see. This is useful to get a\n * global view of all Graffiti objects passsing through the system,\n * for example to build a client-side cache. Additional mechanisms\n * should be in place to ensure that users do not see objects or\n * properties they are not allowed to see.\n *\n * Default: `false`\n */\n omniscient?: boolean;\n}\n\n/**\n * Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * so that changes made or received in one part of an application\n * are automatically routed to other parts of the application.\n * This is an important tool for building responsive\n * and consistent user interfaces, and is built upon to make\n * the [Graffiti Vue Plugin](https://vue.graffiti.garden/variables/GraffitiPlugin.html)\n * and possibly other front-end libraries in the future.\n *\n * [See a live example](/example).\n *\n * Specifically, this library provides the following *synchronize*\n * methods to correspond with each of the following Graffiti API methods:\n *\n * | API Method | Synchronize Method |\n * |------------|--------------------|\n * | {@link get} | {@link synchronizeGet} |\n * | {@link discover} | {@link synchronizeDiscover} |\n *\n * Whenever a change is made via {@link post} and {@link delete} or\n * received from {@link get}, {@link discover}, and {@link continueDiscover},\n * those changes are forwarded to the appropriate synchronize method.\n * Each synchronize method returns an iterator that streams these changes\n * continually until the user calls `return` on the iterator or `break`s out of the loop,\n * allowing for live updates without additional polling.\n *\n * Example 1: Suppose a user publishes a post using {@link post}. If the feed\n * displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,\n * then the user's new post will instantly appear in their feed, giving the UI a\n * responsive feel.\n *\n * Example 2: Suppose one of a user's friends changes their name. As soon as the\n * user's application receives one notice of that change (using {@link get}\n * or {@link discover}), then {@link synchronizeDiscover} listeners can be used to update\n * all instance's of that friend's name in the user's application instantly,\n * providing a consistent user experience.\n *\n * Additionally, the library supplies a {@link synchronizeAll} method that can be used\n * to stream all the Graffiti changes that an application is aware of, which can be used\n * for caching or history building.\n *\n * The source code for this library is [available on GitHub](https://github.com/graffiti-garden/wrapper-synchronize/).\n *\n * @groupDescription 0 - Synchronize Methods\n * This group contains methods that listen for changes made via\n * {@link post}, and {@link delete} or fetched from\n * {@link get}, {@link discover}, or {@link continueDiscover} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n */\nexport class GraffitiSynchronize implements Graffiti {\n protected ajv_: Promise<Ajv> | undefined;\n protected readonly graffiti: Graffiti;\n protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();\n protected readonly options: GraffitiSynchronizeOptions;\n\n login: Graffiti[\"login\"];\n logout: Graffiti[\"logout\"];\n sessionEvents: Graffiti[\"sessionEvents\"];\n postMedia: Graffiti[\"postMedia\"];\n getMedia: Graffiti[\"getMedia\"];\n deleteMedia: Graffiti[\"deleteMedia\"];\n actorToHandle: Graffiti[\"actorToHandle\"];\n handleToActor: Graffiti[\"handleToActor\"];\n\n protected get ajv() {\n if (!this.ajv_) {\n this.ajv_ = (async () => {\n const { default: Ajv } = await import(\"ajv\");\n return new Ajv({ strict: false });\n })();\n }\n return this.ajv_;\n }\n\n /**\n * Wraps a Graffiti API instance to provide the synchronize methods.\n * The GraffitiSyncrhonize class rather than the Graffiti class\n * must be used for all functions for the synchronize methods to work.\n */\n constructor(\n /**\n * The [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * instance to wrap.\n */\n graffiti: Graffiti,\n options?: GraffitiSynchronizeOptions,\n ) {\n this.options = options ?? {};\n this.graffiti = graffiti;\n this.login = graffiti.login.bind(graffiti);\n this.logout = graffiti.logout.bind(graffiti);\n this.sessionEvents = graffiti.sessionEvents;\n this.postMedia = graffiti.postMedia.bind(graffiti);\n this.getMedia = graffiti.getMedia.bind(graffiti);\n this.deleteMedia = graffiti.deleteMedia.bind(graffiti);\n this.actorToHandle = graffiti.actorToHandle.bind(graffiti);\n this.handleToActor = graffiti.handleToActor.bind(graffiti);\n }\n\n protected synchronize<Schema extends JSONSchema>(\n matchObject: (object: GraffitiObjectBase) => boolean,\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n seenUrls: Set<string> = new Set<string>(),\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n const repeater = new Repeater<GraffitiObjectStreamContinueEntry<Schema>>(\n async (push, stop) => {\n const validate = compileGraffitiObjectSchema(await this.ajv, schema);\n const callback: GraffitiSynchronizeCallback = (objectUpdate) => {\n if (objectUpdate?.tombstone) {\n if (seenUrls.has(objectUpdate.object.url)) {\n push(objectUpdate);\n }\n } else if (\n objectUpdate &&\n matchObject(objectUpdate.object) &&\n (this.options.omniscient ||\n isActorAllowedGraffitiObject(objectUpdate.object, session))\n ) {\n // Deep clone the object to prevent mutation\n let object = JSON.parse(\n JSON.stringify(objectUpdate.object),\n ) as GraffitiObjectBase;\n if (!this.options.omniscient) {\n object = maskGraffitiObject(object, channels, session?.actor);\n }\n if (validate(object)) {\n push({ object });\n seenUrls.add(object.url);\n }\n }\n };\n\n this.callbacks.add(callback);\n await stop;\n this.callbacks.delete(callback);\n },\n );\n\n return (async function* () {\n for await (const i of repeater) yield i;\n })();\n }\n\n /**\n * This method has the same signature as {@link discover} but listens for\n * changes made via {@link post} and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link continueDiscover}\n * and then streams appropriate changes to provide a responsive and\n * consistent user experience.\n *\n * Unlike {@link discover}, this method continuously listens for changes\n * and will not terminate unless the user calls the `return` method on the iterator\n * or `break`s out of the loop.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeDiscover<Schema extends JSONSchema>(\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n function matchObject(object: GraffitiObjectBase) {\n return object.channels.some((channel) => channels.includes(channel));\n }\n return this.synchronize<Schema>(matchObject, channels, schema, session);\n }\n\n /**\n * This method has the same signature as {@link get} but\n * listens for changes made via {@link post}, and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link continueDiscover} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n *\n * Unlike {@link get}, which returns a single result, this method continuously\n * listens for changes which are output as an asynchronous stream, similar\n * to {@link discover}.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeGet<Schema extends JSONSchema>(\n objectUrl: string | GraffitiObjectUrl,\n schema: Schema,\n session?: GraffitiSession | null | undefined,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n const url = unpackObjectUrl(objectUrl);\n function matchObject(object: GraffitiObjectBase) {\n return object.url === url;\n }\n return this.synchronize<Schema>(\n matchObject,\n [],\n schema,\n session,\n new Set<string>([url]),\n );\n }\n\n /**\n * Streams changes made to *any* object in *any* channel\n * and made by *any* user. You may want to use it in conjuction with\n * {@link GraffitiSynchronizeOptions.omniscient} to get a global view\n * of all Graffiti objects passing through the system. This is useful\n * for building a client-side cache, for example.\n *\n * Be careful using this method. Without additional filters it can\n * expose the user to content out of context.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeAll<Schema extends JSONSchema>(\n schema: Schema,\n session?: GraffitiSession | null,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n return this.synchronize<Schema>(() => true, [], schema, session);\n }\n\n protected async synchronizeDispatch(\n objectUpdate: GraffitiObjectStreamContinueEntry<{}>,\n waitForListeners = false,\n ) {\n for (const callback of this.callbacks) {\n callback(objectUpdate);\n }\n if (waitForListeners) {\n // Wait for the listeners to receive\n // their objects, before returning the operation\n // that triggered them.\n //\n // This is important for mutators (put, patch, delete)\n // to ensure the application state has been updated\n // everywhere before returning, giving consistent\n // feedback to the user that the operation has completed.\n //\n // The opposite is true for accessors (get, discover, recoverOrphans),\n // where it is a weird user experience to call `get`\n // in one place and have the application update\n // somewhere else first. It is also less efficient.\n //\n // The hack is simply to await one \"macro task cycle\".\n // We need to wait for this cycle rather than using\n // `await push` in the callback, because it turns out\n // that `await push` won't resolve until the following\n // .next() call of the iterator, so if only\n // one .next() is called, this dispatch will hang.\n await new Promise((resolve) => setTimeout(resolve, 0));\n }\n }\n\n get: Graffiti[\"get\"] = async (...args) => {\n try {\n const object = await this.graffiti.get(...args);\n this.synchronizeDispatch({ object });\n return object;\n } catch (error) {\n if (error instanceof GraffitiErrorNotFound) {\n this.synchronizeDispatch({\n tombstone: true,\n object: { url: unpackObjectUrl(args[0]) },\n });\n }\n throw error;\n }\n };\n\n post: Graffiti[\"post\"] = async (...args) => {\n // @ts-ignore\n const object = await this.graffiti.post(...args);\n await this.synchronizeDispatch({ object }, true);\n return object;\n };\n\n delete: Graffiti[\"delete\"] = async (...args) => {\n const update = {\n tombstone: true,\n object: { url: unpackObjectUrl(args[0]) },\n } as const;\n try {\n const oldObject = await this.graffiti.delete(...args);\n await this.synchronizeDispatch(update, true);\n return oldObject;\n } catch (error) {\n if (error instanceof GraffitiErrorNotFound) {\n await this.synchronizeDispatch(update, true);\n }\n throw error;\n }\n };\n\n protected objectStreamContinue<Schema extends JSONSchema>(\n iterator: GraffitiObjectStreamContinue<Schema>,\n ): GraffitiObjectStreamContinue<Schema> {\n const this_ = this;\n return (async function* () {\n while (true) {\n const result = await iterator.next();\n if (result.done) {\n const { continue: continue_, cursor } = result.value;\n return {\n continue: (session?: GraffitiSession | null) =>\n this_.objectStreamContinue<Schema>(continue_(session)),\n cursor,\n };\n }\n if (!result.value.error) {\n this_.synchronizeDispatch(result.value);\n }\n yield result.value;\n }\n })();\n }\n\n protected objectStream<Schema extends JSONSchema>(\n iterator: GraffitiObjectStream<Schema>,\n ): GraffitiObjectStream<Schema> {\n const wrapped = this.objectStreamContinue<Schema>(iterator);\n return (async function* () {\n // Filter out the tombstones for type safety\n while (true) {\n const result = await wrapped.next();\n if (result.done) return result.value;\n if (result.value.error || !result.value.tombstone) yield result.value;\n }\n })();\n }\n\n discover: Graffiti[\"discover\"] = (...args) => {\n const iterator = this.graffiti.discover(...args);\n return this.objectStream<(typeof args)[1]>(iterator);\n };\n\n continueDiscover: Graffiti[\"continueDiscover\"] = (...args) => {\n const iterator = this.graffiti.continueDiscover(...args);\n return this.objectStreamContinue<{}>(iterator);\n };\n}\n\nfunction 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 : String(error),\n );\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type {\n Graffiti,\n GraffitiSession,\n JSONSchema,\n GraffitiObjectBase,\n GraffitiObjectStream,\n GraffitiObjectStreamContinueEntry,\n GraffitiObjectStreamContinue,\n GraffitiObjectUrl,\n} from \"@graffiti-garden/api\";\nimport {\n compileGraffitiObjectSchema,\n GraffitiErrorNotFound,\n isActorAllowedGraffitiObject,\n maskGraffitiObject,\n unpackObjectUrl,\n} from \"@graffiti-garden/api\";\nimport { Repeater } from \"@repeaterjs/repeater\";\nexport type * from \"@graffiti-garden/api\";\n\nexport type GraffitiSynchronizeCallback = (\n object: GraffitiObjectStreamContinueEntry<{}>,\n) => void;\n\nexport interface GraffitiSynchronizeOptions {\n /**\n * Allows synchronize to listen to all objects, not just those\n * that the session is allowed to see. This is useful to get a\n * global view of all Graffiti objects passsing through the system,\n * for example to build a client-side cache. Additional mechanisms\n * should be in place to ensure that users do not see objects or\n * properties they are not allowed to see.\n *\n * Default: `false`\n */\n omniscient?: boolean;\n}\n\n/**\n * Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * so that changes made or received in one part of an application\n * are automatically routed to other parts of the application.\n * This is an important tool for building responsive\n * and consistent user interfaces, and is built upon to make\n * the [Graffiti Vue Plugin](https://vue.graffiti.garden/variables/GraffitiPlugin.html)\n * and possibly other front-end libraries in the future.\n *\n * [See a live example](/example).\n *\n * Specifically, this library provides the following *synchronize*\n * methods to correspond with each of the following Graffiti API methods:\n *\n * | API Method | Synchronize Method |\n * |------------|--------------------|\n * | {@link get} | {@link synchronizeGet} |\n * | {@link discover} | {@link synchronizeDiscover} |\n *\n * Whenever a change is made via {@link post} and {@link delete} or\n * received from {@link get}, {@link discover}, and {@link continueDiscover},\n * those changes are forwarded to the appropriate synchronize method.\n * Each synchronize method returns an iterator that streams these changes\n * continually until the user calls `return` on the iterator or `break`s out of the loop,\n * allowing for live updates without additional polling.\n *\n * Example 1: Suppose a user publishes a post using {@link post}. If the feed\n * displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,\n * then the user's new post will instantly appear in their feed, giving the UI a\n * responsive feel.\n *\n * Example 2: Suppose one of a user's friends changes their name. As soon as the\n * user's application receives one notice of that change (using {@link get}\n * or {@link discover}), then {@link synchronizeDiscover} listeners can be used to update\n * all instance's of that friend's name in the user's application instantly,\n * providing a consistent user experience.\n *\n * Additionally, the library supplies a {@link synchronizeAll} method that can be used\n * to stream all the Graffiti changes that an application is aware of, which can be used\n * for caching or history building.\n *\n * The source code for this library is [available on GitHub](https://github.com/graffiti-garden/wrapper-synchronize/).\n *\n * @groupDescription 0 - Synchronize Methods\n * This group contains methods that listen for changes made via\n * {@link post}, and {@link delete} or fetched from\n * {@link get}, {@link discover}, or {@link continueDiscover} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n */\nexport class GraffitiSynchronize implements Graffiti {\n protected readonly graffiti: Graffiti;\n protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();\n protected readonly options: GraffitiSynchronizeOptions;\n\n login: Graffiti[\"login\"];\n logout: Graffiti[\"logout\"];\n sessionEvents: Graffiti[\"sessionEvents\"];\n postMedia: Graffiti[\"postMedia\"];\n getMedia: Graffiti[\"getMedia\"];\n deleteMedia: Graffiti[\"deleteMedia\"];\n actorToHandle: Graffiti[\"actorToHandle\"];\n handleToActor: Graffiti[\"handleToActor\"];\n\n /**\n * Wraps a Graffiti API instance to provide the synchronize methods.\n * The GraffitiSyncrhonize class rather than the Graffiti class\n * must be used for all functions for the synchronize methods to work.\n */\n constructor(\n /**\n * The [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * instance to wrap.\n */\n graffiti: Graffiti,\n options?: GraffitiSynchronizeOptions,\n ) {\n this.options = options ?? {};\n this.graffiti = graffiti;\n this.login = graffiti.login.bind(graffiti);\n this.logout = graffiti.logout.bind(graffiti);\n this.sessionEvents = graffiti.sessionEvents;\n this.postMedia = graffiti.postMedia.bind(graffiti);\n this.getMedia = graffiti.getMedia.bind(graffiti);\n this.deleteMedia = graffiti.deleteMedia.bind(graffiti);\n this.actorToHandle = graffiti.actorToHandle.bind(graffiti);\n this.handleToActor = graffiti.handleToActor.bind(graffiti);\n }\n\n protected synchronize<Schema extends JSONSchema>(\n matchObject: (object: GraffitiObjectBase) => boolean,\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n seenUrls: Set<string> = new Set<string>(),\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n const repeater = new Repeater<GraffitiObjectStreamContinueEntry<Schema>>(\n async (push, stop) => {\n const validate = await compileGraffitiObjectSchema(schema);\n const callback: GraffitiSynchronizeCallback = (objectUpdate) => {\n if (objectUpdate?.tombstone) {\n if (seenUrls.has(objectUpdate.object.url)) {\n push(objectUpdate);\n }\n } else if (\n objectUpdate &&\n matchObject(objectUpdate.object) &&\n (this.options.omniscient ||\n isActorAllowedGraffitiObject(objectUpdate.object, session))\n ) {\n // Deep clone the object to prevent mutation\n let object = JSON.parse(\n JSON.stringify(objectUpdate.object),\n ) as GraffitiObjectBase;\n if (!this.options.omniscient) {\n object = maskGraffitiObject(object, channels, session?.actor);\n }\n if (validate(object)) {\n push({ object });\n seenUrls.add(object.url);\n }\n }\n };\n\n this.callbacks.add(callback);\n await stop;\n this.callbacks.delete(callback);\n },\n );\n\n return (async function* () {\n for await (const i of repeater) yield i;\n })();\n }\n\n /**\n * This method has the same signature as {@link discover} but listens for\n * changes made via {@link post} and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link continueDiscover}\n * and then streams appropriate changes to provide a responsive and\n * consistent user experience.\n *\n * Unlike {@link discover}, this method continuously listens for changes\n * and will not terminate unless the user calls the `return` method on the iterator\n * or `break`s out of the loop.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeDiscover<Schema extends JSONSchema>(\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n function matchObject(object: GraffitiObjectBase) {\n return object.channels.some((channel) => channels.includes(channel));\n }\n return this.synchronize<Schema>(matchObject, channels, schema, session);\n }\n\n /**\n * This method has the same signature as {@link get} but\n * listens for changes made via {@link post}, and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link continueDiscover} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n *\n * Unlike {@link get}, which returns a single result, this method continuously\n * listens for changes which are output as an asynchronous stream, similar\n * to {@link discover}.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeGet<Schema extends JSONSchema>(\n objectUrl: string | GraffitiObjectUrl,\n schema: Schema,\n session?: GraffitiSession | null | undefined,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n const url = unpackObjectUrl(objectUrl);\n function matchObject(object: GraffitiObjectBase) {\n return object.url === url;\n }\n return this.synchronize<Schema>(\n matchObject,\n [],\n schema,\n session,\n new Set<string>([url]),\n );\n }\n\n /**\n * Streams changes made to *any* object in *any* channel\n * and made by *any* user. You may want to use it in conjuction with\n * {@link GraffitiSynchronizeOptions.omniscient} to get a global view\n * of all Graffiti objects passing through the system. This is useful\n * for building a client-side cache, for example.\n *\n * Be careful using this method. Without additional filters it can\n * expose the user to content out of context.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeAll<Schema extends JSONSchema>(\n schema: Schema,\n session?: GraffitiSession | null,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n return this.synchronize<Schema>(() => true, [], schema, session);\n }\n\n protected async synchronizeDispatch(\n objectUpdate: GraffitiObjectStreamContinueEntry<{}>,\n waitForListeners = false,\n ) {\n for (const callback of this.callbacks) {\n callback(objectUpdate);\n }\n if (waitForListeners) {\n // Wait for the listeners to receive\n // their objects, before returning the operation\n // that triggered them.\n //\n // This is important for mutators (put, patch, delete)\n // to ensure the application state has been updated\n // everywhere before returning, giving consistent\n // feedback to the user that the operation has completed.\n //\n // The opposite is true for accessors (get, discover, recoverOrphans),\n // where it is a weird user experience to call `get`\n // in one place and have the application update\n // somewhere else first. It is also less efficient.\n //\n // The hack is simply to await one \"macro task cycle\".\n // We need to wait for this cycle rather than using\n // `await push` in the callback, because it turns out\n // that `await push` won't resolve until the following\n // .next() call of the iterator, so if only\n // one .next() is called, this dispatch will hang.\n await new Promise((resolve) => setTimeout(resolve, 0));\n }\n }\n\n get: Graffiti[\"get\"] = async (...args) => {\n try {\n const object = await this.graffiti.get(...args);\n this.synchronizeDispatch({ object });\n return object;\n } catch (error) {\n if (error instanceof GraffitiErrorNotFound) {\n this.synchronizeDispatch({\n tombstone: true,\n object: { url: unpackObjectUrl(args[0]) },\n });\n }\n throw error;\n }\n };\n\n post: Graffiti[\"post\"] = async (...args) => {\n // @ts-ignore\n const object = await this.graffiti.post(...args);\n await this.synchronizeDispatch({ object }, true);\n return object;\n };\n\n delete: Graffiti[\"delete\"] = async (...args) => {\n const update = {\n tombstone: true,\n object: { url: unpackObjectUrl(args[0]) },\n } as const;\n try {\n const oldObject = await this.graffiti.delete(...args);\n await this.synchronizeDispatch(update, true);\n return oldObject;\n } catch (error) {\n if (error instanceof GraffitiErrorNotFound) {\n await this.synchronizeDispatch(update, true);\n }\n throw error;\n }\n };\n\n protected objectStreamContinue<Schema extends JSONSchema>(\n iterator: GraffitiObjectStreamContinue<Schema>,\n ): GraffitiObjectStreamContinue<Schema> {\n const this_ = this;\n return (async function* () {\n while (true) {\n const result = await iterator.next();\n if (result.done) {\n const { continue: continue_, cursor } = result.value;\n return {\n continue: (session?: GraffitiSession | null) =>\n this_.objectStreamContinue<Schema>(continue_(session)),\n cursor,\n };\n }\n if (!result.value.error) {\n this_.synchronizeDispatch(result.value);\n }\n yield result.value;\n }\n })();\n }\n\n protected objectStream<Schema extends JSONSchema>(\n iterator: GraffitiObjectStream<Schema>,\n ): GraffitiObjectStream<Schema> {\n const wrapped = this.objectStreamContinue<Schema>(iterator);\n return (async function* () {\n // Filter out the tombstones for type safety\n while (true) {\n const result = await wrapped.next();\n if (result.done) return result.value;\n if (result.value.error || !result.value.tombstone) yield result.value;\n }\n })();\n }\n\n discover: Graffiti[\"discover\"] = (...args) => {\n const iterator = this.graffiti.discover(...args);\n return this.objectStream<(typeof args)[1]>(iterator);\n };\n\n continueDiscover: Graffiti[\"continueDiscover\"] = (...args) => {\n const iterator = this.graffiti.continueDiscover(...args);\n return this.objectStreamContinue<{}>(iterator);\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import {
2
- GraffitiErrorInvalidSchema,
2
+ compileGraffitiObjectSchema,
3
3
  GraffitiErrorNotFound,
4
4
  isActorAllowedGraffitiObject,
5
5
  maskGraffitiObject,
@@ -7,7 +7,6 @@ import {
7
7
  } from "@graffiti-garden/api";
8
8
  import { Repeater } from "@repeaterjs/repeater";
9
9
  class GraffitiSynchronize {
10
- ajv_;
11
10
  graffiti;
12
11
  callbacks = /* @__PURE__ */ new Set();
13
12
  options;
@@ -19,15 +18,6 @@ class GraffitiSynchronize {
19
18
  deleteMedia;
20
19
  actorToHandle;
21
20
  handleToActor;
22
- get ajv() {
23
- if (!this.ajv_) {
24
- this.ajv_ = (async () => {
25
- const { default: Ajv } = await import("ajv");
26
- return new Ajv({ strict: false });
27
- })();
28
- }
29
- return this.ajv_;
30
- }
31
21
  /**
32
22
  * Wraps a Graffiti API instance to provide the synchronize methods.
33
23
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -48,7 +38,7 @@ class GraffitiSynchronize {
48
38
  synchronize(matchObject, channels, schema, session, seenUrls = /* @__PURE__ */ new Set()) {
49
39
  const repeater = new Repeater(
50
40
  async (push, stop) => {
51
- const validate = compileGraffitiObjectSchema(await this.ajv, schema);
41
+ const validate = await compileGraffitiObjectSchema(schema);
52
42
  const callback = (objectUpdate) => {
53
43
  if (objectUpdate?.tombstone) {
54
44
  if (seenUrls.has(objectUpdate.object.url)) {
@@ -217,15 +207,6 @@ class GraffitiSynchronize {
217
207
  return this.objectStreamContinue(iterator);
218
208
  };
219
209
  }
220
- function compileGraffitiObjectSchema(ajv, schema) {
221
- try {
222
- return ajv.compile(schema);
223
- } catch (error) {
224
- throw new GraffitiErrorInvalidSchema(
225
- error instanceof Error ? error.message : String(error)
226
- );
227
- }
228
- }
229
210
  export {
230
211
  GraffitiSynchronize
231
212
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts"],
4
- "sourcesContent": ["import type Ajv from \"ajv\";\nimport type {\n Graffiti,\n GraffitiSession,\n JSONSchema,\n GraffitiObjectBase,\n GraffitiObjectStream,\n GraffitiObjectStreamContinueEntry,\n GraffitiObjectStreamContinue,\n GraffitiObjectUrl,\n GraffitiObject,\n} from \"@graffiti-garden/api\";\nimport {\n GraffitiErrorInvalidSchema,\n GraffitiErrorNotFound,\n isActorAllowedGraffitiObject,\n maskGraffitiObject,\n unpackObjectUrl,\n} from \"@graffiti-garden/api\";\nimport { Repeater } from \"@repeaterjs/repeater\";\nexport type * from \"@graffiti-garden/api\";\n\nexport type GraffitiSynchronizeCallback = (\n object: GraffitiObjectStreamContinueEntry<{}>,\n) => void;\n\nexport interface GraffitiSynchronizeOptions {\n /**\n * Allows synchronize to listen to all objects, not just those\n * that the session is allowed to see. This is useful to get a\n * global view of all Graffiti objects passsing through the system,\n * for example to build a client-side cache. Additional mechanisms\n * should be in place to ensure that users do not see objects or\n * properties they are not allowed to see.\n *\n * Default: `false`\n */\n omniscient?: boolean;\n}\n\n/**\n * Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * so that changes made or received in one part of an application\n * are automatically routed to other parts of the application.\n * This is an important tool for building responsive\n * and consistent user interfaces, and is built upon to make\n * the [Graffiti Vue Plugin](https://vue.graffiti.garden/variables/GraffitiPlugin.html)\n * and possibly other front-end libraries in the future.\n *\n * [See a live example](/example).\n *\n * Specifically, this library provides the following *synchronize*\n * methods to correspond with each of the following Graffiti API methods:\n *\n * | API Method | Synchronize Method |\n * |------------|--------------------|\n * | {@link get} | {@link synchronizeGet} |\n * | {@link discover} | {@link synchronizeDiscover} |\n *\n * Whenever a change is made via {@link post} and {@link delete} or\n * received from {@link get}, {@link discover}, and {@link continueDiscover},\n * those changes are forwarded to the appropriate synchronize method.\n * Each synchronize method returns an iterator that streams these changes\n * continually until the user calls `return` on the iterator or `break`s out of the loop,\n * allowing for live updates without additional polling.\n *\n * Example 1: Suppose a user publishes a post using {@link post}. If the feed\n * displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,\n * then the user's new post will instantly appear in their feed, giving the UI a\n * responsive feel.\n *\n * Example 2: Suppose one of a user's friends changes their name. As soon as the\n * user's application receives one notice of that change (using {@link get}\n * or {@link discover}), then {@link synchronizeDiscover} listeners can be used to update\n * all instance's of that friend's name in the user's application instantly,\n * providing a consistent user experience.\n *\n * Additionally, the library supplies a {@link synchronizeAll} method that can be used\n * to stream all the Graffiti changes that an application is aware of, which can be used\n * for caching or history building.\n *\n * The source code for this library is [available on GitHub](https://github.com/graffiti-garden/wrapper-synchronize/).\n *\n * @groupDescription 0 - Synchronize Methods\n * This group contains methods that listen for changes made via\n * {@link post}, and {@link delete} or fetched from\n * {@link get}, {@link discover}, or {@link continueDiscover} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n */\nexport class GraffitiSynchronize implements Graffiti {\n protected ajv_: Promise<Ajv> | undefined;\n protected readonly graffiti: Graffiti;\n protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();\n protected readonly options: GraffitiSynchronizeOptions;\n\n login: Graffiti[\"login\"];\n logout: Graffiti[\"logout\"];\n sessionEvents: Graffiti[\"sessionEvents\"];\n postMedia: Graffiti[\"postMedia\"];\n getMedia: Graffiti[\"getMedia\"];\n deleteMedia: Graffiti[\"deleteMedia\"];\n actorToHandle: Graffiti[\"actorToHandle\"];\n handleToActor: Graffiti[\"handleToActor\"];\n\n protected get ajv() {\n if (!this.ajv_) {\n this.ajv_ = (async () => {\n const { default: Ajv } = await import(\"ajv\");\n return new Ajv({ strict: false });\n })();\n }\n return this.ajv_;\n }\n\n /**\n * Wraps a Graffiti API instance to provide the synchronize methods.\n * The GraffitiSyncrhonize class rather than the Graffiti class\n * must be used for all functions for the synchronize methods to work.\n */\n constructor(\n /**\n * The [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * instance to wrap.\n */\n graffiti: Graffiti,\n options?: GraffitiSynchronizeOptions,\n ) {\n this.options = options ?? {};\n this.graffiti = graffiti;\n this.login = graffiti.login.bind(graffiti);\n this.logout = graffiti.logout.bind(graffiti);\n this.sessionEvents = graffiti.sessionEvents;\n this.postMedia = graffiti.postMedia.bind(graffiti);\n this.getMedia = graffiti.getMedia.bind(graffiti);\n this.deleteMedia = graffiti.deleteMedia.bind(graffiti);\n this.actorToHandle = graffiti.actorToHandle.bind(graffiti);\n this.handleToActor = graffiti.handleToActor.bind(graffiti);\n }\n\n protected synchronize<Schema extends JSONSchema>(\n matchObject: (object: GraffitiObjectBase) => boolean,\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n seenUrls: Set<string> = new Set<string>(),\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n const repeater = new Repeater<GraffitiObjectStreamContinueEntry<Schema>>(\n async (push, stop) => {\n const validate = compileGraffitiObjectSchema(await this.ajv, schema);\n const callback: GraffitiSynchronizeCallback = (objectUpdate) => {\n if (objectUpdate?.tombstone) {\n if (seenUrls.has(objectUpdate.object.url)) {\n push(objectUpdate);\n }\n } else if (\n objectUpdate &&\n matchObject(objectUpdate.object) &&\n (this.options.omniscient ||\n isActorAllowedGraffitiObject(objectUpdate.object, session))\n ) {\n // Deep clone the object to prevent mutation\n let object = JSON.parse(\n JSON.stringify(objectUpdate.object),\n ) as GraffitiObjectBase;\n if (!this.options.omniscient) {\n object = maskGraffitiObject(object, channels, session?.actor);\n }\n if (validate(object)) {\n push({ object });\n seenUrls.add(object.url);\n }\n }\n };\n\n this.callbacks.add(callback);\n await stop;\n this.callbacks.delete(callback);\n },\n );\n\n return (async function* () {\n for await (const i of repeater) yield i;\n })();\n }\n\n /**\n * This method has the same signature as {@link discover} but listens for\n * changes made via {@link post} and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link continueDiscover}\n * and then streams appropriate changes to provide a responsive and\n * consistent user experience.\n *\n * Unlike {@link discover}, this method continuously listens for changes\n * and will not terminate unless the user calls the `return` method on the iterator\n * or `break`s out of the loop.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeDiscover<Schema extends JSONSchema>(\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n function matchObject(object: GraffitiObjectBase) {\n return object.channels.some((channel) => channels.includes(channel));\n }\n return this.synchronize<Schema>(matchObject, channels, schema, session);\n }\n\n /**\n * This method has the same signature as {@link get} but\n * listens for changes made via {@link post}, and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link continueDiscover} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n *\n * Unlike {@link get}, which returns a single result, this method continuously\n * listens for changes which are output as an asynchronous stream, similar\n * to {@link discover}.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeGet<Schema extends JSONSchema>(\n objectUrl: string | GraffitiObjectUrl,\n schema: Schema,\n session?: GraffitiSession | null | undefined,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n const url = unpackObjectUrl(objectUrl);\n function matchObject(object: GraffitiObjectBase) {\n return object.url === url;\n }\n return this.synchronize<Schema>(\n matchObject,\n [],\n schema,\n session,\n new Set<string>([url]),\n );\n }\n\n /**\n * Streams changes made to *any* object in *any* channel\n * and made by *any* user. You may want to use it in conjuction with\n * {@link GraffitiSynchronizeOptions.omniscient} to get a global view\n * of all Graffiti objects passing through the system. This is useful\n * for building a client-side cache, for example.\n *\n * Be careful using this method. Without additional filters it can\n * expose the user to content out of context.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeAll<Schema extends JSONSchema>(\n schema: Schema,\n session?: GraffitiSession | null,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n return this.synchronize<Schema>(() => true, [], schema, session);\n }\n\n protected async synchronizeDispatch(\n objectUpdate: GraffitiObjectStreamContinueEntry<{}>,\n waitForListeners = false,\n ) {\n for (const callback of this.callbacks) {\n callback(objectUpdate);\n }\n if (waitForListeners) {\n // Wait for the listeners to receive\n // their objects, before returning the operation\n // that triggered them.\n //\n // This is important for mutators (put, patch, delete)\n // to ensure the application state has been updated\n // everywhere before returning, giving consistent\n // feedback to the user that the operation has completed.\n //\n // The opposite is true for accessors (get, discover, recoverOrphans),\n // where it is a weird user experience to call `get`\n // in one place and have the application update\n // somewhere else first. It is also less efficient.\n //\n // The hack is simply to await one \"macro task cycle\".\n // We need to wait for this cycle rather than using\n // `await push` in the callback, because it turns out\n // that `await push` won't resolve until the following\n // .next() call of the iterator, so if only\n // one .next() is called, this dispatch will hang.\n await new Promise((resolve) => setTimeout(resolve, 0));\n }\n }\n\n get: Graffiti[\"get\"] = async (...args) => {\n try {\n const object = await this.graffiti.get(...args);\n this.synchronizeDispatch({ object });\n return object;\n } catch (error) {\n if (error instanceof GraffitiErrorNotFound) {\n this.synchronizeDispatch({\n tombstone: true,\n object: { url: unpackObjectUrl(args[0]) },\n });\n }\n throw error;\n }\n };\n\n post: Graffiti[\"post\"] = async (...args) => {\n // @ts-ignore\n const object = await this.graffiti.post(...args);\n await this.synchronizeDispatch({ object }, true);\n return object;\n };\n\n delete: Graffiti[\"delete\"] = async (...args) => {\n const update = {\n tombstone: true,\n object: { url: unpackObjectUrl(args[0]) },\n } as const;\n try {\n const oldObject = await this.graffiti.delete(...args);\n await this.synchronizeDispatch(update, true);\n return oldObject;\n } catch (error) {\n if (error instanceof GraffitiErrorNotFound) {\n await this.synchronizeDispatch(update, true);\n }\n throw error;\n }\n };\n\n protected objectStreamContinue<Schema extends JSONSchema>(\n iterator: GraffitiObjectStreamContinue<Schema>,\n ): GraffitiObjectStreamContinue<Schema> {\n const this_ = this;\n return (async function* () {\n while (true) {\n const result = await iterator.next();\n if (result.done) {\n const { continue: continue_, cursor } = result.value;\n return {\n continue: (session?: GraffitiSession | null) =>\n this_.objectStreamContinue<Schema>(continue_(session)),\n cursor,\n };\n }\n if (!result.value.error) {\n this_.synchronizeDispatch(result.value);\n }\n yield result.value;\n }\n })();\n }\n\n protected objectStream<Schema extends JSONSchema>(\n iterator: GraffitiObjectStream<Schema>,\n ): GraffitiObjectStream<Schema> {\n const wrapped = this.objectStreamContinue<Schema>(iterator);\n return (async function* () {\n // Filter out the tombstones for type safety\n while (true) {\n const result = await wrapped.next();\n if (result.done) return result.value;\n if (result.value.error || !result.value.tombstone) yield result.value;\n }\n })();\n }\n\n discover: Graffiti[\"discover\"] = (...args) => {\n const iterator = this.graffiti.discover(...args);\n return this.objectStream<(typeof args)[1]>(iterator);\n };\n\n continueDiscover: Graffiti[\"continueDiscover\"] = (...args) => {\n const iterator = this.graffiti.continueDiscover(...args);\n return this.objectStreamContinue<{}>(iterator);\n };\n}\n\nfunction 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 : String(error),\n );\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type {\n Graffiti,\n GraffitiSession,\n JSONSchema,\n GraffitiObjectBase,\n GraffitiObjectStream,\n GraffitiObjectStreamContinueEntry,\n GraffitiObjectStreamContinue,\n GraffitiObjectUrl,\n} from \"@graffiti-garden/api\";\nimport {\n compileGraffitiObjectSchema,\n GraffitiErrorNotFound,\n isActorAllowedGraffitiObject,\n maskGraffitiObject,\n unpackObjectUrl,\n} from \"@graffiti-garden/api\";\nimport { Repeater } from \"@repeaterjs/repeater\";\nexport type * from \"@graffiti-garden/api\";\n\nexport type GraffitiSynchronizeCallback = (\n object: GraffitiObjectStreamContinueEntry<{}>,\n) => void;\n\nexport interface GraffitiSynchronizeOptions {\n /**\n * Allows synchronize to listen to all objects, not just those\n * that the session is allowed to see. This is useful to get a\n * global view of all Graffiti objects passsing through the system,\n * for example to build a client-side cache. Additional mechanisms\n * should be in place to ensure that users do not see objects or\n * properties they are not allowed to see.\n *\n * Default: `false`\n */\n omniscient?: boolean;\n}\n\n/**\n * Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * so that changes made or received in one part of an application\n * are automatically routed to other parts of the application.\n * This is an important tool for building responsive\n * and consistent user interfaces, and is built upon to make\n * the [Graffiti Vue Plugin](https://vue.graffiti.garden/variables/GraffitiPlugin.html)\n * and possibly other front-end libraries in the future.\n *\n * [See a live example](/example).\n *\n * Specifically, this library provides the following *synchronize*\n * methods to correspond with each of the following Graffiti API methods:\n *\n * | API Method | Synchronize Method |\n * |------------|--------------------|\n * | {@link get} | {@link synchronizeGet} |\n * | {@link discover} | {@link synchronizeDiscover} |\n *\n * Whenever a change is made via {@link post} and {@link delete} or\n * received from {@link get}, {@link discover}, and {@link continueDiscover},\n * those changes are forwarded to the appropriate synchronize method.\n * Each synchronize method returns an iterator that streams these changes\n * continually until the user calls `return` on the iterator or `break`s out of the loop,\n * allowing for live updates without additional polling.\n *\n * Example 1: Suppose a user publishes a post using {@link post}. If the feed\n * displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,\n * then the user's new post will instantly appear in their feed, giving the UI a\n * responsive feel.\n *\n * Example 2: Suppose one of a user's friends changes their name. As soon as the\n * user's application receives one notice of that change (using {@link get}\n * or {@link discover}), then {@link synchronizeDiscover} listeners can be used to update\n * all instance's of that friend's name in the user's application instantly,\n * providing a consistent user experience.\n *\n * Additionally, the library supplies a {@link synchronizeAll} method that can be used\n * to stream all the Graffiti changes that an application is aware of, which can be used\n * for caching or history building.\n *\n * The source code for this library is [available on GitHub](https://github.com/graffiti-garden/wrapper-synchronize/).\n *\n * @groupDescription 0 - Synchronize Methods\n * This group contains methods that listen for changes made via\n * {@link post}, and {@link delete} or fetched from\n * {@link get}, {@link discover}, or {@link continueDiscover} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n */\nexport class GraffitiSynchronize implements Graffiti {\n protected readonly graffiti: Graffiti;\n protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();\n protected readonly options: GraffitiSynchronizeOptions;\n\n login: Graffiti[\"login\"];\n logout: Graffiti[\"logout\"];\n sessionEvents: Graffiti[\"sessionEvents\"];\n postMedia: Graffiti[\"postMedia\"];\n getMedia: Graffiti[\"getMedia\"];\n deleteMedia: Graffiti[\"deleteMedia\"];\n actorToHandle: Graffiti[\"actorToHandle\"];\n handleToActor: Graffiti[\"handleToActor\"];\n\n /**\n * Wraps a Graffiti API instance to provide the synchronize methods.\n * The GraffitiSyncrhonize class rather than the Graffiti class\n * must be used for all functions for the synchronize methods to work.\n */\n constructor(\n /**\n * The [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)\n * instance to wrap.\n */\n graffiti: Graffiti,\n options?: GraffitiSynchronizeOptions,\n ) {\n this.options = options ?? {};\n this.graffiti = graffiti;\n this.login = graffiti.login.bind(graffiti);\n this.logout = graffiti.logout.bind(graffiti);\n this.sessionEvents = graffiti.sessionEvents;\n this.postMedia = graffiti.postMedia.bind(graffiti);\n this.getMedia = graffiti.getMedia.bind(graffiti);\n this.deleteMedia = graffiti.deleteMedia.bind(graffiti);\n this.actorToHandle = graffiti.actorToHandle.bind(graffiti);\n this.handleToActor = graffiti.handleToActor.bind(graffiti);\n }\n\n protected synchronize<Schema extends JSONSchema>(\n matchObject: (object: GraffitiObjectBase) => boolean,\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n seenUrls: Set<string> = new Set<string>(),\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n const repeater = new Repeater<GraffitiObjectStreamContinueEntry<Schema>>(\n async (push, stop) => {\n const validate = await compileGraffitiObjectSchema(schema);\n const callback: GraffitiSynchronizeCallback = (objectUpdate) => {\n if (objectUpdate?.tombstone) {\n if (seenUrls.has(objectUpdate.object.url)) {\n push(objectUpdate);\n }\n } else if (\n objectUpdate &&\n matchObject(objectUpdate.object) &&\n (this.options.omniscient ||\n isActorAllowedGraffitiObject(objectUpdate.object, session))\n ) {\n // Deep clone the object to prevent mutation\n let object = JSON.parse(\n JSON.stringify(objectUpdate.object),\n ) as GraffitiObjectBase;\n if (!this.options.omniscient) {\n object = maskGraffitiObject(object, channels, session?.actor);\n }\n if (validate(object)) {\n push({ object });\n seenUrls.add(object.url);\n }\n }\n };\n\n this.callbacks.add(callback);\n await stop;\n this.callbacks.delete(callback);\n },\n );\n\n return (async function* () {\n for await (const i of repeater) yield i;\n })();\n }\n\n /**\n * This method has the same signature as {@link discover} but listens for\n * changes made via {@link post} and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link continueDiscover}\n * and then streams appropriate changes to provide a responsive and\n * consistent user experience.\n *\n * Unlike {@link discover}, this method continuously listens for changes\n * and will not terminate unless the user calls the `return` method on the iterator\n * or `break`s out of the loop.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeDiscover<Schema extends JSONSchema>(\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n function matchObject(object: GraffitiObjectBase) {\n return object.channels.some((channel) => channels.includes(channel));\n }\n return this.synchronize<Schema>(matchObject, channels, schema, session);\n }\n\n /**\n * This method has the same signature as {@link get} but\n * listens for changes made via {@link post}, and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link continueDiscover} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n *\n * Unlike {@link get}, which returns a single result, this method continuously\n * listens for changes which are output as an asynchronous stream, similar\n * to {@link discover}.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeGet<Schema extends JSONSchema>(\n objectUrl: string | GraffitiObjectUrl,\n schema: Schema,\n session?: GraffitiSession | null | undefined,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n const url = unpackObjectUrl(objectUrl);\n function matchObject(object: GraffitiObjectBase) {\n return object.url === url;\n }\n return this.synchronize<Schema>(\n matchObject,\n [],\n schema,\n session,\n new Set<string>([url]),\n );\n }\n\n /**\n * Streams changes made to *any* object in *any* channel\n * and made by *any* user. You may want to use it in conjuction with\n * {@link GraffitiSynchronizeOptions.omniscient} to get a global view\n * of all Graffiti objects passing through the system. This is useful\n * for building a client-side cache, for example.\n *\n * Be careful using this method. Without additional filters it can\n * expose the user to content out of context.\n *\n * @group 0 - Synchronize Methods\n */\n synchronizeAll<Schema extends JSONSchema>(\n schema: Schema,\n session?: GraffitiSession | null,\n ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {\n return this.synchronize<Schema>(() => true, [], schema, session);\n }\n\n protected async synchronizeDispatch(\n objectUpdate: GraffitiObjectStreamContinueEntry<{}>,\n waitForListeners = false,\n ) {\n for (const callback of this.callbacks) {\n callback(objectUpdate);\n }\n if (waitForListeners) {\n // Wait for the listeners to receive\n // their objects, before returning the operation\n // that triggered them.\n //\n // This is important for mutators (put, patch, delete)\n // to ensure the application state has been updated\n // everywhere before returning, giving consistent\n // feedback to the user that the operation has completed.\n //\n // The opposite is true for accessors (get, discover, recoverOrphans),\n // where it is a weird user experience to call `get`\n // in one place and have the application update\n // somewhere else first. It is also less efficient.\n //\n // The hack is simply to await one \"macro task cycle\".\n // We need to wait for this cycle rather than using\n // `await push` in the callback, because it turns out\n // that `await push` won't resolve until the following\n // .next() call of the iterator, so if only\n // one .next() is called, this dispatch will hang.\n await new Promise((resolve) => setTimeout(resolve, 0));\n }\n }\n\n get: Graffiti[\"get\"] = async (...args) => {\n try {\n const object = await this.graffiti.get(...args);\n this.synchronizeDispatch({ object });\n return object;\n } catch (error) {\n if (error instanceof GraffitiErrorNotFound) {\n this.synchronizeDispatch({\n tombstone: true,\n object: { url: unpackObjectUrl(args[0]) },\n });\n }\n throw error;\n }\n };\n\n post: Graffiti[\"post\"] = async (...args) => {\n // @ts-ignore\n const object = await this.graffiti.post(...args);\n await this.synchronizeDispatch({ object }, true);\n return object;\n };\n\n delete: Graffiti[\"delete\"] = async (...args) => {\n const update = {\n tombstone: true,\n object: { url: unpackObjectUrl(args[0]) },\n } as const;\n try {\n const oldObject = await this.graffiti.delete(...args);\n await this.synchronizeDispatch(update, true);\n return oldObject;\n } catch (error) {\n if (error instanceof GraffitiErrorNotFound) {\n await this.synchronizeDispatch(update, true);\n }\n throw error;\n }\n };\n\n protected objectStreamContinue<Schema extends JSONSchema>(\n iterator: GraffitiObjectStreamContinue<Schema>,\n ): GraffitiObjectStreamContinue<Schema> {\n const this_ = this;\n return (async function* () {\n while (true) {\n const result = await iterator.next();\n if (result.done) {\n const { continue: continue_, cursor } = result.value;\n return {\n continue: (session?: GraffitiSession | null) =>\n this_.objectStreamContinue<Schema>(continue_(session)),\n cursor,\n };\n }\n if (!result.value.error) {\n this_.synchronizeDispatch(result.value);\n }\n yield result.value;\n }\n })();\n }\n\n protected objectStream<Schema extends JSONSchema>(\n iterator: GraffitiObjectStream<Schema>,\n ): GraffitiObjectStream<Schema> {\n const wrapped = this.objectStreamContinue<Schema>(iterator);\n return (async function* () {\n // Filter out the tombstones for type safety\n while (true) {\n const result = await wrapped.next();\n if (result.done) return result.value;\n if (result.value.error || !result.value.tombstone) yield result.value;\n }\n })();\n }\n\n discover: Graffiti[\"discover\"] = (...args) => {\n const iterator = this.graffiti.discover(...args);\n return this.objectStream<(typeof args)[1]>(iterator);\n };\n\n continueDiscover: Graffiti[\"continueDiscover\"] = (...args) => {\n const iterator = this.graffiti.continueDiscover(...args);\n return this.objectStreamContinue<{}>(iterator);\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type Ajv from "ajv";
2
1
  import type { Graffiti, GraffitiSession, JSONSchema, GraffitiObjectBase, GraffitiObjectStream, GraffitiObjectStreamContinueEntry, GraffitiObjectStreamContinue, GraffitiObjectUrl } from "@graffiti-garden/api";
3
2
  export type * from "@graffiti-garden/api";
4
3
  export type GraffitiSynchronizeCallback = (object: GraffitiObjectStreamContinueEntry<{}>) => void;
@@ -65,7 +64,6 @@ export interface GraffitiSynchronizeOptions {
65
64
  * streams appropriate changes to provide a responsive and consistent user experience.
66
65
  */
67
66
  export declare class GraffitiSynchronize implements Graffiti {
68
- protected ajv_: Promise<Ajv> | undefined;
69
67
  protected readonly graffiti: Graffiti;
70
68
  protected readonly callbacks: Set<GraffitiSynchronizeCallback>;
71
69
  protected readonly options: GraffitiSynchronizeOptions;
@@ -77,7 +75,6 @@ export declare class GraffitiSynchronize implements Graffiti {
77
75
  deleteMedia: Graffiti["deleteMedia"];
78
76
  actorToHandle: Graffiti["actorToHandle"];
79
77
  handleToActor: Graffiti["handleToActor"];
80
- protected get ajv(): Promise<Ajv>;
81
78
  /**
82
79
  * Wraps a Graffiti API instance to provide the synchronize methods.
83
80
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iCAAiC,EACjC,4BAA4B,EAC5B,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAS9B,mBAAmB,sBAAsB,CAAC;AAE1C,MAAM,MAAM,2BAA2B,GAAG,CACxC,MAAM,EAAE,iCAAiC,CAAC,EAAE,CAAC,KAC1C,IAAI,CAAC;AAEV,MAAM,WAAW,0BAA0B;IACzC;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBAAa,mBAAoB,YAAW,QAAQ;IAClD,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACtC,SAAS,CAAC,QAAQ,CAAC,SAAS,mCAA0C;IACtE,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAEvD,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IACzB,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3B,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACzC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IACjC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC/B,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC;IACrC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACzC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IAEzC;;;;OAIG;;IAED;;;OAGG;IACH,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,0BAA0B;IActC,SAAS,CAAC,WAAW,CAAC,MAAM,SAAS,UAAU,EAC7C,WAAW,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,OAAO,EACpD,QAAQ,EAAE,MAAM,EAAE,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,EAChC,QAAQ,GAAE,GAAG,CAAC,MAAM,CAAqB,GACxC,cAAc,CAAC,iCAAiC,CAAC,MAAM,CAAC,CAAC;IAwC5D;;;;;;;;;;;;OAYG;IACH,mBAAmB,CAAC,MAAM,SAAS,UAAU,EAC3C,QAAQ,EAAE,MAAM,EAAE,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAC/B,cAAc,CAAC,iCAAiC,CAAC,MAAM,CAAC,CAAC;IAO5D;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,MAAM,SAAS,UAAU,EACtC,SAAS,EAAE,MAAM,GAAG,iBAAiB,EACrC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,GAC3C,cAAc,CAAC,iCAAiC,CAAC,MAAM,CAAC,CAAC;IAc5D;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,MAAM,SAAS,UAAU,EACtC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAC/B,cAAc,CAAC,iCAAiC,CAAC,MAAM,CAAC,CAAC;cAI5C,mBAAmB,CACjC,YAAY,EAAE,iCAAiC,CAAC,EAAE,CAAC,EACnD,gBAAgB,UAAQ;IA8B1B,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAclB;IAEF,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAKpB;IAEF,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAexB;IAEF,SAAS,CAAC,oBAAoB,CAAC,MAAM,SAAS,UAAU,EACtD,QAAQ,EAAE,4BAA4B,CAAC,MAAM,CAAC,GAC7C,4BAA4B,CAAC,MAAM,CAAC;IAqBvC,SAAS,CAAC,YAAY,CAAC,MAAM,SAAS,UAAU,EAC9C,QAAQ,EAAE,oBAAoB,CAAC,MAAM,CAAC,GACrC,oBAAoB,CAAC,MAAM,CAAC;IAY/B,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAG5B;IAEF,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAG5C;CACH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiti-garden/wrapper-synchronize",
3
- "version": "1.0.4",
3
+ "version": "1.0.8",
4
4
  "description": "Internal synchronization for the Graffiti API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "homepage": "https://sync.graffiti.garden/classes/GraffitiSynchronize.html",
47
47
  "devDependencies": {
48
- "@graffiti-garden/implementation-local": "^1.0.4",
48
+ "@graffiti-garden/implementation-local": "^1.0.8",
49
49
  "@types/node": "^25.0.6",
50
50
  "@vitest/coverage-v8": "^4.0.17",
51
51
  "esbuild-plugin-polyfill-node": "^0.3.0",
@@ -55,8 +55,7 @@
55
55
  "vitest": "^4.0.17"
56
56
  },
57
57
  "dependencies": {
58
- "@graffiti-garden/api": "^1.0.4",
59
- "@repeaterjs/repeater": "^3.0.6",
60
- "ajv": "^8.17.1"
58
+ "@graffiti-garden/api": "^1.0.8",
59
+ "@repeaterjs/repeater": "^3.0.6"
61
60
  }
62
61
  }
package/src/index.spec.ts CHANGED
@@ -4,7 +4,11 @@ import {
4
4
  type GraffitiSession,
5
5
  } from "@graffiti-garden/api";
6
6
  import { GraffitiLocal } from "@graffiti-garden/implementation-local";
7
- import { randomPostObject, randomString } from "@graffiti-garden/api/tests";
7
+ import {
8
+ randomPostObject,
9
+ randomString,
10
+ randomUrl,
11
+ } from "@graffiti-garden/api/tests";
8
12
  import { GraffitiSynchronize } from "./index";
9
13
  import {
10
14
  graffitiCRUDTests,
@@ -12,17 +16,18 @@ import {
12
16
  graffitiMediaTests,
13
17
  } from "@graffiti-garden/api/tests";
14
18
 
19
+ // @ts-ignore
15
20
  const useGraffiti = () => new GraffitiSynchronize(new GraffitiLocal());
16
21
  const graffiti = useGraffiti();
17
22
 
18
23
  const useSession1 = async () => {
19
24
  return {
20
- actor: randomString(),
25
+ actor: randomUrl(),
21
26
  };
22
27
  };
23
28
  const useSession2 = async () => {
24
29
  return {
25
- actor: randomString(),
30
+ actor: randomUrl(),
26
31
  };
27
32
  };
28
33
 
@@ -171,7 +176,7 @@ describe.concurrent("synchronizeDiscover", () => {
171
176
  });
172
177
 
173
178
  it("not allowed", async () => {
174
- const allChannels = [randomString(), randomString(), randomString()];
179
+ const allChannels = [randomUrl(), randomUrl(), randomUrl()];
175
180
  const channels = allChannels.slice(1);
176
181
 
177
182
  const creatorNext = graffiti
@@ -185,7 +190,7 @@ describe.concurrent("synchronizeDiscover", () => {
185
190
  const value = {
186
191
  hello: "world",
187
192
  };
188
- const allowed = [randomString(), session2.actor];
193
+ const allowed = [randomUrl(), session2.actor];
189
194
  await graffiti.post<{}>(
190
195
  { value, channels: allChannels, allowed },
191
196
  session1,
@@ -336,7 +341,7 @@ describe("synchronizeAll", () => {
336
341
  });
337
342
 
338
343
  const object1 = randomPostObject();
339
- object1.allowed = [randomString()];
344
+ object1.allowed = [randomUrl()];
340
345
 
341
346
  const iterator = graffiti.synchronizeAll({});
342
347
  const next = iterator.next();
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type Ajv from "ajv";
2
1
  import type {
3
2
  Graffiti,
4
3
  GraffitiSession,
@@ -8,10 +7,9 @@ import type {
8
7
  GraffitiObjectStreamContinueEntry,
9
8
  GraffitiObjectStreamContinue,
10
9
  GraffitiObjectUrl,
11
- GraffitiObject,
12
10
  } from "@graffiti-garden/api";
13
11
  import {
14
- GraffitiErrorInvalidSchema,
12
+ compileGraffitiObjectSchema,
15
13
  GraffitiErrorNotFound,
16
14
  isActorAllowedGraffitiObject,
17
15
  maskGraffitiObject,
@@ -88,7 +86,6 @@ export interface GraffitiSynchronizeOptions {
88
86
  * streams appropriate changes to provide a responsive and consistent user experience.
89
87
  */
90
88
  export class GraffitiSynchronize implements Graffiti {
91
- protected ajv_: Promise<Ajv> | undefined;
92
89
  protected readonly graffiti: Graffiti;
93
90
  protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();
94
91
  protected readonly options: GraffitiSynchronizeOptions;
@@ -102,16 +99,6 @@ export class GraffitiSynchronize implements Graffiti {
102
99
  actorToHandle: Graffiti["actorToHandle"];
103
100
  handleToActor: Graffiti["handleToActor"];
104
101
 
105
- protected get ajv() {
106
- if (!this.ajv_) {
107
- this.ajv_ = (async () => {
108
- const { default: Ajv } = await import("ajv");
109
- return new Ajv({ strict: false });
110
- })();
111
- }
112
- return this.ajv_;
113
- }
114
-
115
102
  /**
116
103
  * Wraps a Graffiti API instance to provide the synchronize methods.
117
104
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -146,7 +133,7 @@ export class GraffitiSynchronize implements Graffiti {
146
133
  ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {
147
134
  const repeater = new Repeater<GraffitiObjectStreamContinueEntry<Schema>>(
148
135
  async (push, stop) => {
149
- const validate = compileGraffitiObjectSchema(await this.ajv, schema);
136
+ const validate = await compileGraffitiObjectSchema(schema);
150
137
  const callback: GraffitiSynchronizeCallback = (objectUpdate) => {
151
138
  if (objectUpdate?.tombstone) {
152
139
  if (seenUrls.has(objectUpdate.object.url)) {
@@ -375,22 +362,3 @@ export class GraffitiSynchronize implements Graffiti {
375
362
  return this.objectStreamContinue<{}>(iterator);
376
363
  };
377
364
  }
378
-
379
- function compileGraffitiObjectSchema<Schema extends JSONSchema>(
380
- ajv: Ajv,
381
- schema: Schema,
382
- ) {
383
- try {
384
- // Force the validation guard because
385
- // it is too big for the type checker.
386
- // Fortunately json-schema-to-ts is
387
- // well tested against ajv.
388
- return ajv.compile(schema) as (
389
- data: GraffitiObjectBase,
390
- ) => data is GraffitiObject<Schema>;
391
- } catch (error) {
392
- throw new GraffitiErrorInvalidSchema(
393
- error instanceof Error ? error.message : String(error),
394
- );
395
- }
396
- }