diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ddeaeede5..a261fa963 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -356,6 +356,30 @@ jobs: package: connect/package.json access: public + - name: Publish ad4m hook helpers + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.COASYS_NPM_TOKEN }} + package: ad4m-hooks/helpers/package.json + tag: ${{ env.NPM_TAG }} + access: public + + - name: Publish ad4m react hooks + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.COASYS_NPM_TOKEN }} + package: ad4m-hooks/react/package.json + tag: ${{ env.NPM_TAG }} + access: public + + - name: Publish ad4m vue hooks + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.COASYS_NPM_TOKEN }} + package: ad4m-hooks/vue/package.json + tag: ${{ env.NPM_TAG }} + access: public + - name: Publish executor uses: JS-DevTools/npm-publish@v1 with: diff --git a/.github/workflows/publish_staging.yml b/.github/workflows/publish_staging.yml index cd65315cd..fd16da13c 100644 --- a/.github/workflows/publish_staging.yml +++ b/.github/workflows/publish_staging.yml @@ -382,6 +382,30 @@ jobs: package: connect/package.json tag: ${{ env.NPM_TAG }} access: public + + - name: Publish ad4m hook helpers + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.COASYS_NPM_TOKEN }} + package: ad4m-hooks/helpers/package.json + tag: ${{ env.NPM_TAG }} + access: public + + - name: Publish ad4m react hooks + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.COASYS_NPM_TOKEN }} + package: ad4m-hooks/react/package.json + tag: ${{ env.NPM_TAG }} + access: public + + - name: Publish ad4m vue hooks + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.COASYS_NPM_TOKEN }} + package: ad4m-hooks/vue/package.json + tag: ${{ env.NPM_TAG }} + access: public - name: Publish executor uses: JS-DevTools/npm-publish@v1 diff --git a/ad4m-hooks/.gitignore b/ad4m-hooks/.gitignore new file mode 100644 index 000000000..d97220f46 --- /dev/null +++ b/ad4m-hooks/.gitignore @@ -0,0 +1,25 @@ +node_modules +dist +package-lock.json + +**/.vitepress/cache + +# Log files +*.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +*.turbo +dev-dist +.DS_Store + +cachedb +# Local Netlify folder +.netlify +!register.js \ No newline at end of file diff --git a/ad4m-hooks/helpers/package.json b/ad4m-hooks/helpers/package.json new file mode 100644 index 000000000..9c80e0e0b --- /dev/null +++ b/ad4m-hooks/helpers/package.json @@ -0,0 +1,22 @@ +{ + "name": "@coasys/hooks-helpers", + "version": "0.8.2-prerelease", + "description": "", + "main": "./src/index.ts", + "module": "./src/index.ts", + "private": false, + "type": "module", + "files": [ + "dist", + "src" + ], + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@coasys/ad4m": "*", + "@coasys/ad4m-connect": "*", + "uuid": "*" + } + } \ No newline at end of file diff --git a/ad4m-hooks/helpers/src/cache.ts b/ad4m-hooks/helpers/src/cache.ts new file mode 100644 index 000000000..241cfa136 --- /dev/null +++ b/ad4m-hooks/helpers/src/cache.ts @@ -0,0 +1,70 @@ +import { PerspectiveProxy } from "@coasys/ad4m"; + +const cache: Map = new Map(); +const subscribers: Map = new Map(); + +export function getCache(key: string) { + const match: T | undefined = cache.get(key); + return match; +} + +export function setCache(key: string, value: T) { + cache.set(key, value); + getSubscribers(key).forEach((cb) => cb()); +} + +export function subscribe(key: string, callback: Function) { + getSubscribers(key).push(callback); +} + +export function unsubscribe(key: string, callback: Function) { + const subs = getSubscribers(key); + const index = subs.indexOf(callback); + if (index >= 0) { + subs.splice(index, 1); + } +} + +export function getSubscribers(key: string) { + if (!subscribers.has(key)) subscribers.set(key, []); + return subscribers.get(key)!; +} + +export function subscribeToPerspective( + perspective: PerspectiveProxy, + added: Function, + removed: Function +) { + const addedKey = `perspective-${perspective.uuid}-added`; + const removedKey = `perspective-${perspective.uuid}-removed`; + + if (!subscribers.has(addedKey)) { + console.log("subscribing!"); + perspective.addListener("link-added", (link) => { + subscribers.get(addedKey).forEach((cb) => cb(link)); + return null; + }); + } + + if (!subscribers.has(removedKey)) { + perspective.addListener("link-removed", (link) => { + subscribers.get(removedKey).forEach((cb) => cb(link)); + return null; + }); + } + + subscribe(addedKey, added); + subscribe(removedKey, removed); +} + +export function unsubscribeFromPerspective( + perspective: PerspectiveProxy, + added: Function, + removed: Function +) { + const addedKey = `perspective-${perspective.uuid}-added`; + const removedKey = `perspective-${perspective.uuid}-removed`; + + unsubscribe(addedKey, added); + unsubscribe(removedKey, removed); +} diff --git a/ad4m-hooks/helpers/src/factory/SubjectRepository.ts b/ad4m-hooks/helpers/src/factory/SubjectRepository.ts new file mode 100644 index 000000000..b432ca9e7 --- /dev/null +++ b/ad4m-hooks/helpers/src/factory/SubjectRepository.ts @@ -0,0 +1,257 @@ +import { + PerspectiveProxy, + Link, + Subject, + Literal, + LinkQuery, +} from "@coasys/ad4m"; +import { setProperties } from "./model"; +import { v4 as uuidv4 } from "uuid"; + +export const SELF = "ad4m://self"; + +export type ModelProps = { + perspective: PerspectiveProxy; + source?: string; +}; + +export class SubjectRepository { + source = SELF; + subject: SubjectClass | string; + perspective: PerspectiveProxy; + tempSubject: any | string; + + constructor(subject: { new (): SubjectClass } | string, props: ModelProps) { + this.perspective = props.perspective; + this.source = props.source || this.source; + this.subject = typeof subject === "string" ? subject : new subject(); + this.tempSubject = subject; + } + + get className(): string { + return typeof this.subject === "string" + ? this.subject + : this.subject.className; + } + + async ensureSubject() { + if (typeof this.tempSubject === "string") return; + await this.perspective.ensureSDNASubjectClass(this.tempSubject); + } + + async create( + data: SubjectClass, + id?: string, + source?: string + ): Promise { + await this.ensureSubject(); + const base = id || Literal.from(uuidv4()).toUrl(); + + let newInstance = await this.perspective.createSubject(this.subject, base); + + if (!newInstance) { + throw "Failed to create new instance of " + this.subject; + } + + // Connect new instance to source + await this.perspective.add( + new Link({ + source: source || this.source, + predicate: "ad4m://has_child", + target: base, + }) + ); + + Object.keys(data).forEach((key) => + data[key] === undefined || data[key] === null ? delete data[key] : {} + ); + + setProperties(newInstance, data); + + // @ts-ignore + return this.getSubjectData(newInstance); + } + + async update(id: string, data: QueryPartialEntity) { + await this.ensureSubject(); + + const instance = await this.get(id); + + if (!instance) { + throw "Failed to find instance of " + this.subject + " with id " + id; + } + + Object.keys(data).forEach((key) => + data[key] === undefined ? delete data[key] : {} + ); + + // @ts-ignore + setProperties(instance, data); + + return this.getSubjectData(instance); + } + + async remove(id: string) { + if (this.perspective) { + const linksTo = await this.perspective.get(new LinkQuery({ target: id })); + const linksFrom = await this.perspective.get( + new LinkQuery({ source: id }) + ); + this.perspective.removeLinks([...linksFrom, ...linksTo]); + } + } + + async get(id: string): Promise { + await this.ensureSubject(); + if (id) { + const subjectProxy = await this.perspective.getSubjectProxy( + id, + this.subject + ); + + // @ts-ignore + return subjectProxy || null; + } else { + const all = await this.getAll(); + return all[0] || null; + } + } + + async getData(id: string): Promise { + await this.ensureSubject(); + const entry = await this.get(id); + if (entry) { + // @ts-ignore + return await this.getSubjectData(entry); + } + + return null; + } + + private async getSubjectData(entry: any) { + let links = await this.perspective.get( + new LinkQuery({ source: entry.baseExpression }) + ); + + const getters = Object.entries(Object.getOwnPropertyDescriptors(entry)) + .filter(([key, descriptor]) => typeof descriptor.get === "function") + .map(([key]) => key); + + const promises = getters.map((getter) => entry[getter]); + return Promise.all(promises).then((values) => { + return getters.reduce((acc, getter, index) => { + let value = values[index]; + if (this.tempSubject.prototype?.__properties[getter]?.transform) { + value = + this.tempSubject.prototype.__properties[getter].transform(value); + } + + return { + ...acc, + id: entry.baseExpression, + timestamp: links[0].timestamp, + author: links[0].author, + [getter]: value, + }; + }, {}); + }); + } + + async getAll(source?: string, query?: QueryOptions): Promise { + await this.ensureSubject(); + + const tempSource = source || this.source; + + let res = []; + + if (query) { + try { + const queryResponse = ( + await this.perspective.infer( + `findall([Timestamp, Base], (subject_class("${this.className}", C), instance(C, Base), link("${tempSource}", Predicate, Base, Timestamp, Author)), AllData), length(AllData, DataLength), sort(AllData, SortedData).` + ) + )[0]; + + if (queryResponse.SortedData >= query.size) { + const isOutofBound = + query.size * query.page > queryResponse.DataLength; + + const newPageSize = isOutofBound + ? queryResponse.DataLength - query.size * (query.page - 1) + : query.size; + + const mainQuery = `findall([Timestamp, Base], (subject_class("${this.className}", C), instance(C, Base), link("${tempSource}", Predicate, Base, Timestamp, Author)), AllData), sort(AllData, SortedData), reverse(SortedData, ReverseSortedData), paginate(ReverseSortedData, ${query.page}, ${newPageSize}, PageData).`; + res = await this.perspective.infer(mainQuery); + + res = res[0].PageData.map((r) => ({ + Base: r[1], + Timestamp: r[0], + })); + } else { + res = await this.perspective.infer( + `subject_class("${this.className}", C), instance(C, Base), triple("${tempSource}", Predicate, Base).` + ); + } + } catch (e) { + console.log("Query failed", e); + } + } else { + res = await this.perspective.infer( + `subject_class("${this.className}", C), instance(C, Base), triple("${tempSource}", Predicate, Base).` + ); + } + + const results = + res && + res.filter( + (obj, index, self) => + index === self.findIndex((t) => t.Base === obj.Base) + ); + + if (!res) return []; + + const data = await Promise.all( + results.map(async (result) => { + let subject = new Subject( + this.perspective!, + result.Base, + this.className + ); + + await subject.init(); + + return subject; + }) + ); + + // @ts-ignore + return data; + } + + async getAllData( + source?: string, + query?: QueryOptions + ): Promise { + await this.ensureSubject(); + + const subjects = await this.getAll(source, query); + + const entries = await Promise.all( + subjects.map((e) => this.getSubjectData(e)) + ); + + // @ts-ignore + return entries; + } +} + +export type QueryPartialEntity = { + [P in keyof T]?: T[P] | (() => string); +}; + +export type QueryOptions = { + page: number; + size: number; + infinite: boolean; + uniqueKey: string; +}; diff --git a/ad4m-hooks/helpers/src/factory/index.ts b/ad4m-hooks/helpers/src/factory/index.ts new file mode 100644 index 000000000..3f58419a0 --- /dev/null +++ b/ad4m-hooks/helpers/src/factory/index.ts @@ -0,0 +1 @@ +export * from "./SubjectRepository"; diff --git a/ad4m-hooks/helpers/src/factory/model.ts b/ad4m-hooks/helpers/src/factory/model.ts new file mode 100644 index 000000000..2bae2833d --- /dev/null +++ b/ad4m-hooks/helpers/src/factory/model.ts @@ -0,0 +1,57 @@ +type Target = String; + +export type PropertyValueMap = { + [property: string]: Target | Target[]; +}; + +export function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +// e.g. "name" -> "setName" +export function propertyNameToSetterName(property: string): string { + return `set${capitalize(property)}`; +} + +export function pluralToSingular(plural: string): string { + if (plural.endsWith("ies")) { + return plural.slice(0, -3) + "y"; + } else if (plural.endsWith("s")) { + return plural.slice(0, -1); + } else { + return plural; + } +} + +// e.g. "comments" -> "addComment" +export function collectionToAdderName(collection: string): string { + return `add${capitalize(collection)}`; +} + +export function collectionToSetterName(collection: string): string { + return `setCollection${capitalize(collection)}`; +} + +export function setProperties(subject: any, properties: PropertyValueMap) { + Object.keys(properties).forEach((key) => { + if (Array.isArray(properties[key])) { + // it's a collection + const adderName = collectionToAdderName(key); + const adderFunction = subject[adderName]; + if (adderFunction) { + adderFunction(properties[key]); + } else { + throw "No adder function found for collection: " + key; + } + } else { + // it's a property + const setterName = propertyNameToSetterName(key); + const setterFunction = subject[setterName]; + if (setterFunction) { + setterFunction(properties[key]); + } else { + throw "No setter function found for property: " + key; + } + } + }); +} diff --git a/ad4m-hooks/helpers/src/getProfile.ts b/ad4m-hooks/helpers/src/getProfile.ts new file mode 100644 index 000000000..c58a2f262 --- /dev/null +++ b/ad4m-hooks/helpers/src/getProfile.ts @@ -0,0 +1,26 @@ +import { Ad4mClient } from "@coasys/ad4m"; +// @ts-ignore +import { getAd4mClient } from "@coasys/ad4m-connect/utils"; +import { LinkExpression } from "@coasys/ad4m"; + +export interface Payload { + url: string; + perspectiveUuid: string; +} + +export async function getProfile(did: string, formatter?: (links: LinkExpression[]) => T): Promise { + const cleanedDid = did.replace("did://", ""); + const client: Ad4mClient = await getAd4mClient(); + + const agentPerspective = await client.agent.byDID(cleanedDid); + + if (agentPerspective) { + const links = agentPerspective!.perspective!.links; + + if (formatter) { + return formatter(links); + } + + return agentPerspective + } +} diff --git a/ad4m-hooks/helpers/src/index.ts b/ad4m-hooks/helpers/src/index.ts new file mode 100644 index 000000000..b832fa981 --- /dev/null +++ b/ad4m-hooks/helpers/src/index.ts @@ -0,0 +1,3 @@ +export * from './cache' +export * from './getProfile' +export * from './factory' \ No newline at end of file diff --git a/ad4m-hooks/react/package.json b/ad4m-hooks/react/package.json new file mode 100644 index 000000000..ad83baa18 --- /dev/null +++ b/ad4m-hooks/react/package.json @@ -0,0 +1,28 @@ +{ + "name": "@coasys/ad4m-react-hooks", + "version": "0.8.2-prerelease.1", + "description": "", + "main": "./src/index.ts", + "module": "./src/index.ts", + "type": "module", + "private": false, + "files": [ + "dist", + "src" + ], + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@coasys/ad4m": "*", + "@coasys/ad4m-connect": "*", + "@coasys/hooks-helpers": "*", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19" + }, + "peerDependencies": { + "preact": "*", + "react": "*" + } +} \ No newline at end of file diff --git a/ad4m-hooks/react/src/index.ts b/ad4m-hooks/react/src/index.ts new file mode 100644 index 000000000..312134c23 --- /dev/null +++ b/ad4m-hooks/react/src/index.ts @@ -0,0 +1,19 @@ +import { useSubjects } from "./useSubjects"; +import { useSubject } from "./useSubject"; +import { useAgent } from "./useAgent"; +import { useMe } from "./useMe"; +import { useClient } from "./useClient"; +import { toCustomElement } from "./register.js"; +import { usePerspective } from "./usePerspective.js"; +import { usePerspectives } from "./usePerspectives.js"; + +export { + toCustomElement, + useSubjects, + useSubject, + useAgent, + useMe, + useClient, + usePerspective, + usePerspectives, +}; diff --git a/ad4m-hooks/react/src/register.js b/ad4m-hooks/react/src/register.js new file mode 100644 index 000000000..0f6a1022d --- /dev/null +++ b/ad4m-hooks/react/src/register.js @@ -0,0 +1,177 @@ +import { createElement as h, cloneElement, render, hydrate } from "preact"; + +export function toCustomElement(Component, propNames, options) { + function PreactElement() { + const inst = Reflect.construct(HTMLElement, [], PreactElement); + inst._vdomComponent = Component; + inst._root = + options && options.shadow ? inst.attachShadow({ mode: "open" }) : inst; + return inst; + } + PreactElement.prototype = Object.create(HTMLElement.prototype); + PreactElement.prototype.constructor = PreactElement; + PreactElement.prototype.connectedCallback = connectedCallback; + PreactElement.prototype.attributeChangedCallback = attributeChangedCallback; + PreactElement.prototype.disconnectedCallback = disconnectedCallback; + + propNames = + propNames || + Component.observedAttributes || + Object.keys(Component.propTypes || {}); + PreactElement.observedAttributes = propNames; + + // Keep DOM properties and Preact props in sync + propNames.forEach((name) => { + Object.defineProperty(PreactElement.prototype, name, { + get() { + return this._vdom.props[name]; + }, + set(v) { + if (this._vdom) { + if (!this._props) this._props = {}; + this._props[name] = v; + this.attributeChangedCallback(name, null, v); + } else { + if (!this._props) this._props = {}; + this._props[name] = v; + this.connectedCallback(); + } + + // Reflect property changes to attributes if the value is a primitive + const type = typeof v; + if ( + v == null || + type === "string" || + type === "boolean" || + type === "number" + ) { + this.setAttribute(name, v); + } + }, + }); + }); + + return PreactElement; +} + +export default function register(Component, tagName, propNames, options) { + const PreactElement = toCustomElement(Component, propNames, options); + + return customElements.define( + tagName || Component.tagName || Component.displayName || Component.name, + PreactElement + ); +} + +register.toCustomElement = toCustomElement; + +function ContextProvider(props) { + this.getChildContext = () => props.context; + // eslint-disable-next-line no-unused-vars + const { context, children, ...rest } = props; + return cloneElement(children, rest); +} + +function connectedCallback() { + // Obtain a reference to the previous context by pinging the nearest + // higher up node that was rendered with Preact. If one Preact component + // higher up receives our ping, it will set the `detail` property of + // our custom event. This works because events are dispatched + // synchronously. + const event = new CustomEvent("_preact", { + detail: {}, + bubbles: true, + cancelable: true, + }); + this.dispatchEvent(event); + const context = event.detail.context; + + this._vdom = h( + ContextProvider, + { ...this._props, context }, + toVdom(this, this._vdomComponent) + ); + (this.hasAttribute("hydrate") ? hydrate : render)(this._vdom, this._root); +} + +function toCamelCase(str) { + return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : "")); +} + +function attributeChangedCallback(name, oldValue, newValue) { + if (!this._vdom) return; + // Attributes use `null` as an empty value whereas `undefined` is more + // common in pure JS components, especially with default parameters. + // When calling `node.removeAttribute()` we'll receive `null` as the new + // value. See issue #50. + newValue = newValue == null ? undefined : newValue; + const props = {}; + props[name] = newValue; + props[toCamelCase(name)] = newValue; + this._vdom = cloneElement(this._vdom, props); + render(this._vdom, this._root); +} + +function disconnectedCallback() { + render((this._vdom = null), this._root); +} + +/** + * Pass an event listener to each `` that "forwards" the current + * context value to the rendered child. The child will trigger a custom + * event, where will add the context value to. Because events work + * synchronously, the child can immediately pull of the value right + * after having fired the event. + */ +function Slot(props, context) { + const ref = (r) => { + if (!r) { + this.ref.removeEventListener("_preact", this._listener); + } else { + this.ref = r; + if (!this._listener) { + this._listener = (event) => { + event.stopPropagation(); + event.detail.context = context; + }; + r.addEventListener("_preact", this._listener); + } + } + }; + return h("slot", { ...props, ref }); +} + +function toVdom(element, nodeName) { + if (element.nodeType === 3) return element.data; + if (element.nodeType !== 1) return null; + let children = [], + props = {}, + i = 0, + a = element.attributes, + cn = element.childNodes; + for (i = a.length; i--; ) { + if (a[i].name !== "slot") { + props[a[i].name] = a[i].value; + props[toCamelCase(a[i].name)] = a[i].value; + } + } + + for (i = cn.length; i--; ) { + const vnode = toVdom(cn[i], null); + // Move slots correctly + const name = cn[i].slot; + if (name) { + props[name] = h(Slot, { name }, vnode); + } else { + children[i] = vnode; + } + } + + // Only wrap the topmost node with a slot + const wrappedChildren = nodeName ? h(Slot, null, children) : children; + return h( + nodeName || element.nodeName.toLowerCase(), + { ...props, element }, + wrappedChildren + ); +} diff --git a/ad4m-hooks/react/src/useAgent.tsx b/ad4m-hooks/react/src/useAgent.tsx new file mode 100644 index 000000000..660c245f0 --- /dev/null +++ b/ad4m-hooks/react/src/useAgent.tsx @@ -0,0 +1,61 @@ +import { useState, useCallback, useEffect } from "react"; +import { getCache, setCache, subscribe, unsubscribe, getProfile } from "@coasys/hooks-helpers"; +import { AgentClient, LinkExpression } from "@coasys/ad4m"; +import { Agent } from '@coasys/ad4m' + +type Props = { + client: AgentClient; + did: string | (() => string); + formatter: (links: LinkExpression[]) => T; +}; + +export function useAgent(props: Props) { + const forceUpdate = useForceUpdate(); + const [error, setError] = useState(undefined); + const [profile, setProfile] = useState(null); + const didRef = typeof props.did === "function" ? props.did() : props.did; + + // Create cache key for entry + const cacheKey = `agents/${didRef}`; + + // Mutate shared/cached data for all subscribers + const mutate = useCallback( + (agent: Agent | null) => setCache(cacheKey, agent), + [cacheKey] + ); + + // Fetch data from AD4M and save to cache + const getData = useCallback(() => { + if (didRef) { + if (props.formatter) { + getProfile(didRef).then(profile => setProfile(props.formatter(profile))) + } + + props.client + .byDID(didRef) + .then(async (agent) => { + setError(undefined); + mutate(agent); + }) + .catch((error) => setError(error.toString())); + } + }, [cacheKey]); + + // Trigger initial fetch + useEffect(getData, [getData]); + + // Subscribe to changes (re-render on data change) + useEffect(() => { + subscribe(cacheKey, forceUpdate); + return () => unsubscribe(cacheKey, forceUpdate); + }, [cacheKey, forceUpdate]); + + const agent = getCache(cacheKey); + + return { agent, profile, error, mutate, reload: getData }; +} + +function useForceUpdate() { + const [, setState] = useState([]); + return useCallback(() => setState([]), [setState]); +} diff --git a/ad4m-hooks/react/src/useClient.tsx b/ad4m-hooks/react/src/useClient.tsx new file mode 100644 index 000000000..a73e74e7b --- /dev/null +++ b/ad4m-hooks/react/src/useClient.tsx @@ -0,0 +1,53 @@ +import { useState, useCallback, useEffect } from "react"; +import { getCache, setCache, subscribe, unsubscribe } from "@coasys/hooks-helpers"; +// @ts-ignore +import { getAd4mClient } from "@coasys/ad4m-connect/utils"; +import { Ad4mClient } from "@coasys/ad4m"; + +export function useClient() { + const forceUpdate = useForceUpdate(); + const [error, setError] = useState(undefined); + + // Create cache key for entry + const cacheKey = `client`; + + // Mutate shared/cached data for all subscribers + const mutate = useCallback( + (client: Ad4mClient | undefined) => setCache(cacheKey, client), + [cacheKey] + ); + + // Fetch data from AD4M and save to cache + const getData = useCallback(() => { + console.log("🪝 useClient - running getAd4mClient"); + getAd4mClient() + .then((client) => { + setError(undefined); + mutate(client); + }) + .catch((error) => setError(error.toString())); + }, [mutate]); + + // Trigger initial fetch + useEffect(getData, [getData]); + + // Subscribe to changes (re-render on data change) + useEffect(() => { + subscribe(cacheKey, forceUpdate); + return () => unsubscribe(cacheKey, forceUpdate); + }, [cacheKey, forceUpdate]); + + const client = getCache(cacheKey); + + return { + client, + error, + mutate, + reload: getData, + }; +} + +function useForceUpdate() { + const [, setState] = useState([]); + return useCallback(() => setState([]), [setState]); +} diff --git a/ad4m-hooks/react/src/useEntries.tsx b/ad4m-hooks/react/src/useEntries.tsx new file mode 100644 index 000000000..96ce3b910 --- /dev/null +++ b/ad4m-hooks/react/src/useEntries.tsx @@ -0,0 +1,104 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import { getCache, setCache, subscribe, unsubscribe } from "@coasys/hooks-helpers"; +import { Agent, AgentStatus, LinkExpression } from "@coasys/ad4m"; +import { AgentClient } from "@coasys/ad4m"; + +type MeData = { + agent?: Agent; + status?: AgentStatus; +}; + +type MyInfo = { + me?: Agent; + status?: AgentStatus; + profile: T | null; + error: string | undefined; + mutate: Function; + reload: Function; +}; + +export function useMe(agent: AgentClient | undefined, formatter: (links: LinkExpression[]) => T): MyInfo { + const forceUpdate = useForceUpdate(); + const [error, setError] = useState(undefined); + + // Create cache key for entry + const cacheKey = `agents/me`; + + // Mutate shared/cached data for all subscribers + const mutate = useCallback( + (data: MeData | null) => setCache(cacheKey, data), + [cacheKey] + ); + + // Fetch data from AD4M and save to cache + const getData = useCallback(() => { + if (!agent) { + return; + } + + const promises = Promise.all([agent.status(), agent.me()]); + + promises + .then(async ([status, agent]) => { + setError(undefined); + mutate({ agent, status }); + }) + .catch((error) => setError(error.toString())); + }, [agent, mutate]); + + // Trigger initial fetch + useEffect(getData, [getData]); + + // Subscribe to changes (re-render on data change) + useEffect(() => { + subscribe(cacheKey, forceUpdate); + return () => unsubscribe(cacheKey, forceUpdate); + }, [cacheKey, forceUpdate]); + + // Listen to remote changes + useEffect(() => { + const changed = (status: AgentStatus) => { + const newMeData = { agent: data?.agent, status }; + mutate(newMeData); + return null; + }; + + const updated = (agent: Agent) => { + const newMeData = { agent, status: data?.status }; + mutate(newMeData); + return null; + }; + + if (agent) { + agent.addAgentStatusChangedListener(changed); + agent.addUpdatedListener(updated); + + // TODO need a way to remove listeners + } + }, [agent]); + + const data = getCache(cacheKey); + let profile = null as T | null; + const perspective = data?.agent?.perspective; + + if (perspective) { + if (formatter) { + profile = formatter(perspective.links) + } + + } + + return { + status: data?.status, + me: data?.agent, + profile, + error, + mutate, + reload: getData, + }; +} + +function useForceUpdate() { + const [, setState] = useState([]); + return useCallback(() => setState([]), [setState]); +} diff --git a/ad4m-hooks/react/src/useMe.tsx b/ad4m-hooks/react/src/useMe.tsx new file mode 100644 index 000000000..96ce3b910 --- /dev/null +++ b/ad4m-hooks/react/src/useMe.tsx @@ -0,0 +1,104 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import { getCache, setCache, subscribe, unsubscribe } from "@coasys/hooks-helpers"; +import { Agent, AgentStatus, LinkExpression } from "@coasys/ad4m"; +import { AgentClient } from "@coasys/ad4m"; + +type MeData = { + agent?: Agent; + status?: AgentStatus; +}; + +type MyInfo = { + me?: Agent; + status?: AgentStatus; + profile: T | null; + error: string | undefined; + mutate: Function; + reload: Function; +}; + +export function useMe(agent: AgentClient | undefined, formatter: (links: LinkExpression[]) => T): MyInfo { + const forceUpdate = useForceUpdate(); + const [error, setError] = useState(undefined); + + // Create cache key for entry + const cacheKey = `agents/me`; + + // Mutate shared/cached data for all subscribers + const mutate = useCallback( + (data: MeData | null) => setCache(cacheKey, data), + [cacheKey] + ); + + // Fetch data from AD4M and save to cache + const getData = useCallback(() => { + if (!agent) { + return; + } + + const promises = Promise.all([agent.status(), agent.me()]); + + promises + .then(async ([status, agent]) => { + setError(undefined); + mutate({ agent, status }); + }) + .catch((error) => setError(error.toString())); + }, [agent, mutate]); + + // Trigger initial fetch + useEffect(getData, [getData]); + + // Subscribe to changes (re-render on data change) + useEffect(() => { + subscribe(cacheKey, forceUpdate); + return () => unsubscribe(cacheKey, forceUpdate); + }, [cacheKey, forceUpdate]); + + // Listen to remote changes + useEffect(() => { + const changed = (status: AgentStatus) => { + const newMeData = { agent: data?.agent, status }; + mutate(newMeData); + return null; + }; + + const updated = (agent: Agent) => { + const newMeData = { agent, status: data?.status }; + mutate(newMeData); + return null; + }; + + if (agent) { + agent.addAgentStatusChangedListener(changed); + agent.addUpdatedListener(updated); + + // TODO need a way to remove listeners + } + }, [agent]); + + const data = getCache(cacheKey); + let profile = null as T | null; + const perspective = data?.agent?.perspective; + + if (perspective) { + if (formatter) { + profile = formatter(perspective.links) + } + + } + + return { + status: data?.status, + me: data?.agent, + profile, + error, + mutate, + reload: getData, + }; +} + +function useForceUpdate() { + const [, setState] = useState([]); + return useCallback(() => setState([]), [setState]); +} diff --git a/ad4m-hooks/react/src/usePerspective.tsx b/ad4m-hooks/react/src/usePerspective.tsx new file mode 100644 index 000000000..34dc5d4ef --- /dev/null +++ b/ad4m-hooks/react/src/usePerspective.tsx @@ -0,0 +1,29 @@ +import React, { useState, useEffect } from 'react'; +import { usePerspectives } from './usePerspectives'; +import { Ad4mClient, PerspectiveProxy } from '@coasys/ad4m'; + +export function usePerspective(client: Ad4mClient, uuid: string | Function) { + const [uuidState, setUuidState] = useState(typeof uuid === 'function' ? uuid() : uuid); + + const { perspectives } = usePerspectives(client); + + const [data, setData] = useState<{ perspective: PerspectiveProxy | null, synced: boolean }>({ + perspective: null, + synced: false, + }); + + useEffect(() => { + const pers = perspectives[uuidState]; + setData(prevData => ({ ...prevData, perspective: pers })); + }, [perspectives, uuidState]); + + useEffect(() => { + if (typeof uuid === 'function') { + setUuidState(uuid()); + } else { + setUuidState(uuid); + } + }, [uuid]); + + return { data }; +} \ No newline at end of file diff --git a/ad4m-hooks/react/src/usePerspectives.tsx b/ad4m-hooks/react/src/usePerspectives.tsx new file mode 100644 index 000000000..15e64e6a3 --- /dev/null +++ b/ad4m-hooks/react/src/usePerspectives.tsx @@ -0,0 +1,122 @@ +import { useState, useEffect, useRef } from "react"; +import { Ad4mClient } from "@coasys/ad4m"; + +type UUID = string; + +interface PerspectiveProxy { + uuid: UUID; + sharedUrl: string; + addListener(event: string, callback: Function): void; + removeListener(event: string, callback: Function): void; +} + +export function usePerspectives(client: Ad4mClient) { + const [perspectives, setPerspectives] = useState<{ [x: UUID]: PerspectiveProxy }>({}); + const [neighbourhoods, setNeighbourhoods] = useState<{ [x: UUID]: PerspectiveProxy }>({}); + const onAddedLinkCbs = useRef([]); + const onRemovedLinkCbs = useRef([]); + const hasFetched = useRef(false); + + useEffect(() => { + const fetchPerspectives = async () => { + if (hasFetched.current) return; + hasFetched.current = true; + + const allPerspectives = await client.perspective.all(); + const newPerspectives: { [x: UUID]: PerspectiveProxy } = {}; + + allPerspectives.forEach((p) => { + newPerspectives[p.uuid] = p; + addListeners(p); + }); + + setPerspectives(newPerspectives); + }; + + const addListeners = (p: PerspectiveProxy) => { + p.addListener("link-added", (link: any) => { + onAddedLinkCbs.current.forEach((cb) => { + cb(p, link); + }); + }); + + p.addListener("link-removed", (link: any) => { + onRemovedLinkCbs.current.forEach((cb) => { + cb(p, link); + }); + }); + }; + + const perspectiveUpdatedListener = async (handle: any) => { + const perspective = await client.perspective.byUUID(handle.uuid); + if (perspective) { + setPerspectives((prevPerspectives) => ({ + ...prevPerspectives, + [handle.uuid]: perspective, + })); + } + }; + + const perspectiveAddedListener = async (handle: any) => { + const perspective = await client.perspective.byUUID(handle.uuid); + if (perspective) { + setPerspectives((prevPerspectives) => ({ + ...prevPerspectives, + [handle.uuid]: perspective, + })); + addListeners(perspective); + } + }; + + const perspectiveRemovedListener = (uuid: UUID) => { + setPerspectives((prevPerspectives) => { + const newPerspectives = { ...prevPerspectives }; + delete newPerspectives[uuid]; + return newPerspectives; + }); + }; + + fetchPerspectives(); + + // @ts-ignore + client.perspective.addPerspectiveUpdatedListener(perspectiveUpdatedListener); + // @ts-ignore + client.perspective.addPerspectiveAddedListener(perspectiveAddedListener); + // @ts-ignore + client.perspective.addPerspectiveRemovedListener(perspectiveRemovedListener); + + return () => { + // @ts-ignore + client.perspective.removePerspectiveUpdatedListener(perspectiveUpdatedListener); + // @ts-ignore + client.perspective.removePerspectiveAddedListener(perspectiveAddedListener); + // @ts-ignore + client.perspective.removePerspectiveRemovedListener(perspectiveRemovedListener); + }; + }, []); + + useEffect(() => { + const newNeighbourhoods = Object.keys(perspectives).reduce((acc, key) => { + if (perspectives[key]?.sharedUrl) { + return { + ...acc, + [key]: perspectives[key], + }; + } else { + return acc; + } + }, {}); + + setNeighbourhoods(newNeighbourhoods); + }, [perspectives]); + + function onLinkAdded(cb: Function) { + onAddedLinkCbs.current.push(cb); + } + + function onLinkRemoved(cb: Function) { + onRemovedLinkCbs.current.push(cb); + } + + return { perspectives, neighbourhoods, onLinkAdded, onLinkRemoved }; +} diff --git a/ad4m-hooks/react/src/useSubject.tsx b/ad4m-hooks/react/src/useSubject.tsx new file mode 100644 index 000000000..8be510017 --- /dev/null +++ b/ad4m-hooks/react/src/useSubject.tsx @@ -0,0 +1,105 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import { + getCache, + setCache, + subscribe, + subscribeToPerspective, + unsubscribe, + unsubscribeFromPerspective, +} from "@coasys/hooks-helpers"; +import { PerspectiveProxy, LinkExpression } from "@coasys/ad4m"; +import { SubjectRepository } from "@coasys/hooks-helpers"; + +type Props = { + id: string; + perspective: PerspectiveProxy; + subject: string | (new () => SubjectClass); +}; + +export function useSubject(props: Props) { + const forceUpdate = useForceUpdate(); + const [error, setError] = useState(undefined); + const { perspective, id, subject } = props; + + // Create subject + const Repo = useMemo(() => { + return new SubjectRepository(subject, { + perspective: perspective, + source: null, + }); + }, [perspective.uuid, subject]); + + // Create cache key for entry + // @ts-ignore + const cacheKey = `${perspective.uuid}/${subject.name}/${id}`; + + // Mutate shared/cached data for all subscribers + const mutate = useCallback( + (entry: SubjectClass | null) => setCache(cacheKey, entry), + [cacheKey] + ); + + // Fetch data from AD4M and save to cache + const getData = useCallback(() => { + if (id) { + Repo.getData(id) + .then(async (entry) => { + setError(undefined); + mutate(entry); + }) + .catch((error) => setError(error.toString())); + } + }, [cacheKey]); + + // Trigger initial fetch + useEffect(getData, [getData]); + + async function linkAdded(link: LinkExpression) { + const isUpdated = link.data.source === id; + + if (isUpdated) { + getData(); + } + + return null; + } + + async function linkRemoved(link: LinkExpression) { + if (link.data.source === id) { + getData(); + } + return null; + } + + // Listen to remote changes + useEffect(() => { + if (perspective.uuid) { + subscribeToPerspective(perspective, linkAdded, linkRemoved); + + return () => { + unsubscribeFromPerspective(perspective, linkAdded, linkRemoved); + }; + } + }, [perspective.uuid, id]); + + // Subscribe to changes (re-render on data change) + useEffect(() => { + subscribe(cacheKey, forceUpdate); + return () => unsubscribe(cacheKey, forceUpdate); + }, [cacheKey, forceUpdate]); + + type ExtendedSubjectClass = SubjectClass & { + id: string; + timestamp: number; + author: string; + }; + + const entry = getCache(cacheKey); + + return { entry, error, mutate, repo: Repo, reload: getData }; +} + +function useForceUpdate() { + const [, setState] = useState([]); + return useCallback(() => setState([]), [setState]); +} diff --git a/ad4m-hooks/react/src/useSubjects.tsx b/ad4m-hooks/react/src/useSubjects.tsx new file mode 100644 index 000000000..7e7c924a5 --- /dev/null +++ b/ad4m-hooks/react/src/useSubjects.tsx @@ -0,0 +1,208 @@ +import { useState, useCallback, useEffect, useMemo } from "react"; +import { + getCache, + setCache, + subscribe, + subscribeToPerspective, + unsubscribe, + unsubscribeFromPerspective, +} from "@coasys/hooks-helpers"; +import { PerspectiveProxy, LinkExpression } from "@coasys/ad4m"; +import { QueryOptions, SubjectRepository } from "@coasys/hooks-helpers"; + +type Props = { + source: string; + perspective: PerspectiveProxy; + subject: (new () => SubjectClass) | string; + query?: QueryOptions; +}; + +export function useSubjects(props: Props) { + const forceUpdate = useForceUpdate(); + const [query, setQuery] = useState(props.query); + const [isMore, setIsMore] = useState(false); + const [error, setError] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const { perspective, source, subject } = props; + + // Create cache key for entry + const cacheKey = `${perspective.uuid}/${source || ""}/${ + typeof subject === "string" ? subject : subject.prototype.className + }/${query?.uniqueKey}`; + + // Create model + const Repo = useMemo(() => { + return new SubjectRepository(subject, { + perspective: perspective, + source, + }); + }, [cacheKey]); + + // Mutate shared/cached data for all subscribers + const mutate = useCallback( + (entries: SubjectClass[]) => setCache(cacheKey, entries), + [cacheKey] + ); + + // Fetch data from AD4M and save to cache + const getData = useCallback(() => { + if (source) { + setIsLoading(true); + console.debug(`fetching data from remote`, source, query, cacheKey); + Repo.getAllData(source, query) + .then((newEntries) => { + setError(undefined); + if (query?.infinite) { + setIsMore(newEntries.length >= query.size); + // @ts-ignore + const updated = mergeArrays(entries, newEntries); + mutate(updated); + } else { + mutate(newEntries); + } + }) + .catch((error) => { + setError(error.toString()); + }) + .finally(() => { + setIsLoading(false); + }); + } + }, [cacheKey, query?.page, query?.infinite, query?.size]); + + // Trigger initial fetch + useEffect(getData, [cacheKey, query?.page, query?.infinite, query?.size]); + + // Get single entry + async function fetchEntry(id) { + const entry = (await Repo.getData(id)) as SubjectClass; + const oldEntries = (getCache(cacheKey) as SubjectClass[]) || []; + // @ts-ignore + const isOldEntry = oldEntries?.some((i) => i.id === id); + + const newEntries = isOldEntry + ? oldEntries?.map((oldEntry) => { + // @ts-ignore + const isUpdatedEntry = id === oldEntry.id; + return isUpdatedEntry ? entry : oldEntry; + }) + : [...oldEntries, entry]; + + mutate(newEntries); + } + + async function linkAdded(link: LinkExpression) { + const allEntries = (getCache(cacheKey) || []) as SubjectClass[]; + const isNewEntry = link.data.source === source; + // @ts-ignore + const isUpdated = allEntries?.find((e) => e.id === link.data.source); + + const id = isNewEntry + ? link.data.target + : isUpdated + ? link.data.source + : false; + + if (id) { + const isInstance = await perspective.isSubjectInstance( + id, + typeof subject === "string" ? subject : new subject() + ); + + if (isInstance) { + fetchEntry(id); + } + } + + return null; + } + + async function linkRemoved(link: LinkExpression) { + const allEntries = (getCache(cacheKey) || []) as SubjectClass[]; + + // Check if an association/property was removed + const removedAssociation = allEntries.some( + // @ts-ignore + (e) => e.id === link.data.source + ); + + if (removedAssociation) { + getData(); + } + + // Remove entries if they are removed from source + if (link.data.source === source) { + // @ts-ignore + const newEntries = allEntries?.filter((e) => e.id !== link.data.target); + mutate(newEntries || []); + } + return null; + } + + // Listen to remote changes + useEffect(() => { + if (perspective.uuid) { + subscribeToPerspective(perspective, linkAdded, linkRemoved); + + return () => { + unsubscribeFromPerspective(perspective, linkAdded, linkRemoved); + }; + } + }, [perspective.uuid, cacheKey, query]); + + // Subscribe to changes (re-render on data change) + useEffect(() => { + subscribe(cacheKey, forceUpdate); + return () => unsubscribe(cacheKey, forceUpdate); + }, [cacheKey, forceUpdate, query]); + + type ExtendedSubjectClass = SubjectClass & { + id: string; + timestamp: number; + author: string; + }; + + const entries = (getCache(cacheKey) || []) as ExtendedSubjectClass[]; + + return { + entries: [...entries], + error, + mutate, + setQuery, + repo: Repo, + isLoading, + reload: getData, + isMore, + }; +} + +function useForceUpdate() { + const [, setState] = useState([]); + return useCallback(() => setState([]), [setState]); +} + +interface MyObject { + id: number; + [key: string]: any; +} + +function mergeArrays(arr1: MyObject[], arr2: MyObject[]): MyObject[] { + const map = new Map(); + + // Function to add objects from array to map + function addArrayToMap(arr: MyObject[]) { + for (const obj of arr) { + if (obj && obj.id != null) { + // Ensure object and id property exist + map.set(obj.id, obj); + } + } + } + + // Add objects from both arrays to map + addArrayToMap(arr1); + addArrayToMap(arr2); + + // Convert map values to an array and return it + return Array.from(map.values()); +} diff --git a/ad4m-hooks/vue/package.json b/ad4m-hooks/vue/package.json new file mode 100644 index 000000000..c25a40e71 --- /dev/null +++ b/ad4m-hooks/vue/package.json @@ -0,0 +1,23 @@ +{ + "name": "@coasys/ad4m-vue-hooks", + "version": "0.8.2-prerelease", + "description": "", + "main": "./src/index.ts", + "module": "./src/index.ts", + "type": "module", + "files": [ + "dist", + "src" + ], + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@coasys/ad4m": "*", + "@coasys/hooks-helpers": "*" + }, + "peerDependencies": { + "vue": "^3.2.47" + } +} \ No newline at end of file diff --git a/ad4m-hooks/vue/src/index.ts b/ad4m-hooks/vue/src/index.ts new file mode 100644 index 000000000..ab090a981 --- /dev/null +++ b/ad4m-hooks/vue/src/index.ts @@ -0,0 +1,7 @@ +export * from './useAgent' +export * from './useClient'; +export * from './useMe' +export * from './usePerspective' +export * from './usePerspectives' +export * from './useSubject'; +export * from './useSubjects' \ No newline at end of file diff --git a/ad4m-hooks/vue/src/useAgent.ts b/ad4m-hooks/vue/src/useAgent.ts new file mode 100644 index 000000000..f5db9b4f0 --- /dev/null +++ b/ad4m-hooks/vue/src/useAgent.ts @@ -0,0 +1,32 @@ +import { computed, ref, shallowRef, watch } from "vue"; +import { Agent, LinkExpression } from "@coasys/ad4m"; +import { AgentClient } from "@coasys/ad4m"; + + + +export function useAgent(client: AgentClient, did: string | Function, formatter: (links: LinkExpression[]) => T) { + const agent = shallowRef(null); + const profile = shallowRef(null); + const didRef = typeof did === "function" ? (did as any) : ref(did); + + watch( + [client, didRef], + async ([c, d]) => { + if (d) { + agent.value = await client.byDID(d); + if (agent.value?.perspective) { + const perspective = agent.value.perspective; + + const prof = formatter(perspective.links); + + profile.value = { ...prof, did: d} as T; + } else { + profile.value = null; + } + } + }, + { immediate: true } + ); + + return { agent, profile }; +} diff --git a/ad4m-hooks/vue/src/useClient.ts b/ad4m-hooks/vue/src/useClient.ts new file mode 100644 index 000000000..b4201773c --- /dev/null +++ b/ad4m-hooks/vue/src/useClient.ts @@ -0,0 +1,48 @@ +import { ref, onMounted, watch } from 'vue'; +import { getCache, setCache } from '@coasys/hooks-helpers'; +// @ts-ignore +import { getAd4mClient } from '@coasys/ad4m-connect/utils'; + +export function useClient() { + const error = ref(null); + const client = ref(null); + + // Create cache key for entry + const cacheKey = 'client'; + + // Mutate shared/cached data for all subscribers + const mutate = (client) => { + setCache(cacheKey, client); + }; + + // Fetch data from AD4M and save to cache + const getData = async () => { + console.debug('🪝 useClient - running getAd4mClient'); + try { + const client = await getAd4mClient(); + error.value = null; + mutate(client); + } catch (error) { + error.value = error.toString(); + } + }; + + // Trigger initial fetch + onMounted(getData); + + // Subscribe to changes (re-render on data change) + watch( + () => getCache(cacheKey), + (newClient) => { + client.value = newClient; + }, + { immediate: true } + ); + + return { + client, + error, + mutate, + reload: getData, + }; +} \ No newline at end of file diff --git a/ad4m-hooks/vue/src/useMe.ts b/ad4m-hooks/vue/src/useMe.ts new file mode 100644 index 000000000..b5225d568 --- /dev/null +++ b/ad4m-hooks/vue/src/useMe.ts @@ -0,0 +1,44 @@ +import { computed, effect, ref, shallowRef, watch } from "vue"; +import { Agent, AgentStatus, LinkExpression } from "@coasys/ad4m"; +import { AgentClient } from "@coasys/ad4m"; + +const status = shallowRef({ isInitialized: false, isUnlocked: false }); +const agent = shallowRef(); +const isListening = shallowRef(false); +const profile = shallowRef(null); + +export function useMe(client: AgentClient, formatter: (links: LinkExpression[]) => T) { + effect(async () => { + if (isListening.value) return; + + status.value = await client.status(); + agent.value = await client.me(); + + isListening.value = true; + + client.addAgentStatusChangedListener(async (s: AgentStatus) => { + status.value = s; + }); + + client.addUpdatedListener(async (a: Agent) => { + agent.value = a; + }); + }, {}); + + watch( + () => agent.value, + (newAgent) => { + if (agent.value?.perspective) { + const perspective = newAgent.perspective; + + profile.value = formatter(perspective.links); + } else { + profile.value = null; + } + }, + { immediate: true } + ) + + + return { status, me: agent, profile }; +} diff --git a/ad4m-hooks/vue/src/usePerspective.ts b/ad4m-hooks/vue/src/usePerspective.ts new file mode 100644 index 000000000..5f46919bc --- /dev/null +++ b/ad4m-hooks/vue/src/usePerspective.ts @@ -0,0 +1,37 @@ +import { ref, watch, shallowRef } from "vue"; +import { usePerspectives } from "./usePerspectives"; +import { Ad4mClient, PerspectiveProxy } from "@coasys/ad4m"; + +export function usePerspective(client: Ad4mClient, uuid: string | Function) { + const uuidRef = typeof uuid === "function" ? ref(uuid()) : ref(uuid); + + const { perspectives } = usePerspectives(client); + + const data = shallowRef<{ + perspective: PerspectiveProxy | null; + synced: boolean; + }>({ + perspective: null, + synced: false, + }); + + watch( + [perspectives, uuidRef], + ([perspectives, id]) => { + const pers = perspectives[id]; + data.value = { ...data.value, perspective: pers }; + }, + { immediate: true } + ); + + watch( + // @ts-ignore + uuid, + (id) => { + uuidRef.value = id as string; + }, + { immediate: true } + ); + + return { data }; +} diff --git a/ad4m-hooks/vue/src/usePerspectives.ts b/ad4m-hooks/vue/src/usePerspectives.ts new file mode 100644 index 000000000..8e5813cd7 --- /dev/null +++ b/ad4m-hooks/vue/src/usePerspectives.ts @@ -0,0 +1,115 @@ +import { ref, effect, shallowRef, watch, triggerRef } from "vue"; +import { Ad4mClient, PerspectiveProxy } from "@coasys/ad4m"; + +type UUID = string; + +const perspectives = shallowRef<{ [x: UUID]: PerspectiveProxy }>({}); +const neighbourhoods = shallowRef<{ [x: UUID]: PerspectiveProxy }>({}); +const onAddedLinkCbs = ref([]); +const onRemovedLinkCbs = ref([]); +const hasFetched = ref(false); + +watch( + () => perspectives.value, + (newPers) => { + neighbourhoods.value = Object.keys(newPers).reduce((acc, key) => { + if (newPers[key]?.sharedUrl) { + return { + ...acc, + [key]: newPers[key], + }; + } else { + return acc; + } + }, {}); + }, + { immediate: true } +); + +function addListeners(p: PerspectiveProxy) { + p.addListener("link-added", (link) => { + onAddedLinkCbs.value.forEach((cb) => { + cb(p, link); + }); + return null; + }); + + p.removeListener("link-removed", (link) => { + onAddedLinkCbs.value.forEach((cb) => { + cb(p, link); + }); + return null; + }); +} + +export function usePerspectives(client: Ad4mClient) { + effect(async () => { + if (hasFetched.value) return; + // First component that uses this hook will set this to true, + // so the next components will not fetch and add listeners + hasFetched.value = true; + + // Get all perspectives + const allPerspectives = await client.perspective.all(); + + perspectives.value = allPerspectives.reduce((acc, p) => { + return { ...acc, [p.uuid]: p }; + }, {}); + + // Add each perspective to our state + allPerspectives.forEach((p) => { + addListeners(p); + }); + + // @ts-ignore + client.perspective.addPerspectiveUpdatedListener(async (handle) => { + const perspective = await client.perspective.byUUID(handle.uuid); + + if (perspective) { + perspectives.value = { + ...perspectives.value, + [handle.uuid]: perspective, + }; + } + return null; + }); + + // Add new incoming perspectives + // @ts-ignore + client.perspective.addPerspectiveAddedListener(async (handle) => { + const perspective = await client.perspective.byUUID(handle.uuid); + + if (perspective) { + perspectives.value = { + ...perspectives.value, + [handle.uuid]: perspective, + }; + addListeners(perspective); + } + }); + + // Remove new deleted perspectives + client.perspective.addPerspectiveRemovedListener((uuid) => { + perspectives.value = Object.keys(perspectives.value).reduce( + (acc, key) => { + const p = perspectives.value[key]; + return key === uuid ? acc : { ...acc, [key]: p }; + }, + {} + ); + return null; + }); + }, {}); + + function fetchPerspectives() {} + + function onLinkAdded(cb: Function) { + onAddedLinkCbs.value.push(cb); + } + + function onLinkRemoved(cb: Function) { + onRemovedLinkCbs.value.push(cb); + } + + return { perspectives, neighbourhoods, onLinkAdded, onLinkRemoved }; +} diff --git a/ad4m-hooks/vue/src/useSubject.ts b/ad4m-hooks/vue/src/useSubject.ts new file mode 100644 index 000000000..44eb527dc --- /dev/null +++ b/ad4m-hooks/vue/src/useSubject.ts @@ -0,0 +1,109 @@ +import { watch, ref, shallowRef, triggerRef } from "vue"; +import { SubjectRepository } from "@coasys/hooks-helpers"; +import { PerspectiveProxy, LinkExpression } from "@coasys/ad4m"; + +export function useSubject({ + perspective, + source, + id, + subject, +}: { + perspective: PerspectiveProxy | Function; + id?: string | Function; + source?: string | Function; + subject: SubjectClass; +}) { + const idRef = typeof id === "function" ? (id as any) : ref(id); + const sourceRef = + typeof source === "function" + ? (source as any) + : ref(source || "ad4m://self"); + const perspectiveRef = + typeof perspective === "function" ? (perspective as any) : perspective; + + let entry = ref | null>(null); + let repo = shallowRef | null>(null); + + watch( + [perspectiveRef, sourceRef, idRef], + async ([p, s, id]) => { + if (p?.uuid) { + // @ts-ignore + const r = new SubjectRepository(subject, { + perspective: p, + source: s, + }); + + const res = await r.getData(id); + repo.value = r; + triggerRef(repo); + + subscribe(p, s); + + if (res) { + // @ts-ignore + entry.value = res; + } + } + }, + { immediate: true } + ); + + async function fetchEntry(id: string) { + const res = await repo.value?.getData(id); + + if (!res) return; + + entry.value = res; + } + + async function subscribe(p: PerspectiveProxy, s: string) { + const added = async (link: LinkExpression) => { + const isNewEntry = link.data.source === s; + const isUpdated = entry.value?.id === link.data.source; + + const id = isUpdated + ? link.data.source + : isNewEntry + ? link.data.target + : false; + + if (id) { + // @ts-ignore + const isInstance = await p.isSubjectInstance(id, new subject()); + + if (isInstance) { + fetchEntry(id); + } + } + + return null; + }; + + const removed = async (link: LinkExpression) => { + // TODO: When a channel or something else attached to AD4M get removed + // the community also thinks it's getting remove as it also point to self + const removedEntry = link.data.source === s && s !== "ad4m://self"; + if (removedEntry) { + const isInstance = await p.isSubjectInstance( + link.data.source, + // @ts-ignore + new subject() + ); + if (isInstance) { + entry.value = null; + } + } + return null; + }; + + // @ts-ignore + p.addListener("link-added", added); + // @ts-ignore + p.addListener("link-removed", removed); + + return { added }; + } + + return { entry, repo }; +} diff --git a/ad4m-hooks/vue/src/useSubjects.ts b/ad4m-hooks/vue/src/useSubjects.ts new file mode 100644 index 000000000..bf49f929b --- /dev/null +++ b/ad4m-hooks/vue/src/useSubjects.ts @@ -0,0 +1,104 @@ +import { ref, shallowRef, triggerRef, watch } from "vue"; +import { SubjectRepository } from "@coasys/hooks-helpers"; +import { PerspectiveProxy, LinkExpression } from "@coasys/ad4m"; + +// @ts-ignore +export function useSubjects({ + perspective, + source, + subject, +}: { + perspective: PerspectiveProxy | Function; + source?: string | Function; + subject: SubjectClass; +}) { + const sourceRef = + typeof source === "function" + ? (source as any) + : ref(source || "ad4m://self"); + const perspectiveRef = + typeof perspective === "function" ? (perspective as any) : ref(perspective); + + let entries = ref<{ [x: string]: any }[]>([]); + let repo = shallowRef | null>(null); + + watch( + [perspectiveRef, sourceRef], + ([p, s]) => { + if (p?.uuid) { + // @ts-ignore + const rep = new SubjectRepository(subject, { + perspective: p, + source: s, + }); + + rep.getAllData(s).then((res) => { + entries.value = res; + }); + + repo.value = rep; + triggerRef(repo); + + subscribe(p, s); + } + }, + { immediate: true } + ); + + async function fetchEntry(id: string) { + const entry = await repo.value?.getData(id); + + if (!entry) return; + + const isUpdatedEntry = entries.value.find((e) => e.id === entry.id); + + if (isUpdatedEntry) { + entries.value = entries.value.map((e) => { + const isTheUpdatedOne = e.id === isUpdatedEntry.id; + return isTheUpdatedOne ? entry : e; + }); + } else { + entries.value.push(entry); + } + } + + async function subscribe(p: PerspectiveProxy, s: string) { + const added = async (link: LinkExpression) => { + const isNewEntry = link.data.source === s; + const isUpdated = entries.value.find((e) => e.id === link.data.source); + + const id = isNewEntry + ? link.data.target + : isUpdated + ? link.data.source + : false; + + if (id) { + // @ts-ignore + const isInstance = await p.isSubjectInstance(id, new subject()); + + if (isInstance) { + fetchEntry(id); + } + } + + return null; + }; + + const removed = (link: LinkExpression) => { + const removedEntry = link.data.source === s; + if (removedEntry) { + entries.value = entries.value.filter((e) => e.id !== link.data.target); + } + return null; + }; + + // @ts-ignore + p.addListener("link-added", added); + p.addListener("link-removed", removed); + + return { added }; + } + + return { entries, repo }; +} diff --git a/docs/pages/hooks.mdx b/docs/pages/hooks.mdx new file mode 100644 index 000000000..2191137f5 --- /dev/null +++ b/docs/pages/hooks.mdx @@ -0,0 +1,380 @@ +# Hooks + +The following are a set of React hooks designed to easily work with ADAM principles like Perspectives, Expressions and Subject Classes within React applications: + +## useAgent + +The `useAgent` hook is allows designed to manage state related to fetching and caching data about ADAM agents, i.e. users, based on their DID. + +### Props + +The `useAgent` hook accepts the following props: + +- `client`: An instance of `AgentClient` which represents a client to interact with the AD4M network. +- `did`: A string or a function that returns a string representing the decentralized identifier (DID) of the agent. +- `formatter(links: LinkExpression[])`: A function that takes a links and formatted data structure. + +### Return Values + +The `useAgent` hook returns an object with the following properties: + +- `agent`: The cached `Agent` object fetched from the AD4M network. +- `profile`: The profile data formatted using the provided formatter function. +- `error`: Any error encountered during data fetching. +- `mutate`: A function to mutate the shared/cached data for all subscribers. +- `reload`: A function to trigger a re-fetch of data from the AD4M network. + +### Example Usage + +```javascript +import { useAgent } from '@coasys/ad4m/hooks/react'; + +const MyComponent = () => { + const client = new AgentClient(); + const did = "some-did"; + const formatter = (links) => ({ id: links[0].data.target, name: links[1].data.target }); + + const { agent, profile, error, mutate, reload } = useAgent({ client, did, formatter }); + + useEffect(() => { + console.log("Agent:", agent); + console.log("Profile:", profile); + console.log("Error:", error); + }, [agent, profile, error]); + + return ( +
+ {/* Render your component using the fetched data */} +
+ ); +}; +``` + +## useClient + +The `useClient` hook is a hook provides access to the underlying `Ad4mClient. + +### Props +This hook does not accept any props. + +### Return Value +The `useClient` hook returns an object with the following properties: +- `client`: The cached `Ad4mClient` object fetched from the AD4M network. +- `error`: Any error encountered during data fetching. +- `mutate`: A function to mutate the shared/cached data for all subscribers. +- `reload`: A function to trigger a re-fetch of the AD4M client data. + +### Example Usage +```javascript +import { useEffect } from "react"; +import { useClient } from '@coasys/ad4m/hooks/react'; + +const MyComponent = () => { + const { client, error, reload } = useClient(); + + useEffect(() => { + if (error) { + console.error("Error fetching AD4M client:", error); + } + }, [error]); + + return ( +
+

