diff --git a/package-lock.json b/package-lock.json index 827faf4..7a9c7eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9810,7 +9810,7 @@ }, "packages/elements": { "name": "@holochain-open-dev/elements", - "version": "0.8.3", + "version": "0.8.4", "dependencies": { "@holo-host/identicon": "^0.1.0", "@holochain-open-dev/stores": "^0.7.12", @@ -9832,6 +9832,21 @@ "vite-plugin-checker": "^0.5.5" } }, + "packages/elements/node_modules/@holochain-open-dev/stores": { + "version": "0.7.16", + "resolved": "https://registry.npmjs.org/@holochain-open-dev/stores/-/stores-0.7.16.tgz", + "integrity": "sha512-hBiTq7d2fIgqMyhQHlyPWi9RMkfhjFOsQG+1MRSleIOZRPepp/IkFeg8yqjImbdEj9GgHkZQPV72JZyWAohqgg==", + "dependencies": { + "@alenaksu/json-viewer": "^2.0.1", + "@holochain-open-dev/utils": "^0.16.2", + "@holochain/client": "^0.16.0", + "@scoped-elements/cytoscape": "^0.2.0", + "@shoelace-style/shoelace": "^2.11.2", + "lit": "^3.0.2", + "lit-svelte-stores": "^0.3.0", + "svelte": "^3.53.1" + } + }, "packages/elements/node_modules/@lit/reactive-element": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.1.tgz", @@ -9870,10 +9885,10 @@ }, "packages/stores": { "name": "@holochain-open-dev/stores", - "version": "0.8.2", + "version": "0.8.10", "dependencies": { "@alenaksu/json-viewer": "^2.0.1", - "@holochain-open-dev/utils": "^0.16.2", + "@holochain-open-dev/utils": "^0.16.5", "@holochain/client": "^0.16.6", "@scoped-elements/cytoscape": "^0.2.0", "@shoelace-style/shoelace": "^2.11.2", @@ -9930,7 +9945,7 @@ }, "packages/utils": { "name": "@holochain-open-dev/utils", - "version": "0.16.3", + "version": "0.16.5", "dependencies": { "@holochain/client": "^0.16.2", "@msgpack/msgpack": "^2.7.2", @@ -11417,6 +11432,21 @@ "vite-plugin-checker": "^0.5.5" }, "dependencies": { + "@holochain-open-dev/stores": { + "version": "0.7.16", + "resolved": "https://registry.npmjs.org/@holochain-open-dev/stores/-/stores-0.7.16.tgz", + "integrity": "sha512-hBiTq7d2fIgqMyhQHlyPWi9RMkfhjFOsQG+1MRSleIOZRPepp/IkFeg8yqjImbdEj9GgHkZQPV72JZyWAohqgg==", + "requires": { + "@alenaksu/json-viewer": "^2.0.1", + "@holochain-open-dev/utils": "^0.16.2", + "@holochain/client": "^0.16.0", + "@scoped-elements/cytoscape": "^0.2.0", + "@shoelace-style/shoelace": "^2.11.2", + "lit": "^3.0.2", + "lit-svelte-stores": "^0.3.0", + "svelte": "^3.53.1" + } + }, "@lit/reactive-element": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.1.tgz", @@ -11460,7 +11490,7 @@ "requires": { "@alenaksu/json-viewer": "^2.0.1", "@esm-bundle/chai": "^4.3.4-fix.0", - "@holochain-open-dev/utils": "^0.16.2", + "@holochain-open-dev/utils": "^0.16.5", "@holochain/client": "^0.16.6", "@scoped-elements/cytoscape": "^0.2.0", "@shoelace-style/shoelace": "^2.11.2", diff --git a/packages/stores/package.json b/packages/stores/package.json index fbf33dd..79bd36f 100644 --- a/packages/stores/package.json +++ b/packages/stores/package.json @@ -1,6 +1,6 @@ { "name": "@holochain-open-dev/stores", - "version": "0.8.10", + "version": "0.8.11", "description": "Re-export of svelte/store, with additional utilities to build reusable holochain-open-dev modules", "author": "guillem.cordoba@gmail.com", "main": "dist/index.js", @@ -19,7 +19,7 @@ }, "dependencies": { "@alenaksu/json-viewer": "^2.0.1", - "@holochain-open-dev/utils": "^0.16.2", + "@holochain-open-dev/utils": "^0.16.5", "@holochain/client": "^0.16.6", "@scoped-elements/cytoscape": "^0.2.0", "@shoelace-style/shoelace": "^2.11.2", diff --git a/packages/stores/src/holochain.ts b/packages/stores/src/holochain.ts index 18a7a02..e2524aa 100644 --- a/packages/stores/src/holochain.ts +++ b/packages/stores/src/holochain.ts @@ -27,6 +27,8 @@ import cloneDeep from "lodash-es/cloneDeep.js"; import { asyncReadable, AsyncReadable, AsyncStatus } from "./async-readable.js"; import { retryUntilSuccess } from "./retry-until-success.js"; +const DEFAULT_POLL_INTERVAL_MS = 20_000; // 20 seconds + export function createLinkToLink( createLink: SignedActionHashed ): Link { @@ -55,9 +57,11 @@ export function collectionStore< >( client: ZomeClient, fetchCollection: () => Promise, - linkType: string + linkType: string, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS ): AsyncReadable> { return asyncReadable(async (set) => { + let active = true; let links: Link[]; const maybeSet = (newLinksValue: Link[]) => { @@ -77,11 +81,14 @@ export function collectionStore< }; const fetch = async () => { - const nlinks = await fetchCollection(); + const nlinks = await fetchCollection().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); maybeSet(nlinks); }; await fetch(); - const interval = setInterval(() => fetch(), 4000); const unsubs = client.onSignal((originalSignal) => { if (!(originalSignal as ActionCommittedSignal).type) return; const signal = originalSignal as ActionCommittedSignal; @@ -103,7 +110,7 @@ export function collectionStore< } }); return () => { - clearInterval(interval); + active = false; unsubs(); }; }); @@ -151,9 +158,11 @@ export function latestVersionOfEntryStore< S extends ActionCommittedSignal & any >( client: ZomeClient, - fetchLatestVersion: () => Promise | undefined> + fetchLatestVersion: () => Promise | undefined>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS ): AsyncReadable> { return readable>>({ status: "pending" }, (set) => { + let active = true; let latestVersion: EntryRecord | undefined; const fetch = async () => { try { @@ -180,10 +189,13 @@ export function latestVersionOfEntryStore< status: "error", error: e, }); + } finally { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } } }; fetch(); - const interval = setInterval(() => fetch(), 4000); const unsubs = client.onSignal((originalSignal) => { if (!(originalSignal as ActionCommittedSignal).type) return; const signal = originalSignal as ActionCommittedSignal; @@ -211,7 +223,7 @@ export function latestVersionOfEntryStore< }); return () => { set({ status: "pending" }); - clearInterval(interval); + active = false; unsubs(); }; }); @@ -231,12 +243,18 @@ export function allRevisionsOfEntryStore< S extends ActionCommittedSignal & any >( client: ZomeClient, - fetchAllRevisions: () => Promise>> + fetchAllRevisions: () => Promise>>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS ): AsyncReadable>> { return asyncReadable(async (set) => { + let active = true; let allRevisions: Array>; const fetch = async () => { - const nAllRevisions = await fetchAllRevisions(); + const nAllRevisions = await fetchAllRevisions().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); if ( allRevisions === undefined || !areArrayHashesEqual( @@ -249,7 +267,6 @@ export function allRevisionsOfEntryStore< } }; await fetch(); - const interval = setInterval(() => fetch(), 4000); const unsubs = client.onSignal(async (originalSignal) => { if (!(originalSignal as ActionCommittedSignal).type) return; const signal = originalSignal as ActionCommittedSignal; @@ -277,7 +294,7 @@ export function allRevisionsOfEntryStore< } }); return () => { - clearInterval(interval); + active = false; unsubs(); }; }); @@ -297,12 +314,18 @@ export function deletesForEntryStore< >( client: ZomeClient, originalActionHash: ActionHash, - fetchDeletes: () => Promise>> + fetchDeletes: () => Promise>>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS ): AsyncReadable>> { return asyncReadable(async (set) => { + let active = true; let deletes: Array>; const fetch = async () => { - const ndeletes = await fetchDeletes(); + const ndeletes = await fetchDeletes().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); if ( deletes === undefined || !areArrayHashesEqual( @@ -315,7 +338,6 @@ export function deletesForEntryStore< } }; await fetch(); - const interval = setInterval(() => fetch(), 4000); const unsubs = client.onSignal((originalSignal) => { if (!(originalSignal as ActionCommittedSignal).type) return; const signal = originalSignal as ActionCommittedSignal; @@ -330,7 +352,7 @@ export function deletesForEntryStore< } }); return () => { - clearInterval(interval); + active = false; unsubs(); }; }); @@ -392,7 +414,7 @@ function uniquifyActions( * Keeps an up to date list of the links for the non-deleted links in this DHT * Makes requests only while it has some subscriber * - * Will do so by calling the given every 4 seconds calling the given fetch function, + * Will do so by calling the given fetch callback every 20 seconds, * and listening to `LinkCreated` and `LinkDeleted` signals * * Useful for link types @@ -404,7 +426,8 @@ export function liveLinksStore< client: ZomeClient, baseAddress: BASE, fetchLinks: () => Promise>, - linkType: LinkTypeForSignal + linkType: LinkTypeForSignal, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS ): AsyncReadable> { let innerBaseAddress = baseAddress; if (getHashType(innerBaseAddress) === HashType.AGENT) { @@ -412,6 +435,7 @@ export function liveLinksStore< } return asyncReadable(async (set) => { let links: Link[]; + let active = true; const maybeSet = (newLinksValue: Link[]) => { const orderedNewLinks = uniquifyLinks(newLinksValue).sort( @@ -430,13 +454,15 @@ export function liveLinksStore< } }; const fetch = async () => { - const nlinks = await fetchLinks(); + const nlinks = await fetchLinks().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); maybeSet(nlinks); }; await fetch(); - - const interval = setInterval(() => fetch(), 4000); const unsubs = client.onSignal((originalSignal) => { if (!(originalSignal as ActionCommittedSignal).type) return; const signal = originalSignal as ActionCommittedSignal; @@ -466,7 +492,7 @@ export function liveLinksStore< } }); return () => { - clearInterval(interval); + active = false; unsubs(); }; }); @@ -492,7 +518,8 @@ export function deletedLinksStore< [SignedActionHashed, Array>] > >, - linkType: LinkTypeForSignal + linkType: LinkTypeForSignal, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS ): AsyncReadable< Array<[SignedActionHashed, Array>]> > { @@ -504,6 +531,7 @@ export function deletedLinksStore< let deletedLinks: Array< [SignedActionHashed, Array>] >; + let active = true; const maybeSet = ( newDeletedLinks: Array< @@ -539,11 +567,14 @@ export function deletedLinksStore< } }; const fetch = async () => { - const ndeletedLinks = await fetchDeletedLinks(); + const ndeletedLinks = await fetchDeletedLinks().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); maybeSet(ndeletedLinks); }; await fetch(); - const interval = setInterval(() => fetch(), 4000); const unsubs = client.onSignal((originalSignal) => { if (!(originalSignal as ActionCommittedSignal).type) return; const signal = originalSignal as ActionCommittedSignal; @@ -582,7 +613,7 @@ export function deletedLinksStore< } }); return () => { - clearInterval(interval); + active = true; unsubs(); }; }); diff --git a/packages/stores/tests/holochain.test.js b/packages/stores/tests/holochain.test.js index c99725e..0dc298d 100644 --- a/packages/stores/tests/holochain.test.js +++ b/packages/stores/tests/holochain.test.js @@ -1,11 +1,29 @@ -import { ZomeClient, ZomeMock } from "@holochain-open-dev/utils"; +import { + EntryRecord, + fakeCreateAction, + fakeCreateLinkAction, + fakeDeleteEntry, + fakeDeleteLinkAction, + fakeEntry, + fakeRecord, + ZomeClient, + ZomeMock, +} from "@holochain-open-dev/utils"; import { fakeActionHash, fakeAgentPubKey, fakeEntryHash, } from "@holochain/client"; -import { test } from "vitest"; -import { liveLinksStore } from "../src"; +import { assert, test } from "vitest"; +import { + allRevisionsOfEntryStore, + collectionStore, + deletedLinksStore, + deletesForEntryStore, + latestVersionOfEntryStore, + liveLinksStore, + toPromise, +} from "../src"; const sleep = (ms) => new Promise((r) => setTimeout(() => r(), ms)); @@ -64,3 +82,182 @@ test("liveLinks only updates once if no new links exist", async () => { resolve(); }); }); + +test("collection store works", async () => { + const links = [await fakeLink()]; + const collection = collectionStore( + new ZomeClient(new ZomeMock("", "")), + async () => links, + "", + 100 + ); + + collection.subscribe(() => {}); + + let collectionLinks = await toPromise(collection); + + assert.equal(collectionLinks.length, 1); + + links.push(await fakeLink()); + + collectionLinks = await toPromise(collection); + + assert.equal(collectionLinks.length, 1); + + await sleep(110); + + collectionLinks = await toPromise(collection); + + assert.equal(collectionLinks.length, 2); +}); + +test("latestVersionOfEntry store works", async () => { + let record = new EntryRecord( + await fakeRecord(await fakeCreateAction(), fakeEntry({ some: "entry" })) + ); + const firstRecord = record; + const latestVersion = latestVersionOfEntryStore( + new ZomeClient(new ZomeMock("", "")), + async () => record, + 100 + ); + + latestVersion.subscribe(() => {}); + + let latestRecord = await toPromise(latestVersion); + + assert.deepEqual(latestRecord, record); + + record = new EntryRecord( + await fakeRecord( + await fakeCreateAction(), + fakeEntry({ some: "other-entry" }) + ) + ); + + latestRecord = await toPromise(latestVersion); + + assert.deepEqual(latestRecord, firstRecord); + + await sleep(110); + + latestRecord = await toPromise(latestVersion); + + assert.deepEqual(latestRecord, record); +}); + +test("allRevisionsOfEntryStore works", async () => { + let record = await fakeRecord( + await fakeCreateAction(), + fakeEntry({ some: "entry" }) + ); + const allRevisions = [new EntryRecord(record)]; + const allRevisionsStore = allRevisionsOfEntryStore( + new ZomeClient(new ZomeMock("", "")), + async () => allRevisions, + 100 + ); + + allRevisionsStore.subscribe(() => {}); + + let latestAllRevisions = await toPromise(allRevisionsStore); + + assert.equal(latestAllRevisions.length, 1); + + allRevisions.push( + new EntryRecord( + await fakeRecord( + await fakeCreateAction(), + fakeEntry({ some: "other-entry" }) + ) + ) + ); + + await sleep(110); + + latestAllRevisions = await toPromise(allRevisionsStore); + + assert.equal(latestAllRevisions.length, 2); +}); + +test("deletesForEntry works", async () => { + const deletes = [(await fakeRecord(await fakeDeleteEntry())).signed_action]; + const deletesStore = deletesForEntryStore( + new ZomeClient(new ZomeMock("", "")), + await fakeActionHash(), + async () => deletes, + 100 + ); + + deletesStore.subscribe(() => {}); + + let latestDeletes = await toPromise(deletesStore); + + assert.equal(latestDeletes.length, 1); + + deletes.push((await fakeRecord(await fakeDeleteEntry())).signed_action); + + await sleep(110); + + latestDeletes = await toPromise(deletesStore); + + assert.equal(latestDeletes.length, 2); +}); + +test("liveLinksStore works", async () => { + const links = [await fakeLink()]; + const linksStore = liveLinksStore( + new ZomeClient(new ZomeMock("", "")), + await fakeActionHash(), + async () => links, + "", + 100 + ); + + linksStore.subscribe(() => {}); + + let latestLinks = await toPromise(linksStore); + + assert.equal(latestLinks.length, 1); + + links.push(await fakeLink()); + + await sleep(110); + + latestLinks = await toPromise(linksStore); + + assert.equal(latestLinks.length, 2); +}); + +test("deleteLinksStore works", async () => { + const deletedLinks = [ + [ + (await fakeRecord(await fakeCreateLinkAction())).signed_action, + [(await fakeRecord(await fakeDeleteLinkAction())).signed_action], + ], + ]; + const deletedStore = deletedLinksStore( + new ZomeClient(new ZomeMock("", "")), + await fakeActionHash(), + async () => deletedLinks, + "", + 100 + ); + + deletedStore.subscribe(() => {}); + + let latestDeletedLinks = await toPromise(deletedStore); + + assert.equal(latestDeletedLinks.length, 1); + + deletedLinks.push([ + (await fakeRecord(await fakeCreateLinkAction())).signed_action, + [(await fakeRecord(await fakeDeleteLinkAction())).signed_action], + ]); + + await sleep(110); + + latestDeletedLinks = await toPromise(deletedStore); + + assert.equal(latestDeletedLinks.length, 2); +}); diff --git a/packages/utils/package.json b/packages/utils/package.json index 66849f2..a71ab97 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@holochain-open-dev/utils", - "version": "0.16.4", + "version": "0.16.5", "description": "Common utilities to build Holochain web applications", "author": "guillem.cordoba@gmail.com", "main": "dist/index.js", diff --git a/packages/utils/src/cell.ts b/packages/utils/src/cell.ts index ff7ebd9..667c42c 100644 --- a/packages/utils/src/cell.ts +++ b/packages/utils/src/cell.ts @@ -1,5 +1,6 @@ import { AppAgentClient, + AppAgentWebsocket, AppInfo, AppSignal, CellId, @@ -30,6 +31,19 @@ export async function isSignalFromCellWithRole( roleName: RoleName, signal: AppSignal ): Promise { + if ((client as AppAgentWebsocket).cachedAppInfo) { + const role = roleNameForCellId( + (client as AppAgentWebsocket).cachedAppInfo, + signal.cell_id + ); + if (role) { + return roleName === role; + } + } + + // Cache miss: most likely due to a new clone having been created, + // So in this case we _should_ trigger a new fetch of the app info + const appInfo = await client.appInfo(); const role = roleNameForCellId(appInfo, signal.cell_id); diff --git a/packages/utils/src/fake.ts b/packages/utils/src/fake.ts index d4177e0..672ab6a 100644 --- a/packages/utils/src/fake.ts +++ b/packages/utils/src/fake.ts @@ -117,3 +117,46 @@ export async function fakeRecord( }, }; } + +export async function fakeCreateLinkAction( + base_address?: ActionHash, + target_address?: ActionHash, + link_type: number = 0, + tag: any = undefined +): Promise { + if (base_address) base_address = await fakeActionHash(); + if (target_address) target_address = await fakeActionHash(); + + return { + type: ActionType.CreateLink, + author: await fakeAgentPubKey(), + timestamp: Date.now() * 1000, + action_seq: 10, + prev_action: await fakeActionHash(), + base_address, + target_address, + link_type, + tag, + zome_index: 0, + weight: { + bucket_id: 0, + units: 1, + }, + }; +} + +export async function fakeDeleteLinkAction( + link_add_address?: ActionHash +): Promise { + if (link_add_address) link_add_address = await fakeActionHash(); + + return { + type: ActionType.DeleteLink, + author: await fakeAgentPubKey(), + timestamp: Date.now() * 1000, + action_seq: 10, + prev_action: await fakeActionHash(), + base_address: link_add_address, + link_add_address, + }; +}