@graffiti-garden/wrapper-synchronize 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +3 -3
- package/dist/index.browser.js +5 -5
- package/dist/index.browser.js.map +4 -4
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/package.json +5 -6
- package/src/index.spec.ts +20 -13
- package/src/index.ts +15 -15
package/dist/cjs/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var O=Object.create;var h=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var
|
|
1
|
+
"use strict";var O=Object.create;var h=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,z=Object.prototype.hasOwnProperty;var k=(r,t)=>{for(var e in t)h(r,e,{get:t[e],enumerable:!0})},b=(r,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let a of w(t))!z.call(r,a)&&a!==e&&h(r,a,{get:()=>t[a],enumerable:!(i=v(t,a))||i.enumerable});return r};var x=(r,t,e)=>(e=r!=null?O(g(r)):{},b(t||!r||!r.__esModule?h(e,"default",{value:r,enumerable:!0}):e,r)),B=r=>b(h({},"__esModule",{value:!0}),r);var U={};k(U,{GraffitiSynchronize:()=>T});module.exports=B(U);var u=x(require("ajv")),d=require("@graffiti-garden/api"),G=require("@repeaterjs/repeater"),S=require("fast-json-patch"),c=require("@graffiti-garden/implementation-local/utilities");class T extends d.Graffiti{ajv;graffiti;callbacks=new Set;channelStats;locationToUri;uriToLocation;login;logout;sessionEvents;constructor(t,e){super(),this.ajv=e??new u.default({strict:!1}),this.graffiti=t,this.channelStats=t.channelStats.bind(t),this.locationToUri=t.locationToUri.bind(t),this.uriToLocation=t.uriToLocation.bind(t),this.login=t.login.bind(t),this.logout=t.logout.bind(t),this.sessionEvents=t.sessionEvents}synchronize(t,e,i,a){const o=(0,c.compileGraffitiObjectSchema)(this.ajv,i);return new G.Repeater(async(n,l)=>{const m=(y,j)=>{for(const f of[j,y])if(f&&t(f)&&(0,c.isActorAllowedGraffitiObject)(f,a)){const p={...f};if((0,c.maskGraffitiObject)(p,e,a),o(p)){n({value:p});break}}};this.callbacks.add(m),await l,this.callbacks.delete(m)})}synchronizeDiscover(...t){const[e,i,a]=t;function o(s){return s.channels.some(n=>e.includes(n))}return this.synchronize(o,e,i,a)}synchronizeGet(...t){const[e,i,a]=t;function o(s){const n=(0,c.locationToUri)(s),{uri:l}=(0,c.unpackLocationOrUri)(e);return n===l}return this.synchronize(o,[],i,a)}synchronizeRecoverOrphans(...t){const[e,i]=t;function a(o){return o.actor===i.actor&&o.channels.length===0}return this.synchronize(a,[],e,i)}async synchronizeDispatch(t,e,i=!1){for(const a of this.callbacks)a(t,e);i&&await new Promise(a=>setTimeout(a,0))}get=async(...t)=>{const e=await this.graffiti.get(...t);return this.synchronizeDispatch(e),e};put=async(...t)=>{const e=await this.graffiti.put(...t),i=t[0],a={...e,value:i.value,channels:i.channels,allowed:i.allowed,tombstone:!1};return await this.synchronizeDispatch(e,a,!0),e};patch=async(...t)=>{const e=await this.graffiti.patch(...t),i={...e};i.tombstone=!1;for(const a of["value","channels","allowed"])(0,c.applyGraffitiPatch)(S.applyPatch,a,t[0],i);return await this.synchronizeDispatch(e,i,!0),e};delete=async(...t)=>{const e=await this.graffiti.delete(...t);return await this.synchronizeDispatch(e,void 0,!0),e};objectStream(t){const e=this.synchronizeDispatch.bind(this);return async function*(){let a=await t.next();for(;!a.done;)a.value.error||e(a.value.value),yield a.value,a=await t.next();return a.value}()}discover=(...t)=>{const e=this.graffiti.discover(...t);return this.objectStream(e)};recoverOrphans=(...t)=>{const e=this.graffiti.recoverOrphans(...t);return this.objectStream(e)}}
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import Ajv from \"ajv-draft-04\";\nimport { Graffiti } from \"@graffiti-garden/api\";\nimport type {\n GraffitiSession,\n GraffitiObject,\n JSONSchema4,\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 attemptAjvCompile,\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 JSONSchema4>(\n matchObject: (object: GraffitiObjectBase) => boolean,\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n ) {\n const validate = attemptAjvCompile(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 JSONSchema4>(\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(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 JSONSchema4>(\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(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 JSONSchema4>(\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(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 JSONSchema4>(\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(iterator);\n };\n\n recoverOrphans: Graffiti[\"recoverOrphans\"] = (...args) => {\n const iterator = this.graffiti.recoverOrphans(...args);\n return this.objectStream(iterator);\n };\n}\n"],
|
|
5
|
-
"mappings": "0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,yBAAAE,IAAA,eAAAC,EAAAH,GAAA,IAAAI,EAAgB,
|
|
6
|
-
"names": ["src_exports", "__export", "GraffitiSynchronize", "__toCommonJS", "
|
|
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": "0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,yBAAAE,IAAA,eAAAC,EAAAH,GAAA,IAAAI,EAAgB,kBAChBC,EAAyB,gCAQzBC,EAAyB,gCACzBC,EAA2B,2BAC3BC,EAOO,2DAkDA,MAAMN,UAA4B,UAAS,CAC7B,IACA,SACA,UAAY,IAAI,IAEnC,aACA,cACA,cACA,MACA,OACA,cAOA,YAKEO,EAMAC,EACA,CACA,MAAM,EACN,KAAK,IAAMA,GAAO,IAAI,EAAAC,QAAI,CAAE,OAAQ,EAAM,CAAC,EAC3C,KAAK,SAAWF,EAChB,KAAK,aAAeA,EAAS,aAAa,KAAKA,CAAQ,EACvD,KAAK,cAAgBA,EAAS,cAAc,KAAKA,CAAQ,EACzD,KAAK,cAAgBA,EAAS,cAAc,KAAKA,CAAQ,EACzD,KAAK,MAAQA,EAAS,MAAM,KAAKA,CAAQ,EACzC,KAAK,OAASA,EAAS,OAAO,KAAKA,CAAQ,EAC3C,KAAK,cAAgBA,EAAS,aAChC,CAEU,YACRG,EACAC,EACAC,EACAC,EACA,CACA,MAAMC,KAAW,+BAA4B,KAAK,IAAKF,CAAM,EA8B7D,OA5ByD,IAAI,WAC3D,MAAOG,EAAMC,IAAS,CACpB,MAAMC,EAAwC,CAC5CC,EACAC,IACG,CACH,UAAWC,IAAa,CAACD,EAAcD,CAAY,EACjD,GACEE,GACAV,EAAYU,CAAS,MACrB,gCAA6BA,EAAWP,CAAO,EAC/C,CACA,MAAMQ,EAAS,CAAE,GAAGD,CAAU,EAE9B,MADA,sBAAmBC,EAAQV,EAAUE,CAAO,EACxCC,EAASO,CAAM,EAAG,CACpBN,EAAK,CAAE,MAAOM,CAAO,CAAC,EACtB,KACF,CACF,CAEJ,EAEA,KAAK,UAAU,IAAIJ,CAAQ,EAC3B,MAAMD,EACN,KAAK,UAAU,OAAOC,CAAQ,CAChC,CACF,CAGF,CAeA,uBACKK,EACqC,CACxC,KAAM,CAACX,EAAUC,EAAQC,CAAO,EAAIS,EACpC,SAASZ,EAAYW,EAA4B,CAC/C,OAAOA,EAAO,SAAS,KAAME,GAAYZ,EAAS,SAASY,CAAO,CAAC,CACrE,CACA,OAAO,KAAK,YAAoBb,EAAaC,EAAUC,EAAQC,CAAO,CACxE,CAaA,kBACKS,EACqC,CACxC,KAAM,CAACE,EAAeZ,EAAQC,CAAO,EAAIS,EACzC,SAASZ,EAAYW,EAA4B,CAC/C,MAAMI,KAAY,iBAAcJ,CAAM,EAChC,CAAE,IAAAK,CAAI,KAAI,uBAAoBF,CAAa,EACjD,OAAOC,IAAcC,CACvB,CACA,OAAO,KAAK,YAAoBhB,EAAa,CAAC,EAAGE,EAAQC,CAAO,CAClE,CAeA,6BACKS,EACqC,CACxC,KAAM,CAACV,EAAQC,CAAO,EAAIS,EAC1B,SAASZ,EAAYW,EAA4B,CAC/C,OAAOA,EAAO,QAAUR,EAAQ,OAASQ,EAAO,SAAS,SAAW,CACtE,CACA,OAAO,KAAK,YAAoBX,EAAa,CAAC,EAAGE,EAAQC,CAAO,CAClE,CAEA,MAAgB,oBACdc,EACAC,EACAC,EAAmB,GACnB,CACA,UAAWZ,KAAY,KAAK,UAC1BA,EAASU,EAAWC,CAAS,EAE3BC,GAqBF,MAAM,IAAI,QAASC,GAAY,WAAWA,EAAS,CAAC,CAAC,CAEzD,CAEA,IAAuB,SAAUR,IAAS,CACxC,MAAMD,EAAS,MAAM,KAAK,SAAS,IAAI,GAAGC,CAAI,EAC9C,YAAK,oBAAoBD,CAAM,EACxBA,CACT,EAEA,IAAuB,SAAUC,IAAS,CACxC,MAAMK,EAAY,MAAM,KAAK,SAAS,IAAQ,GAAGL,CAAI,EAC/CS,EAAgBT,EAAK,CAAC,EACtBM,EAAgC,CACpC,GAAGD,EACH,MAAOI,EAAc,MACrB,SAAUA,EAAc,SACxB,QAASA,EAAc,QACvB,UAAW,EACb,EACA,aAAM,KAAK,oBAAoBJ,EAAWC,EAAW,EAAI,EAClDD,CACT,EAEA,MAA2B,SAAUL,IAAS,CAC5C,MAAMK,EAAY,MAAM,KAAK,SAAS,MAAM,GAAGL,CAAI,EAC7CM,EAAgC,CAAE,GAAGD,CAAU,EACrDC,EAAU,UAAY,GACtB,UAAWI,IAAQ,CAAC,QAAS,WAAY,SAAS,KAChD,sBAAmB,aAAYA,EAAMV,EAAK,CAAC,EAAGM,CAAS,EAEzD,aAAM,KAAK,oBAAoBD,EAAWC,EAAW,EAAI,EAClDD,CACT,EAEA,OAA6B,SAAUL,IAAS,CAC9C,MAAMK,EAAY,MAAM,KAAK,SAAS,OAAO,GAAGL,CAAI,EACpD,aAAM,KAAK,oBAAoBK,EAAW,OAAW,EAAI,EAClDA,CACT,EAEU,aACRM,EACA,CACA,MAAMC,EAAW,KAAK,oBAAoB,KAAK,IAAI,EAYnD,OAXgB,iBAAmB,CACjC,IAAIC,EAAS,MAAMF,EAAS,KAAK,EACjC,KAAO,CAACE,EAAO,MACRA,EAAO,MAAM,OAChBD,EAASC,EAAO,MAAM,KAAK,EAE7B,MAAMA,EAAO,MACbA,EAAS,MAAMF,EAAS,KAAK,EAE/B,OAAOE,EAAO,KAChB,EACe,CACjB,CAEA,SAAiC,IAAIb,IAAS,CAC5C,MAAMW,EAAW,KAAK,SAAS,SAAS,GAAGX,CAAI,EAC/C,OAAO,KAAK,aAA+BW,CAAQ,CACrD,EAEA,eAA6C,IAAIX,IAAS,CACxD,MAAMW,EAAW,KAAK,SAAS,eAAe,GAAGX,CAAI,EACrD,OAAO,KAAK,aAA+BW,CAAQ,CACrD,CACF",
|
|
6
|
+
"names": ["src_exports", "__export", "GraffitiSynchronize", "__toCommonJS", "import_ajv", "import_api", "import_repeater", "import_fast_json_patch", "import_utilities", "graffiti", "ajv", "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"]
|
|
7
7
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import m from"ajv
|
|
1
|
+
import m from"ajv";import{Graffiti as b}from"@graffiti-garden/api";import{Repeater as u}from"@repeaterjs/repeater";import{applyPatch as d}from"fast-json-patch";import{applyGraffitiPatch as G,compileGraffitiObjectSchema as S,isActorAllowedGraffitiObject as y,locationToUri as j,maskGraffitiObject as O,unpackLocationOrUri as v}from"@graffiti-garden/implementation-local/utilities";class T extends b{ajv;graffiti;callbacks=new Set;channelStats;locationToUri;uriToLocation;login;logout;sessionEvents;constructor(t,e){super(),this.ajv=e??new m({strict:!1}),this.graffiti=t,this.channelStats=t.channelStats.bind(t),this.locationToUri=t.locationToUri.bind(t),this.uriToLocation=t.uriToLocation.bind(t),this.login=t.login.bind(t),this.logout=t.logout.bind(t),this.sessionEvents=t.sessionEvents}synchronize(t,e,i,a){const r=S(this.ajv,i);return new u(async(c,s)=>{const h=(l,p)=>{for(const n of[p,l])if(n&&t(n)&&y(n,a)){const f={...n};if(O(f,e,a),r(f)){c({value:f});break}}};this.callbacks.add(h),await s,this.callbacks.delete(h)})}synchronizeDiscover(...t){const[e,i,a]=t;function r(o){return o.channels.some(c=>e.includes(c))}return this.synchronize(r,e,i,a)}synchronizeGet(...t){const[e,i,a]=t;function r(o){const c=j(o),{uri:s}=v(e);return c===s}return this.synchronize(r,[],i,a)}synchronizeRecoverOrphans(...t){const[e,i]=t;function a(r){return r.actor===i.actor&&r.channels.length===0}return this.synchronize(a,[],e,i)}async synchronizeDispatch(t,e,i=!1){for(const a of this.callbacks)a(t,e);i&&await new Promise(a=>setTimeout(a,0))}get=async(...t)=>{const e=await this.graffiti.get(...t);return this.synchronizeDispatch(e),e};put=async(...t)=>{const e=await this.graffiti.put(...t),i=t[0],a={...e,value:i.value,channels:i.channels,allowed:i.allowed,tombstone:!1};return await this.synchronizeDispatch(e,a,!0),e};patch=async(...t)=>{const e=await this.graffiti.patch(...t),i={...e};i.tombstone=!1;for(const a of["value","channels","allowed"])G(d,a,t[0],i);return await this.synchronizeDispatch(e,i,!0),e};delete=async(...t)=>{const e=await this.graffiti.delete(...t);return await this.synchronizeDispatch(e,void 0,!0),e};objectStream(t){const e=this.synchronizeDispatch.bind(this);return async function*(){let a=await t.next();for(;!a.done;)a.value.error||e(a.value.value),yield a.value,a=await t.next();return a.value}()}discover=(...t)=>{const e=this.graffiti.discover(...t);return this.objectStream(e)};recoverOrphans=(...t)=>{const e=this.graffiti.recoverOrphans(...t);return this.objectStream(e)}}export{T as GraffitiSynchronize};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import Ajv from \"ajv-draft-04\";\nimport { Graffiti } from \"@graffiti-garden/api\";\nimport type {\n GraffitiSession,\n GraffitiObject,\n JSONSchema4,\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 attemptAjvCompile,\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 JSONSchema4>(\n matchObject: (object: GraffitiObjectBase) => boolean,\n channels: string[],\n schema: Schema,\n session?: GraffitiSession | null,\n ) {\n const validate = attemptAjvCompile(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 JSONSchema4>(\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(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 JSONSchema4>(\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(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 JSONSchema4>(\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(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 JSONSchema4>(\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(iterator);\n };\n\n recoverOrphans: Graffiti[\"recoverOrphans\"] = (...args) => {\n const iterator = this.graffiti.recoverOrphans(...args);\n return this.objectStream(iterator);\n };\n}\n"],
|
|
5
|
-
"mappings": "AAAA,OAAOA,MAAS,
|
|
6
|
-
"names": ["Ajv", "Graffiti", "Repeater", "applyPatch", "applyGraffitiPatch", "
|
|
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"]
|
|
7
7
|
}
|