diff --git a/crates/holochain_scaffolding_utils/src/lib.rs b/crates/holochain_scaffolding_utils/src/lib.rs index f756334..00b6477 100644 --- a/crates/holochain_scaffolding_utils/src/lib.rs +++ b/crates/holochain_scaffolding_utils/src/lib.rs @@ -2,12 +2,12 @@ use std::{collections::BTreeMap, path::PathBuf}; use dialoguer::{theme::ColorfulTheme, Select}; use file_tree_utils::{find_files_by_name, FileTree, FileTreeError}; -use holochain_types::web_app::WebAppManifest; +use holochain_types::{app::AppManifest, web_app::WebAppManifest}; use mr_bundle::Manifest; use thiserror::Error; #[derive(Error, Debug)] -pub enum GetOrChooseWebAppManifestError { +pub enum Error { #[error(transparent)] FileTreeError(#[from] FileTreeError), @@ -19,19 +19,22 @@ pub enum GetOrChooseWebAppManifestError { #[error("No web-happ.yaml files were found in this repository")] WebAppManifestNotFound, + + #[error("No happ.yaml files were found in this repository")] + AppManifestNotFound, } pub fn get_or_choose_web_app_manifest( file_tree: &FileTree, -) -> Result<(PathBuf, WebAppManifest), GetOrChooseWebAppManifestError> { +) -> Result<(PathBuf, WebAppManifest), Error> { let web_app_manifests = find_web_app_manifests(&file_tree)?; let app_manifest = match web_app_manifests.len() { - 0 => Err(GetOrChooseWebAppManifestError::WebAppManifestNotFound), + 0 => Err(Error::WebAppManifestNotFound), 1 => web_app_manifests .into_iter() .last() - .ok_or(GetOrChooseWebAppManifestError::WebAppManifestNotFound), + .ok_or(Error::WebAppManifestNotFound), _ => choose_web_app(web_app_manifests), }?; @@ -40,7 +43,7 @@ pub fn get_or_choose_web_app_manifest( pub fn choose_web_app( app_manifests: BTreeMap, -) -> Result<(PathBuf, WebAppManifest), GetOrChooseWebAppManifestError> { +) -> Result<(PathBuf, WebAppManifest), Error> { let manifest_vec: Vec<(PathBuf, WebAppManifest)> = app_manifests.into_iter().collect(); let app_names: Vec = manifest_vec .iter() @@ -59,7 +62,7 @@ pub fn choose_web_app( /// Returns the path to the existing app manifests in the given project structure pub fn find_web_app_manifests( app_file_tree: &FileTree, -) -> Result, GetOrChooseWebAppManifestError> { +) -> Result, Error> { let files = find_files_by_name(app_file_tree, &WebAppManifest::path()); let manifests: BTreeMap = files @@ -74,3 +77,55 @@ pub fn find_web_app_manifests( Ok(manifests) } + +pub fn get_or_choose_app_manifest(file_tree: &FileTree) -> Result<(PathBuf, AppManifest), Error> { + let app_manifests = find_app_manifests(&file_tree)?; + + let app_manifest = match app_manifests.len() { + 0 => Err(Error::AppManifestNotFound), + 1 => app_manifests + .into_iter() + .last() + .ok_or(Error::AppManifestNotFound), + _ => choose_app(app_manifests), + }?; + + Ok(app_manifest) +} + +pub fn choose_app( + app_manifests: BTreeMap, +) -> Result<(PathBuf, AppManifest), Error> { + let manifest_vec: Vec<(PathBuf, AppManifest)> = app_manifests.into_iter().collect(); + let app_names: Vec = manifest_vec + .iter() + .map(|(_, m)| m.app_name().to_string()) + .collect(); + + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Multiple happs were found in this repository, choose one:") + .default(0) + .items(&app_names[..]) + .interact()?; + + Ok(manifest_vec[selection].clone()) +} + +/// Returns the path to the existing app manifests in the given project structure +pub fn find_app_manifests( + app_file_tree: &FileTree, +) -> Result, Error> { + let files = find_files_by_name(app_file_tree, &AppManifest::path()); + + let manifests: BTreeMap = files + .into_iter() + .map(|(key, manifest_str)| { + let manifest: AppManifest = serde_yaml::from_str(manifest_str.as_str())?; + Ok((key, manifest)) + }) + .collect::>>()? + .into_iter() + .collect(); + + Ok(manifests) +} diff --git a/packages/signals/package.json b/packages/signals/package.json index d76fa79..7484236 100644 --- a/packages/signals/package.json +++ b/packages/signals/package.json @@ -1,6 +1,6 @@ { "name": "@holochain-open-dev/signals", - "version": "0.300.0-rc.0", + "version": "0.300.0-rc.1", "description": "Holochain async-signals to build reusable holochain-open-dev modules", "author": "guillem.cordoba@gmail.com", "main": "dist/index.js", diff --git a/packages/signals/src/holochain.ts b/packages/signals/src/holochain.ts index 476f75f..f06c44d 100644 --- a/packages/signals/src/holochain.ts +++ b/packages/signals/src/holochain.ts @@ -1,44 +1,44 @@ import { - ActionCommittedSignal, - EntryRecord, - HashType, - HoloHashMap, - LinkTypeForSignal, - ZomeClient, - getHashType, - retype, -} from "@holochain-open-dev/utils"; + ActionCommittedSignal, + EntryRecord, + HashType, + HoloHashMap, + LinkTypeForSignal, + ZomeClient, + getHashType, + retype, +} from '@holochain-open-dev/utils'; import { - Action, - ActionHash, - CreateLink, - Delete, - DeleteLink, - HoloHash, - Link, - SignedActionHashed, - decodeHashFromBase64, - encodeHashToBase64, -} from "@holochain/client"; -import { encode } from "@msgpack/msgpack"; -import cloneDeep from "lodash-es/cloneDeep.js"; -import { Signal } from "signal-polyfill"; -import { AsyncSignal, AsyncResult } from "async-signals"; + Action, + ActionHash, + CreateLink, + Delete, + DeleteLink, + HoloHash, + Link, + SignedActionHashed, + decodeHashFromBase64, + encodeHashToBase64, +} from '@holochain/client'; +import { encode } from '@msgpack/msgpack'; +import { AsyncResult, AsyncSignal } from 'async-signals'; +import cloneDeep from 'lodash-es/cloneDeep.js'; +import { Signal } from 'signal-polyfill'; const DEFAULT_POLL_INTERVAL_MS = 20_000; // 20 seconds export function createLinkToLink( - createLink: SignedActionHashed + createLink: SignedActionHashed, ): Link { - return { - author: createLink.hashed.content.author, - link_type: createLink.hashed.content.link_type, - tag: createLink.hashed.content.tag, - target: createLink.hashed.content.target_address, - timestamp: createLink.hashed.content.timestamp, - zome_index: createLink.hashed.content.zome_index, - create_link_hash: createLink.hashed.hash, - }; + return { + author: createLink.hashed.content.author, + link_type: createLink.hashed.content.link_type, + tag: createLink.hashed.content.tag, + target: createLink.hashed.content.target_address, + timestamp: createLink.hashed.content.timestamp, + zome_index: createLink.hashed.content.zome_index, + create_link_hash: createLink.hashed.hash, + }; } /** @@ -51,103 +51,103 @@ export function createLinkToLink( * Useful for collections */ export function collectionSignal< - S extends ActionCommittedSignal & any + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - fetchCollection: () => Promise, - linkType: string, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + fetchCollection: () => Promise, + linkType: string, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal> { - let active = false; - let unsubs: () => void | undefined; - const signal = new Signal.State>>( - { status: "pending" }, - { - [Signal.subtle.watched]: () => { - active = true; - let links: Link[]; - - const maybeSet = (newLinksValue: Link[]) => { - if (!active) return; - const orderedNewLinks = uniquifyLinks(newLinksValue).sort( - sortLinksByTimestampAscending - ); - if ( - links === undefined || - !areArrayHashesEqual( - orderedNewLinks.map((l) => l.create_link_hash), - links.map((l) => l.create_link_hash) - ) - ) { - links = orderedNewLinks; - signal.set({ - status: "completed", - value: links, - }); - } - }; - - const fetch = () => { - if (!active) return; - fetchCollection() - .then(maybeSet) - .catch((e) => - signal.set({ - status: "error", - error: e, - }) - ) - .finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - }; - fetch(); - unsubs = client.onSignal((originalSignal) => { - if (!(originalSignal as ActionCommittedSignal).type) return; - const signal = originalSignal as ActionCommittedSignal; - - if (signal.type === "LinkCreated") { - if (linkType in signal.link_type) { - maybeSet([...links, createLinkToLink(signal.action)]); - } - } else if (signal.type === "LinkDeleted") { - if (linkType in signal.link_type) { - maybeSet( - links.filter( - (link) => - link.create_link_hash.toString() !== - signal.create_link_action.hashed.hash.toString() - ) - ); - } - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: "pending", - }); - active = false; - unsubs(); - }, - } - ); - - return signal; + let active = false; + let unsubs: () => void | undefined; + const signal = new Signal.State>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + let links: Link[]; + + const maybeSet = (newLinksValue: Link[]) => { + if (!active) return; + const orderedNewLinks = uniquifyLinks(newLinksValue).sort( + sortLinksByTimestampAscending, + ); + if ( + links === undefined || + !areArrayHashesEqual( + orderedNewLinks.map(l => l.create_link_hash), + links.map(l => l.create_link_hash), + ) + ) { + links = orderedNewLinks; + signal.set({ + status: 'completed', + value: links, + }); + } + }; + + const fetch = () => { + if (!active) return; + fetchCollection() + .then(maybeSet) + .catch(e => + signal.set({ + status: 'error', + error: e, + }), + ) + .finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + }; + fetch(); + unsubs = client.onSignal(originalSignal => { + if (!(originalSignal as ActionCommittedSignal).type) return; + const signal = originalSignal as ActionCommittedSignal; + + if (signal.type === 'LinkCreated') { + if (linkType === signal.link_type) { + maybeSet([...links, createLinkToLink(signal.action)]); + } + } else if (signal.type === 'LinkDeleted') { + if (linkType === signal.link_type) { + maybeSet( + links.filter( + link => + link.create_link_hash.toString() !== + signal.create_link_action.hashed.hash.toString(), + ), + ); + } + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + unsubs(); + }, + }, + ); + + return signal; } export class NotFoundError extends Error { - constructor() { - super("NOT_FOUND"); - } + constructor() { + super('NOT_FOUND'); + } } export class ConflictingUpdatesError extends Error { - constructor(public conflictingUpdates: Array>) { - super("CONFLICTING_UPDATES"); - } + constructor(public conflictingUpdates: Array>) { + super('CONFLICTING_UPDATES'); + } } /** @@ -161,62 +161,62 @@ export class ConflictingUpdatesError extends Error { * Useful for entries that can't be updated */ export function immutableEntrySignal( - fetch: () => Promise | undefined>, - pollInterval: number = 1000, - maxRetries = 4 + fetch: () => Promise | undefined>, + pollInterval: number = 1000, + maxRetries = 4, ): AsyncSignal> { - let cached = false; - const signal = new Signal.State>>( - { status: "pending" }, - { - [Signal.subtle.watched]: () => { - if (cached) return; - let retries = 0; - - const tryFetch = () => { - retries += 1; - fetch() - .then((value) => { - if (value) { - cached = true; - signal.set({ - status: "completed", - value, - }); - } else { - if (retries < maxRetries) { - setTimeout(() => tryFetch, pollInterval); - } else { - signal.set({ - status: "error", - error: new NotFoundError(), - }); - } - } - }) - .catch((error) => { - if (retries < maxRetries) { - setTimeout(() => tryFetch, pollInterval); - } else { - signal.set({ - status: "error", - error, - }); - } - }); - }; - tryFetch(); - }, - [Signal.subtle.unwatched]: () => { - if (cached) return; - signal.set({ - status: "pending", - }); - }, - } - ); - - return signal; + let cached = false; + const signal = new Signal.State>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + if (cached) return; + let retries = 0; + + const tryFetch = () => { + retries += 1; + fetch() + .then(value => { + if (value) { + cached = true; + signal.set({ + status: 'completed', + value, + }); + } else { + if (retries < maxRetries) { + setTimeout(() => tryFetch, pollInterval); + } else { + signal.set({ + status: 'error', + error: new NotFoundError(), + }); + } + } + }) + .catch(error => { + if (retries < maxRetries) { + setTimeout(() => tryFetch, pollInterval); + } else { + signal.set({ + status: 'error', + error, + }); + } + }); + }; + tryFetch(); + }, + [Signal.subtle.unwatched]: () => { + if (cached) return; + signal.set({ + status: 'pending', + }); + }, + }, + ); + + return signal; } /** @@ -229,94 +229,94 @@ export function immutableEntrySignal( * Useful for entries that can be updated */ export function latestVersionOfEntrySignal< - T, - S extends ActionCommittedSignal & any + T, + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - fetchLatestVersion: () => Promise | undefined>, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + fetchLatestVersion: () => Promise | undefined>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal> { - let active = false; - let unsubs: () => void | undefined; - let latestVersion: EntryRecord | undefined; - - const signal = new Signal.State>>( - { status: "pending" }, - { - [Signal.subtle.watched]: () => { - active = true; - const fetch = async () => { - if (!active) return; - try { - const nlatestVersion = await fetchLatestVersion(); - if (nlatestVersion) { - if ( - latestVersion?.actionHash.toString() !== - nlatestVersion?.actionHash.toString() - ) { - latestVersion = nlatestVersion; - signal.set({ - status: "completed", - value: latestVersion, - }); - } - } else { - signal.set({ - status: "error", - error: new NotFoundError(), - }); - } - } catch (e) { - signal.set({ - status: "error", - error: e, - }); - } finally { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - } - }; - fetch(); - unsubs = client.onSignal((originalSignal) => { - if (!active) return; - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if ( - hcSignal.type === "EntryUpdated" && - latestVersion && - latestVersion.actionHash.toString() === - hcSignal.action.hashed.content.original_action_address.toString() - ) { - latestVersion = new EntryRecord({ - entry: { - Present: { - entry_type: "App", - entry: encode(hcSignal.app_entry), - }, - }, - signed_action: hcSignal.action, - }); - signal.set({ - status: "completed", - value: latestVersion, - }); - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: "pending", - }); - active = false; - latestVersion = undefined; - unsubs(); - }, - } - ); - - return signal; + let active = false; + let unsubs: () => void | undefined; + let latestVersion: EntryRecord | undefined; + + const signal = new Signal.State>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + const fetch = async () => { + if (!active) return; + try { + const nlatestVersion = await fetchLatestVersion(); + if (nlatestVersion) { + if ( + latestVersion?.actionHash.toString() !== + nlatestVersion?.actionHash.toString() + ) { + latestVersion = nlatestVersion; + signal.set({ + status: 'completed', + value: latestVersion, + }); + } + } else { + signal.set({ + status: 'error', + error: new NotFoundError(), + }); + } + } catch (e) { + signal.set({ + status: 'error', + error: e, + }); + } finally { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + } + }; + fetch(); + unsubs = client.onSignal(originalSignal => { + if (!active) return; + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if ( + hcSignal.type === 'EntryUpdated' && + latestVersion && + latestVersion.actionHash.toString() === + hcSignal.action.hashed.content.original_action_address.toString() + ) { + latestVersion = new EntryRecord({ + entry: { + Present: { + entry_type: 'App', + entry: encode(hcSignal.app_entry), + }, + }, + signed_action: hcSignal.action, + }); + signal.set({ + status: 'completed', + value: latestVersion, + }); + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + latestVersion = undefined; + unsubs(); + }, + }, + ); + + return signal; } /** @@ -329,92 +329,92 @@ export function latestVersionOfEntrySignal< * Useful for entries that can be updated */ export function allRevisionsOfEntrySignal< - T, - S extends ActionCommittedSignal & any + T, + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - fetchAllRevisions: () => Promise>>, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + fetchAllRevisions: () => Promise>>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal>> { - let active = false; - let unsubs: () => void | undefined; - let allRevisions: Array> | undefined; - const signal = new Signal.State>>>( - { status: "pending" }, - { - [Signal.subtle.watched]: () => { - active = true; - const fetch = async () => { - if (!active) return; - - const nAllRevisions = await fetchAllRevisions().finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - if ( - allRevisions === undefined || - !areArrayHashesEqual( - allRevisions.map((r) => r.actionHash), - nAllRevisions.map((r) => r.actionHash) - ) - ) { - allRevisions = nAllRevisions; - signal.set({ - status: "completed", - value: allRevisions, - }); - } - }; - fetch().catch((error) => { - signal.set({ - status: "error", - error, - }); - }); - unsubs = client.onSignal(async (originalSignal) => { - if (!active) return; - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if ( - hcSignal.type === "EntryUpdated" && - allRevisions && - allRevisions.find( - (revision) => - revision.actionHash.toString() === - hcSignal.action.hashed.content.original_action_address.toString() - ) - ) { - const newRevision = new EntryRecord({ - entry: { - Present: { - entry_type: "App", - entry: encode(hcSignal.app_entry), - }, - }, - signed_action: hcSignal.action, - }); - allRevisions.push(newRevision); - signal.set({ - status: "completed", - value: allRevisions, - }); - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: "pending", - }); - active = false; - allRevisions = undefined; - unsubs(); - }, - } - ); - - return signal; + let active = false; + let unsubs: () => void | undefined; + let allRevisions: Array> | undefined; + const signal = new Signal.State>>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + const fetch = async () => { + if (!active) return; + + const nAllRevisions = await fetchAllRevisions().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + if ( + allRevisions === undefined || + !areArrayHashesEqual( + allRevisions.map(r => r.actionHash), + nAllRevisions.map(r => r.actionHash), + ) + ) { + allRevisions = nAllRevisions; + signal.set({ + status: 'completed', + value: allRevisions, + }); + } + }; + fetch().catch(error => { + signal.set({ + status: 'error', + error, + }); + }); + unsubs = client.onSignal(async originalSignal => { + if (!active) return; + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if ( + hcSignal.type === 'EntryUpdated' && + allRevisions && + allRevisions.find( + revision => + revision.actionHash.toString() === + hcSignal.action.hashed.content.original_action_address.toString(), + ) + ) { + const newRevision = new EntryRecord({ + entry: { + Present: { + entry_type: 'App', + entry: encode(hcSignal.app_entry), + }, + }, + signed_action: hcSignal.action, + }); + allRevisions.push(newRevision); + signal.set({ + status: 'completed', + value: allRevisions, + }); + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + allRevisions = undefined; + unsubs(); + }, + }, + ); + + return signal; } /** @@ -427,134 +427,134 @@ export function allRevisionsOfEntrySignal< * Useful for entries that can be deleted */ export function deletesForEntrySignal< - S extends ActionCommittedSignal & any + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - originalActionHash: ActionHash, - fetchDeletes: () => Promise>>, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + originalActionHash: ActionHash, + fetchDeletes: () => Promise>>, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal>> { - let active = false; - let unsubs: () => void | undefined; - let deletes: Array> | undefined; - const signal = new Signal.State< - AsyncResult>> - >( - { status: "pending" }, - { - [Signal.subtle.watched]: () => { - active = true; - const fetch = async () => { - if (!active) return; - - const ndeletes = await fetchDeletes().finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - if ( - deletes === undefined || - !areArrayHashesEqual( - deletes.map((d) => d.hashed.hash), - ndeletes.map((d) => d.hashed.hash) - ) - ) { - deletes = ndeletes; - signal.set({ - status: "completed", - value: deletes, - }); - } - }; - fetch().catch((error) => { - signal.set({ - status: "error", - error, - }); - }); - unsubs = client.onSignal((originalSignal) => { - if (!active) return; - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if ( - hcSignal.type === "EntryDeleted" && - hcSignal.action.hashed.content.deletes_address.toString() === - originalActionHash.toString() - ) { - const lastDeletes = deletes ? deletes : []; - deletes = [...lastDeletes, hcSignal.action]; - signal.set({ - status: "completed", - value: deletes, - }); - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: "pending", - }); - active = false; - deletes = undefined; - unsubs(); - }, - } - ); - - return signal; + let active = false; + let unsubs: () => void | undefined; + let deletes: Array> | undefined; + const signal = new Signal.State< + AsyncResult>> + >( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + const fetch = async () => { + if (!active) return; + + const ndeletes = await fetchDeletes().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + if ( + deletes === undefined || + !areArrayHashesEqual( + deletes.map(d => d.hashed.hash), + ndeletes.map(d => d.hashed.hash), + ) + ) { + deletes = ndeletes; + signal.set({ + status: 'completed', + value: deletes, + }); + } + }; + fetch().catch(error => { + signal.set({ + status: 'error', + error, + }); + }); + unsubs = client.onSignal(originalSignal => { + if (!active) return; + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if ( + hcSignal.type === 'EntryDeleted' && + hcSignal.action.hashed.content.deletes_address.toString() === + originalActionHash.toString() + ) { + const lastDeletes = deletes ? deletes : []; + deletes = [...lastDeletes, hcSignal.action]; + signal.set({ + status: 'completed', + value: deletes, + }); + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + deletes = undefined; + unsubs(); + }, + }, + ); + + return signal; } export const sortLinksByTimestampAscending = (linkA: Link, linkB: Link) => - linkA.timestamp - linkB.timestamp; + linkA.timestamp - linkB.timestamp; export const sortDeletedLinksByTimestampAscending = ( - linkA: [SignedActionHashed, SignedActionHashed[]], - linkB: [SignedActionHashed, SignedActionHashed[]] + linkA: [SignedActionHashed, SignedActionHashed[]], + linkB: [SignedActionHashed, SignedActionHashed[]], ) => linkA[0].hashed.content.timestamp - linkB[0].hashed.content.timestamp; export const sortActionsByTimestampAscending = ( - actionA: SignedActionHashed, - actionB: SignedActionHashed + actionA: SignedActionHashed, + actionB: SignedActionHashed, ) => actionA.hashed.content.timestamp - actionB.hashed.content.timestamp; export function uniquify(array: Array): Array { - const strArray = array.map((h) => encodeHashToBase64(h)); - const uniqueArray = [...new Set(strArray)]; - return uniqueArray.map((h) => decodeHashFromBase64(h) as H); + const strArray = array.map(h => encodeHashToBase64(h)); + const uniqueArray = [...new Set(strArray)]; + return uniqueArray.map(h => decodeHashFromBase64(h) as H); } export function uniquifyLinks(links: Array): Array { - const map = new HoloHashMap(); - for (const link of links) { - map.set(link.create_link_hash, link); - } + const map = new HoloHashMap(); + for (const link of links) { + map.set(link.create_link_hash, link); + } - return Array.from(map.values()); + return Array.from(map.values()); } function areArrayHashesEqual( - array1: Array, - array2: Array + array1: Array, + array2: Array, ): boolean { - if (array1.length !== array2.length) return false; + if (array1.length !== array2.length) return false; - for (let i = 0; i < array1.length; i += 1) { - if (array1[i].toString() !== array2[i].toString()) { - return false; - } - } + for (let i = 0; i < array1.length; i += 1) { + if (array1[i].toString() !== array2[i].toString()) { + return false; + } + } - return true; + return true; } function uniquifyActions( - actions: Array> + actions: Array>, ): Array> { - const map = new HoloHashMap>(); - for (const a of actions) { - map.set(a.hashed.hash, a); - } + const map = new HoloHashMap>(); + for (const a of actions) { + map.set(a.hashed.hash, a); + } - return Array.from(map.values()); + return Array.from(map.values()); } /** @@ -567,110 +567,110 @@ function uniquifyActions( * Useful for link types */ export function liveLinksSignal< - BASE extends HoloHash, - S extends ActionCommittedSignal & any + BASE extends HoloHash, + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - baseAddress: BASE, - fetchLinks: () => Promise>, - linkType: LinkTypeForSignal, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + baseAddress: BASE, + fetchLinks: () => Promise>, + linkType: LinkTypeForSignal, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal> { - let innerBaseAddress = baseAddress; - if (getHashType(innerBaseAddress) === HashType.AGENT) { - innerBaseAddress = retype(innerBaseAddress, HashType.ENTRY) as BASE; - } - - let active = false; - let unsubs: () => void | undefined; - - let links: Array | undefined; - const signal = new Signal.State>>( - { status: "pending" }, - { - [Signal.subtle.watched]: () => { - active = true; - - const maybeSet = (newLinksValue: Link[]) => { - if (!active) return; - const orderedNewLinks = uniquifyLinks(newLinksValue).sort( - sortLinksByTimestampAscending - ); - - if ( - links === undefined || - !areArrayHashesEqual( - orderedNewLinks.map((l) => l.create_link_hash), - links.map((l) => l.create_link_hash) - ) - ) { - links = orderedNewLinks; - signal.set({ - status: "completed", - value: links, - }); - } - }; - const fetch = async () => { - if (!active) return; - fetchLinks() - .then(maybeSet) - .finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - }; - - fetch().catch((error) => { - signal.set({ - status: "error", - error, - }); - }); - unsubs = client.onSignal((originalSignal) => { - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if (hcSignal.type === "LinkCreated") { - if ( - linkType in hcSignal.link_type && - hcSignal.action.hashed.content.base_address.toString() === - innerBaseAddress.toString() - ) { - const lastLinks = links ? links : []; - maybeSet([...lastLinks, createLinkToLink(hcSignal.action)]); - } - } else if (hcSignal.type === "LinkDeleted") { - if ( - linkType in hcSignal.link_type && - hcSignal.create_link_action.hashed.content.base_address.toString() === - innerBaseAddress.toString() - ) { - maybeSet( - (links ? links : []).filter( - (link) => - link.create_link_hash.toString() !== - hcSignal.create_link_action.hashed.hash.toString() - ) - ); - } - } - }); - }, - - [Signal.subtle.unwatched]: () => { - signal.set({ - status: "pending", - }); - active = false; - links = undefined; - unsubs(); - }, - } - ); - - return signal; + let innerBaseAddress = baseAddress; + if (getHashType(innerBaseAddress) === HashType.AGENT) { + innerBaseAddress = retype(innerBaseAddress, HashType.ENTRY) as BASE; + } + + let active = false; + let unsubs: () => void | undefined; + + let links: Array | undefined; + const signal = new Signal.State>>( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + + const maybeSet = (newLinksValue: Link[]) => { + if (!active) return; + const orderedNewLinks = uniquifyLinks(newLinksValue).sort( + sortLinksByTimestampAscending, + ); + + if ( + links === undefined || + !areArrayHashesEqual( + orderedNewLinks.map(l => l.create_link_hash), + links.map(l => l.create_link_hash), + ) + ) { + links = orderedNewLinks; + signal.set({ + status: 'completed', + value: links, + }); + } + }; + const fetch = async () => { + if (!active) return; + fetchLinks() + .then(maybeSet) + .finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + }; + + fetch().catch(error => { + signal.set({ + status: 'error', + error, + }); + }); + unsubs = client.onSignal(originalSignal => { + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if (hcSignal.type === 'LinkCreated') { + if ( + linkType === hcSignal.link_type && + hcSignal.action.hashed.content.base_address.toString() === + innerBaseAddress.toString() + ) { + const lastLinks = links ? links : []; + maybeSet([...lastLinks, createLinkToLink(hcSignal.action)]); + } + } else if (hcSignal.type === 'LinkDeleted') { + if ( + linkType === hcSignal.link_type && + hcSignal.create_link_action.hashed.content.base_address.toString() === + innerBaseAddress.toString() + ) { + maybeSet( + (links ? links : []).filter( + link => + link.create_link_hash.toString() !== + hcSignal.create_link_action.hashed.hash.toString(), + ), + ); + } + } + }); + }, + + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + links = undefined; + unsubs(); + }, + }, + ); + + return signal; } /** @@ -683,151 +683,151 @@ export function liveLinksSignal< * Useful for link types and collections with some form of archive retrieving functionality */ export function deletedLinksSignal< - BASE extends HoloHash, - S extends ActionCommittedSignal & any + BASE extends HoloHash, + S extends ActionCommittedSignal & any, >( - client: ZomeClient, - baseAddress: BASE, - fetchDeletedLinks: () => Promise< - Array< - [SignedActionHashed, Array>] - > - >, - linkType: LinkTypeForSignal, - pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS + client: ZomeClient, + baseAddress: BASE, + fetchDeletedLinks: () => Promise< + Array< + [SignedActionHashed, Array>] + > + >, + linkType: LinkTypeForSignal, + pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS, ): AsyncSignal< - Array<[SignedActionHashed, Array>]> + Array<[SignedActionHashed, Array>]> > { - let innerBaseAddress = baseAddress; - if (getHashType(innerBaseAddress) === HashType.AGENT) { - innerBaseAddress = retype(innerBaseAddress, HashType.ENTRY) as BASE; - } - - let active = false; - let unsubs: () => void | undefined; - let deletedLinks: - | Array< - [SignedActionHashed, Array>] - > - | undefined; - const signal = new Signal.State< - AsyncResult< - Array< - [SignedActionHashed, Array>] - > - > - >( - { status: "pending" }, - { - [Signal.subtle.watched]: () => { - active = true; - - const maybeSet = ( - newDeletedLinks: Array< - [ - SignedActionHashed, - Array> - ] - > - ) => { - if (!active) return; - - const orderedNewLinks = newDeletedLinks.sort( - sortDeletedLinksByTimestampAscending - ); - - for (let i = 0; i < orderedNewLinks.length; i += 1) { - orderedNewLinks[i][1] = orderedNewLinks[i][1].sort( - sortActionsByTimestampAscending - ); - if ( - deletedLinks !== undefined && - (!deletedLinks[i] || - !areArrayHashesEqual( - orderedNewLinks[i][1].map((d) => d.hashed.hash), - deletedLinks[i][1].map((d) => d.hashed.hash) - )) - ) - return; - } - if ( - deletedLinks === undefined || - !areArrayHashesEqual( - orderedNewLinks.map((l) => l[0].hashed.hash), - deletedLinks.map((l) => l[0].hashed.hash) - ) - ) { - deletedLinks = orderedNewLinks; - signal.set({ - status: "completed", - value: deletedLinks, - }); - } - }; - const fetch = async () => { - if (!active) return; - const ndeletedLinks = await fetchDeletedLinks().finally(() => { - if (active) { - setTimeout(() => fetch(), pollIntervalMs); - } - }); - maybeSet(ndeletedLinks); - }; - fetch().catch((error) => { - signal.set({ - status: "error", - error, - }); - }); - unsubs = client.onSignal((originalSignal) => { - if (!(originalSignal as ActionCommittedSignal).type) return; - const hcSignal = originalSignal as ActionCommittedSignal; - - if (hcSignal.type === "LinkDeleted") { - if ( - linkType in hcSignal.link_type && - hcSignal.create_link_action.hashed.content.base_address.toString() === - innerBaseAddress.toString() - ) { - const lastDeletedLinks = deletedLinks ? deletedLinks : []; - const alreadyDeletedTargetIndex = lastDeletedLinks.findIndex( - ([cl]) => - cl.hashed.hash.toString() === - hcSignal.create_link_action.hashed.hash.toString() - ); - - if (alreadyDeletedTargetIndex !== -1) { - if ( - !lastDeletedLinks[alreadyDeletedTargetIndex][1].find( - (dl) => - dl.hashed.hash.toString() === - hcSignal.action.hashed.hash.toString() - ) - ) { - const clone = cloneDeep(deletedLinks); - clone[alreadyDeletedTargetIndex][1].push(hcSignal.action); - maybeSet(clone); - } - } else { - maybeSet([ - ...lastDeletedLinks, - [hcSignal.create_link_action, [hcSignal.action]], - ]); - } - } - } - }); - }, - [Signal.subtle.unwatched]: () => { - signal.set({ - status: "pending", - }); - active = false; - deletedLinks = undefined; - unsubs(); - }, - } - ); - - return signal; + let innerBaseAddress = baseAddress; + if (getHashType(innerBaseAddress) === HashType.AGENT) { + innerBaseAddress = retype(innerBaseAddress, HashType.ENTRY) as BASE; + } + + let active = false; + let unsubs: () => void | undefined; + let deletedLinks: + | Array< + [SignedActionHashed, Array>] + > + | undefined; + const signal = new Signal.State< + AsyncResult< + Array< + [SignedActionHashed, Array>] + > + > + >( + { status: 'pending' }, + { + [Signal.subtle.watched]: () => { + active = true; + + const maybeSet = ( + newDeletedLinks: Array< + [ + SignedActionHashed, + Array>, + ] + >, + ) => { + if (!active) return; + + const orderedNewLinks = newDeletedLinks.sort( + sortDeletedLinksByTimestampAscending, + ); + + for (let i = 0; i < orderedNewLinks.length; i += 1) { + orderedNewLinks[i][1] = orderedNewLinks[i][1].sort( + sortActionsByTimestampAscending, + ); + if ( + deletedLinks !== undefined && + (!deletedLinks[i] || + !areArrayHashesEqual( + orderedNewLinks[i][1].map(d => d.hashed.hash), + deletedLinks[i][1].map(d => d.hashed.hash), + )) + ) + return; + } + if ( + deletedLinks === undefined || + !areArrayHashesEqual( + orderedNewLinks.map(l => l[0].hashed.hash), + deletedLinks.map(l => l[0].hashed.hash), + ) + ) { + deletedLinks = orderedNewLinks; + signal.set({ + status: 'completed', + value: deletedLinks, + }); + } + }; + const fetch = async () => { + if (!active) return; + const ndeletedLinks = await fetchDeletedLinks().finally(() => { + if (active) { + setTimeout(() => fetch(), pollIntervalMs); + } + }); + maybeSet(ndeletedLinks); + }; + fetch().catch(error => { + signal.set({ + status: 'error', + error, + }); + }); + unsubs = client.onSignal(originalSignal => { + if (!(originalSignal as ActionCommittedSignal).type) return; + const hcSignal = originalSignal as ActionCommittedSignal; + + if (hcSignal.type === 'LinkDeleted') { + if ( + linkType === hcSignal.link_type && + hcSignal.create_link_action.hashed.content.base_address.toString() === + innerBaseAddress.toString() + ) { + const lastDeletedLinks = deletedLinks ? deletedLinks : []; + const alreadyDeletedTargetIndex = lastDeletedLinks.findIndex( + ([cl]) => + cl.hashed.hash.toString() === + hcSignal.create_link_action.hashed.hash.toString(), + ); + + if (alreadyDeletedTargetIndex !== -1) { + if ( + !lastDeletedLinks[alreadyDeletedTargetIndex][1].find( + dl => + dl.hashed.hash.toString() === + hcSignal.action.hashed.hash.toString(), + ) + ) { + const clone = cloneDeep(deletedLinks); + clone[alreadyDeletedTargetIndex][1].push(hcSignal.action); + maybeSet(clone); + } + } else { + maybeSet([ + ...lastDeletedLinks, + [hcSignal.create_link_action, [hcSignal.action]], + ]); + } + } + } + }); + }, + [Signal.subtle.unwatched]: () => { + signal.set({ + status: 'pending', + }); + active = false; + deletedLinks = undefined; + unsubs(); + }, + }, + ); + + return signal; }