add map & eq to getServiceInterface

This commit is contained in:
Aiden McClelland
2025-12-17 19:14:21 -07:00
parent 7b3c74179b
commit 5a9510238e
5 changed files with 160 additions and 106 deletions

View File

@@ -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) {

View File

@@ -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<Mapped = ServiceInterfaceFilled | null> {
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,
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 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({
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<ServiceInterfaceFilled | null, void, unknown> {
watch(abort?: AbortSignal): AsyncGenerator<Mapped, void, unknown> {
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<ServiceInterfaceFilled | null> {
waitFor(pred: (value: Mapped) => boolean): Promise<Mapped> {
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<Mapped>(
effects: Effects,
id: ServiceInterfaceId,
map: (interfaces: ServiceInterfaceFilled | null) => Mapped,
eq?: (a: Mapped, b: Mapped) => boolean,
): GetServiceInterface<Mapped>
export function getOwnServiceInterface<Mapped>(
effects: Effects,
id: ServiceInterfaceId,
map?: (interfaces: ServiceInterfaceFilled | null) => Mapped,
eq?: (a: Mapped, b: Mapped) => boolean,
): GetServiceInterface<Mapped> {
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<Mapped>(
effects: Effects,
opts: { id: ServiceInterfaceId; packageId: PackageId },
map: (interfaces: ServiceInterfaceFilled | null) => Mapped,
eq?: (a: Mapped, b: Mapped) => boolean,
): GetServiceInterface<Mapped>
export function getServiceInterface<Mapped>(
effects: Effects,
opts: { id: ServiceInterfaceId; packageId: PackageId },
map?: (interfaces: ServiceInterfaceFilled | null) => Mapped,
eq?: (a: Mapped, b: Mapped) => boolean,
): GetServiceInterface<Mapped> {
return new GetServiceInterface(
effects,
opts,
map ?? ((a) => a as Mapped),
eq ?? ((a, b) => deepEqual(a, b)),
)
}

View File

@@ -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<Mapped = ServiceInterfaceFilled[]> {
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({
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<ServiceInterfaceFilled[], void, unknown> {
watch(abort?: AbortSignal): AsyncGenerator<Mapped, void, unknown> {
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<ServiceInterfaceFilled[] | null> {
waitFor(pred: (value: Mapped) => boolean): Promise<Mapped> {
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<Mapped>(
effects: Effects,
map: (interfaces: ServiceInterfaceFilled[]) => Mapped,
eq?: (a: Mapped, b: Mapped) => boolean,
): GetServiceInterfaces<Mapped>
export function getOwnServiceInterfaces<Mapped>(
effects: Effects,
map?: (interfaces: ServiceInterfaceFilled[]) => Mapped,
eq?: (a: Mapped, b: Mapped) => boolean,
): GetServiceInterfaces<Mapped> {
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<Mapped>(
effects: Effects,
opts: { packageId: PackageId },
map: (interfaces: ServiceInterfaceFilled[]) => Mapped,
eq?: (a: Mapped, b: Mapped) => boolean,
): GetServiceInterfaces<Mapped>
export function getServiceInterfaces<Mapped>(
effects: Effects,
opts: { packageId: PackageId },
map?: (interfaces: ServiceInterfaceFilled[]) => Mapped,
eq?: (a: Mapped, b: Mapped) => boolean,
): GetServiceInterfaces<Mapped> {
return new GetServiceInterfaces(
effects,
opts,
map ?? ((a) => a as Mapped),
eq ?? ((a, b) => deepEqual(a, b)),
)
}

View File

@@ -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<Manifest extends T.SDKManifest> {
packageIds?: DependencyId[],
) => Promise<CheckDependencies<DependencyId>>,
serviceInterface: {
getOwn: <E extends Effects>(effects: E, id: ServiceInterfaceId) =>
getServiceInterface(effects, {
id,
}),
get: <E extends Effects>(
effects: E,
opts: { id: ServiceInterfaceId; packageId: PackageId },
) => getServiceInterface(effects, opts),
getAllOwn: <E extends Effects>(effects: E) =>
getServiceInterfaces(effects, {}),
getAll: <E extends Effects>(
effects: E,
opts: { packageId: PackageId },
) => getServiceInterfaces(effects, opts),
getOwn: getOwnServiceInterface,
get: getServiceInterface,
getAllOwn: getOwnServiceInterfaces,
getAll: getServiceInterfaces,
},
getContainerIp: (
effects: T.Effects,

View File

@@ -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<keyof typeof left | keyof typeof right>([
...(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<A> {
eq?: (left: any, right: any) => boolean,
): ReadType<any> {
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),