@graffiti-garden/wrapper-synchronize 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.ts"],
4
- "sourcesContent": ["import Ajv from \"ajv\";\nimport { Graffiti } from \"@graffiti-garden/api\";\nimport type {\n GraffitiSession,\n GraffitiObject,\n JSONSchema,\n GraffitiStream,\n} from \"@graffiti-garden/api\";\nimport type { GraffitiObjectBase } from \"@graffiti-garden/api\";\nimport { Repeater } from \"@repeaterjs/repeater\";\nimport { applyPatch } from \"fast-json-patch\";\nimport {\n applyGraffitiPatch,\n compileGraffitiObjectSchema,\n isActorAllowedGraffitiObject,\n locationToUri,\n maskGraffitiObject,\n unpackLocationOrUri,\n} from \"@graffiti-garden/implementation-local/utilities\";\nexport type * from \"@graffiti-garden/api\";\n\nexport type GraffitiSynchronizeCallback = (\n oldObject: GraffitiObjectBase,\n newObject?: GraffitiObjectBase,\n) => void;\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 * Specifically, it provides the following *synchronize*\n * methods for each of the following API methods:\n *\n * | API Method | Synchronize Method |\n * |------------|--------------------|\n * | {@link get} | {@link synchronizeGet} |\n * | {@link discover} | {@link synchronizeDiscover} |\n * | {@link recoverOrphans} | {@link synchronizeRecoverOrphans} |\n *\n * Whenever a change is made via {@link put}, {@link patch}, and {@link delete} or\n * received from {@link get}, {@link discover}, and {@link recoverOrphans},\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 put}. 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 * @groupDescription Synchronize Methods\n * This group contains methods that listen for changes made via\n * {@link put}, {@link patch}, and {@link delete} or fetched from\n * {@link get}, {@link discover}, and {@link recoverOrphans} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n */\nexport class GraffitiSynchronize extends Graffiti {\n protected readonly ajv: Ajv;\n protected readonly graffiti: Graffiti;\n protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();\n\n channelStats: Graffiti[\"channelStats\"];\n locationToUri: Graffiti[\"locationToUri\"];\n uriToLocation: Graffiti[\"uriToLocation\"];\n login: Graffiti[\"login\"];\n logout: Graffiti[\"logout\"];\n sessionEvents: Graffiti[\"sessionEvents\"];\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 /**\n * An optional instance of Ajv to use for validating\n * objects before dispatching them to listeners.\n * If not provided, a new instance of Ajv will be created.\n */\n ajv?: Ajv,\n ) {\n super();\n this.ajv = ajv ?? new Ajv({ strict: false });\n this.graffiti = graffiti;\n this.channelStats = graffiti.channelStats.bind(graffiti);\n this.locationToUri = graffiti.locationToUri.bind(graffiti);\n this.uriToLocation = graffiti.uriToLocation.bind(graffiti);\n this.login = graffiti.login.bind(graffiti);\n this.logout = graffiti.logout.bind(graffiti);\n this.sessionEvents = graffiti.sessionEvents;\n }\n\n protected synchronize<Schema extends JSONSchema>(\n matchObject: (object: GraffitiObjectBase) => boolean,\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n ) {\n const validate = compileGraffitiObjectSchema(this.ajv, schema);\n\n const repeater: GraffitiStream<GraffitiObject<Schema>> = new Repeater(\n async (push, stop) => {\n const callback: GraffitiSynchronizeCallback = (\n oldObjectRaw,\n newObjectRaw,\n ) => {\n for (const objectRaw of [newObjectRaw, oldObjectRaw]) {\n if (\n objectRaw &&\n matchObject(objectRaw) &&\n isActorAllowedGraffitiObject(objectRaw, session)\n ) {\n const object = { ...objectRaw };\n maskGraffitiObject(object, channels, session);\n if (validate(object)) {\n push({ value: object });\n break;\n }\n }\n }\n };\n\n this.callbacks.add(callback);\n await stop;\n this.callbacks.delete(callback);\n },\n );\n\n return repeater;\n }\n\n /**\n * This method has the same signature as {@link discover} but listens for\n * changes made via {@link put}, {@link patch}, and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link recoverOrphans}\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 Synchronize Methods\n */\n synchronizeDiscover<Schema extends JSONSchema>(\n ...args: Parameters<typeof Graffiti.prototype.discover<Schema>>\n ): GraffitiStream<GraffitiObject<Schema>> {\n const [channels, schema, session] = args;\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 put}, {@link patch}, and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link recoverOrphans} 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 {@link GraffitiStream}.\n *\n * @group Synchronize Methods\n */\n synchronizeGet<Schema extends JSONSchema>(\n ...args: Parameters<typeof Graffiti.prototype.get<Schema>>\n ): GraffitiStream<GraffitiObject<Schema>> {\n const [locationOrUri, schema, session] = args;\n function matchObject(object: GraffitiObjectBase) {\n const objectUri = locationToUri(object);\n const { uri } = unpackLocationOrUri(locationOrUri);\n return objectUri === uri;\n }\n return this.synchronize<Schema>(matchObject, [], schema, session);\n }\n\n /**\n * This method has the same signature as {@link recoverOrphans} but\n * listens for changes made via\n * {@link put}, {@link patch}, and {@link delete} or fetched from\n * {@link get}, {@link discover}, and {@link recoverOrphans} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n *\n * Unlike {@link recoverOrphans}, 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 Synchronize Methods\n */\n synchronizeRecoverOrphans<Schema extends JSONSchema>(\n ...args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>\n ): GraffitiStream<GraffitiObject<Schema>> {\n const [schema, session] = args;\n function matchObject(object: GraffitiObjectBase) {\n return object.actor === session.actor && object.channels.length === 0;\n }\n return this.synchronize<Schema>(matchObject, [], schema, session);\n }\n\n protected async synchronizeDispatch(\n oldObject: GraffitiObjectBase,\n newObject?: GraffitiObjectBase,\n waitForListeners = false,\n ) {\n for (const callback of this.callbacks) {\n callback(oldObject, newObject);\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 const object = await this.graffiti.get(...args);\n this.synchronizeDispatch(object);\n return object;\n };\n\n put: Graffiti[\"put\"] = async (...args) => {\n const oldObject = await this.graffiti.put<{}>(...args);\n const partialObject = args[0];\n const newObject: GraffitiObjectBase = {\n ...oldObject,\n value: partialObject.value,\n channels: partialObject.channels,\n allowed: partialObject.allowed,\n tombstone: false,\n };\n await this.synchronizeDispatch(oldObject, newObject, true);\n return oldObject;\n };\n\n patch: Graffiti[\"patch\"] = async (...args) => {\n const oldObject = await this.graffiti.patch(...args);\n const newObject: GraffitiObjectBase = { ...oldObject };\n newObject.tombstone = false;\n for (const prop of [\"value\", \"channels\", \"allowed\"] as const) {\n applyGraffitiPatch(applyPatch, prop, args[0], newObject);\n }\n await this.synchronizeDispatch(oldObject, newObject, true);\n return oldObject;\n };\n\n delete: Graffiti[\"delete\"] = async (...args) => {\n const oldObject = await this.graffiti.delete(...args);\n await this.synchronizeDispatch(oldObject, undefined, true);\n return oldObject;\n };\n\n protected objectStream<Schema extends JSONSchema>(\n iterator: ReturnType<typeof Graffiti.prototype.discover<Schema>>,\n ) {\n const dispatch = this.synchronizeDispatch.bind(this);\n const wrapper = async function* () {\n let result = await iterator.next();\n while (!result.done) {\n if (!result.value.error) {\n dispatch(result.value.value);\n }\n yield result.value;\n result = await iterator.next();\n }\n return result.value;\n };\n return wrapper();\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 recoverOrphans: Graffiti[\"recoverOrphans\"] = (...args) => {\n const iterator = this.graffiti.recoverOrphans(...args);\n return this.objectStream<(typeof args)[0]>(iterator);\n };\n}\n"],
5
- "mappings": "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",
6
- "names": ["Ajv", "Graffiti", "Repeater", "applyPatch", "applyGraffitiPatch", "compileGraffitiObjectSchema", "isActorAllowedGraffitiObject", "locationToUri", "maskGraffitiObject", "unpackLocationOrUri", "GraffitiSynchronize", "graffiti", "ajv", "matchObject", "channels", "schema", "session", "validate", "push", "stop", "callback", "oldObjectRaw", "newObjectRaw", "objectRaw", "object", "args", "channel", "locationOrUri", "objectUri", "uri", "oldObject", "newObject", "waitForListeners", "resolve", "partialObject", "prop", "iterator", "dispatch", "result"]
4
+ "sourcesContent": ["import type Ajv from \"ajv\";\nimport { Graffiti } from \"@graffiti-garden/api\";\nimport type {\n GraffitiSession,\n GraffitiObject,\n JSONSchema,\n GraffitiStream,\n} from \"@graffiti-garden/api\";\nimport type { GraffitiObjectBase } from \"@graffiti-garden/api\";\nimport { Repeater } from \"@repeaterjs/repeater\";\nimport type { applyPatch } from \"fast-json-patch\";\nimport {\n applyGraffitiPatch,\n compileGraffitiObjectSchema,\n isActorAllowedGraffitiObject,\n maskGraffitiObject,\n unpackLocationOrUri,\n} from \"@graffiti-garden/implementation-local/utilities\";\nexport type * from \"@graffiti-garden/api\";\n\nexport type GraffitiSynchronizeCallback = (\n oldObject: GraffitiObjectBase,\n newObject?: GraffitiObjectBase,\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 * Specifically, it provides the following *synchronize*\n * methods for each of the following API methods:\n *\n * | API Method | Synchronize Method |\n * |------------|--------------------|\n * | {@link get} | {@link synchronizeGet} |\n * | {@link discover} | {@link synchronizeDiscover} |\n * | {@link recoverOrphans} | {@link synchronizeRecoverOrphans} |\n *\n * Whenever a change is made via {@link put}, {@link patch}, and {@link delete} or\n * received from {@link get}, {@link discover}, and {@link recoverOrphans},\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 put}. 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 * @groupDescription Synchronize Methods\n * This group contains methods that listen for changes made via\n * {@link put}, {@link patch}, and {@link delete} or fetched from\n * {@link get}, {@link discover}, and {@link recoverOrphans} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n */\nexport class GraffitiSynchronize extends Graffiti {\n protected ajv_: Promise<Ajv> | undefined;\n protected applyPatch_: Promise<typeof applyPatch> | undefined;\n protected readonly graffiti: Graffiti;\n protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();\n protected readonly options: GraffitiSynchronizeOptions;\n\n channelStats: Graffiti[\"channelStats\"];\n login: Graffiti[\"login\"];\n logout: Graffiti[\"logout\"];\n sessionEvents: Graffiti[\"sessionEvents\"];\n\n 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 get applyPatch() {\n if (!this.applyPatch_) {\n this.applyPatch_ = (async () => {\n const { applyPatch } = await import(\"fast-json-patch\");\n return applyPatch;\n })();\n }\n return this.applyPatch_;\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 super();\n this.options = options ?? {};\n this.graffiti = graffiti;\n this.channelStats = graffiti.channelStats.bind(graffiti);\n this.login = graffiti.login.bind(graffiti);\n this.logout = graffiti.logout.bind(graffiti);\n this.sessionEvents = graffiti.sessionEvents;\n }\n\n protected synchronize<Schema extends JSONSchema>(\n matchObject: (object: GraffitiObjectBase) => boolean,\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n ) {\n const repeater: GraffitiStream<GraffitiObject<Schema>> = new Repeater(\n async (push, stop) => {\n const validate = compileGraffitiObjectSchema(await this.ajv, schema);\n const callback: GraffitiSynchronizeCallback = (\n oldObjectRaw,\n newObjectRaw,\n ) => {\n for (const objectRaw of [newObjectRaw, oldObjectRaw]) {\n if (\n objectRaw &&\n matchObject(objectRaw) &&\n (this.options.omniscient ||\n isActorAllowedGraffitiObject(objectRaw, session))\n ) {\n const object = { ...objectRaw };\n if (!this.options.omniscient) {\n maskGraffitiObject(object, channels, session);\n }\n if (validate(object)) {\n push({ value: object });\n break;\n }\n }\n }\n };\n\n this.callbacks.add(callback);\n await stop;\n this.callbacks.delete(callback);\n },\n );\n\n return repeater;\n }\n\n /**\n * This method has the same signature as {@link discover} but listens for\n * changes made via {@link put}, {@link patch}, and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link recoverOrphans}\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 Synchronize Methods\n */\n synchronizeDiscover<Schema extends JSONSchema>(\n ...args: Parameters<typeof Graffiti.prototype.discover<Schema>>\n ): GraffitiStream<GraffitiObject<Schema>> {\n const [channels, schema, session] = args;\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 put}, {@link patch}, and {@link delete} or\n * fetched from {@link get}, {@link discover}, and {@link recoverOrphans} 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 {@link GraffitiStream}.\n *\n * @group Synchronize Methods\n */\n synchronizeGet<Schema extends JSONSchema>(\n ...args: Parameters<typeof Graffiti.prototype.get<Schema>>\n ): GraffitiStream<GraffitiObject<Schema>> {\n const [locationOrUri, schema, session] = args;\n const uri = unpackLocationOrUri(locationOrUri);\n function matchObject(object: GraffitiObjectBase) {\n return object.uri === uri;\n }\n return this.synchronize<Schema>(matchObject, [], schema, session);\n }\n\n /**\n * This method has the same signature as {@link recoverOrphans} but\n * listens for changes made via\n * {@link put}, {@link patch}, and {@link delete} or fetched from\n * {@link get}, {@link discover}, and {@link recoverOrphans} and then\n * streams appropriate changes to provide a responsive and consistent user experience.\n *\n * Unlike {@link recoverOrphans}, 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 Synchronize Methods\n */\n synchronizeRecoverOrphans<Schema extends JSONSchema>(\n ...args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>\n ): GraffitiStream<GraffitiObject<Schema>> {\n const [schema, session] = args;\n function matchObject(object: GraffitiObjectBase) {\n return object.actor === session.actor && object.channels.length === 0;\n }\n return this.synchronize<Schema>(matchObject, [], schema, session);\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 synchronizeAll<Schema extends JSONSchema>(\n schema?: Schema,\n session?: GraffitiSession | null,\n ): GraffitiStream<GraffitiObjectBase> {\n return this.synchronize(() => true, [], schema ?? {}, session);\n }\n\n protected async synchronizeDispatch(\n oldObject: GraffitiObjectBase,\n newObject?: GraffitiObjectBase,\n waitForListeners = false,\n ) {\n for (const callback of this.callbacks) {\n callback(oldObject, newObject);\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 const object = await this.graffiti.get(...args);\n this.synchronizeDispatch(object);\n return object;\n };\n\n put: Graffiti[\"put\"] = async (...args) => {\n const oldObject = await this.graffiti.put<{}>(...args);\n const partialObject = args[0];\n const newObject: GraffitiObjectBase = {\n ...oldObject,\n value: partialObject.value,\n channels: partialObject.channels,\n allowed: partialObject.allowed,\n tombstone: false,\n };\n await this.synchronizeDispatch(oldObject, newObject, true);\n return oldObject;\n };\n\n patch: Graffiti[\"patch\"] = async (...args) => {\n const oldObject = await this.graffiti.patch(...args);\n const newObject: GraffitiObjectBase = { ...oldObject };\n newObject.tombstone = false;\n for (const prop of [\"value\", \"channels\", \"allowed\"] as const) {\n applyGraffitiPatch(await this.applyPatch, prop, args[0], newObject);\n }\n await this.synchronizeDispatch(oldObject, newObject, true);\n return oldObject;\n };\n\n delete: Graffiti[\"delete\"] = async (...args) => {\n const oldObject = await this.graffiti.delete(...args);\n await this.synchronizeDispatch(oldObject, undefined, true);\n return oldObject;\n };\n\n protected objectStream<Schema extends JSONSchema>(\n iterator: ReturnType<typeof Graffiti.prototype.discover<Schema>>,\n ) {\n const dispatch = this.synchronizeDispatch.bind(this);\n const wrapper = async function* () {\n let result = await iterator.next();\n while (!result.done) {\n if (!result.value.error) {\n dispatch(result.value.value);\n }\n yield result.value;\n result = await iterator.next();\n }\n return result.value;\n };\n return wrapper();\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 recoverOrphans: Graffiti[\"recoverOrphans\"] = (...args) => {\n const iterator = this.graffiti.recoverOrphans(...args);\n return this.objectStream<(typeof args)[0]>(iterator);\n };\n}\n"],
5
+ "mappings": "AACA,SAAS,gBAAgB;AAQzB,SAAS,gBAAgB;AAEzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAgEA,MAAM,4BAA4B,SAAS;AAAA,EACtC;AAAA,EACA;AAAA,EACS;AAAA,EACA,YAAY,oBAAI,IAAiC;AAAA,EACjD;AAAA,EAEnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA,IAAI,MAAM;AACR,QAAI,CAAC,KAAK,MAAM;AACd,WAAK,QAAQ,YAAY;AACvB,cAAM,EAAE,SAAS,IAAI,IAAI,MAAM,OAAO,KAAK;AAC3C,eAAO,IAAI,IAAI,EAAE,QAAQ,MAAM,CAAC;AAAA,MAClC,GAAG;AAAA,IACL;AACA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,aAAa;AACf,QAAI,CAAC,KAAK,aAAa;AACrB,WAAK,eAAe,YAAY;AAC9B,cAAM,EAAE,WAAW,IAAI,MAAM,OAAO,iBAAiB;AACrD,eAAO;AAAA,MACT,GAAG;AAAA,IACL;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAKE,UACA,SACA;AACA,UAAM;AACN,SAAK,UAAU,WAAW,CAAC;AAC3B,SAAK,WAAW;AAChB,SAAK,eAAe,SAAS,aAAa,KAAK,QAAQ;AACvD,SAAK,QAAQ,SAAS,MAAM,KAAK,QAAQ;AACzC,SAAK,SAAS,SAAS,OAAO,KAAK,QAAQ;AAC3C,SAAK,gBAAgB,SAAS;AAAA,EAChC;AAAA,EAEU,YACR,aACA,UACA,QACA,SACA;AACA,UAAM,WAAmD,IAAI;AAAA,MAC3D,OAAO,MAAM,SAAS;AACpB,cAAM,WAAW,4BAA4B,MAAM,KAAK,KAAK,MAAM;AACnE,cAAM,WAAwC,CAC5C,cACA,iBACG;AACH,qBAAW,aAAa,CAAC,cAAc,YAAY,GAAG;AACpD,gBACE,aACA,YAAY,SAAS,MACpB,KAAK,QAAQ,cACZ,6BAA6B,WAAW,OAAO,IACjD;AACA,oBAAM,SAAS,EAAE,GAAG,UAAU;AAC9B,kBAAI,CAAC,KAAK,QAAQ,YAAY;AAC5B,mCAAmB,QAAQ,UAAU,OAAO;AAAA,cAC9C;AACA,kBAAI,SAAS,MAAM,GAAG;AACpB,qBAAK,EAAE,OAAO,OAAO,CAAC;AACtB;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,aAAK,UAAU,IAAI,QAAQ;AAC3B,cAAM;AACN,aAAK,UAAU,OAAO,QAAQ;AAAA,MAChC;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,uBACK,MACqC;AACxC,UAAM,CAAC,UAAU,QAAQ,OAAO,IAAI;AACpC,aAAS,YAAY,QAA4B;AAC/C,aAAO,OAAO,SAAS,KAAK,CAAC,YAAY,SAAS,SAAS,OAAO,CAAC;AAAA,IACrE;AACA,WAAO,KAAK,YAAoB,aAAa,UAAU,QAAQ,OAAO;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,kBACK,MACqC;AACxC,UAAM,CAAC,eAAe,QAAQ,OAAO,IAAI;AACzC,UAAM,MAAM,oBAAoB,aAAa;AAC7C,aAAS,YAAY,QAA4B;AAC/C,aAAO,OAAO,QAAQ;AAAA,IACxB;AACA,WAAO,KAAK,YAAoB,aAAa,CAAC,GAAG,QAAQ,OAAO;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,6BACK,MACqC;AACxC,UAAM,CAAC,QAAQ,OAAO,IAAI;AAC1B,aAAS,YAAY,QAA4B;AAC/C,aAAO,OAAO,UAAU,QAAQ,SAAS,OAAO,SAAS,WAAW;AAAA,IACtE;AACA,WAAO,KAAK,YAAoB,aAAa,CAAC,GAAG,QAAQ,OAAO;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,eACE,QACA,SACoC;AACpC,WAAO,KAAK,YAAY,MAAM,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,OAAO;AAAA,EAC/D;AAAA,EAEA,MAAgB,oBACd,WACA,WACA,mBAAmB,OACnB;AACA,eAAW,YAAY,KAAK,WAAW;AACrC,eAAS,WAAW,SAAS;AAAA,IAC/B;AACA,QAAI,kBAAkB;AAqBpB,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,CAAC,CAAC;AAAA,IACvD;AAAA,EACF;AAAA,EAEA,MAAuB,UAAU,SAAS;AACxC,UAAM,SAAS,MAAM,KAAK,SAAS,IAAI,GAAG,IAAI;AAC9C,SAAK,oBAAoB,MAAM;AAC/B,WAAO;AAAA,EACT;AAAA,EAEA,MAAuB,UAAU,SAAS;AACxC,UAAM,YAAY,MAAM,KAAK,SAAS,IAAQ,GAAG,IAAI;AACrD,UAAM,gBAAgB,KAAK,CAAC;AAC5B,UAAM,YAAgC;AAAA,MACpC,GAAG;AAAA,MACH,OAAO,cAAc;AAAA,MACrB,UAAU,cAAc;AAAA,MACxB,SAAS,cAAc;AAAA,MACvB,WAAW;AAAA,IACb;AACA,UAAM,KAAK,oBAAoB,WAAW,WAAW,IAAI;AACzD,WAAO;AAAA,EACT;AAAA,EAEA,QAA2B,UAAU,SAAS;AAC5C,UAAM,YAAY,MAAM,KAAK,SAAS,MAAM,GAAG,IAAI;AACnD,UAAM,YAAgC,EAAE,GAAG,UAAU;AACrD,cAAU,YAAY;AACtB,eAAW,QAAQ,CAAC,SAAS,YAAY,SAAS,GAAY;AAC5D,yBAAmB,MAAM,KAAK,YAAY,MAAM,KAAK,CAAC,GAAG,SAAS;AAAA,IACpE;AACA,UAAM,KAAK,oBAAoB,WAAW,WAAW,IAAI;AACzD,WAAO;AAAA,EACT;AAAA,EAEA,SAA6B,UAAU,SAAS;AAC9C,UAAM,YAAY,MAAM,KAAK,SAAS,OAAO,GAAG,IAAI;AACpD,UAAM,KAAK,oBAAoB,WAAW,QAAW,IAAI;AACzD,WAAO;AAAA,EACT;AAAA,EAEU,aACR,UACA;AACA,UAAM,WAAW,KAAK,oBAAoB,KAAK,IAAI;AACnD,UAAM,UAAU,mBAAmB;AACjC,UAAI,SAAS,MAAM,SAAS,KAAK;AACjC,aAAO,CAAC,OAAO,MAAM;AACnB,YAAI,CAAC,OAAO,MAAM,OAAO;AACvB,mBAAS,OAAO,MAAM,KAAK;AAAA,QAC7B;AACA,cAAM,OAAO;AACb,iBAAS,MAAM,SAAS,KAAK;AAAA,MAC/B;AACA,aAAO,OAAO;AAAA,IAChB;AACA,WAAO,QAAQ;AAAA,EACjB;AAAA,EAEA,WAAiC,IAAI,SAAS;AAC5C,UAAM,WAAW,KAAK,SAAS,SAAS,GAAG,IAAI;AAC/C,WAAO,KAAK,aAA+B,QAAQ;AAAA,EACrD;AAAA,EAEA,iBAA6C,IAAI,SAAS;AACxD,UAAM,WAAW,KAAK,SAAS,eAAe,GAAG,IAAI;AACrD,WAAO,KAAK,aAA+B,QAAQ;AAAA,EACrD;AACF;",
6
+ "names": []
7
7
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,23 @@
1
- import Ajv from "ajv";
1
+ import type Ajv from "ajv";
2
2
  import { Graffiti } from "@graffiti-garden/api";