My AD4M Application

+

Client: {client ? "Connected" : "Disconnected"}

+ +
+ ); +}; + +export default MyComponent; +``` + +## useMe + +The `useMe` hook is a custom React hook designed to manage state related to fetching and caching user data, including agent information and profile data. + +### Props +- `agent`: An instance of `AgentClient` representing the user's agent in the AD4M network. +- `formatter(links: LinkExpression[])`: A function that takes a links and formatted data structure. + +### Return Value +The `useMe` hook returns an object with the following properties: +- `me`: The user's agent object. +- `status`: The status of the user's agent. +- `profile`: The user's profile data formatted using the provided formatter function. +- `error`: Any error encountered during data fetching. +- `mutate`: A function to mutate the shared/cached data for all subscribers. +- `reload`: A function to trigger a re-fetch of the user's data. + +### Example Usage +```javascript +import { useEffect } from "react"; +import { useMe } from from '@coasys/ad4m/hooks/react'; + +const MyComponent = () => { + const { me, status, profile, error, reload } = useMe(agentClient, formatProfile); + + useEffect(() => { + if (error) { + console.error("Error fetching user data:", error); + } + }, [error]); + + return ( +
+

User Profile

+ {me && ( +
+

Name: {me.name}

+

Email: {me.email}

+

Status: {status}

+
+ )} + {profile && ( +
+ {/* Render profile data here */} +
+ )} + +
+ ); +}; + +export default MyComponent; +``` + +## usePerspective + +The `usePerspective` hook is a hook that allows to fetching and caching a specific perspective from the AD4M. + +### Props +- `client`: An instance of `Ad4mClient` representing the client to interact with the AD4M network. +- `uuid`: A string or a function that returns a string representing the UUID of the perspective. + +### Return Value +The `usePerspective` hook returns an object with the following properties: +- `data`: An object containing the fetched perspective and its synchronization status. + +### Example Usage +```javascript +import React, { useState, useEffect } from 'react'; +import { usePerspectives } from './usePerspectives'; +import { Ad4mClient, PerspectiveProxy } from '../../index'; +import { usePerspective } from from '@coasys/ad4m/hooks/react'; + +const MyComponent = () => { + const client = new Ad4mClient(); // Initialize your Ad4m client + const perspectiveUuid = "some-uuid"; // Provide the UUID of the perspective you want to fetch + + const { data } = usePerspective(client, perspectiveUuid); + + useEffect(() => { + if (data.perspective) { + console.log("Fetched perspective:", data.perspective); + console.log("Synced:", data.synced); + } + }, [data]); + + return ( +
+ {/* Render your component using the fetched perspective data */} +
+ ); +}; + +export default MyComponent; +``` + +## usePerspectives + +The `usePerspectives` hook is a hook that allows to fetching and caching all the perspectives from the AD4M. + +### Props +- `client`: An instance of `Ad4mClient` representing the client to interact with the AD4M network. + +### Return Value +The `usePerspectives` hook returns an object with the following properties: +- `perspectives`: An object containing all fetched perspectives, indexed by their UUIDs. +- `neighbourhoods`: An object containing only the fetched perspectives that have a shared URL, indexed by their UUIDs. +- `onLinkAdded`: A function to register a callback to be called when a link is added to any perspective. +- `onLinkRemoved`: A function to register a callback to be called when a link is removed from any perspective. + +### Example Usage +```javascript +import React, { useEffect } from "react"; +import { Ad4mClient } from "../../index"; +import { usePerspectives } from from '@coasys/ad4m/hooks/react'; + +const MyComponent = () => { + const client = new Ad4mClient(); // Initialize your Ad4m client + const { perspectives, neighbourhoods, onLinkAdded, onLinkRemoved } = usePerspectives(client); + + useEffect(() => { + // Example of registering a callback for link added event + const linkAddedCallback = (perspective, link) => { + console.log("Link added to perspective:", perspective.uuid); + console.log("Link details:", link); + }; + onLinkAdded(linkAddedCallback); + + // Example of registering a callback for link removed event + const linkRemovedCallback = (perspective, link) => { + console.log("Link removed from perspective:", perspective.uuid); + console.log("Link details:", link); + }; + onLinkRemoved(linkRemovedCallback); + + return () => { + // Clean up by removing the registered callbacks + onLinkAdded(linkAddedCallback); + onLinkRemoved(linkRemovedCallback); + }; + }, [onLinkAdded, onLinkRemoved]); + + return ( +
+

Perspectives

+

All Perspectives

+
    + {Object.values(perspectives).map((perspective) => ( +
  • {perspective.uuid}
  • + ))} +
+

Neighbourhoods

+
    + {Object.values(neighbourhoods).map((neighbourhood) => ( +
  • {neighbourhood.uuid}
  • + ))} +
+
+ ); +}; + +export default MyComponent; +``` + +## useSubject + +The `useSubject` hook is a hook that allows you to interact with a single subject instance and listen to any changes on that instance. + +### Props +- `id`: A string representing the unique identifier of the subject. +- `perspective`: An instance of `PerspectiveProxy` representing the perspective containing the subject. +- `subject`: A string or a class representing the type of subject. + +### Return Value +The `useSubject` hook returns an object with the following properties: +- `entry`: The fetched subject data. +- `error`: Any error encountered during data fetching. +- `mutate`: A function to mutate the shared/cached data for all subscribers. +- `repo`: An instance of `SubjectRepository` for interacting with the subject data. +- `reload`: A function to trigger a re-fetch of the subject data. + +### Example Usage +```javascript +import { useState, useEffect } from "react"; +import { PerspectiveProxy, LinkExpression } from "../../index"; +import { useSubject } from from '@coasys/ad4m/hooks/react'; // SDNA class + +const MyComponent = () => { + const perspective = new PerspectiveProxy(); // Initialize your perspective + const subjectId = "some-unique-id"; // Provide the ID of the subject you want to fetch + const subjectType = "SomeSubject"; // Provide the type of the subject + const { entry, error, reload } = useSubject({ + id: subjectId, + perspective: perspective, + subject: subjectType, // SDNA Class + }); + + useEffect(() => { + if (error) { + console.error("Error fetching subject data:", error); + } + }, [error]); + + return ( +
+

