mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
misc sdk changes (#2934)
* misc sdk changes * delete the store ☠️ * port comments * fix build * fix removing * fix tests * beta.20 --------- Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
@@ -1,18 +1,5 @@
|
||||
import { Value } from "../../base/lib/actions/input/builder/value"
|
||||
import {
|
||||
InputSpec,
|
||||
ExtractInputSpecType,
|
||||
LazyBuild,
|
||||
} from "../../base/lib/actions/input/builder/inputSpec"
|
||||
import {
|
||||
DefaultString,
|
||||
ListValueSpecText,
|
||||
Pattern,
|
||||
RandomString,
|
||||
UniqueBy,
|
||||
ValueSpecDatetime,
|
||||
ValueSpecText,
|
||||
} from "../../base/lib/actions/input/inputSpecTypes"
|
||||
import { InputSpec } from "../../base/lib/actions/input/builder/inputSpec"
|
||||
import { Variants } from "../../base/lib/actions/input/builder/variants"
|
||||
import { Action, Actions } from "../../base/lib/actions/setupActions"
|
||||
import {
|
||||
@@ -25,17 +12,12 @@ import {
|
||||
import * as patterns from "../../base/lib/util/patterns"
|
||||
import { BackupSync, Backups } from "./backup/Backups"
|
||||
import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants"
|
||||
import { CommandController, Daemon, Daemons } from "./mainFn/Daemons"
|
||||
import { Daemon, Daemons } from "./mainFn/Daemons"
|
||||
import { HealthCheck } from "./health/HealthCheck"
|
||||
import { checkPortListening } from "./health/checkFns/checkPortListening"
|
||||
import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
||||
import { List } from "../../base/lib/actions/input/builder/list"
|
||||
import {
|
||||
Install,
|
||||
InstallFn,
|
||||
PostInstall,
|
||||
PreInstall,
|
||||
} from "./inits/setupInstall"
|
||||
import { InstallFn, PostInstall, PreInstall } from "./inits/setupInstall"
|
||||
import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
|
||||
import { UninstallFn, setupUninstall } from "./inits/setupUninstall"
|
||||
import { setupMain } from "./mainFn"
|
||||
@@ -51,24 +33,12 @@ import { ServiceInterfaceBuilder } from "../../base/lib/interfaces/ServiceInterf
|
||||
import { GetSystemSmtp } from "./util"
|
||||
import { nullIfEmpty } from "./util"
|
||||
import { getServiceInterface, getServiceInterfaces } from "./util"
|
||||
import { getStore } from "./store/getStore"
|
||||
import {
|
||||
CommandOptions,
|
||||
ExitError,
|
||||
MountOptions,
|
||||
SubContainer,
|
||||
} from "./util/SubContainer"
|
||||
import { CommandOptions, ExitError, SubContainer } from "./util/SubContainer"
|
||||
import { splitCommand } from "./util"
|
||||
import { Mounts } from "./mainFn/Mounts"
|
||||
import { setupDependencies } from "../../base/lib/dependencies/setupDependencies"
|
||||
import * as T from "../../base/lib/types"
|
||||
import { testTypeVersion } from "../../base/lib/exver"
|
||||
import { ExposedStorePaths } from "./store/setupExposeStore"
|
||||
import {
|
||||
PathBuilder,
|
||||
extractJsonPath,
|
||||
pathBuilder,
|
||||
} from "../../base/lib/util/PathBuilder"
|
||||
import {
|
||||
CheckDependencies,
|
||||
checkDependencies,
|
||||
@@ -91,19 +61,16 @@ type AnyNeverCond<T extends any[], Then, Else> =
|
||||
T extends [any, ...infer U] ? AnyNeverCond<U,Then, Else> :
|
||||
never
|
||||
|
||||
export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
private constructor(readonly manifest: Manifest) {}
|
||||
static of() {
|
||||
return new StartSdk<never, never>(null as never)
|
||||
return new StartSdk<never>(null as never)
|
||||
}
|
||||
withManifest<Manifest extends T.SDKManifest = never>(manifest: Manifest) {
|
||||
return new StartSdk<Manifest, Store>(manifest)
|
||||
}
|
||||
withStore<Store extends Record<string, any>>() {
|
||||
return new StartSdk<Manifest, Store>(this.manifest)
|
||||
return new StartSdk<Manifest>(manifest)
|
||||
}
|
||||
|
||||
build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) {
|
||||
build(isReady: AnyNeverCond<[Manifest], "Build not ready", true>) {
|
||||
type NestedEffects = "subcontainer" | "store" | "action"
|
||||
type InterfaceEffects =
|
||||
| "getServiceInterface"
|
||||
@@ -136,8 +103,6 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
mount: (effects, ...args) => effects.mount(...args),
|
||||
getInstalledPackages: (effects, ...args) =>
|
||||
effects.getInstalledPackages(...args),
|
||||
exposeForDependents: (effects, ...args) =>
|
||||
effects.exposeForDependents(...args),
|
||||
getServicePortForward: (effects, ...args) =>
|
||||
effects.getServicePortForward(...args),
|
||||
clearBindings: (effects, ...args) => effects.clearBindings(...args),
|
||||
@@ -155,7 +120,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
...startSdkEffectWrapper,
|
||||
action: {
|
||||
run: actions.runAction,
|
||||
request: <T extends Action<T.ActionId, any, any>>(
|
||||
request: <T extends Action<T.ActionId, any>>(
|
||||
effects: T.Effects,
|
||||
packageId: T.PackageId,
|
||||
action: T,
|
||||
@@ -169,7 +134,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
severity,
|
||||
options: options,
|
||||
}),
|
||||
requestOwn: <T extends Action<T.ActionId, Store, any>>(
|
||||
requestOwn: <T extends Action<T.ActionId, any>>(
|
||||
effects: T.Effects,
|
||||
action: T,
|
||||
severity: T.ActionSeverity,
|
||||
@@ -268,29 +233,6 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
},
|
||||
}
|
||||
},
|
||||
store: {
|
||||
get: <E extends Effects, StoreValue = unknown>(
|
||||
effects: E,
|
||||
packageId: string,
|
||||
path: PathBuilder<Store, StoreValue>,
|
||||
) =>
|
||||
getStore<Store, StoreValue>(effects, path, {
|
||||
packageId,
|
||||
}),
|
||||
getOwn: <E extends Effects, StoreValue = unknown>(
|
||||
effects: E,
|
||||
path: PathBuilder<Store, StoreValue>,
|
||||
) => getStore<Store, StoreValue>(effects, path),
|
||||
setOwn: <E extends Effects, Path extends PathBuilder<Store, unknown>>(
|
||||
effects: E,
|
||||
path: Path,
|
||||
value: Path extends PathBuilder<Store, infer Value> ? Value : never,
|
||||
) =>
|
||||
effects.store.set<Store>({
|
||||
value,
|
||||
path: extractJsonPath(path),
|
||||
}),
|
||||
},
|
||||
|
||||
MultiHost: {
|
||||
of: (effects: Effects, id: string) => new MultiHost({ id, effects }),
|
||||
@@ -362,10 +304,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
*/
|
||||
withInput: <
|
||||
Id extends T.ActionId,
|
||||
InputSpecType extends
|
||||
| Record<string, any>
|
||||
| InputSpec<any, any>
|
||||
| InputSpec<any, never>,
|
||||
InputSpecType extends Record<string, any> | InputSpec<any>,
|
||||
>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
@@ -382,6 +321,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
* In this example, we create an action that returns a secret phrase for the user to see.
|
||||
*
|
||||
* ```
|
||||
import { store } from '../file-models/store.json'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const showSecretPhrase = sdk.Action.withoutInput(
|
||||
@@ -406,9 +346,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
'Below is your secret phrase. Use it to gain access to extraordinary places',
|
||||
result: {
|
||||
type: 'single',
|
||||
value: await sdk.store
|
||||
.getOwn(effects, sdk.StorePath.secretPhrase)
|
||||
.const(),
|
||||
value: (await store.read.once())?.secretPhrase,
|
||||
copyable: true,
|
||||
qr: true,
|
||||
masked: true,
|
||||
@@ -499,7 +437,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
export const actions = sdk.Actions.of().addAction(config).addAction(nameToLogs)
|
||||
* ```
|
||||
*/
|
||||
Actions: Actions<Store, {}>,
|
||||
Actions: Actions<{}>,
|
||||
/**
|
||||
* @description Use this function to determine which volumes are backed up when a user creates a backup, including advanced options.
|
||||
* @example
|
||||
@@ -530,11 +468,11 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
/**
|
||||
* @description Use this function to set dependency information.
|
||||
* @example
|
||||
* In this example, we create a perpetual dependency on Hello World >=1.0.0:0, where Hello World must be running and passing its "primary" health check.
|
||||
* In this example, we create a dependency on Hello World >=1.0.0:0, where Hello World must be running and passing its "primary" health check.
|
||||
*
|
||||
* ```
|
||||
export const setDependencies = sdk.setupDependencies(
|
||||
async ({ effects, input }) => {
|
||||
async ({ effects }) => {
|
||||
return {
|
||||
'hello-world': {
|
||||
kind: 'running',
|
||||
@@ -545,29 +483,9 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
},
|
||||
)
|
||||
* ```
|
||||
* @example
|
||||
* In this example, we create a conditional dependency on Hello World based on a hypothetical "needsWorld" boolean in our Store.
|
||||
* Using .const() ensures that if the "needsWorld" boolean changes, setupDependencies will re-run.
|
||||
*
|
||||
* ```
|
||||
export const setDependencies = sdk.setupDependencies(
|
||||
async ({ effects }) => {
|
||||
if (sdk.store.getOwn(sdk.StorePath.needsWorld).const()) {
|
||||
return {
|
||||
'hello-world': {
|
||||
kind: 'running',
|
||||
versionRange: '>=1.0.0',
|
||||
healthChecks: ['primary'],
|
||||
},
|
||||
}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
)
|
||||
* ```
|
||||
*/
|
||||
setupDependencies: setupDependencies<Manifest>,
|
||||
setupInit: setupInit<Manifest, Store>,
|
||||
setupInit: setupInit<Manifest>,
|
||||
/**
|
||||
* @description Use this function to execute arbitrary logic *once*, on initial install *before* interfaces, actions, and dependencies are updated.
|
||||
* @example
|
||||
@@ -579,26 +497,21 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
setupPreInstall: (fn: InstallFn<Manifest, Store>) => PreInstall.of(fn),
|
||||
setupPreInstall: (fn: InstallFn<Manifest>) => PreInstall.of(fn),
|
||||
/**
|
||||
* @description Use this function to execute arbitrary logic *once*, on initial install *after* interfaces, actions, and dependencies are updated.
|
||||
* @example
|
||||
* In the this example, we bootstrap our Store with a random, 16-char admin password.
|
||||
* In the this example, we create a task for the user to perform.
|
||||
*
|
||||
* ```
|
||||
const postInstall = sdk.setupPostInstall(async ({ effects }) => {
|
||||
await sdk.store.setOwn(
|
||||
effects,
|
||||
sdk.StorePath.adminPassword,
|
||||
utils.getDefaultString({
|
||||
charset: 'a-z,A-Z,1-9,!,@,$,%,&,',
|
||||
len: 16,
|
||||
}),
|
||||
)
|
||||
await sdk.action.requestOwn(effects, showSecretPhrase, 'important', {
|
||||
reason: 'Check out your secret phrase!',
|
||||
})
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
setupPostInstall: (fn: InstallFn<Manifest, Store>) => PostInstall.of(fn),
|
||||
setupPostInstall: (fn: InstallFn<Manifest>) => PostInstall.of(fn),
|
||||
/**
|
||||
* @description Use this function to determine how this service will be hosted and served. The function executes on service install, service update, and inputSpec save.
|
||||
* @param inputSpec - The inputSpec spec of this service as exported from /inputSpec/spec.
|
||||
@@ -673,12 +586,12 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
effects: Effects
|
||||
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
|
||||
}) => Promise<Daemons<Manifest, any>>,
|
||||
) => setupMain<Manifest, Store>(fn),
|
||||
) => setupMain<Manifest>(fn),
|
||||
/**
|
||||
* Use this function to execute arbitrary logic *once*, on uninstall only. Most services will not use this.
|
||||
*/
|
||||
setupUninstall: (fn: UninstallFn<Manifest, Store>) =>
|
||||
setupUninstall<Manifest, Store>(fn),
|
||||
setupUninstall: (fn: UninstallFn<Manifest>) =>
|
||||
setupUninstall<Manifest>(fn),
|
||||
trigger: {
|
||||
defaultTrigger,
|
||||
cooldownTrigger,
|
||||
@@ -728,11 +641,8 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
of: <
|
||||
Spec extends Record<string, Value<any, Store> | Value<any, never>>,
|
||||
>(
|
||||
spec: Spec,
|
||||
) => InputSpec.of<Spec, Store>(spec),
|
||||
of: <Spec extends Record<string, Value<any>>>(spec: Spec) =>
|
||||
InputSpec.of<Spec>(spec),
|
||||
},
|
||||
Daemon: {
|
||||
get of() {
|
||||
@@ -787,372 +697,9 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
return SubContainer.withTemp(effects, image, mounts, name, fn)
|
||||
},
|
||||
},
|
||||
List: {
|
||||
/**
|
||||
* @description Create a list of text inputs.
|
||||
* @param a - attributes of the list itself.
|
||||
* @param aSpec - attributes describing each member of the list.
|
||||
*/
|
||||
text: List.text,
|
||||
/**
|
||||
* @description Create a list of objects.
|
||||
* @param a - attributes of the list itself.
|
||||
* @param aSpec - attributes describing each member of the list.
|
||||
*/
|
||||
obj: <Type extends Record<string, any>>(
|
||||
a: Parameters<typeof List.obj<Type, Store>>[0],
|
||||
aSpec: Parameters<typeof List.obj<Type, Store>>[1],
|
||||
) => List.obj<Type, Store>(a, aSpec),
|
||||
/**
|
||||
* @description Create a list of dynamic text inputs.
|
||||
* @param a - attributes of the list itself.
|
||||
* @param aSpec - attributes describing each member of the list.
|
||||
*/
|
||||
dynamicText: List.dynamicText<Store>,
|
||||
},
|
||||
StorePath: pathBuilder<Store>(),
|
||||
Value: {
|
||||
/**
|
||||
* @description Displays a boolean toggle to enable/disable
|
||||
* @example
|
||||
* ```
|
||||
toggleExample: Value.toggle({
|
||||
// required
|
||||
name: 'Toggle Example',
|
||||
default: true,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
immutable: false,
|
||||
}),
|
||||
* ```
|
||||
*/
|
||||
toggle: Value.toggle,
|
||||
/**
|
||||
* @description Displays a text input field
|
||||
* @example
|
||||
* ```
|
||||
textExample: Value.text({
|
||||
// required
|
||||
name: 'Text Example',
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
placeholder: null,
|
||||
warning: null,
|
||||
generate: null,
|
||||
inputmode: 'text',
|
||||
masked: false,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
immutable: false,
|
||||
}),
|
||||
* ```
|
||||
*/
|
||||
text: Value.text,
|
||||
/**
|
||||
* @description Displays a large textarea field for long form entry.
|
||||
* @example
|
||||
* ```
|
||||
textareaExample: Value.textarea({
|
||||
// required
|
||||
name: 'Textarea Example',
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
placeholder: null,
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
immutable: false,
|
||||
}),
|
||||
* ```
|
||||
*/
|
||||
textarea: Value.textarea,
|
||||
/**
|
||||
* @description Displays a number input field
|
||||
* @example
|
||||
* ```
|
||||
numberExample: Value.number({
|
||||
// required
|
||||
name: 'Number Example',
|
||||
required: false,
|
||||
default: null,
|
||||
integer: true,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
placeholder: null,
|
||||
warning: null,
|
||||
min: null,
|
||||
max: null,
|
||||
immutable: false,
|
||||
step: null,
|
||||
units: null,
|
||||
}),
|
||||
* ```
|
||||
*/
|
||||
number: Value.number,
|
||||
/**
|
||||
* @description Displays a browser-native color selector.
|
||||
* @example
|
||||
* ```
|
||||
colorExample: Value.color({
|
||||
// required
|
||||
name: 'Color Example',
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
immutable: false,
|
||||
}),
|
||||
* ```
|
||||
*/
|
||||
color: Value.color,
|
||||
/**
|
||||
* @description Displays a browser-native date/time selector.
|
||||
* @example
|
||||
* ```
|
||||
datetimeExample: Value.datetime({
|
||||
// required
|
||||
name: 'Datetime Example',
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
immutable: false,
|
||||
inputmode: 'datetime-local',
|
||||
min: null,
|
||||
max: null,
|
||||
}),
|
||||
* ```
|
||||
*/
|
||||
datetime: Value.datetime,
|
||||
/**
|
||||
* @description Displays a select modal with radio buttons, allowing for a single selection.
|
||||
* @example
|
||||
* ```
|
||||
selectExample: Value.select({
|
||||
// required
|
||||
name: 'Select Example',
|
||||
default: 'radio1',
|
||||
values: {
|
||||
radio1: 'Radio 1',
|
||||
radio2: 'Radio 2',
|
||||
},
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
immutable: false,
|
||||
disabled: false,
|
||||
}),
|
||||
* ```
|
||||
*/
|
||||
select: Value.select,
|
||||
/**
|
||||
* @description Displays a select modal with checkboxes, allowing for multiple selections.
|
||||
* @example
|
||||
* ```
|
||||
multiselectExample: Value.multiselect({
|
||||
// required
|
||||
name: 'Multiselect Example',
|
||||
values: {
|
||||
option1: 'Option 1',
|
||||
option2: 'Option 2',
|
||||
},
|
||||
default: [],
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
immutable: false,
|
||||
disabled: false,
|
||||
minlength: null,
|
||||
maxLength: null,
|
||||
}),
|
||||
* ```
|
||||
*/
|
||||
multiselect: Value.multiselect,
|
||||
/**
|
||||
* @description Display a collapsable grouping of additional fields, a "sub form". The second value is the inputSpec spec for the sub form.
|
||||
* @example
|
||||
* ```
|
||||
objectExample: Value.object(
|
||||
{
|
||||
// required
|
||||
name: 'Object Example',
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
},
|
||||
InputSpec.of({}),
|
||||
),
|
||||
* ```
|
||||
*/
|
||||
object: Value.object,
|
||||
/**
|
||||
* @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented.
|
||||
* @example
|
||||
* ```
|
||||
unionExample: Value.union(
|
||||
{
|
||||
// required
|
||||
name: 'Union Example',
|
||||
default: 'option1',
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
},
|
||||
Variants.of({
|
||||
option1: {
|
||||
name: 'Option 1',
|
||||
spec: InputSpec.of({}),
|
||||
},
|
||||
option2: {
|
||||
name: 'Option 2',
|
||||
spec: InputSpec.of({}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
* ```
|
||||
*/
|
||||
union: Value.union,
|
||||
/**
|
||||
* @description Presents an interface to add/remove/edit items in a list.
|
||||
* @example
|
||||
* In this example, we create a list of text inputs.
|
||||
*
|
||||
* ```
|
||||
listExampleText: Value.list(
|
||||
List.text(
|
||||
{
|
||||
// required
|
||||
name: 'Text List',
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
default: [],
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
},
|
||||
{
|
||||
// required
|
||||
patterns: [],
|
||||
|
||||
// optional
|
||||
placeholder: null,
|
||||
generate: null,
|
||||
inputmode: 'url',
|
||||
masked: false,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
},
|
||||
),
|
||||
),
|
||||
* ```
|
||||
* @example
|
||||
* In this example, we create a list of objects.
|
||||
*
|
||||
* ```
|
||||
listExampleObject: Value.list(
|
||||
List.obj(
|
||||
{
|
||||
// required
|
||||
name: 'Object List',
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
warning: null,
|
||||
default: [],
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
},
|
||||
{
|
||||
// required
|
||||
spec: InputSpec.of({}),
|
||||
|
||||
// optional
|
||||
displayAs: null,
|
||||
uniqueBy: null,
|
||||
},
|
||||
),
|
||||
),
|
||||
* ```
|
||||
*/
|
||||
list: Value.list,
|
||||
hidden: Value.hidden,
|
||||
dynamicToggle: Value.dynamicToggle<Store>,
|
||||
dynamicText: Value.dynamicText<Store>,
|
||||
dynamicTextarea: Value.dynamicTextarea<Store>,
|
||||
dynamicNumber: Value.dynamicNumber<Store>,
|
||||
dynamicColor: Value.dynamicColor<Store>,
|
||||
dynamicDatetime: Value.dynamicDatetime<Store>,
|
||||
dynamicSelect: Value.dynamicSelect<Store>,
|
||||
dynamicMultiselect: Value.dynamicMultiselect<Store>,
|
||||
filteredUnion: <
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
>(
|
||||
getDisabledFn: Parameters<
|
||||
typeof Value.filteredUnion<VariantValues, Store>
|
||||
>[0],
|
||||
a: Parameters<typeof Value.filteredUnion<VariantValues, Store>>[1],
|
||||
aVariants: Parameters<
|
||||
typeof Value.filteredUnion<VariantValues, Store>
|
||||
>[2],
|
||||
) =>
|
||||
Value.filteredUnion<VariantValues, Store>(
|
||||
getDisabledFn,
|
||||
a,
|
||||
aVariants,
|
||||
),
|
||||
|
||||
dynamicUnion: <
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
>(
|
||||
getA: Parameters<typeof Value.dynamicUnion<VariantValues, Store>>[0],
|
||||
aVariants: Parameters<
|
||||
typeof Value.dynamicUnion<VariantValues, Store>
|
||||
>[1],
|
||||
) => Value.dynamicUnion<VariantValues, Store>(getA, aVariants),
|
||||
},
|
||||
Variants: {
|
||||
of: <
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: InputSpec<any, Store>
|
||||
}
|
||||
},
|
||||
>(
|
||||
a: VariantValues,
|
||||
) => Variants.of<VariantValues, Store>(a),
|
||||
},
|
||||
List,
|
||||
Value,
|
||||
Variants,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as child_process from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import { Affine, asError, StorePath } from "../util"
|
||||
import { Affine, asError } from "../util"
|
||||
|
||||
export const DEFAULT_OPTIONS: T.SyncOptions = {
|
||||
delete: true,
|
||||
@@ -96,7 +96,7 @@ export class Backups<M extends T.SDKManifest> {
|
||||
return this
|
||||
}
|
||||
|
||||
addVolume(
|
||||
mountVolume(
|
||||
volume: M["volumes"][number],
|
||||
options?: Partial<{
|
||||
options: T.SyncOptions
|
||||
@@ -133,11 +133,7 @@ export class Backups<M extends T.SDKManifest> {
|
||||
})
|
||||
await rsyncResults.wait()
|
||||
}
|
||||
await fs.writeFile(
|
||||
"/media/startos/backup/store.json",
|
||||
JSON.stringify(await effects.store.get({ path: "" as StorePath })),
|
||||
{ encoding: "utf-8" },
|
||||
)
|
||||
|
||||
const dataVersion = await effects.getDataVersion()
|
||||
if (dataVersion)
|
||||
await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, {
|
||||
@@ -149,16 +145,7 @@ export class Backups<M extends T.SDKManifest> {
|
||||
|
||||
async restoreBackup(effects: T.Effects) {
|
||||
this.preRestore(effects as BackupEffects)
|
||||
const store = await fs
|
||||
.readFile("/media/startos/backup/store.json", {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
.catch((_) => null)
|
||||
if (store)
|
||||
await effects.store.set({
|
||||
path: "" as StorePath,
|
||||
value: JSON.parse(store),
|
||||
})
|
||||
|
||||
for (const item of this.backupSet) {
|
||||
const rsyncResults = await runRsync({
|
||||
srcPath: item.backupPath,
|
||||
|
||||
@@ -30,7 +30,7 @@ export class HealthCheck extends Drop {
|
||||
super()
|
||||
this.promise = Promise.resolve().then(async () => {
|
||||
const getCurrentValue = () => this.currentValue
|
||||
const gracePeriod = o.gracePeriod ?? 5000
|
||||
const gracePeriod = o.gracePeriod ?? 10_000
|
||||
const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue)
|
||||
const triggerFirstSuccess = once(() =>
|
||||
Promise.resolve(
|
||||
|
||||
@@ -29,8 +29,6 @@ export { SubContainer } from "./util/SubContainer"
|
||||
export { StartSdk } from "./StartSdk"
|
||||
export { setupManifest, buildManifest } from "./manifest/setupManifest"
|
||||
export { FileHelper } from "./util/fileHelper"
|
||||
export { setupExposeStore } from "./store/setupExposeStore"
|
||||
export { pathBuilder } from "../../base/lib/util/PathBuilder"
|
||||
|
||||
export * as actions from "../../base/lib/actions"
|
||||
export * as backup from "./backup"
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { Actions } from "../../../base/lib/actions/setupActions"
|
||||
import { ExtendedVersion } from "../../../base/lib/exver"
|
||||
import { UpdateServiceInterfaces } from "../../../base/lib/interfaces/setupInterfaces"
|
||||
import { ExposedStorePaths } from "../../../base/lib/types"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { StorePath } from "../util"
|
||||
import { VersionGraph } from "../version/VersionGraph"
|
||||
import { PostInstall, PreInstall } from "./setupInstall"
|
||||
import { Uninstall } from "./setupUninstall"
|
||||
|
||||
export function setupInit<Manifest extends T.SDKManifest, Store>(
|
||||
export function setupInit<Manifest extends T.SDKManifest>(
|
||||
versions: VersionGraph<string>,
|
||||
preInstall: PreInstall<Manifest, Store>,
|
||||
postInstall: PostInstall<Manifest, Store>,
|
||||
uninstall: Uninstall<Manifest, Store>,
|
||||
preInstall: PreInstall<Manifest>,
|
||||
postInstall: PostInstall<Manifest>,
|
||||
uninstall: Uninstall<Manifest>,
|
||||
setServiceInterfaces: UpdateServiceInterfaces<any>,
|
||||
setDependencies: (options: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | void | undefined>,
|
||||
actions: Actions<Store, any>,
|
||||
initStore: Store,
|
||||
exposedStore: ExposedStorePaths,
|
||||
actions: Actions<any>,
|
||||
): {
|
||||
packageInit: T.ExpectedExports.packageInit
|
||||
packageUninit: T.ExpectedExports.packageUninit
|
||||
@@ -58,17 +54,12 @@ export function setupInit<Manifest extends T.SDKManifest, Store>(
|
||||
containerInit: async (opts) => {
|
||||
const prev = await opts.effects.getDataVersion()
|
||||
if (!prev) {
|
||||
await opts.effects.store.set({
|
||||
path: "" as StorePath,
|
||||
value: initStore,
|
||||
})
|
||||
await preInstall.preInstall(opts)
|
||||
}
|
||||
await setServiceInterfaces({
|
||||
...opts,
|
||||
})
|
||||
await actions.update({ effects: opts.effects })
|
||||
await opts.effects.exposeForDependents({ paths: exposedStore })
|
||||
await setDependencies({ effects: opts.effects })
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
|
||||
export type InstallFn<Manifest extends T.SDKManifest, Store> = (opts: {
|
||||
export type InstallFn<Manifest extends T.SDKManifest> = (opts: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | void | undefined>
|
||||
export class Install<Manifest extends T.SDKManifest, Store> {
|
||||
protected constructor(readonly fn: InstallFn<Manifest, Store>) {}
|
||||
export class Install<Manifest extends T.SDKManifest> {
|
||||
protected constructor(readonly fn: InstallFn<Manifest>) {}
|
||||
}
|
||||
|
||||
export class PreInstall<Manifest extends T.SDKManifest, Store> extends Install<
|
||||
Manifest,
|
||||
Store
|
||||
> {
|
||||
private constructor(fn: InstallFn<Manifest, Store>) {
|
||||
export class PreInstall<
|
||||
Manifest extends T.SDKManifest,
|
||||
> extends Install<Manifest> {
|
||||
private constructor(fn: InstallFn<Manifest>) {
|
||||
super(fn)
|
||||
}
|
||||
static of<Manifest extends T.SDKManifest, Store>(
|
||||
fn: InstallFn<Manifest, Store>,
|
||||
) {
|
||||
static of<Manifest extends T.SDKManifest>(fn: InstallFn<Manifest>) {
|
||||
return new PreInstall(fn)
|
||||
}
|
||||
|
||||
@@ -27,22 +24,19 @@ export class PreInstall<Manifest extends T.SDKManifest, Store> extends Install<
|
||||
}
|
||||
}
|
||||
|
||||
export function setupPreInstall<Manifest extends T.SDKManifest, Store>(
|
||||
fn: InstallFn<Manifest, Store>,
|
||||
export function setupPreInstall<Manifest extends T.SDKManifest>(
|
||||
fn: InstallFn<Manifest>,
|
||||
) {
|
||||
return PreInstall.of(fn)
|
||||
}
|
||||
|
||||
export class PostInstall<Manifest extends T.SDKManifest, Store> extends Install<
|
||||
Manifest,
|
||||
Store
|
||||
> {
|
||||
private constructor(fn: InstallFn<Manifest, Store>) {
|
||||
export class PostInstall<
|
||||
Manifest extends T.SDKManifest,
|
||||
> extends Install<Manifest> {
|
||||
private constructor(fn: InstallFn<Manifest>) {
|
||||
super(fn)
|
||||
}
|
||||
static of<Manifest extends T.SDKManifest, Store>(
|
||||
fn: InstallFn<Manifest, Store>,
|
||||
) {
|
||||
static of<Manifest extends T.SDKManifest>(fn: InstallFn<Manifest>) {
|
||||
return new PostInstall(fn)
|
||||
}
|
||||
|
||||
@@ -53,8 +47,8 @@ export class PostInstall<Manifest extends T.SDKManifest, Store> extends Install<
|
||||
}
|
||||
}
|
||||
|
||||
export function setupPostInstall<Manifest extends T.SDKManifest, Store>(
|
||||
fn: InstallFn<Manifest, Store>,
|
||||
export function setupPostInstall<Manifest extends T.SDKManifest>(
|
||||
fn: InstallFn<Manifest>,
|
||||
) {
|
||||
return PostInstall.of(fn)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
|
||||
export type UninstallFn<Manifest extends T.SDKManifest, Store> = (opts: {
|
||||
export type UninstallFn<Manifest extends T.SDKManifest> = (opts: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | void | undefined>
|
||||
export class Uninstall<Manifest extends T.SDKManifest, Store> {
|
||||
private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
|
||||
static of<Manifest extends T.SDKManifest, Store>(
|
||||
fn: UninstallFn<Manifest, Store>,
|
||||
) {
|
||||
export class Uninstall<Manifest extends T.SDKManifest> {
|
||||
private constructor(readonly fn: UninstallFn<Manifest>) {}
|
||||
static of<Manifest extends T.SDKManifest>(fn: UninstallFn<Manifest>) {
|
||||
return new Uninstall(fn)
|
||||
}
|
||||
|
||||
@@ -22,8 +20,8 @@ export class Uninstall<Manifest extends T.SDKManifest, Store> {
|
||||
}
|
||||
}
|
||||
|
||||
export function setupUninstall<Manifest extends T.SDKManifest, Store>(
|
||||
fn: UninstallFn<Manifest, Store>,
|
||||
export function setupUninstall<Manifest extends T.SDKManifest>(
|
||||
fn: UninstallFn<Manifest>,
|
||||
) {
|
||||
return Uninstall.of(fn)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ type NewDaemonParams<Manifest extends T.SDKManifest> = {
|
||||
subcontainer: SubContainer<Manifest>
|
||||
runAsInit?: boolean
|
||||
env?: Record<string, string>
|
||||
cwd?: string
|
||||
user?: string
|
||||
sigtermTimeout?: number
|
||||
onStdout?: (chunk: Buffer | string | any) => void
|
||||
onStderr?: (chunk: Buffer | string | any) => void
|
||||
|
||||
@@ -8,8 +8,12 @@ type SharedOptions = {
|
||||
subpath: string | null
|
||||
/** Where to mount the resource. e.g. /data */
|
||||
mountpoint: string
|
||||
/** Whether to mount this as a file or directory */
|
||||
type?: "file" | "directory"
|
||||
/**
|
||||
* Whether to mount this as a file or directory
|
||||
*
|
||||
* defaults to "directory"
|
||||
* */
|
||||
type?: "file" | "directory" | "infer"
|
||||
}
|
||||
|
||||
type VolumeOpts<Manifest extends T.SDKManifest> = {
|
||||
@@ -43,7 +47,7 @@ export class Mounts<
|
||||
return new Mounts<Manifest>([], [], [], [])
|
||||
}
|
||||
|
||||
addVolume(options: VolumeOpts<Manifest>) {
|
||||
mountVolume(options: VolumeOpts<Manifest>) {
|
||||
return new Mounts<Manifest, Backups>(
|
||||
[...this.volumes, options],
|
||||
[...this.assets],
|
||||
@@ -52,7 +56,7 @@ export class Mounts<
|
||||
)
|
||||
}
|
||||
|
||||
addAssets(options: SharedOptions) {
|
||||
mountAssets(options: SharedOptions) {
|
||||
return new Mounts<Manifest, Backups>(
|
||||
[...this.volumes],
|
||||
[...this.assets, options],
|
||||
@@ -61,7 +65,7 @@ export class Mounts<
|
||||
)
|
||||
}
|
||||
|
||||
addDependency<DependencyManifest extends T.SDKManifest>(
|
||||
mountDependency<DependencyManifest extends T.SDKManifest>(
|
||||
options: DependencyOpts<DependencyManifest>,
|
||||
) {
|
||||
return new Mounts<Manifest, Backups>(
|
||||
@@ -72,7 +76,7 @@ export class Mounts<
|
||||
)
|
||||
}
|
||||
|
||||
addBackups(options: SharedOptions) {
|
||||
mountBackups(options: SharedOptions) {
|
||||
return new Mounts<
|
||||
Manifest,
|
||||
{
|
||||
@@ -109,7 +113,7 @@ export class Mounts<
|
||||
volumeId: v.volumeId,
|
||||
subpath: v.subpath,
|
||||
readonly: v.readonly,
|
||||
filetype: v.type,
|
||||
filetype: v.type ?? "directory",
|
||||
},
|
||||
})),
|
||||
)
|
||||
@@ -119,7 +123,7 @@ export class Mounts<
|
||||
options: {
|
||||
type: "assets",
|
||||
subpath: a.subpath,
|
||||
filetype: a.type,
|
||||
filetype: a.type ?? "directory",
|
||||
},
|
||||
})),
|
||||
)
|
||||
@@ -132,13 +136,13 @@ export class Mounts<
|
||||
volumeId: d.volumeId,
|
||||
subpath: d.subpath,
|
||||
readonly: d.readonly,
|
||||
filetype: d.type,
|
||||
filetype: d.type ?? "directory",
|
||||
},
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const a = Mounts.of().addBackups({ subpath: null, mountpoint: "" })
|
||||
const a = Mounts.of().mountBackups({ subpath: null, mountpoint: "" })
|
||||
// @ts-expect-error
|
||||
const m: Mounts<T.SDKManifest, never> = a
|
||||
|
||||
@@ -14,7 +14,7 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000
|
||||
* @param fn
|
||||
* @returns
|
||||
*/
|
||||
export const setupMain = <Manifest extends T.SDKManifest, Store>(
|
||||
export const setupMain = <Manifest extends T.SDKManifest>(
|
||||
fn: (o: {
|
||||
effects: T.Effects
|
||||
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Effects } from "../../../base/lib/Effects"
|
||||
import { PathBuilder, extractJsonPath } from "../util"
|
||||
|
||||
export class GetStore<Store, StoreValue> {
|
||||
constructor(
|
||||
readonly effects: Effects,
|
||||
readonly path: PathBuilder<Store, StoreValue>,
|
||||
readonly options: {
|
||||
/** Defaults to what ever the package currently in */
|
||||
packageId?: string | undefined
|
||||
} = {},
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the value of Store at the provided path. Reruns the context from which it has been called if the underlying value changes
|
||||
*/
|
||||
const() {
|
||||
return this.effects.store.get<Store, StoreValue>({
|
||||
...this.options,
|
||||
path: extractJsonPath(this.path),
|
||||
callback:
|
||||
this.effects.constRetry &&
|
||||
(() => this.effects.constRetry && this.effects.constRetry()),
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Returns the value of Store at the provided path. Does nothing if the value changes
|
||||
*/
|
||||
once() {
|
||||
return this.effects.store.get<Store, StoreValue>({
|
||||
...this.options,
|
||||
path: extractJsonPath(this.path),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the value of Store at the provided path. Returns an async iterator that yields whenever the value changes
|
||||
*/
|
||||
async *watch() {
|
||||
const resolveCell = { resolve: () => {} }
|
||||
this.effects.onLeaveContext(() => {
|
||||
resolveCell.resolve()
|
||||
})
|
||||
while (this.effects.isInContext) {
|
||||
let callback: () => void = () => {}
|
||||
const waitForNext = new Promise<void>((resolve) => {
|
||||
callback = resolve
|
||||
resolveCell.resolve = resolve
|
||||
})
|
||||
yield await this.effects.store.get<Store, StoreValue>({
|
||||
...this.options,
|
||||
path: extractJsonPath(this.path),
|
||||
callback: () => callback(),
|
||||
})
|
||||
await waitForNext
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the value of Store at the provided path. Takes a custom callback function to run whenever the value changes
|
||||
*/
|
||||
onChange(
|
||||
callback: (value: StoreValue | null, error?: Error) => void | Promise<void>,
|
||||
) {
|
||||
;(async () => {
|
||||
for await (const value of this.watch()) {
|
||||
try {
|
||||
await callback(value)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"callback function threw an error @ GetStore.onChange",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
})()
|
||||
.catch((e) => callback(null, e))
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
"callback function threw an error @ GetStore.onChange",
|
||||
e,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
export function getStore<Store, StoreValue>(
|
||||
effects: Effects,
|
||||
path: PathBuilder<Store, StoreValue>,
|
||||
options: {
|
||||
/** Defaults to what ever the package currently in */
|
||||
packageId?: string | undefined
|
||||
} = {},
|
||||
) {
|
||||
return new GetStore<Store, StoreValue>(effects, path, options)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ExposedStorePaths } from "../../../base/lib/types"
|
||||
import {
|
||||
PathBuilder,
|
||||
extractJsonPath,
|
||||
pathBuilder,
|
||||
} from "../../../base/lib/util/PathBuilder"
|
||||
|
||||
/**
|
||||
* @description Use this function to determine which Store values to expose and make available to other services running on StartOS. Store values not exposed here will be kept private. Use the type safe pathBuilder to traverse your Store's structure.
|
||||
* @example
|
||||
* In this example, we expose the hypothetical Store values "adminPassword" and "nameLastUpdatedAt".
|
||||
*
|
||||
* ```
|
||||
export const exposedStore = setupExposeStore<Store>((pathBuilder) => [
|
||||
pathBuilder.adminPassword
|
||||
pathBuilder.nameLastUpdatedAt,
|
||||
])
|
||||
* ```
|
||||
*/
|
||||
export const setupExposeStore = <Store extends Record<string, any>>(
|
||||
fn: (pathBuilder: PathBuilder<Store>) => PathBuilder<Store, any>[],
|
||||
) => {
|
||||
return fn(pathBuilder<Store>()).map(
|
||||
(x) => extractJsonPath(x) as string,
|
||||
) as ExposedStorePaths
|
||||
}
|
||||
export { ExposedStorePaths }
|
||||
@@ -421,25 +421,16 @@ describe("values", () => {
|
||||
},
|
||||
}),
|
||||
)
|
||||
.withStore<{ test: "a" }>()
|
||||
.build(true)
|
||||
|
||||
const value = Value.dynamicDatetime<{ test: "a" }>(
|
||||
async ({ effects }) => {
|
||||
;async () => {
|
||||
;(await sdk.store
|
||||
.getOwn(effects, sdk.StorePath.test)
|
||||
.once()) satisfies "a"
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Testing",
|
||||
required: true,
|
||||
default: null,
|
||||
inputmode: "date",
|
||||
}
|
||||
},
|
||||
)
|
||||
const value = Value.dynamicDatetime(async ({ effects }) => {
|
||||
return {
|
||||
name: "Testing",
|
||||
required: true,
|
||||
default: null,
|
||||
inputmode: "date",
|
||||
}
|
||||
})
|
||||
const validator = value.validator
|
||||
validator.unsafeCast("2021-01-01")
|
||||
validator.unsafeCast(null)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CurrentDependenciesResult } from "../../../base/lib/dependencies/setupDependencies"
|
||||
import { StartSdk } from "../StartSdk"
|
||||
import { setupManifest } from "../manifest/setupManifest"
|
||||
import { VersionGraph } from "../version/VersionGraph"
|
||||
@@ -49,5 +48,4 @@ export const sdk = StartSdk.of()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.withStore<{ storeRoot: { storeLeaf: "value" } }>()
|
||||
.build(true)
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Effects } from "../../../base/lib/types"
|
||||
import { extractJsonPath } from "../../../base/lib/util/PathBuilder"
|
||||
import { StartSdk } from "../StartSdk"
|
||||
|
||||
type Store = {
|
||||
inputSpec: {
|
||||
someValue: "a" | "b"
|
||||
}
|
||||
}
|
||||
type Manifest = any
|
||||
const todo = <A>(): A => {
|
||||
throw new Error("not implemented")
|
||||
}
|
||||
const noop = () => {}
|
||||
|
||||
const sdk = StartSdk.of()
|
||||
.withManifest({} as Manifest)
|
||||
.withStore<Store>()
|
||||
.build(true)
|
||||
|
||||
const storePath = sdk.StorePath
|
||||
|
||||
describe("Store", () => {
|
||||
test("types", async () => {
|
||||
;async () => {
|
||||
sdk.store.setOwn(todo<Effects>(), storePath.inputSpec, {
|
||||
someValue: "a",
|
||||
})
|
||||
sdk.store.setOwn(todo<Effects>(), storePath.inputSpec.someValue, "b")
|
||||
sdk.store.setOwn(todo<Effects>(), storePath, {
|
||||
inputSpec: { someValue: "b" },
|
||||
})
|
||||
sdk.store.setOwn(
|
||||
todo<Effects>(),
|
||||
storePath.inputSpec.someValue,
|
||||
|
||||
// @ts-expect-error Type is wrong for the setting value
|
||||
5,
|
||||
)
|
||||
sdk.store.setOwn(
|
||||
todo<Effects>(),
|
||||
// @ts-expect-error Path is wrong
|
||||
"/inputSpec/someVae3lue",
|
||||
"someValue",
|
||||
)
|
||||
|
||||
todo<Effects>().store.set<Store>({
|
||||
path: extractJsonPath(storePath.inputSpec.someValue),
|
||||
value: "b",
|
||||
})
|
||||
todo<Effects>().store.set<Store, "/inputSpec/some2Value">({
|
||||
path: extractJsonPath(storePath.inputSpec.someValue),
|
||||
//@ts-expect-error Path is wrong
|
||||
value: "someValueIn",
|
||||
})
|
||||
;(await sdk.store
|
||||
.getOwn(todo<Effects>(), storePath.inputSpec.someValue)
|
||||
.const()) satisfies string
|
||||
;(await sdk.store
|
||||
.getOwn(todo<Effects>(), storePath.inputSpec)
|
||||
.const()) satisfies Store["inputSpec"]
|
||||
await sdk.store // @ts-expect-error Path is wrong
|
||||
.getOwn(todo<Effects>(), "/inputSpec/somdsfeValue")
|
||||
.const()
|
||||
/// ----------------- ERRORS -----------------
|
||||
|
||||
sdk.store.setOwn(todo<Effects>(), storePath, {
|
||||
// @ts-expect-error Type is wrong for the setting value
|
||||
inputSpec: { someValue: "notInAOrB" },
|
||||
})
|
||||
sdk.store.setOwn(
|
||||
todo<Effects>(),
|
||||
sdk.StorePath.inputSpec.someValue,
|
||||
// @ts-expect-error Type is wrong for the setting value
|
||||
"notInAOrB",
|
||||
)
|
||||
;(await sdk.store
|
||||
.getOwn(todo<Effects>(), storePath.inputSpec.someValue)
|
||||
.const()) satisfies string
|
||||
;(await sdk.store
|
||||
.getOwn(todo<Effects>(), storePath.inputSpec)
|
||||
.const()) satisfies Store["inputSpec"]
|
||||
await sdk.store // @ts-expect-error Path is wrong
|
||||
.getOwn("/inputSpec/somdsfeValue")
|
||||
.const()
|
||||
|
||||
///
|
||||
;(await sdk.store
|
||||
.getOwn(todo<Effects>(), storePath.inputSpec.someValue)
|
||||
// @ts-expect-error satisfies type is wrong
|
||||
.const()) satisfies number
|
||||
await sdk.store // @ts-expect-error Path is wrong
|
||||
.getOwn(todo<Effects>(), extractJsonPath(storePath.inputSpec))
|
||||
.const()
|
||||
;(await todo<Effects>().store.get({
|
||||
path: extractJsonPath(storePath.inputSpec.someValue),
|
||||
callback: noop,
|
||||
})) satisfies string
|
||||
await todo<Effects>().store.get<Store, "/inputSpec/someValue">({
|
||||
// @ts-expect-error Path is wrong as in it doesn't match above
|
||||
path: "/inputSpec/someV2alue",
|
||||
callback: noop,
|
||||
})
|
||||
await todo<Effects>().store.get<Store, "/inputSpec/someV2alue">({
|
||||
// @ts-expect-error Path is wrong as in it doesn't exists in wrapper type
|
||||
path: "/inputSpec/someV2alue",
|
||||
callback: noop,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -27,12 +27,12 @@ const TIMES_TO_WAIT_FOR_PROC = 100
|
||||
async function prepBind(
|
||||
from: string | null,
|
||||
to: string,
|
||||
type?: "file" | "directory",
|
||||
type: "file" | "directory" | "infer",
|
||||
) {
|
||||
const fromMeta = from ? await fs.stat(from).catch((_) => null) : null
|
||||
const toMeta = await fs.stat(to).catch((_) => null)
|
||||
|
||||
if (type === "file" || (!type && from && fromMeta?.isFile())) {
|
||||
if (type === "file" || (type === "infer" && from && fromMeta?.isFile())) {
|
||||
if (toMeta && toMeta.isDirectory()) await fs.rmdir(to, { recursive: false })
|
||||
if (from && !fromMeta) {
|
||||
await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ""), { recursive: true })
|
||||
@@ -49,7 +49,11 @@ async function prepBind(
|
||||
}
|
||||
}
|
||||
|
||||
async function bind(from: string, to: string, type?: "file" | "directory") {
|
||||
async function bind(
|
||||
from: string,
|
||||
to: string,
|
||||
type: "file" | "directory" | "infer",
|
||||
) {
|
||||
await prepBind(from, to, type)
|
||||
|
||||
await execFile("mount", ["--bind", from, to])
|
||||
@@ -589,13 +593,13 @@ export type MountOptionsVolume = {
|
||||
volumeId: string
|
||||
subpath: string | null
|
||||
readonly: boolean
|
||||
filetype?: "file" | "directory"
|
||||
filetype: "file" | "directory" | "infer"
|
||||
}
|
||||
|
||||
export type MountOptionsAssets = {
|
||||
type: "assets"
|
||||
subpath: string | null
|
||||
filetype?: "file" | "directory"
|
||||
filetype: "file" | "directory" | "infer"
|
||||
}
|
||||
|
||||
export type MountOptionsPointer = {
|
||||
@@ -604,13 +608,13 @@ export type MountOptionsPointer = {
|
||||
volumeId: string
|
||||
subpath: string | null
|
||||
readonly: boolean
|
||||
filetype?: "file" | "directory"
|
||||
filetype: "file" | "directory" | "infer"
|
||||
}
|
||||
|
||||
export type MountOptionsBackup = {
|
||||
type: "backup"
|
||||
subpath: string | null
|
||||
filetype?: "file" | "directory"
|
||||
filetype: "file" | "directory" | "infer"
|
||||
}
|
||||
function wait(time: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time))
|
||||
|
||||
@@ -48,6 +48,8 @@ function fileMerge(...args: any[]): any {
|
||||
for (const arg of args) {
|
||||
if (res === arg) continue
|
||||
else if (
|
||||
res &&
|
||||
arg &&
|
||||
typeof res === "object" &&
|
||||
typeof arg === "object" &&
|
||||
!Array.isArray(res) &&
|
||||
@@ -81,8 +83,25 @@ export type Transformers<Raw = unknown, Transformed = unknown> = {
|
||||
onWrite: (value: Transformed) => Raw
|
||||
}
|
||||
|
||||
type ToPath = string | { volumeId: T.VolumeId; subpath: string }
|
||||
function toPath(path: ToPath): string {
|
||||
return typeof path === "string"
|
||||
? path
|
||||
: `/media/startos/volumes/${path.volumeId}/${path.subpath}`
|
||||
}
|
||||
|
||||
type Validator<T, U> = matches.Validator<T, U> | matches.Validator<unknown, U>
|
||||
|
||||
type ReadType<A> = {
|
||||
once: () => Promise<A | null>
|
||||
const: (effects: T.Effects) => Promise<A | null>
|
||||
watch: (effects: T.Effects) => AsyncGenerator<A | null, null, unknown>
|
||||
onChange: (
|
||||
effects: T.Effects,
|
||||
callback: (value: A | null, error?: Error) => void | Promise<void>,
|
||||
) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Use this class to read/write an underlying configuration file belonging to the upstream service.
|
||||
*
|
||||
@@ -174,8 +193,12 @@ export class FileHelper<A> {
|
||||
return this.validate(data)
|
||||
}
|
||||
|
||||
private async readConst(effects: T.Effects): Promise<A | null> {
|
||||
const watch = this.readWatch(effects)
|
||||
private async readConst<B>(
|
||||
effects: T.Effects,
|
||||
map: (value: A) => B,
|
||||
eq: (left: B | null | undefined, right: B | null) => boolean,
|
||||
): Promise<B | null> {
|
||||
const watch = this.readWatch(effects, map, eq)
|
||||
const res = await watch.next()
|
||||
if (effects.constRetry) {
|
||||
if (!this.consts.includes(effects.constRetry))
|
||||
@@ -188,7 +211,11 @@ export class FileHelper<A> {
|
||||
return res.value
|
||||
}
|
||||
|
||||
private async *readWatch(effects: T.Effects) {
|
||||
private async *readWatch<B>(
|
||||
effects: T.Effects,
|
||||
map: (value: A) => B,
|
||||
eq: (left: B | null | undefined, right: B | null) => boolean,
|
||||
) {
|
||||
let res
|
||||
while (effects.isInContext) {
|
||||
if (await exists(this.path)) {
|
||||
@@ -197,7 +224,8 @@ export class FileHelper<A> {
|
||||
persistent: false,
|
||||
signal: ctrl.signal,
|
||||
})
|
||||
res = await this.readOnce()
|
||||
const newResFull = await this.readOnce()
|
||||
const newRes = newResFull ? map(newResFull) : null
|
||||
const listen = Promise.resolve()
|
||||
.then(async () => {
|
||||
for await (const _ of watch) {
|
||||
@@ -206,7 +234,8 @@ export class FileHelper<A> {
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error(asError(e)))
|
||||
yield res
|
||||
if (!eq(res, newRes)) yield newRes
|
||||
res = newRes
|
||||
await listen
|
||||
} else {
|
||||
yield null
|
||||
@@ -216,12 +245,14 @@ export class FileHelper<A> {
|
||||
return null
|
||||
}
|
||||
|
||||
private readOnChange(
|
||||
private readOnChange<B>(
|
||||
effects: T.Effects,
|
||||
callback: (value: A | null, error?: Error) => void | Promise<void>,
|
||||
callback: (value: B | null, error?: Error) => void | Promise<void>,
|
||||
map: (value: A) => B,
|
||||
eq: (left: B | null | undefined, right: B | null) => boolean,
|
||||
) {
|
||||
;(async () => {
|
||||
for await (const value of this.readWatch(effects)) {
|
||||
for await (const value of this.readWatch(effects, map, eq)) {
|
||||
try {
|
||||
await callback(value)
|
||||
} catch (e) {
|
||||
@@ -241,15 +272,25 @@ export class FileHelper<A> {
|
||||
)
|
||||
}
|
||||
|
||||
get read() {
|
||||
read(): ReadType<A>
|
||||
read<B>(
|
||||
map: (value: A) => B,
|
||||
eq?: (left: B | null | undefined, right: B | null) => boolean,
|
||||
): ReadType<B>
|
||||
read(
|
||||
map?: (value: A) => any,
|
||||
eq?: (left: any, right: any) => boolean,
|
||||
): ReadType<any> {
|
||||
map = map ?? ((a: A) => a)
|
||||
eq = eq ?? ((left: any, right: any) => !partialDiff(left, right))
|
||||
return {
|
||||
once: () => this.readOnce(),
|
||||
const: (effects: T.Effects) => this.readConst(effects),
|
||||
watch: (effects: T.Effects) => this.readWatch(effects),
|
||||
const: (effects: T.Effects) => this.readConst(effects, map, eq),
|
||||
watch: (effects: T.Effects) => this.readWatch(effects, map, eq),
|
||||
onChange: (
|
||||
effects: T.Effects,
|
||||
callback: (value: A | null, error?: Error) => void | Promise<void>,
|
||||
) => this.readOnChange(effects, callback),
|
||||
) => this.readOnChange(effects, callback, map, eq),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,8 +332,13 @@ export class FileHelper<A> {
|
||||
* We wanted to be able to have a fileHelper, and just modify the path later in time.
|
||||
* Like one behavior of another dependency or something similar.
|
||||
*/
|
||||
withPath(path: string) {
|
||||
return new FileHelper<A>(path, this.writeData, this.readData, this.validate)
|
||||
withPath(path: ToPath) {
|
||||
return new FileHelper<A>(
|
||||
toPath(path),
|
||||
this.writeData,
|
||||
this.readData,
|
||||
this.validate,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,22 +347,22 @@ export class FileHelper<A> {
|
||||
* Provide custom functions for translating data to/from the file format.
|
||||
*/
|
||||
static raw<A>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
toFile: (dataIn: A) => string,
|
||||
fromFile: (rawData: string) => unknown,
|
||||
validate: (data: unknown) => A,
|
||||
) {
|
||||
return new FileHelper<A>(path, toFile, fromFile, validate)
|
||||
return new FileHelper<A>(toPath(path), toFile, fromFile, validate)
|
||||
}
|
||||
|
||||
private static rawTransformed<A extends Transformed, Raw, Transformed>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
toFile: (dataIn: Raw) => string,
|
||||
fromFile: (rawData: string) => Raw,
|
||||
validate: (data: Transformed) => A,
|
||||
transformers: Transformers<Raw, Transformed> | undefined,
|
||||
) {
|
||||
return new FileHelper<A>(
|
||||
return FileHelper.raw<A>(
|
||||
path,
|
||||
(inData) => {
|
||||
if (transformers) {
|
||||
@@ -332,18 +378,18 @@ export class FileHelper<A> {
|
||||
/**
|
||||
* Create a File Helper for a text file
|
||||
*/
|
||||
static string(path: string): FileHelper<string>
|
||||
static string(path: ToPath): FileHelper<string>
|
||||
static string<A extends string>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<string, A>,
|
||||
): FileHelper<A>
|
||||
static string<A extends Transformed, Transformed = string>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
transformers: Transformers<string, Transformed>,
|
||||
): FileHelper<A>
|
||||
static string<A extends Transformed, Transformed = string>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape?: Validator<Transformed, A>,
|
||||
transformers?: Transformers<string, Transformed>,
|
||||
) {
|
||||
@@ -363,7 +409,7 @@ export class FileHelper<A> {
|
||||
* Create a File Helper for a .json file.
|
||||
*/
|
||||
static json<A>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<unknown, A>,
|
||||
transformers?: Transformers,
|
||||
) {
|
||||
@@ -380,16 +426,16 @@ export class FileHelper<A> {
|
||||
* Create a File Helper for a .yaml file
|
||||
*/
|
||||
static yaml<A extends Record<string, unknown>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Record<string, unknown>, A>,
|
||||
): FileHelper<A>
|
||||
static yaml<A extends Transformed, Transformed = Record<string, unknown>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
transformers: Transformers<Record<string, unknown>, Transformed>,
|
||||
): FileHelper<A>
|
||||
static yaml<A extends Transformed, Transformed = Record<string, unknown>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
transformers?: Transformers<Record<string, unknown>, Transformed>,
|
||||
) {
|
||||
@@ -406,16 +452,16 @@ export class FileHelper<A> {
|
||||
* Create a File Helper for a .toml file
|
||||
*/
|
||||
static toml<A extends TOML.JsonMap>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<TOML.JsonMap, A>,
|
||||
): FileHelper<A>
|
||||
static toml<A extends Transformed, Transformed = TOML.JsonMap>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
transformers: Transformers<TOML.JsonMap, Transformed>,
|
||||
): FileHelper<A>
|
||||
static toml<A extends Transformed, Transformed = TOML.JsonMap>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
transformers?: Transformers<TOML.JsonMap, Transformed>,
|
||||
) {
|
||||
@@ -429,18 +475,18 @@ export class FileHelper<A> {
|
||||
}
|
||||
|
||||
static ini<A extends Record<string, unknown>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Record<string, unknown>, A>,
|
||||
options?: INI.EncodeOptions & INI.DecodeOptions,
|
||||
): FileHelper<A>
|
||||
static ini<A extends Transformed, Transformed = Record<string, unknown>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
options: INI.EncodeOptions & INI.DecodeOptions,
|
||||
transformers: Transformers<Record<string, unknown>, Transformed>,
|
||||
): FileHelper<A>
|
||||
static ini<A extends Transformed, Transformed = Record<string, unknown>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
options?: INI.EncodeOptions & INI.DecodeOptions,
|
||||
transformers?: Transformers<Record<string, unknown>, Transformed>,
|
||||
@@ -455,16 +501,16 @@ export class FileHelper<A> {
|
||||
}
|
||||
|
||||
static env<A extends Record<string, string>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Record<string, string>, A>,
|
||||
): FileHelper<A>
|
||||
static env<A extends Transformed, Transformed = Record<string, string>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
transformers: Transformers<Record<string, string>, Transformed>,
|
||||
): FileHelper<A>
|
||||
static env<A extends Transformed, Transformed = Record<string, string>>(
|
||||
path: string,
|
||||
path: ToPath,
|
||||
shape: Validator<Transformed, A>,
|
||||
transformers?: Transformers<Record<string, string>, Transformed>,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user