From b3f72d06df76229a79c3491faa39ed5e3a2c1e03 Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Fri, 16 Feb 2024 20:17:38 +0530 Subject: [PATCH 01/10] langauge docs updated --- docs/pages/languages.mdx | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/pages/languages.mdx b/docs/pages/languages.mdx index b238b07ba..980843606 100644 --- a/docs/pages/languages.mdx +++ b/docs/pages/languages.mdx @@ -3,6 +3,38 @@ 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 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. + +- [JavaScript Template](https://github.com/your-org/js-template) +- [TypeScript Template](https://github.com/your-org/ts-template) +- [Python Template](https://github.com/your-org/py-template) + +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. From f2e0caf68d7512641b936eb125d03baf813b68e1 Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Fri, 16 Feb 2024 20:18:29 +0530 Subject: [PATCH 02/10] vue hooks added --- ad4m-hooks/.gitignore | 25 ++ ad4m-hooks/helpers/package.json | 24 ++ ad4m-hooks/helpers/src/cache.ts | 70 +++++ .../helpers/src/factory/SubjectRepository.ts | 257 ++++++++++++++++++ ad4m-hooks/helpers/src/factory/index.ts | 1 + ad4m-hooks/helpers/src/factory/model.ts | 57 ++++ ad4m-hooks/helpers/src/getProfile.ts | 26 ++ ad4m-hooks/helpers/src/index.ts | 3 + ad4m-hooks/vue/package.json | 25 ++ ad4m-hooks/vue/src/index.ts | 7 + ad4m-hooks/vue/src/useAgent.ts | 42 +++ ad4m-hooks/vue/src/useClient.ts | 48 ++++ ad4m-hooks/vue/src/useMe.ts | 44 +++ ad4m-hooks/vue/src/usePerspective.ts | 37 +++ ad4m-hooks/vue/src/usePerspectives.ts | 115 ++++++++ ad4m-hooks/vue/src/useSubject.ts | 109 ++++++++ ad4m-hooks/vue/src/useSubjects.ts | 104 +++++++ 17 files changed, 994 insertions(+) create mode 100644 ad4m-hooks/.gitignore create mode 100644 ad4m-hooks/helpers/package.json create mode 100644 ad4m-hooks/helpers/src/cache.ts create mode 100644 ad4m-hooks/helpers/src/factory/SubjectRepository.ts create mode 100644 ad4m-hooks/helpers/src/factory/index.ts create mode 100644 ad4m-hooks/helpers/src/factory/model.ts create mode 100644 ad4m-hooks/helpers/src/getProfile.ts create mode 100644 ad4m-hooks/helpers/src/index.ts create mode 100644 ad4m-hooks/vue/package.json create mode 100644 ad4m-hooks/vue/src/index.ts create mode 100644 ad4m-hooks/vue/src/useAgent.ts create mode 100644 ad4m-hooks/vue/src/useClient.ts create mode 100644 ad4m-hooks/vue/src/useMe.ts create mode 100644 ad4m-hooks/vue/src/usePerspective.ts create mode 100644 ad4m-hooks/vue/src/usePerspectives.ts create mode 100644 ad4m-hooks/vue/src/useSubject.ts create mode 100644 ad4m-hooks/vue/src/useSubjects.ts 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..35fc83c7f --- /dev/null +++ b/ad4m-hooks/helpers/package.json @@ -0,0 +1,24 @@ +{ + "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": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "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..07c734c16 --- /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 unsubscribeToPerspective( + 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/vue/package.json b/ad4m-hooks/vue/package.json new file mode 100644 index 000000000..4d4f760a5 --- /dev/null +++ b/ad4m-hooks/vue/package.json @@ -0,0 +1,25 @@ +{ + "name": "@coasys/vue-hooks", + "version": "0.8.2-prerelease", + "description": "", + "main": "./src/index.ts", + "module": "./src/index.ts", + "type": "module", + "files": [ + "dist", + "src" + ], + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "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..01cbc51dd --- /dev/null +++ b/ad4m-hooks/vue/src/useAgent.ts @@ -0,0 +1,42 @@ +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]) => { + console.log('meow', c, d) + if (d) { + console.log('meow', d) + agent.value = await client.byDID(d); + console.log('meow 0', agent) + if (agent.value?.perspective) { + console.log('meow 1') + const perspective = agent.value.perspective; + + console.log("perspective", perspective); + + const prof = formatter(perspective.links); + + console.log("prof", prof, { ...prof, did: d}); + + + profile.value = { ...prof, did: d} as T; + } else { + console.log('meow 2') + 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..582964c16 --- /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.log('🪝 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 }; +} From ab5bdf601da867d4b10f6fd5109da17eaa448b14 Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Fri, 16 Feb 2024 20:18:41 +0530 Subject: [PATCH 03/10] hooks docs added --- docs/pages/hooks.mdx | 380 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 docs/pages/hooks.mdx diff --git a/docs/pages/hooks.mdx b/docs/pages/hooks.mdx new file mode 100644 index 000000000..1efbc4894 --- /dev/null +++ b/docs/pages/hooks.mdx @@ -0,0 +1,380 @@ +# Hooks + +The following are a set of React hooks designed to manage state related to interacting with the AD4M (Agent-centric Decentralized Data Management) network within React applications: + +## useAgent + +The `useAgent` hook is a custom React hook designed to manage state related to fetching and caching data from the AD4M (Agent-centric Decentralized Data Management) network. + +### 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`: A function that takes a DID string and returns a 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 = (did) => ({ id: did, name: "John Doe" }); + + 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 custom React hook designed to manage state related to fetching and caching an AD4M (Agent-centric Decentralized Data Management) client in React applications. It provides an easy-to-use interface for interacting with the AD4M client, abstracting away the complexities of data fetching, caching, and error handling. + +### 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, from the AD4M (Agent-centric Decentralized Data Management) network in React applications. It provides an easy-to-use interface for interacting with the user's agent and profile data, abstracting away the complexities of data fetching, caching, and error handling. + +### Props +- `agent`: An instance of `AgentClient` representing the user's agent in the AD4M network. +- `formatter`: A function that takes an array of `LinkExpression` objects and returns a 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 custom React hook designed to manage state related to fetching and caching a specific perspective from the AD4M (Agent-centric Decentralized Data Management) network in React applications. It provides an easy-to-use interface for interacting with the perspective data, abstracting away the complexities of data fetching, caching, and error handling. + +### 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 custom React hook designed to manage state related to fetching and caching perspectives from the AD4M (Agent-centric Decentralized Data Management) network in React applications. It provides an easy-to-use interface for interacting with perspectives and their associated events, abstracting away the complexities of data fetching, caching, and event handling. + +### 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 custom React hook designed to manage state related to fetching, caching, and subscribing to a specific subject within a perspective on the AD4M (Agent-centric Decentralized Data Management) network. It provides an easy-to-use interface for interacting with subject data, abstracting away the complexities of data fetching, caching, and event handling. + +### 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 is a custom React hook designed to manage state related to fetching, caching, and subscribing to multiple subjects within a perspective on the AD4M (Agent-centric Decentralized Data Management) network. It provides an easy-to-use interface for interacting with subject data, abstracting away the complexities of data fetching, caching, and event handling. + +### 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; +``` From ccafba6660195810158d76ad998c5eafba2aeac6 Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Fri, 16 Feb 2024 20:19:16 +0530 Subject: [PATCH 04/10] react hooks added --- ad4m-hooks/react/package.json | 30 ++++ ad4m-hooks/react/src/index.ts | 19 +++ ad4m-hooks/react/src/register.js | 177 +++++++++++++++++++ ad4m-hooks/react/src/useAgent.tsx | 61 +++++++ ad4m-hooks/react/src/useClient.tsx | 53 ++++++ ad4m-hooks/react/src/useEntries.tsx | 104 ++++++++++++ ad4m-hooks/react/src/useMe.tsx | 104 ++++++++++++ ad4m-hooks/react/src/usePerspective.tsx | 29 ++++ ad4m-hooks/react/src/usePerspectives.tsx | 122 +++++++++++++ ad4m-hooks/react/src/useSubject.tsx | 105 ++++++++++++ ad4m-hooks/react/src/useSubjects.tsx | 208 +++++++++++++++++++++++ package.json | 5 +- pnpm-lock.yaml | 158 +++++++++++++++++ pnpm-workspace.yaml | 3 + 14 files changed, 1177 insertions(+), 1 deletion(-) create mode 100644 ad4m-hooks/react/package.json create mode 100644 ad4m-hooks/react/src/index.ts create mode 100644 ad4m-hooks/react/src/register.js create mode 100644 ad4m-hooks/react/src/useAgent.tsx create mode 100644 ad4m-hooks/react/src/useClient.tsx create mode 100644 ad4m-hooks/react/src/useEntries.tsx create mode 100644 ad4m-hooks/react/src/useMe.tsx create mode 100644 ad4m-hooks/react/src/usePerspective.tsx create mode 100644 ad4m-hooks/react/src/usePerspectives.tsx create mode 100644 ad4m-hooks/react/src/useSubject.tsx create mode 100644 ad4m-hooks/react/src/useSubjects.tsx diff --git a/ad4m-hooks/react/package.json b/ad4m-hooks/react/package.json new file mode 100644 index 000000000..f28c0f5e0 --- /dev/null +++ b/ad4m-hooks/react/package.json @@ -0,0 +1,30 @@ +{ + "name": "@coasys/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": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "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..de7e53269 --- /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, + unsubscribeToPerspective, +} 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 () => { + unsubscribeToPerspective(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..73b17dc68 --- /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, + unsubscribeToPerspective, +} 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.log(`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 () => { + unsubscribeToPerspective(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/package.json b/package.json index 4044c9837..9122cba37 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 From f6ec6fcdbc15000549df33ba8381d1acc20decbc Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Mon, 19 Feb 2024 12:05:33 +0530 Subject: [PATCH 05/10] Template links updated --- docs/pages/languages.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/pages/languages.mdx b/docs/pages/languages.mdx index 980843606..c98c86809 100644 --- a/docs/pages/languages.mdx +++ b/docs/pages/languages.mdx @@ -29,9 +29,10 @@ Remember, making your Language Deno compatible means it can run in more environm 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. -- [JavaScript Template](https://github.com/your-org/js-template) -- [TypeScript Template](https://github.com/your-org/ts-template) -- [Python Template](https://github.com/your-org/py-template) +- [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. From 8255fb5ead7c37c75766f4bde91d2a55a8bd7377 Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Mon, 19 Feb 2024 19:31:00 +0530 Subject: [PATCH 06/10] Updated ci to publish new packages --- .github/workflows/publish.yml | 24 ++++++++++++++++++++++++ .github/workflows/publish_staging.yml | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) 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 From 3c5ded1a8cca8c85ce497f42386c48758adb2886 Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Tue, 20 Feb 2024 09:56:35 +0530 Subject: [PATCH 07/10] Remove test script from package.json --- ad4m-hooks/helpers/package.json | 4 +--- ad4m-hooks/react/package.json | 4 +--- ad4m-hooks/vue/package.json | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/ad4m-hooks/helpers/package.json b/ad4m-hooks/helpers/package.json index 35fc83c7f..9c80e0e0b 100644 --- a/ad4m-hooks/helpers/package.json +++ b/ad4m-hooks/helpers/package.json @@ -10,9 +10,7 @@ "dist", "src" ], - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, + "scripts": {}, "keywords": [], "author": "", "license": "ISC", diff --git a/ad4m-hooks/react/package.json b/ad4m-hooks/react/package.json index f28c0f5e0..e9f28d535 100644 --- a/ad4m-hooks/react/package.json +++ b/ad4m-hooks/react/package.json @@ -10,9 +10,7 @@ "dist", "src" ], - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, + "scripts": {}, "keywords": [], "author": "", "license": "ISC", diff --git a/ad4m-hooks/vue/package.json b/ad4m-hooks/vue/package.json index 4d4f760a5..fa9393b43 100644 --- a/ad4m-hooks/vue/package.json +++ b/ad4m-hooks/vue/package.json @@ -9,9 +9,7 @@ "dist", "src" ], - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, + "scripts": {}, "keywords": [], "author": "", "license": "ISC", From 4270945ca7199e33f5d391acd12fe23c2e22069c Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Tue, 20 Feb 2024 14:57:41 +0530 Subject: [PATCH 08/10] Resolve comments --- ad4m-hooks/helpers/src/cache.ts | 2 +- ad4m-hooks/react/package.json | 2 +- ad4m-hooks/react/src/useSubject.tsx | 4 ++-- ad4m-hooks/react/src/useSubjects.tsx | 6 +++--- ad4m-hooks/vue/package.json | 2 +- ad4m-hooks/vue/src/useAgent.ts | 10 ---------- ad4m-hooks/vue/src/useClient.ts | 2 +- executor/esbuild.ts | 1 - 8 files changed, 9 insertions(+), 20 deletions(-) diff --git a/ad4m-hooks/helpers/src/cache.ts b/ad4m-hooks/helpers/src/cache.ts index 07c734c16..241cfa136 100644 --- a/ad4m-hooks/helpers/src/cache.ts +++ b/ad4m-hooks/helpers/src/cache.ts @@ -57,7 +57,7 @@ export function subscribeToPerspective( subscribe(removedKey, removed); } -export function unsubscribeToPerspective( +export function unsubscribeFromPerspective( perspective: PerspectiveProxy, added: Function, removed: Function diff --git a/ad4m-hooks/react/package.json b/ad4m-hooks/react/package.json index e9f28d535..ad83baa18 100644 --- a/ad4m-hooks/react/package.json +++ b/ad4m-hooks/react/package.json @@ -1,5 +1,5 @@ { - "name": "@coasys/react-hooks", + "name": "@coasys/ad4m-react-hooks", "version": "0.8.2-prerelease.1", "description": "", "main": "./src/index.ts", diff --git a/ad4m-hooks/react/src/useSubject.tsx b/ad4m-hooks/react/src/useSubject.tsx index de7e53269..8be510017 100644 --- a/ad4m-hooks/react/src/useSubject.tsx +++ b/ad4m-hooks/react/src/useSubject.tsx @@ -5,7 +5,7 @@ import { subscribe, subscribeToPerspective, unsubscribe, - unsubscribeToPerspective, + unsubscribeFromPerspective, } from "@coasys/hooks-helpers"; import { PerspectiveProxy, LinkExpression } from "@coasys/ad4m"; import { SubjectRepository } from "@coasys/hooks-helpers"; @@ -77,7 +77,7 @@ export function useSubject(props: Props) { subscribeToPerspective(perspective, linkAdded, linkRemoved); return () => { - unsubscribeToPerspective(perspective, linkAdded, linkRemoved); + unsubscribeFromPerspective(perspective, linkAdded, linkRemoved); }; } }, [perspective.uuid, id]); diff --git a/ad4m-hooks/react/src/useSubjects.tsx b/ad4m-hooks/react/src/useSubjects.tsx index 73b17dc68..7e7c924a5 100644 --- a/ad4m-hooks/react/src/useSubjects.tsx +++ b/ad4m-hooks/react/src/useSubjects.tsx @@ -5,7 +5,7 @@ import { subscribe, subscribeToPerspective, unsubscribe, - unsubscribeToPerspective, + unsubscribeFromPerspective, } from "@coasys/hooks-helpers"; import { PerspectiveProxy, LinkExpression } from "@coasys/ad4m"; import { QueryOptions, SubjectRepository } from "@coasys/hooks-helpers"; @@ -48,7 +48,7 @@ export function useSubjects(props: Props) { const getData = useCallback(() => { if (source) { setIsLoading(true); - console.log(`fetching data from remote`, source, query, cacheKey); + console.debug(`fetching data from remote`, source, query, cacheKey); Repo.getAllData(source, query) .then((newEntries) => { setError(undefined); @@ -145,7 +145,7 @@ export function useSubjects(props: Props) { subscribeToPerspective(perspective, linkAdded, linkRemoved); return () => { - unsubscribeToPerspective(perspective, linkAdded, linkRemoved); + unsubscribeFromPerspective(perspective, linkAdded, linkRemoved); }; } }, [perspective.uuid, cacheKey, query]); diff --git a/ad4m-hooks/vue/package.json b/ad4m-hooks/vue/package.json index fa9393b43..c25a40e71 100644 --- a/ad4m-hooks/vue/package.json +++ b/ad4m-hooks/vue/package.json @@ -1,5 +1,5 @@ { - "name": "@coasys/vue-hooks", + "name": "@coasys/ad4m-vue-hooks", "version": "0.8.2-prerelease", "description": "", "main": "./src/index.ts", diff --git a/ad4m-hooks/vue/src/useAgent.ts b/ad4m-hooks/vue/src/useAgent.ts index 01cbc51dd..f5db9b4f0 100644 --- a/ad4m-hooks/vue/src/useAgent.ts +++ b/ad4m-hooks/vue/src/useAgent.ts @@ -12,25 +12,15 @@ export function useAgent(client: AgentClient, did: string | Function, formatt watch( [client, didRef], async ([c, d]) => { - console.log('meow', c, d) if (d) { - console.log('meow', d) agent.value = await client.byDID(d); - console.log('meow 0', agent) if (agent.value?.perspective) { - console.log('meow 1') const perspective = agent.value.perspective; - console.log("perspective", perspective); - const prof = formatter(perspective.links); - console.log("prof", prof, { ...prof, did: d}); - - profile.value = { ...prof, did: d} as T; } else { - console.log('meow 2') profile.value = null; } } diff --git a/ad4m-hooks/vue/src/useClient.ts b/ad4m-hooks/vue/src/useClient.ts index 582964c16..b4201773c 100644 --- a/ad4m-hooks/vue/src/useClient.ts +++ b/ad4m-hooks/vue/src/useClient.ts @@ -17,7 +17,7 @@ export function useClient() { // Fetch data from AD4M and save to cache const getData = async () => { - console.log('🪝 useClient - running getAd4mClient'); + console.debug('🪝 useClient - running getAd4mClient'); try { const client = await getAd4mClient(); error.value = null; 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' }; }); }, From 7a9622834f41d83e1936e71094b43af2db3af9c3 Mon Sep 17 00:00:00 2001 From: Fayeed Pawaskar Date: Tue, 20 Feb 2024 15:29:38 +0530 Subject: [PATCH 09/10] Update ADAM terminology in React hooks --- docs/pages/hooks.mdx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/pages/hooks.mdx b/docs/pages/hooks.mdx index 1efbc4894..2191137f5 100644 --- a/docs/pages/hooks.mdx +++ b/docs/pages/hooks.mdx @@ -1,10 +1,10 @@ # Hooks -The following are a set of React hooks designed to manage state related to interacting with the AD4M (Agent-centric Decentralized Data Management) network within React applications: +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 a custom React hook designed to manage state related to fetching and caching data from the AD4M (Agent-centric Decentralized Data Management) network. +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 @@ -12,7 +12,7 @@ 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`: A function that takes a DID string and returns a formatted data structure. +- `formatter(links: LinkExpression[])`: A function that takes a links and formatted data structure. ### Return Values @@ -32,7 +32,7 @@ import { useAgent } from '@coasys/ad4m/hooks/react'; const MyComponent = () => { const client = new AgentClient(); const did = "some-did"; - const formatter = (did) => ({ id: did, name: "John Doe" }); + const formatter = (links) => ({ id: links[0].data.target, name: links[1].data.target }); const { agent, profile, error, mutate, reload } = useAgent({ client, did, formatter }); @@ -52,7 +52,7 @@ const MyComponent = () => { ## useClient -The `useClient` hook is a custom React hook designed to manage state related to fetching and caching an AD4M (Agent-centric Decentralized Data Management) client in React applications. It provides an easy-to-use interface for interacting with the AD4M client, abstracting away the complexities of data fetching, caching, and error handling. +The `useClient` hook is a hook provides access to the underlying `Ad4mClient. ### Props This hook does not accept any props. @@ -92,11 +92,11 @@ 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, from the AD4M (Agent-centric Decentralized Data Management) network in React applications. It provides an easy-to-use interface for interacting with the user's agent and profile data, abstracting away the complexities of data fetching, caching, and error handling. +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`: A function that takes an array of `LinkExpression` objects and returns a formatted data structure. +- `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: @@ -146,7 +146,7 @@ export default MyComponent; ## usePerspective -The `usePerspective` hook is a custom React hook designed to manage state related to fetching and caching a specific perspective from the AD4M (Agent-centric Decentralized Data Management) network in React applications. It provides an easy-to-use interface for interacting with the perspective data, abstracting away the complexities of data fetching, caching, and error handling. +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. @@ -188,7 +188,7 @@ export default MyComponent; ## usePerspectives -The `usePerspectives` hook is a custom React hook designed to manage state related to fetching and caching perspectives from the AD4M (Agent-centric Decentralized Data Management) network in React applications. It provides an easy-to-use interface for interacting with perspectives and their associated events, abstracting away the complexities of data fetching, caching, and event handling. +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. @@ -256,7 +256,7 @@ export default MyComponent; ## useSubject -The `useSubject` hook is a custom React hook designed to manage state related to fetching, caching, and subscribing to a specific subject within a perspective on the AD4M (Agent-centric Decentralized Data Management) network. It provides an easy-to-use interface for interacting with subject data, abstracting away the complexities of data fetching, caching, and event handling. +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. @@ -314,7 +314,7 @@ export default MyComponent; ## useSubjects -The `useSubjects` hook is a custom React hook designed to manage state related to fetching, caching, and subscribing to multiple subjects within a perspective on the AD4M (Agent-centric Decentralized Data Management) network. It provides an easy-to-use interface for interacting with subject data, abstracting away the complexities of data fetching, caching, and event handling. +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. From ade3a2ed1a65f1ce80a2dda8aa060324a75eacee Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Tue, 20 Feb 2024 11:41:55 +0100 Subject: [PATCH 10/10] Update docs/pages/languages.mdx --- docs/pages/languages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/languages.mdx b/docs/pages/languages.mdx index c98c86809..43847cf9d 100644 --- a/docs/pages/languages.mdx +++ b/docs/pages/languages.mdx @@ -5,7 +5,7 @@ and create content. You can think of them as **small edge functions** that are e ## Why Deno Compatibility? -Deno compatibility is required because of its several advantages: +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.