diff --git a/sdk/base/lib/util/deepEqual.ts b/sdk/base/lib/util/deepEqual.ts index 8e6ba4b65..7a1d534cc 100644 --- a/sdk/base/lib/util/deepEqual.ts +++ b/sdk/base/lib/util/deepEqual.ts @@ -1,13 +1,13 @@ import { object } from "ts-matches" export function deepEqual(...args: unknown[]) { - if (!object.test(args[args.length - 1])) return args[args.length - 1] const objects = args.filter(object.test) if (objects.length === 0) { for (const x of args) if (x !== args[0]) return false return true } if (objects.length !== args.length) return false + if (objects.some(Array.isArray) && !objects.every(Array.isArray)) return false const allKeys = new Set(objects.flatMap((x) => Object.keys(x))) for (const key of allKeys) { for (const x of objects) { diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index b9ebb6296..97196cd1c 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -1,9 +1,10 @@ -import { ServiceInterfaceType } from "../types" +import { PackageId, ServiceInterfaceId, ServiceInterfaceType } from "../types" import { knownProtocols } from "../interfaces/Host" import { AddressInfo, Host, Hostname, HostnameInfo } from "../types" import { Effects } from "../Effects" import { DropGenerator, DropPromise } from "./Drop" import { IpAddress, IPV6_LINK_LOCAL } from "./ip" +import { deepEqual } from "./deepEqual" export type UrlString = string export type HostId = string @@ -227,7 +228,7 @@ function filterRec( (kind.has("ipv4") && h.kind === "ip" && h.hostname.kind === "ipv4") || (kind.has("ipv6") && h.kind === "ip" && h.hostname.kind === "ipv6") || (kind.has("localhost") && - ["localhost", "127.0.0.1", "[::1]"].includes(h.hostname.value)) || + ["localhost", "127.0.0.1", "::1"].includes(h.hostname.value)) || (kind.has("link-local") && h.kind === "ip" && h.hostname.kind === "ipv6" && @@ -328,28 +329,28 @@ const makeInterfaceFilled = async ({ return interfaceFilled } -export class GetServiceInterface { +export class GetServiceInterface { constructor( readonly effects: Effects, readonly opts: { id: string; packageId?: string }, + readonly map: (interfaces: ServiceInterfaceFilled | null) => Mapped, + readonly eq: (a: Mapped, b: Mapped) => boolean, ) {} /** * Returns the requested service interface. Reruns the context from which it has been called if the underlying value changes */ async const() { - const { id, packageId } = this.opts - const callback = - this.effects.constRetry && - (() => this.effects.constRetry && this.effects.constRetry()) - const interfaceFilled = await makeInterfaceFilled({ - effects: this.effects, - id, - packageId, - callback, - }) - - return interfaceFilled + let abort = new AbortController() + const watch = this.watch(abort.signal) + const res = await watch.next() + if (this.effects.constRetry) { + watch.next().then(() => { + abort.abort() + this.effects.constRetry && this.effects.constRetry() + }) + } + return res.value } /** * Returns the requested service interface. Does nothing if the value changes @@ -362,10 +363,11 @@ export class GetServiceInterface { packageId, }) - return interfaceFilled + return this.map(interfaceFilled) } private async *watchGen(abort?: AbortSignal) { + let prev = null as { value: Mapped } | null const { id, packageId } = this.opts const resolveCell = { resolve: () => {} } this.effects.onLeaveContext(() => { @@ -378,12 +380,17 @@ export class GetServiceInterface { callback = resolve resolveCell.resolve = resolve }) - yield await makeInterfaceFilled({ - effects: this.effects, - id, - packageId, - callback, - }) + const next = this.map( + await makeInterfaceFilled({ + effects: this.effects, + id, + packageId, + callback, + }), + ) + if (!prev || !this.eq(prev.value, next)) { + yield next + } await waitForNext } } @@ -391,9 +398,7 @@ export class GetServiceInterface { /** * Watches the requested service interface. Returns an async iterator that yields whenever the value changes */ - watch( - abort?: AbortSignal, - ): AsyncGenerator { + watch(abort?: AbortSignal): AsyncGenerator { const ctrl = new AbortController() abort?.addEventListener("abort", () => ctrl.abort()) return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort()) @@ -404,7 +409,7 @@ export class GetServiceInterface { */ onChange( callback: ( - value: ServiceInterfaceFilled | null, + value: Mapped | null, error?: Error, ) => { cancel: boolean } | Promise<{ cancel: boolean }>, ) { @@ -437,9 +442,7 @@ export class GetServiceInterface { /** * Watches the requested service interface. Returns when the predicate is true */ - waitFor( - pred: (value: ServiceInterfaceFilled | null) => boolean, - ): Promise { + waitFor(pred: (value: Mapped) => boolean): Promise { const ctrl = new AbortController() return DropPromise.of( Promise.resolve().then(async () => { @@ -448,15 +451,57 @@ export class GetServiceInterface { return next } } - return null + throw new Error("context left before predicate passed") }), () => ctrl.abort(), ) } } + +export function getOwnServiceInterface( + effects: Effects, + id: ServiceInterfaceId, +): GetServiceInterface +export function getOwnServiceInterface( + effects: Effects, + id: ServiceInterfaceId, + map: (interfaces: ServiceInterfaceFilled | null) => Mapped, + eq?: (a: Mapped, b: Mapped) => boolean, +): GetServiceInterface +export function getOwnServiceInterface( + effects: Effects, + id: ServiceInterfaceId, + map?: (interfaces: ServiceInterfaceFilled | null) => Mapped, + eq?: (a: Mapped, b: Mapped) => boolean, +): GetServiceInterface { + return new GetServiceInterface( + effects, + { id }, + map ?? ((a) => a as Mapped), + eq ?? ((a, b) => deepEqual(a, b)), + ) +} + export function getServiceInterface( effects: Effects, - opts: { id: string; packageId?: string }, -) { - return new GetServiceInterface(effects, opts) + opts: { id: ServiceInterfaceId; packageId: PackageId }, +): GetServiceInterface +export function getServiceInterface( + effects: Effects, + opts: { id: ServiceInterfaceId; packageId: PackageId }, + map: (interfaces: ServiceInterfaceFilled | null) => Mapped, + eq?: (a: Mapped, b: Mapped) => boolean, +): GetServiceInterface +export function getServiceInterface( + effects: Effects, + opts: { id: ServiceInterfaceId; packageId: PackageId }, + map?: (interfaces: ServiceInterfaceFilled | null) => Mapped, + eq?: (a: Mapped, b: Mapped) => boolean, +): GetServiceInterface { + return new GetServiceInterface( + effects, + opts, + map ?? ((a) => a as Mapped), + eq ?? ((a, b) => deepEqual(a, b)), + ) } diff --git a/sdk/base/lib/util/getServiceInterfaces.ts b/sdk/base/lib/util/getServiceInterfaces.ts index 34636981a..b4bf8fc39 100644 --- a/sdk/base/lib/util/getServiceInterfaces.ts +++ b/sdk/base/lib/util/getServiceInterfaces.ts @@ -1,4 +1,6 @@ import { Effects } from "../Effects" +import { PackageId } from "../osBindings" +import { deepEqual } from "./deepEqual" import { DropGenerator, DropPromise } from "./Drop" import { ServiceInterfaceFilled, @@ -41,28 +43,28 @@ const makeManyInterfaceFilled = async ({ return serviceInterfacesFilled } -export class GetServiceInterfaces { +export class GetServiceInterfaces { constructor( readonly effects: Effects, readonly opts: { packageId?: string }, + readonly map: (interfaces: ServiceInterfaceFilled[]) => Mapped, + readonly eq: (a: Mapped, b: Mapped) => boolean, ) {} /** * Returns the service interfaces for the package. Reruns the context from which it has been called if the underlying value changes */ async const() { - const { packageId } = this.opts - const callback = - this.effects.constRetry && - (() => this.effects.constRetry && this.effects.constRetry()) - const interfaceFilled: ServiceInterfaceFilled[] = - await makeManyInterfaceFilled({ - effects: this.effects, - packageId, - callback, + let abort = new AbortController() + const watch = this.watch(abort.signal) + const res = await watch.next() + if (this.effects.constRetry) { + watch.next().then(() => { + abort.abort() + this.effects.constRetry && this.effects.constRetry() }) - - return interfaceFilled + } + return res.value } /** * Returns the service interfaces for the package. Does nothing if the value changes @@ -75,10 +77,11 @@ export class GetServiceInterfaces { packageId, }) - return interfaceFilled + return this.map(interfaceFilled) } private async *watchGen(abort?: AbortSignal) { + let prev = null as { value: Mapped } | null const { packageId } = this.opts const resolveCell = { resolve: () => {} } this.effects.onLeaveContext(() => { @@ -91,11 +94,16 @@ export class GetServiceInterfaces { callback = resolve resolveCell.resolve = resolve }) - yield await makeManyInterfaceFilled({ - effects: this.effects, - packageId, - callback, - }) + const next = this.map( + await makeManyInterfaceFilled({ + effects: this.effects, + packageId, + callback, + }), + ) + if (!prev || !this.eq(prev.value, next)) { + yield next + } await waitForNext } } @@ -103,9 +111,7 @@ export class GetServiceInterfaces { /** * Watches the service interfaces for the package. Returns an async iterator that yields whenever the value changes */ - watch( - abort?: AbortSignal, - ): AsyncGenerator { + watch(abort?: AbortSignal): AsyncGenerator { const ctrl = new AbortController() abort?.addEventListener("abort", () => ctrl.abort()) return DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort()) @@ -116,7 +122,7 @@ export class GetServiceInterfaces { */ onChange( callback: ( - value: ServiceInterfaceFilled[] | null, + value: Mapped | null, error?: Error, ) => { cancel: boolean } | Promise<{ cancel: boolean }>, ) { @@ -149,9 +155,7 @@ export class GetServiceInterfaces { /** * Watches the service interfaces for the package. Returns when the predicate is true */ - waitFor( - pred: (value: ServiceInterfaceFilled[] | null) => boolean, - ): Promise { + waitFor(pred: (value: Mapped) => boolean): Promise { const ctrl = new AbortController() return DropPromise.of( Promise.resolve().then(async () => { @@ -160,15 +164,52 @@ export class GetServiceInterfaces { return next } } - return null + throw new Error("context left before predicate passed") }), () => ctrl.abort(), ) } } + +export function getOwnServiceInterfaces(effects: Effects): GetServiceInterfaces +export function getOwnServiceInterfaces( + effects: Effects, + map: (interfaces: ServiceInterfaceFilled[]) => Mapped, + eq?: (a: Mapped, b: Mapped) => boolean, +): GetServiceInterfaces +export function getOwnServiceInterfaces( + effects: Effects, + map?: (interfaces: ServiceInterfaceFilled[]) => Mapped, + eq?: (a: Mapped, b: Mapped) => boolean, +): GetServiceInterfaces { + return new GetServiceInterfaces( + effects, + {}, + map ?? ((a) => a as Mapped), + eq ?? ((a, b) => deepEqual(a, b)), + ) +} + export function getServiceInterfaces( effects: Effects, - opts: { packageId?: string }, -) { - return new GetServiceInterfaces(effects, opts) + opts: { packageId: PackageId }, +): GetServiceInterfaces +export function getServiceInterfaces( + effects: Effects, + opts: { packageId: PackageId }, + map: (interfaces: ServiceInterfaceFilled[]) => Mapped, + eq?: (a: Mapped, b: Mapped) => boolean, +): GetServiceInterfaces +export function getServiceInterfaces( + effects: Effects, + opts: { packageId: PackageId }, + map?: (interfaces: ServiceInterfaceFilled[]) => Mapped, + eq?: (a: Mapped, b: Mapped) => boolean, +): GetServiceInterfaces { + return new GetServiceInterfaces( + effects, + opts, + map ?? ((a) => a as Mapped), + eq ?? ((a, b) => deepEqual(a, b)), + ) } diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 8510b251f..6fef55afb 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -60,6 +60,11 @@ import { setupOnUninit, } from "../../base/lib/inits" import { DropGenerator } from "../../base/lib/util/Drop" +import { + getOwnServiceInterface, + ServiceInterfaceFilled, +} from "../../base/lib/util/getServiceInterface" +import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces" export const OSVersion = testTypeVersion("0.4.0-alpha.16") @@ -170,20 +175,10 @@ export class StartSdk { packageIds?: DependencyId[], ) => Promise>, serviceInterface: { - getOwn: (effects: E, id: ServiceInterfaceId) => - getServiceInterface(effects, { - id, - }), - get: ( - effects: E, - opts: { id: ServiceInterfaceId; packageId: PackageId }, - ) => getServiceInterface(effects, opts), - getAllOwn: (effects: E) => - getServiceInterfaces(effects, {}), - getAll: ( - effects: E, - opts: { packageId: PackageId }, - ) => getServiceInterfaces(effects, opts), + getOwn: getOwnServiceInterface, + get: getServiceInterface, + getAllOwn: getOwnServiceInterfaces, + getAll: getServiceInterfaces, }, getContainerIp: ( effects: T.Effects, diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index 1bf2766b9..ad4d9970a 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -4,38 +4,11 @@ import * as TOML from "@iarna/toml" import * as INI from "ini" import * as T from "../../../base/lib/types" import * as fs from "node:fs/promises" -import { asError } from "../../../base/lib/util" +import { asError, deepEqual } from "../../../base/lib/util" import { DropGenerator, DropPromise } from "../../../base/lib/util/Drop" const previousPath = /(.+?)\/([^/]*)$/ -const deepEq = (left: unknown, right: unknown) => { - if (left === right) return true - if (Array.isArray(left) && Array.isArray(right)) { - if (left.length === right.length) { - for (const idx in left) { - if (!deepEq(left[idx], right[idx])) return false - } - return true - } - } else if ( - typeof left === "object" && - typeof right === "object" && - left && - right - ) { - const keys = new Set([ - ...(Object.keys(left) as (keyof typeof left)[]), - ...(Object.keys(right) as (keyof typeof right)[]), - ]) - for (let key of keys) { - if (!deepEq(left[key], right[key])) return false - } - return true - } - return false -} - const exists = (path: string) => fs.access(path).then( () => true, @@ -374,7 +347,7 @@ export class FileHelper { eq?: (left: any, right: any) => boolean, ): ReadType { map = map ?? ((a: A) => a) - eq = eq ?? deepEq + eq = eq ?? deepEqual return { once: () => this.readOnce(map), const: (effects: T.Effects) => this.readConst(effects, map, eq),