3
3
  import type { GraffitiSession, GraffitiObject, JSONSchema, GraffitiStream } from "@graffiti-garden/api";
4
4
  import type { GraffitiObjectBase } from "@graffiti-garden/api";
5
+ import type { applyPatch } from "fast-json-patch";
5
6
  export type * from "@graffiti-garden/api";
6
7
  export type GraffitiSynchronizeCallback = (oldObject: GraffitiObjectBase, newObject?: GraffitiObjectBase) => void;
8
+ export interface GraffitiSynchronizeOptions {
9
+ /**
10
+ * Allows synchronize to listen to all objects, not just those
11
+ * that the session is allowed to see. This is useful to get a
12
+ * global view of all Graffiti objects passsing through the system,
13
+ * for example to build a client-side cache. Additional mechanisms
14
+ * should be in place to ensure that users do not see objects or
15
+ * properties they are not allowed to see.
16
+ *
17
+ * Default: `false`
18
+ */
19
+ omniscient?: boolean;
20
+ }
7
21
  /**
8
22
  * Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
9
23
  * so that changes made or received in one part of an application
@@ -47,15 +61,17 @@ export type GraffitiSynchronizeCallback = (oldObject: GraffitiObjectBase, newObj
47
61
  * streams appropriate changes to provide a responsive and consistent user experience.
48
62
  */
