add error status (#2746)

* add error status

* update types

* ṗ̶̰̙̓͒̈́ͅü̵̢̙̫̣ŗ̷̪̺̺͛g̴̲͉͎̬̒̇e̵̪̎̅͌ ̶̡̜̘͐͛t̶͎͍̣̿̍̐h̴͕̩͗̈́̎̑e̵͚͒̂͝ ̸̛͙̦͈͝v̶̱͙̬̽̔ọ̶̧̡̒̓i̸̬̲͍̋̈́d̴͉̀

* fix some extra voids

* add `package.rebuild`

* introduce error status and pkg rebuild and fix mocks

* minor fixes

* fix build

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2024-09-26 20:19:06 -06:00
committed by GitHub
parent db0695126f
commit e7fa94c3d3
49 changed files with 642 additions and 413 deletions

View File

@@ -31,14 +31,14 @@ export type Effects = {
constRetry: () => void
clearCallbacks: (
options: { only: number[] } | { except: number[] },
) => Promise<void>
) => Promise<null>
// action
action: {
/** Define an action that can be invoked by a user or service */
export(options: { id: ActionId; metadata: ActionMetadata }): Promise<void>
export(options: { id: ActionId; metadata: ActionMetadata }): Promise<null>
/** Remove all exported actions */
clear(options: { except: ActionId[] }): Promise<void>
clear(options: { except: ActionId[] }): Promise<null>
getInput(options: {
packageId?: PackageId
actionId: ActionId
@@ -50,23 +50,23 @@ export type Effects = {
}): Promise<ActionResult | null>
request<Input extends Record<string, unknown>>(
options: RequestActionParams,
): Promise<void>
): Promise<null>
clearRequests(
options: { only: ActionId[] } | { except: ActionId[] },
): Promise<void>
): Promise<null>
}
// control
/** restart this service's main function */
restart(): Promise<void>
restart(): Promise<null>
/** stop this service's main function */
shutdown(): Promise<void>
shutdown(): Promise<null>
/** indicate to the host os what runstate the service is in */
setMainStatus(options: SetMainStatus): Promise<void>
setMainStatus(options: SetMainStatus): Promise<null>
// dependency
/** Set the dependencies of what the service needs, usually run during the inputSpec action as a best practice */
setDependencies(options: { dependencies: Dependencies }): Promise<void>
setDependencies(options: { dependencies: Dependencies }): Promise<null>
/** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */
getDependencies(): Promise<DependencyRequirement[]>
/** Test whether current dependency requirements are satisfied */
@@ -86,11 +86,11 @@ export type Effects = {
/** Returns a list of the ids of all installed packages */
getInstalledPackages(): Promise<string[]>
/** grants access to certain paths in the store to dependents */
exposeForDependents(options: { paths: string[] }): Promise<void>
exposeForDependents(options: { paths: string[] }): Promise<null>
// health
/** sets the result of a health check */
setHealth(o: SetHealth): Promise<void>
setHealth(o: SetHealth): Promise<null>
// subcontainer
subcontainer: {
@@ -100,13 +100,13 @@ export type Effects = {
name: string | null
}): Promise<[string, string]>
/** A low level api used by SubContainer */
destroyFs(options: { guid: string }): Promise<void>
destroyFs(options: { guid: string }): Promise<null>
}
// net
// bind
/** Creates a host connected to the specified port with the provided options */
bind(options: BindParams): Promise<void>
bind(options: BindParams): Promise<null>
/** Get the port address for a service */
getServicePortForward(options: {
packageId?: PackageId
@@ -116,7 +116,7 @@ export type Effects = {
/** Removes all network bindings, called in the setupInputSpec */
clearBindings(options: {
except: { id: HostId; internalPort: number }[]
}): Promise<void>
}): Promise<null>
// host
/** Returns information about the specified host, if it exists */
getHostInfo(options: {
@@ -134,7 +134,7 @@ export type Effects = {
getContainerIp(): Promise<string>
// interface
/** Creates an interface bound to a specific host and port to show to the user */
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<void>
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<null>
/** Returns an exported service interface */
getServiceInterface(options: {
packageId?: PackageId
@@ -149,7 +149,7 @@ export type Effects = {
/** Removes all service interfaces */
clearServiceInterfaces(options: {
except: ServiceInterfaceId[]
}): Promise<void>
}): Promise<null>
// ssl
/** Returns a PEM encoded fullchain for the hostnames specified */
getSslCertificate: (options: {
@@ -178,10 +178,10 @@ export type Effects = {
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
path: StorePath
value: ExtractStore
}): Promise<void>
}): Promise<null>
}
/** sets the version that this service's data has been migrated to */
setDataVersion(options: { version: string }): Promise<void>
setDataVersion(options: { version: string }): Promise<null>
/** returns the version that this service's data has been migrated to */
getDataVersion(): Promise<string | null>

View File

@@ -1,6 +1,7 @@
import { InputSpec } from "./input/builder"
import { ExtractInputSpecType } from "./input/builder/inputSpec"
import * as T from "../types"
import { once } from "../util"
export type Run<
A extends
@@ -130,21 +131,19 @@ export class Actions<
): Actions<Store, AllActions & { [id in A["id"]]: A }> {
return new Actions({ ...this.actions, [action.id]: action })
}
update(options: { effects: T.Effects }): Promise<void> {
const updater = async (options: { effects: T.Effects }) => {
for (let action of Object.values(this.actions)) {
await action.exportMetadata(options)
}
await options.effects.action.clear({ except: Object.keys(this.actions) })
async update(options: { effects: T.Effects }): Promise<null> {
options.effects = {
...options.effects,
constRetry: once(() => {
this.update(options) // yes, this reuses the options object, but the const retry function will be overwritten each time, so the once-ness is not a problem
}),
}
const updaterCtx = { options }
updaterCtx.options = {
effects: {
...options.effects,
constRetry: () => updater(updaterCtx.options),
},
for (let action of Object.values(this.actions)) {
await action.exportMetadata(options)
}
return updater(updaterCtx.options)
await options.effects.action.clear({ except: Object.keys(this.actions) })
return null
}
get<Id extends T.ActionId>(actionId: Id): AllActions[Id] {
return this.actions[actionId]

View File

@@ -13,15 +13,15 @@ export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
) => boolean
satisfied: () => boolean
throwIfInstalledNotSatisfied: (packageId: DependencyId) => void
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => void
throwIfRunningNotSatisfied: (packageId: DependencyId) => void
throwIfActionsNotSatisfied: (packageId: DependencyId) => void
throwIfInstalledNotSatisfied: (packageId: DependencyId) => null
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => null
throwIfRunningNotSatisfied: (packageId: DependencyId) => null
throwIfActionsNotSatisfied: (packageId: DependencyId) => null
throwIfHealthNotSatisfied: (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => void
throwIfNotSatisfied: (packageId?: DependencyId) => void
) => null
throwIfNotSatisfied: (packageId?: DependencyId) => null
}
export async function checkDependencies<
DependencyId extends PackageId = PackageId,
@@ -100,6 +100,7 @@ export async function checkDependencies<
if (!dep.result.installedVersion) {
throw new Error(`${dep.result.title || packageId} is not installed`)
}
return null
}
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
@@ -117,12 +118,14 @@ export async function checkDependencies<
`Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`,
)
}
return null
}
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
throw new Error(`${dep.result.title || packageId} is not running`)
}
return null
}
const throwIfActionsNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
@@ -132,6 +135,7 @@ export async function checkDependencies<
`The following action requests have not been fulfilled: ${reqs.join(", ")}`,
)
}
return null
}
const throwIfHealthNotSatisfied = (
packageId: DependencyId,
@@ -158,6 +162,7 @@ export async function checkDependencies<
.join("; "),
)
}
return null
}
const throwIfPkgNotSatisfied = (packageId: DependencyId) => {
throwIfInstalledNotSatisfied(packageId)
@@ -165,6 +170,7 @@ export async function checkDependencies<
throwIfRunningNotSatisfied(packageId)
throwIfActionsNotSatisfied(packageId)
throwIfHealthNotSatisfied(packageId)
return null
}
const throwIfNotSatisfied = (packageId?: DependencyId) =>
packageId
@@ -182,6 +188,7 @@ export async function checkDependencies<
if (err.length) {
throw new Error(err.join("; "))
}
return null
})()
return {

View File

@@ -1,4 +1,5 @@
import * as T from "../types"
import { once } from "../util"
import { Dependency } from "./Dependency"
type DependencyType<Manifest extends T.Manifest> = {
@@ -17,40 +18,38 @@ type DependencyType<Manifest extends T.Manifest> = {
export function setupDependencies<Manifest extends T.Manifest>(
fn: (options: { effects: T.Effects }) => Promise<DependencyType<Manifest>>,
): (options: { effects: T.Effects }) => Promise<void> {
return (options: { effects: T.Effects }) => {
const updater = async (options: { effects: T.Effects }) => {
const dependencyType = await fn(options)
return await options.effects.setDependencies({
dependencies: Object.entries(dependencyType).map(
([
id,
{
data: { versionRange, ...x },
},
]) => ({
id,
...x,
...(x.type === "running"
? {
kind: "running",
healthChecks: x.healthChecks,
}
: {
kind: "exists",
}),
versionRange: versionRange.toString(),
}),
),
})
): (options: { effects: T.Effects }) => Promise<null> {
const cell = { updater: async (_: { effects: T.Effects }) => null }
cell.updater = async (options: { effects: T.Effects }) => {
options.effects = {
...options.effects,
constRetry: once(() => {
cell.updater(options)
}),
}
const updaterCtx = { options }
updaterCtx.options = {
effects: {
...options.effects,
constRetry: () => updater(updaterCtx.options),
},
}
return updater(updaterCtx.options)
const dependencyType = await fn(options)
return await options.effects.setDependencies({
dependencies: Object.entries(dependencyType).map(
([
id,
{
data: { versionRange, ...x },
},
]) => ({
id,
...x,
...(x.type === "running"
? {
kind: "running",
healthChecks: x.healthChecks,
}
: {
kind: "exists",
}),
versionRange: versionRange.toString(),
}),
),
})
}
return cell.updater
}

View File

@@ -1,4 +1,5 @@
import * as T from "../types"
import { once } from "../util"
import { AddressReceipt } from "./AddressReceipt"
declare const UpdateServiceInterfacesProof: unique symbol
@@ -21,34 +22,36 @@ export const setupServiceInterfaces: SetupServiceInterfaces = <
Output extends ServiceInterfacesReceipt,
>(
fn: SetServiceInterfaces<Output>,
) =>
((options: { effects: T.Effects }) => {
const updater = async (options: { effects: T.Effects }) => {
const bindings: T.BindId[] = []
const interfaces: T.ServiceInterfaceId[] = []
const res = await fn({
effects: {
...options.effects,
bind: (params: T.BindParams) => {
bindings.push({ id: params.id, internalPort: params.internalPort })
return options.effects.bind(params)
},
exportServiceInterface: (params: T.ExportServiceInterfaceParams) => {
interfaces.push(params.id)
return options.effects.exportServiceInterface(params)
},
},
})
await options.effects.clearBindings({ except: bindings })
await options.effects.clearServiceInterfaces({ except: interfaces })
return res
) => {
const cell = {
updater: (async (options: { effects: T.Effects }) =>
[] as any as Output) as UpdateServiceInterfaces<Output>,
}
cell.updater = (async (options: { effects: T.Effects }) => {
options.effects = {
...options.effects,
constRetry: once(() => {
cell.updater(options)
}),
}
const updaterCtx = { options }
updaterCtx.options = {
const bindings: T.BindId[] = []
const interfaces: T.ServiceInterfaceId[] = []
const res = await fn({
effects: {
...options.effects,
constRetry: () => updater(updaterCtx.options),
bind: (params: T.BindParams) => {
bindings.push({ id: params.id, internalPort: params.internalPort })
return options.effects.bind(params)
},
exportServiceInterface: (params: T.ExportServiceInterfaceParams) => {
interfaces.push(params.id)
return options.effects.exportServiceInterface(params)
},
},
}
return updater(updaterCtx.options)
})
await options.effects.clearBindings({ except: bindings })
await options.effects.clearServiceInterfaces({ except: interfaces })
return res
}) as UpdateServiceInterfaces<Output>
return cell.updater
}

View File

@@ -4,6 +4,12 @@ import type { NamedHealthCheckResult } from "./NamedHealthCheckResult"
import type { StartStop } from "./StartStop"
export type MainStatus =
| {
main: "error"
onRebuild: StartStop
message: string
debug: string | null
}
| { main: "stopped" }
| { main: "restarting" }
| { main: "restoring" }

View File

@@ -1,6 +1,6 @@
import { DataUrl, Manifest, MerkleArchiveCommitment } from "../osBindings"
import { ArrayBufferReader, MerkleArchive } from "./merkleArchive"
import mime from "mime"
import mime from "mime-types"
const magicAndVersion = new Uint8Array([59, 59, 2])
@@ -52,13 +52,14 @@ export class S9pk {
async icon(): Promise<DataUrl> {
const iconName = Object.keys(this.archive.contents.contents).find(
(name) =>
name.startsWith("icon.") && mime.getType(name)?.startsWith("image/"),
name.startsWith("icon.") &&
(mime.contentType(name) || null)?.startsWith("image/"),
)
if (!iconName) {
throw new Error("no icon found in archive")
}
return (
`data:${mime.getType(iconName)};base64,` +
`data:${mime.contentType(iconName)};base64,` +
Buffer.from(
await this.archive.contents.getPath([iconName])!.verifiedFileContents(),
).toString("base64")

View File

@@ -54,7 +54,7 @@ export namespace ExpectedExports {
*/
export type main = (options: {
effects: Effects
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
}) => Promise<DaemonBuildable>
/**
@@ -118,7 +118,7 @@ export type DaemonReceipt = {
}
export type Daemon = {
wait(): Promise<string>
term(): Promise<void>
term(): Promise<null>
[DaemonProof]: never
}
@@ -135,7 +135,7 @@ export type CommandType = string | [string, ...string[]]
export type DaemonReturned = {
wait(): Promise<unknown>
term(options?: { signal?: Signals; timeout?: number }): Promise<void>
term(options?: { signal?: Signals; timeout?: number }): Promise<null>
}
export declare const hostName: unique symbol

View File

@@ -1,17 +1,17 @@
import { boolean } from "ts-matches"
export type Vertex<VMetadata = void, EMetadata = void> = {
export type Vertex<VMetadata = null, EMetadata = null> = {
metadata: VMetadata
edges: Array<Edge<EMetadata, VMetadata>>
}
export type Edge<EMetadata = void, VMetadata = void> = {
export type Edge<EMetadata = null, VMetadata = null> = {
metadata: EMetadata
from: Vertex<VMetadata, EMetadata>
to: Vertex<VMetadata, EMetadata>
}
export class Graph<VMetadata = void, EMetadata = void> {
export class Graph<VMetadata = null, EMetadata = null> {
private readonly vertices: Array<Vertex<VMetadata, EMetadata>> = []
constructor() {}
addVertex(
@@ -46,7 +46,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
}
findVertex(
predicate: (vertex: Vertex<VMetadata, EMetadata>) => boolean,
): Generator<Vertex<VMetadata, EMetadata>, void> {
): Generator<Vertex<VMetadata, EMetadata>, null> {
const veritces = this.vertices
function* gen() {
for (let vertex of veritces) {
@@ -54,6 +54,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
yield vertex
}
}
return null
}
return gen()
}
@@ -75,13 +76,13 @@ export class Graph<VMetadata = void, EMetadata = void> {
from:
| Vertex<VMetadata, EMetadata>
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
): Generator<Vertex<VMetadata, EMetadata>, void> {
): Generator<Vertex<VMetadata, EMetadata>, null> {
const visited: Array<Vertex<VMetadata, EMetadata>> = []
function* rec(
vertex: Vertex<VMetadata, EMetadata>,
): Generator<Vertex<VMetadata, EMetadata>, void> {
): Generator<Vertex<VMetadata, EMetadata>, null> {
if (visited.includes(vertex)) {
return
return null
}
visited.push(vertex)
yield vertex
@@ -99,6 +100,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
}
}
}
return null
}
if (from instanceof Function) {
@@ -115,6 +117,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
}
}
}
return null
})()
} else {
return rec(from)
@@ -124,13 +127,13 @@ export class Graph<VMetadata = void, EMetadata = void> {
to:
| Vertex<VMetadata, EMetadata>
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
): Generator<Vertex<VMetadata, EMetadata>, void> {
): Generator<Vertex<VMetadata, EMetadata>, null> {
const visited: Array<Vertex<VMetadata, EMetadata>> = []
function* rec(
vertex: Vertex<VMetadata, EMetadata>,
): Generator<Vertex<VMetadata, EMetadata>, void> {
): Generator<Vertex<VMetadata, EMetadata>, null> {
if (visited.includes(vertex)) {
return
return null
}
visited.push(vertex)
yield vertex
@@ -148,6 +151,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
}
}
}
return null
}
if (to instanceof Function) {
@@ -164,6 +168,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
}
}
}
return null
})()
} else {
return rec(to)
@@ -176,7 +181,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
to:
| Vertex<VMetadata, EMetadata>
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
): Array<Edge<EMetadata, VMetadata>> | void {
): Array<Edge<EMetadata, VMetadata>> | null {
const isDone =
to instanceof Function
? to
@@ -186,12 +191,12 @@ export class Graph<VMetadata = void, EMetadata = void> {
function* check(
vertex: Vertex<VMetadata, EMetadata>,
path: Array<Edge<EMetadata, VMetadata>>,
): Generator<undefined, Array<Edge<EMetadata, VMetadata>> | undefined> {
): Generator<undefined, Array<Edge<EMetadata, VMetadata>> | null> {
if (isDone(vertex)) {
return path
}
if (visited.includes(vertex)) {
return
return null
}
visited.push(vertex)
yield
@@ -213,6 +218,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
}
}
}
return null
}
if (from instanceof Function) {
@@ -240,5 +246,6 @@ export class Graph<VMetadata = void, EMetadata = void> {
}
}
}
return null
}
}