Subject Data

+ {entry && ( +
+

ID: {entry.id}

+

Timestamp: {entry.timestamp}

+

Author: {entry.author}

+ {/* Render additional subject data here */} +
+ )} + +
+ ); +}; + +export default MyComponent; +``` + +## useSubjects + +The `useSubjects` hook that allows to listen to all the subject instances of a subject class. + +### Props +- `source`: A string representing the source of the subjects. +- `perspective`: An instance of `PerspectiveProxy` representing the perspective containing the subjects. +- `subject`: A class or a string representing the type of the subjects. +- `query` (optional): An object representing query options for fetching subjects. + +### Return Value +The `useSubjects` hook returns an object with the following properties: +- `entries`: An array of fetched subject data. +- `error`: Any error encountered during data fetching. +- `mutate`: A function to mutate the shared/cached data for all subscribers. +- `setQuery`: A function to update the query options for fetching subjects. +- `repo`: An instance of `SubjectRepository` for interacting with the subject data. +- `isLoading`: A boolean indicating whether data is currently being fetched. +- `reload`: A function to trigger a re-fetch of the subject data. +- `isMore`: A boolean indicating whether there are more subjects available to fetch based on query options. + +### Example Usage +```javascript +import { useState, useEffect } from "react"; +import { PerspectiveProxy, LinkExpression } from "../../index"; +import { useSubjects } from from '@coasys/ad4m/hooks/react'; + +const MyComponent = () => { + const perspective = new PerspectiveProxy(); // Initialize your perspective + const source = "some-source"; // Provide the source of the subjects + + const { entries, error, isLoading, reload, isMore, setQuery } = useSubjects({ + source: source, + perspective: perspective, + subject: subjectType, // SDNA Class + query: { page: 1, size: 10, infinite: false, uniqueKey: "uniqueKey" } + }); + + useEffect(() => { + if (error) { + console.error("Error fetching subjects data:", error); + } + }, [error]); + + return ( +
+

Subjects Data

+ {isLoading ? ( +

Loading...

+ ) : ( +
    + {entries.map(entry => ( +
  • + {/* Render subject data here */} +
  • + ))} +
+ )} + + {isMore && } +
+ ); +}; + +export default MyComponent; +``` diff --git a/docs/pages/languages.mdx b/docs/pages/languages.mdx index b238b07ba..43847cf9d 100644 --- a/docs/pages/languages.mdx +++ b/docs/pages/languages.mdx @@ -3,6 +3,39 @@ Languages are essentially Node.js modules that encapsulate how to retrieve and create content. You can think of them as **small edge functions** that are executed on the Agents device and that can communicate with different backends and technologies. +## Why Deno Compatibility? + +Deno compatibility is required because Languages get executed inside a sandbox that the ADAM executor spawns, and since it needs access to internals of ADAM, this needs to be an integrated JavaScript interpreter that is intertwined with the ADAM implementation. We've decided to build this Language engine on Deno of its several advantages: + +- **Security**: Deno is secure by default. No file, network, or environment access (unless explicitly enabled). +- **TypeScript Support**: Deno supports TypeScript out of the box. +- **Standard Modules**: Deno provides a set of reviewed (audited) standard modules that are guaranteed to work with Deno. + +## How to Make Your Language Deno Compatible? + +To make your Language Deno compatible, you need to follow these steps: + +1. **Use ES Modules**: Deno uses ES Modules (ESM) instead of CommonJS, which is used by Node.js. So, you need to use `import` and `export` instead of `require()` and `module.exports`. + +2. **Use Node Specifiers**: Unlike Node.js, Deno requires the full file name including its extension when importing modules. For example, use `import { serve } from "./server.ts";` instead of `import { serve } from "./server";`. + +3. **No `node_modules`**: Deno doesn't use the `node_modules` directory or `package.json`. Instead, it imports modules from URLs or file paths. + +4. **Use Built-in Functions and Standard Modules**: Deno has several built-in functions and does not rely on a `package.json`. So, you need to use Deno's built-in functions and standard modules instead of npm packages. You can find more about Deno's standard modules [here](https://deno.land/std). + +Remember, making your Language Deno compatible means it can run in more environments and take advantage of the benefits that Deno provides. + +# Language Templates + +To help you get started with creating your own languages, we have provided some templates that you can use as a starting point. These templates are Deno compatible and provide a basic structure for your language. + +- [Expression Language without DNA template](https://github.com/coasys/ad4m-language-template-js) +- [Expression Language with DNA template](https://github.com/coasys/ad4m-expression-language-with-dna) +- [Link Language without DNA template](https://github.com/coasys/ad4m-link-template-js) +- [Link Language with DNA template](https://github.com/coasys/ad4m-link-template-language-dna) + +You can clone these repositories and modify them to create your own language. Remember to follow the guidelines for making your language Deno compatible. + ## Creating a Language There are several types of Languages, but let's start with the most common one – an Expression Language. diff --git a/executor/esbuild.ts b/executor/esbuild.ts index 7848ed982..364a81bd0 100644 --- a/executor/esbuild.ts +++ b/executor/esbuild.ts @@ -7,7 +7,6 @@ function denoAlias(nodeModule) { name: `${nodeModule}-alias`, setup(build) { build.onResolve({ filter: new RegExp(`^node:${nodeModule}$`) }, (args) => { - console.log('meow 1111', args) return { path: nodeModule, namespace: 'imports' }; }); }, diff --git a/package.json b/package.json index 8edd7e20c..51c6dcaf5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "tests/js", "rust-executor", "cli", - "dapp" + "dapp", + "ad4m-hooks/react", + "ad4m-hooks/vue", + "ad4m-hooks/helpers" ], "private": true, "scripts": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf36aa159..fd3c4d48b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,54 @@ importers: specifier: latest version: 1.11.3 + ad4m-hooks/helpers: + dependencies: + '@coasys/ad4m': + specifier: '*' + version: link:../../core + '@coasys/ad4m-connect': + specifier: '*' + version: link:../../connect + uuid: + specifier: '*' + version: 9.0.1 + + ad4m-hooks/react: + dependencies: + '@coasys/ad4m': + specifier: '*' + version: link:../../core + '@coasys/ad4m-connect': + specifier: '*' + version: link:../../connect + '@coasys/hooks-helpers': + specifier: link:../helpers + version: link:../helpers + '@types/react': + specifier: ^18.2.55 + version: 18.2.55 + '@types/react-dom': + specifier: ^18.2.19 + version: 18.2.19 + preact: + specifier: '*' + version: 10.19.3 + react: + specifier: '*' + version: 18.2.0 + + ad4m-hooks/vue: + dependencies: + '@coasys/ad4m': + specifier: '*' + version: link:../../core + '@coasys/hooks-helpers': + specifier: '*' + version: link:../helpers + vue: + specifier: ^3.2.47 + version: 3.4.19(typescript@4.9.5) + bootstrap-languages/agent-language: dependencies: email-validator: @@ -6568,6 +6616,12 @@ packages: dependencies: '@types/react': 18.2.48 + /@types/react-dom@18.2.19: + resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==} + dependencies: + '@types/react': 18.2.55 + dev: false + /@types/react@17.0.75: resolution: {integrity: sha512-MSA+NzEzXnQKrqpO63CYqNstFjsESgvJAdAyyJ1n6ZQq/GLgf6nOfIKwk+Twuz0L1N6xPe+qz5xRCJrbhMaLsw==} dependencies: @@ -6583,6 +6637,14 @@ packages: '@types/scheduler': 0.16.8 csstype: 3.1.3 + /@types/react@18.2.55: + resolution: {integrity: sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==} + dependencies: + '@types/prop-types': 15.7.11 + '@types/scheduler': 0.16.8 + csstype: 3.1.3 + dev: false + /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: @@ -6899,6 +6961,79 @@ packages: - supports-color dev: true + /@vue/compiler-core@3.4.19: + resolution: {integrity: sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==} + dependencies: + '@babel/parser': 7.23.9 + '@vue/shared': 3.4.19 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.0.2 + dev: false + + /@vue/compiler-dom@3.4.19: + resolution: {integrity: sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==} + dependencies: + '@vue/compiler-core': 3.4.19 + '@vue/shared': 3.4.19 + dev: false + + /@vue/compiler-sfc@3.4.19: + resolution: {integrity: sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==} + dependencies: + '@babel/parser': 7.23.9 + '@vue/compiler-core': 3.4.19 + '@vue/compiler-dom': 3.4.19 + '@vue/compiler-ssr': 3.4.19 + '@vue/shared': 3.4.19 + estree-walker: 2.0.2 + magic-string: 0.30.7 + postcss: 8.4.33 + source-map-js: 1.0.2 + dev: false + + /@vue/compiler-ssr@3.4.19: + resolution: {integrity: sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==} + dependencies: + '@vue/compiler-dom': 3.4.19 + '@vue/shared': 3.4.19 + dev: false + + /@vue/reactivity@3.4.19: + resolution: {integrity: sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==} + dependencies: + '@vue/shared': 3.4.19 + dev: false + + /@vue/runtime-core@3.4.19: + resolution: {integrity: sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==} + dependencies: + '@vue/reactivity': 3.4.19 + '@vue/shared': 3.4.19 + dev: false + + /@vue/runtime-dom@3.4.19: + resolution: {integrity: sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==} + dependencies: + '@vue/runtime-core': 3.4.19 + '@vue/shared': 3.4.19 + csstype: 3.1.3 + dev: false + + /@vue/server-renderer@3.4.19(vue@3.4.19): + resolution: {integrity: sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==} + peerDependencies: + vue: 3.4.19 + dependencies: + '@vue/compiler-ssr': 3.4.19 + '@vue/shared': 3.4.19 + vue: 3.4.19(typescript@4.9.5) + dev: false + + /@vue/shared@3.4.19: + resolution: {integrity: sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==} + dev: false + /@wagmi/connectors@3.1.11(@types/react@18.2.48)(react@18.2.0)(typescript@5.3.3)(viem@1.21.4): resolution: {integrity: sha512-wzxp9f9PtSUFjDUP/QDjc1t7HON4D8wrVKsw35ejdO8hToDpx1gU9lwH/47Zo/1zExGezQc392sjoHSszYd7OA==} peerDependencies: @@ -17049,6 +17184,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: false + /magic-string@0.30.7: + resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -25057,6 +25199,22 @@ packages: /vscode-textmate@8.0.0: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + /vue@3.4.19(typescript@4.9.5): + resolution: {integrity: sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/compiler-dom': 3.4.19 + '@vue/compiler-sfc': 3.4.19 + '@vue/runtime-dom': 3.4.19 + '@vue/server-renderer': 3.4.19(vue@3.4.19) + '@vue/shared': 3.4.19 + typescript: 4.9.5 + dev: false + /w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} deprecated: Use your platform's native performance.now() and performance.timeOrigin. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4b4f64813..569b0c713 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,6 +10,9 @@ packages: - 'rust-executor' - 'cli' - 'dapp' + - 'ad4m-hooks/react' + - 'ad4m-hooks/vue' + - 'ad4m-hooks/helpers' # exclude packages that are inside test directories - '!**/test/**' hoist: false