49
63
  export declare class GraffitiSynchronize extends Graffiti {
50
- protected readonly ajv: Ajv;
64
+ protected ajv_: Promise<Ajv> | undefined;
65
+ protected applyPatch_: Promise<typeof applyPatch> | undefined;
51
66
  protected readonly graffiti: Graffiti;
52
67
  protected readonly callbacks: Set<GraffitiSynchronizeCallback>;
68
+ protected readonly options: GraffitiSynchronizeOptions;
53
69
  channelStats: Graffiti["channelStats"];
54
- locationToUri: Graffiti["locationToUri"];
55
- uriToLocation: Graffiti["uriToLocation"];
56
70
  login: Graffiti["login"];
57
71
  logout: Graffiti["logout"];
58
72
  sessionEvents: Graffiti["sessionEvents"];
73
+ get ajv(): Promise<Ajv>;
74
+ get applyPatch(): Promise<typeof applyPatch>;
59
75
  /**
60
76
  * Wraps a Graffiti API instance to provide the synchronize methods.
61
77
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -66,13 +82,7 @@ export declare class GraffitiSynchronize extends Graffiti {
66
82
  * The [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
67
83
  * instance to wrap.
68
84
  */
69
- graffiti: Graffiti,
70
- /**
71
- * An optional instance of Ajv to use for validating
72
- * objects before dispatching them to listeners.
73
- * If not provided, a new instance of Ajv will be created.
74
- */
75
- ajv?: Ajv);
85
+ graffiti: Graffiti, options?: GraffitiSynchronizeOptions);
76
86
  protected synchronize<Schema extends JSONSchema>(matchObject: (object: GraffitiObjectBase) => boolean, channels: string[], schema: Schema, session?: GraffitiSession | null): GraffitiStream<GraffitiObject<Schema>>;
