From 8fdeeab5bbbb7d2f8bfcdf6f30ace678dbe26b6d Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 5 Mar 2026 15:12:50 -0700 Subject: [PATCH] sdk beta.56 --- container-runtime/package-lock.json | 2 +- core/CLAUDE.md | 1 + sdk/package/lib/StartSdk.ts | 100 +++++++++++++++++++++++++++- sdk/package/package-lock.json | 4 +- sdk/package/package.json | 2 +- 5 files changed, 104 insertions(+), 5 deletions(-) diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 7ff3f092f..d550f2972 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -37,7 +37,7 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.55", + "version": "0.4.0-beta.56", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/core/CLAUDE.md b/core/CLAUDE.md index dbb053348..883e68991 100644 --- a/core/CLAUDE.md +++ b/core/CLAUDE.md @@ -25,3 +25,4 @@ cd sdk && make baseDist dist # Rebuild SDK after ts-bindings - When adding i18n keys, add all 5 locales in `core/locales/i18n.yaml` (see [i18n-patterns.md](i18n-patterns.md)) - When using DB watches, follow the `TypedDbWatch` patterns in [patchdb.md](patchdb.md) - **Always use `.invoke(ErrorKind::...)` instead of `.status()` when running CLI commands** via `tokio::process::Command`. The `Invoke` trait (from `crate::util::Invoke`) captures stdout/stderr and checks exit codes properly. Using `.status()` leaks stderr directly to system logs, creating noise. For check-then-act patterns (e.g. `iptables -C`), use `.invoke(...).await.is_ok()` / `.is_err()` instead of `.status().await.map_or(false, |s| s.success())`. +- Always use file utils in util::io instead of tokio::fs when available diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 755425ccd..9450e6b8e 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -141,6 +141,7 @@ export class StartSdk { | 'getSystemSmtp' | 'getOutboundGateway' | 'getContainerIp' + | 'getStatus' | 'getDataVersion' | 'setDataVersion' | 'getServiceManifest' @@ -164,7 +165,6 @@ export class StartSdk { getSslKey: (effects, ...args) => effects.getSslKey(...args), shutdown: (effects, ...args) => effects.shutdown(...args), getDependencies: (effects, ...args) => effects.getDependencies(...args), - getStatus: (effects, ...args) => effects.getStatus(...args), setHealth: (effects, ...args) => effects.setHealth(...args), } @@ -342,6 +342,104 @@ export class StartSdk { } }, + /** + * Get the service's current status with reactive subscription support. + * + * Returns an object with multiple read strategies: `const()` for a value + * that retries on change, `once()` for a single read, `watch()` for an async + * generator, `onChange()` for a callback, and `waitFor()` to block until a predicate is met. + * + * @param effects - The effects context + * @param options - Optional filtering options (e.g. `packageId`) + */ + getStatus: ( + effects: T.Effects, + options: Omit[0], 'callback'> = {}, + ) => { + async function* watch(abort?: AbortSignal) { + const resolveCell = { resolve: () => {} } + effects.onLeaveContext(() => { + resolveCell.resolve() + }) + abort?.addEventListener('abort', () => resolveCell.resolve()) + while (effects.isInContext && !abort?.aborted) { + let callback: () => void = () => {} + const waitForNext = new Promise((resolve) => { + callback = resolve + resolveCell.resolve = resolve + }) + yield await effects.getStatus({ ...options, callback }) + await waitForNext + } + } + return { + const: () => + effects.getStatus({ + ...options, + callback: + effects.constRetry && + (() => effects.constRetry && effects.constRetry()), + }), + once: () => effects.getStatus(options), + watch: (abort?: AbortSignal) => { + const ctrl = new AbortController() + abort?.addEventListener('abort', () => ctrl.abort()) + return DropGenerator.of(watch(ctrl.signal), () => ctrl.abort()) + }, + onChange: ( + callback: ( + value: T.StatusInfo | null, + error?: Error, + ) => { cancel: boolean } | Promise<{ cancel: boolean }>, + ) => { + ;(async () => { + const ctrl = new AbortController() + for await (const value of watch(ctrl.signal)) { + try { + const res = await callback(value) + if (res.cancel) { + ctrl.abort() + break + } + } catch (e) { + console.error( + 'callback function threw an error @ getStatus.onChange', + e, + ) + } + } + })() + .catch((e) => callback(null, e)) + .catch((e) => + console.error( + 'callback function threw an error @ getStatus.onChange', + e, + ), + ) + }, + waitFor: async (pred: (value: T.StatusInfo | null) => boolean) => { + const resolveCell = { resolve: () => {} } + effects.onLeaveContext(() => { + resolveCell.resolve() + }) + while (effects.isInContext) { + let callback: () => void = () => {} + const waitForNext = new Promise((resolve) => { + callback = resolve + resolveCell.resolve = resolve + }) + const res = await effects.getStatus({ ...options, callback }) + if (pred(res)) { + resolveCell.resolve() + return res + } + await waitForNext + } + return null + }, + } + }, + MultiHost: { /** * Create a new MultiHost instance for binding ports and exporting interfaces. diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index b6bf59802..8c8a391c3 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.55", + "version": "0.4.0-beta.56", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.55", + "version": "0.4.0-beta.56", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/sdk/package/package.json b/sdk/package/package.json index 31265ba99..713e2bf98 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.55", + "version": "0.4.0-beta.56", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts",