mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
add map & eq to getServiceInterface
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user