77
87
  /**
78
88
  * This method has the same signature as {@link discover} but listens for
@@ -114,6 +124,17 @@ export declare class GraffitiSynchronize extends Graffiti {
114
124
  * @group Synchronize Methods
115
125
  */
116
126
  synchronizeRecoverOrphans<Schema extends JSONSchema>(...args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
127
+ /**
128
+ * Streams changes made to *any* object in *any* channel
129
+ * and made by *any* user. You may want to use it in conjuction with
130
+ * {@link GraffitiSynchronizeOptions.omniscient} to get a global view
131
+ * of all Graffiti objects passing through the system. This is useful
132
+ * for building a client-side cache, for example.
133
+ *
134
+ * Be careful using this method. Without additional filters it can
135
+ * expose the user to content out of context.
136
+ */
137
+ synchronizeAll<Schema extends JSONSchema>(schema?: Schema, session?: GraffitiSession | null): GraffitiStream<GraffitiObjectBase>;
117
138
  protected synchronizeDispatch(oldObject: GraffitiObjectBase, newObject?: GraffitiObjectBase, waitForListeners?: boolean): Promise<void>;
118
139
  get: Graffiti["get"];
119
140
  put: Graffiti["put"];
@@ -121,7 +142,7 @@ export declare class GraffitiSynchronize extends Graffiti {
121
142
  delete: Graffiti["delete"];
122
143
  protected objectStream<Schema extends JSONSchema>(iterator: ReturnType<typeof Graffiti.prototype.discover<Schema>>): AsyncGenerator<{
123
144
  error: Error;
124
- source: string;
145
+ origin: string;
125
146
  } | {
126
147
  error?: undefined;
127
148
  value: GraffitiObject<Schema>;
@@ -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":"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"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@graffiti-garden/wrapper-synchronize",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "Internal synchronization for the Graffiti API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
7
7
  "main": "./dist/cjs/index.js",
8
- "browser": "./dist/index.browser.js",
8
+ "browser": "./dist/browser/index.js",
9
9
  "exports": {
10
10
  ".": {
11
11
  "import": {
@@ -31,7 +31,6 @@
31
31
  "files": [
32
32
  "src",
33
33
  "dist",
34
- "tests",
35
34
  "package.json",
36
35
  "README.md"
37
36
  ],
@@ -47,16 +46,18 @@
47
46
  "homepage": "https://sync.graffiti.garden/classes/GraffitiSynchronize.html",
48
47
  "devDependencies": {
49
48
  "@types/node": "^22.13.1",
49
+ "@vitest/coverage-v8": "^3.0.6",
50
50
  "tsx": "^4.19.2",
51
51
  "typedoc": "^0.27.6",
52
52
  "typescript": "^5.7.3",
53
53
  "vitest": "^3.0.5"
54
54
  },
55
55
  "dependencies": {
56
- "@graffiti-garden/api": "^0.4.1",
57
- "@graffiti-garden/implementation-local": "^0.4.0",
56
+ "@graffiti-garden/api": "^0.5.0",
57
+ "@graffiti-garden/implementation-local": "^0.5.0",
58
58
  "@repeaterjs/repeater": "^3.0.6",
59
59
  "ajv": "^8.17.1",
60
+ "esbuild-plugin-polyfill-node": "^0.3.0",
60
61
  "fast-json-patch": "^3.1.1"
61
62
  }
62
63
  }
package/src/index.spec.ts CHANGED
@@ -74,7 +74,7 @@ describe.concurrent("synchronizeDiscover", () => {
74
74
  const newChannels = [afterChannel, sharedChannel];
75
75
  await graffiti.put<{}>(
76
76
  {
77
- ...putted,
77
+ uri: putted.uri,
78
78
  value: newValue,
79
79
  channels: newChannels,
80
80
  },
@@ -265,6 +265,9 @@ describe.concurrent("synchronizeDiscover", () => {
265
265
 
266
266
  expect(third).toBe("synchronize");
267
267
  }
268
+
269
+ // Try returning...
270
+ iterator.return();
268
271
  });
269
272
 
270
273
  it("not allowed", async () => {
@@ -340,7 +343,8 @@ describe.concurrent("synchronizeGet", () => {
340
343
  const newValue = { goodbye: "world" };
341
344
  const putted2 = await graffiti.put<{}>(
342
345
  {
343
- ...putted,
346
+ uri: putted.uri,
347
+ channels: object.channels,
344
348
  value: newValue,
345
349
  },
346
350
  session,
@@ -413,3 +417,59 @@ describe.concurrent("synchronizeGet", () => {
413
417
  expect(result2.value.lastModified).toEqual(putted2.lastModified);
414
418
  });
415
419
  });
420
+
421
+ // can't be concurrent because it gets ALL
422
+ describe("synchronizeAll", () => {
423
+ let session: GraffitiSession;
424
+ let session1: GraffitiSession;
425
+ let session2: GraffitiSession;
426
+ beforeAll(async () => {
427
+ session1 = await useSession1();
428
+ session = session1;
429
+ session2 = await useSession2();
430
+ });
431
+
432
+ it("sync from multiple channels and actors", async () => {
433
+ const object1 = randomPutObject();
434
+ const object2 = randomPutObject();
435
+
436
+ expect(object1.channels).not.toEqual(object2.channels);
437
+
438
+ const iterator = graffiti.synchronizeAll();
439
+
440
+ const next1 = iterator.next();
441
+ const next2 = iterator.next();
442
+
443
+ await graffiti.put<{}>(object1, session1);
444
+ await graffiti.put<{}>(object2, session2);
445
+
446
+ const result1 = (await next1).value;
447
+ const result2 = (await next2).value;
448
+ assert(result1 && !result1.error);
449
+ assert(result2 && !result2.error);
450
+
451
+ expect(result1.value.value).toEqual(object1.value);
452
+ expect(result1.value.channels).toEqual([]);
453
+ expect(result2.value.value).toEqual(object2.value);
454
+ });
455
+
456
+ it("omniscient", async () => {
457
+ const graffiti = new GraffitiSynchronize(new GraffitiLocal(), {
458
+ omniscient: true,
459
+ });
460
+
461
+ const object1 = randomPutObject();
462
+ object1.allowed = [randomString()];
463
+
464
+ const iterator = graffiti.synchronizeAll();
465
+ const next = iterator.next();
466
+
467
+ await graffiti.put<{}>(object1, session1);
468
+
469
+ const result = (await next).value;
470
+ assert(result && !result.error);
471
+ expect(result.value.value).toEqual(object1.value);
472
+ expect(result.value.channels).toEqual(object1.channels);
473
+ expect(result.value.allowed).toEqual(object1.allowed);
474
+ });
475
+ });
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import Ajv from "ajv";
1
+ import type Ajv from "ajv";
2
2
  import { Graffiti } from "@graffiti-garden/api";
3
3
  import type {
4
4
  GraffitiSession,
@@ -8,12 +8,11 @@ import type {
8
8
  } from "@graffiti-garden/api";
9
9
  import type { GraffitiObjectBase } from "@graffiti-garden/api";
10
10
  import { Repeater } from "@repeaterjs/repeater";
11
- import { applyPatch } from "fast-json-patch";
11
+ import type { applyPatch } from "fast-json-patch";
12
12
  import {
13
13
  applyGraffitiPatch,
14
14
  compileGraffitiObjectSchema,
15
15
  isActorAllowedGraffitiObject,
16
- locationToUri,
17
16
  maskGraffitiObject,
18
17
  unpackLocationOrUri,
19
18
  } from "@graffiti-garden/implementation-local/utilities";
@@ -24,6 +23,20 @@ export type GraffitiSynchronizeCallback = (
24
23
  newObject?: GraffitiObjectBase,
25
24
  ) => void;
26
25
 
26
+ export interface GraffitiSynchronizeOptions {
27
+ /**
28
+ * Allows synchronize to listen to all objects, not just those
29
+ * that the session is allowed to see. This is useful to get a
30
+ * global view of all Graffiti objects passsing through the system,
31
+ * for example to build a client-side cache. Additional mechanisms
32
+ * should be in place to ensure that users do not see objects or
33
+ * properties they are not allowed to see.
34
+ *
35
+ * Default: `false`
36
+ */
37
+ omniscient?: boolean;
38
+ }
39
+
27
40
  /**
28
41
  * Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
29
42
  * so that changes made or received in one part of an application
@@ -67,17 +80,37 @@ export type GraffitiSynchronizeCallback = (
67
80
  * streams appropriate changes to provide a responsive and consistent user experience.
68
81
  */
69
82
  export class GraffitiSynchronize extends Graffiti {
70
- protected readonly ajv: Ajv;
83
+ protected ajv_: Promise<Ajv> | undefined;
84
+ protected applyPatch_: Promise<typeof applyPatch> | undefined;
71
85
  protected readonly graffiti: Graffiti;
72
86
  protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();
87
+ protected readonly options: GraffitiSynchronizeOptions;
73
88
 
74
89
  channelStats: Graffiti["channelStats"];
75
- locationToUri: Graffiti["locationToUri"];
76
- uriToLocation: Graffiti["uriToLocation"];
77
90
  login: Graffiti["login"];
78
91
  logout: Graffiti["logout"];
79
92
  sessionEvents: Graffiti["sessionEvents"];
80
93
 
94
+ get ajv() {
95
+ if (!this.ajv_) {
96
+ this.ajv_ = (async () => {
97
+ const { default: Ajv } = await import("ajv");
98
+ return new Ajv({ strict: false });
99
+ })();
100
+ }
101
+ return this.ajv_;
102
+ }
103
+
104
+ get applyPatch() {
105
+ if (!this.applyPatch_) {
106
+ this.applyPatch_ = (async () => {
107
+ const { applyPatch } = await import("fast-json-patch");
108
+ return applyPatch;
109
+ })();
110
+ }
111
+ return this.applyPatch_;
112
+ }
113
+
81
114
  /**
82
115
  * Wraps a Graffiti API instance to provide the synchronize methods.
83
116
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -89,19 +122,12 @@ export class GraffitiSynchronize extends Graffiti {
89
122
  * instance to wrap.
90
123
  */
91
124
  graffiti: Graffiti,
92
- /**
93
- * An optional instance of Ajv to use for validating
94
- * objects before dispatching them to listeners.
95
- * If not provided, a new instance of Ajv will be created.
96
- */
97
- ajv?: Ajv,
125
+ options?: GraffitiSynchronizeOptions,
98
126
  ) {
99
127
  super();
100
- this.ajv = ajv ?? new Ajv({ strict: false });
128
+ this.options = options ?? {};
101
129
  this.graffiti = graffiti;
102
130
  this.channelStats = graffiti.channelStats.bind(graffiti);
103
- this.locationToUri = graffiti.locationToUri.bind(graffiti);
104
- this.uriToLocation = graffiti.uriToLocation.bind(graffiti);
105
131
  this.login = graffiti.login.bind(graffiti);
106
132
  this.logout = graffiti.logout.bind(graffiti);
107
133
  this.sessionEvents = graffiti.sessionEvents;
@@ -113,10 +139,9 @@ export class GraffitiSynchronize extends Graffiti {
113
139
  schema: Schema,
114
140
  session?: GraffitiSession | null,
115
141
  ) {
116
- const validate = compileGraffitiObjectSchema(this.ajv, schema);
117
-
118
142
  const repeater: GraffitiStream<GraffitiObject<Schema>> = new Repeater(
119
143
  async (push, stop) => {
144
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
120
145
  const callback: GraffitiSynchronizeCallback = (
121
146
  oldObjectRaw,
122
147
  newObjectRaw,
@@ -125,10 +150,13 @@ export class GraffitiSynchronize extends Graffiti {
125
150
  if (
126
151
  objectRaw &&
127
152
  matchObject(objectRaw) &&
128
- isActorAllowedGraffitiObject(objectRaw, session)
153
+ (this.options.omniscient ||
154
+ isActorAllowedGraffitiObject(objectRaw, session))
129
155
  ) {
130
156
  const object = { ...objectRaw };
131
- maskGraffitiObject(object, channels, session);
157
+ if (!this.options.omniscient) {
158
+ maskGraffitiObject(object, channels, session);
159
+ }
132
160
  if (validate(object)) {
133
161
  push({ value: object });
134
162
  break;
@@ -184,10 +212,9 @@ export class GraffitiSynchronize extends Graffiti {
184
212
  ...args: Parameters<typeof Graffiti.prototype.get<Schema>>
185
213
  ): GraffitiStream<GraffitiObject<Schema>> {
186
214
  const [locationOrUri, schema, session] = args;
215
+ const uri = unpackLocationOrUri(locationOrUri);
187
216
  function matchObject(object: GraffitiObjectBase) {
188
- const objectUri = locationToUri(object);
189
- const { uri } = unpackLocationOrUri(locationOrUri);
190
- return objectUri === uri;
217
+ return object.uri === uri;
191
218
  }
192
219
  return this.synchronize<Schema>(matchObject, [], schema, session);
193
220
  }
@@ -215,6 +242,23 @@ export class GraffitiSynchronize extends Graffiti {
215
242
  return this.synchronize<Schema>(matchObject, [], schema, session);
216
243
  }
217
244
 
245
+ /**
246
+ * Streams changes made to *any* object in *any* channel
247
+ * and made by *any* user. You may want to use it in conjuction with
248
+ * {@link GraffitiSynchronizeOptions.omniscient} to get a global view
249
+ * of all Graffiti objects passing through the system. This is useful
250
+ * for building a client-side cache, for example.
251
+ *
252
+ * Be careful using this method. Without additional filters it can
253
+ * expose the user to content out of context.
254
+ */
255
+ synchronizeAll<Schema extends JSONSchema>(
256
+ schema?: Schema,
257
+ session?: GraffitiSession | null,
258
+ ): GraffitiStream<GraffitiObjectBase> {
259
+ return this.synchronize(() => true, [], schema ?? {}, session);
260
+ }
261
+
218
262
  protected async synchronizeDispatch(
219
263
  oldObject: GraffitiObjectBase,
220
264
  newObject?: GraffitiObjectBase,
@@ -273,7 +317,7 @@ export class GraffitiSynchronize extends Graffiti {
273
317
  const newObject: GraffitiObjectBase = { ...oldObject };
274
318
  newObject.tombstone = false;
275
319
  for (const prop of ["value", "channels", "allowed"] as const) {
276
- applyGraffitiPatch(applyPatch, prop, args[0], newObject);
320
+ applyGraffitiPatch(await this.applyPatch, prop, args[0], newObject);
277
321
  }
278
322
  await this.synchronizeDispatch(oldObject, newObject, true);
279
323
  return oldObject;