mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
create version graph to handle migrations (#2708)
* create version graph to handle migrations * Fix some version alpha test * connect dataVersion api * rename init fns * improve types and add tests * set data version after backup restore * chore: Add some types tests for version info * wip: More changes to versionInfo tests * wip: fix my stupid * update mocks * update runtime * chore: Fix the loop --------- Co-authored-by: Jade <2364004+Blu-J@users.noreply.github.com> Co-authored-by: J H <dragondef@gmail.com>
This commit is contained in:
@@ -284,6 +284,16 @@ function makeEffects(context: EffectContext): Effects {
|
|||||||
set: async (options: any) =>
|
set: async (options: any) =>
|
||||||
rpcRound("setStore", options) as ReturnType<T.Effects["store"]["set"]>,
|
rpcRound("setStore", options) as ReturnType<T.Effects["store"]["set"]>,
|
||||||
} as T.Effects["store"],
|
} as T.Effects["store"],
|
||||||
|
getDataVersion() {
|
||||||
|
return rpcRound("getDataVersion", {}) as ReturnType<
|
||||||
|
T.Effects["getDataVersion"]
|
||||||
|
>
|
||||||
|
},
|
||||||
|
setDataVersion(...[options]: Parameters<T.Effects["setDataVersion"]>) {
|
||||||
|
return rpcRound("setDataVersion", options) as ReturnType<
|
||||||
|
T.Effects["setDataVersion"]
|
||||||
|
>
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,9 +145,7 @@ export class SystemForStartOs implements System {
|
|||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
switch (options.procedure) {
|
switch (options.procedure) {
|
||||||
case "/init": {
|
case "/init": {
|
||||||
const previousVersion =
|
return this.abi.init({ effects })
|
||||||
string.optional().unsafeCast(options.input) || null
|
|
||||||
return this.abi.init({ effects, previousVersion })
|
|
||||||
}
|
}
|
||||||
case "/uninit": {
|
case "/uninit": {
|
||||||
const nextVersion = string.optional().unsafeCast(options.input) || null
|
const nextVersion = string.optional().unsafeCast(options.input) || null
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use exver::VersionRange;
|
use exver::VersionRange;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use models::{ActionId, DataUrl, HealthCheckId, HostId, PackageId, ServiceInterfaceId};
|
use models::{
|
||||||
|
ActionId, DataUrl, HealthCheckId, HostId, PackageId, ServiceInterfaceId, VersionString,
|
||||||
|
};
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
use patch_db::HasModel;
|
use patch_db::HasModel;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
@@ -335,6 +337,7 @@ pub struct ActionMetadata {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct PackageDataEntry {
|
pub struct PackageDataEntry {
|
||||||
pub state_info: PackageState,
|
pub state_info: PackageState,
|
||||||
|
pub data_version: Option<VersionString>,
|
||||||
pub status: Status,
|
pub status: Status,
|
||||||
#[ts(type = "string | null")]
|
#[ts(type = "string | null")]
|
||||||
pub registry: Option<Url>,
|
pub registry: Option<Url>,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use exver::ExtendedVersion;
|
use exver::{ExtendedVersion, VersionRange};
|
||||||
use models::ImageId;
|
use models::ImageId;
|
||||||
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
@@ -203,6 +203,8 @@ impl From<ManifestV1> for Manifest {
|
|||||||
version: ExtendedVersion::from(value.version).into(),
|
version: ExtendedVersion::from(value.version).into(),
|
||||||
satisfies: BTreeSet::new(),
|
satisfies: BTreeSet::new(),
|
||||||
release_notes: value.release_notes,
|
release_notes: value.release_notes,
|
||||||
|
can_migrate_from: VersionRange::any(),
|
||||||
|
can_migrate_to: VersionRange::none(),
|
||||||
license: value.license.into(),
|
license: value.license.into(),
|
||||||
wrapper_repo: value.wrapper_repo,
|
wrapper_repo: value.wrapper_repo,
|
||||||
upstream_repo: value.upstream_repo,
|
upstream_repo: value.upstream_repo,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use exver::Version;
|
use exver::{Version, VersionRange};
|
||||||
use helpers::const_true;
|
use helpers::const_true;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
pub use models::PackageId;
|
pub use models::PackageId;
|
||||||
@@ -37,6 +37,10 @@ pub struct Manifest {
|
|||||||
pub satisfies: BTreeSet<VersionString>,
|
pub satisfies: BTreeSet<VersionString>,
|
||||||
pub release_notes: String,
|
pub release_notes: String,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
|
pub can_migrate_to: VersionRange,
|
||||||
|
#[ts(type = "string")]
|
||||||
|
pub can_migrate_from: VersionRange,
|
||||||
|
#[ts(type = "string")]
|
||||||
pub license: InternedString, // type of license
|
pub license: InternedString, // type of license
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
pub wrapper_repo: Url,
|
pub wrapper_repo: Url,
|
||||||
@@ -159,8 +163,8 @@ impl Manifest {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct HardwareRequirements {
|
pub struct HardwareRequirements {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[ts(type = "{ device?: string, processor?: string }")]
|
#[ts(type = "{ display?: string, processor?: string }")]
|
||||||
pub device: BTreeMap<String, Regex>,
|
pub device: BTreeMap<String, Regex>, // TODO: array
|
||||||
#[ts(type = "number | null")]
|
#[ts(type = "number | null")]
|
||||||
pub ram: Option<u64>,
|
pub ram: Option<u64>,
|
||||||
#[ts(type = "string[] | null")]
|
#[ts(type = "string[] | null")]
|
||||||
|
|||||||
@@ -164,6 +164,25 @@ pub fn handler<C: Context>() -> ParentHandler<C> {
|
|||||||
// store
|
// store
|
||||||
.subcommand("getStore", from_fn_async(store::get_store).no_cli())
|
.subcommand("getStore", from_fn_async(store::get_store).no_cli())
|
||||||
.subcommand("setStore", from_fn_async(store::set_store).no_cli())
|
.subcommand("setStore", from_fn_async(store::set_store).no_cli())
|
||||||
|
.subcommand(
|
||||||
|
"setDataVersion",
|
||||||
|
from_fn_async(store::set_data_version)
|
||||||
|
.no_display()
|
||||||
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"getDataVersion",
|
||||||
|
from_fn_async(store::get_data_version)
|
||||||
|
.with_custom_display_fn(|_, v| {
|
||||||
|
if let Some(v) = v {
|
||||||
|
println!("{v}")
|
||||||
|
} else {
|
||||||
|
println!("N/A")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
|
)
|
||||||
// system
|
// system
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getSystemSmtp",
|
"getSystemSmtp",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use imbl::vector;
|
use imbl::vector;
|
||||||
use imbl_value::json;
|
use imbl_value::json;
|
||||||
use models::PackageId;
|
use models::{PackageId, VersionString};
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
|
|
||||||
use crate::service::effects::callbacks::CallbackHandler;
|
use crate::service::effects::callbacks::CallbackHandler;
|
||||||
@@ -91,3 +91,50 @@ pub async fn set_store(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct SetDataVersionParams {
|
||||||
|
#[ts(type = "string")]
|
||||||
|
version: VersionString,
|
||||||
|
}
|
||||||
|
pub async fn set_data_version(
|
||||||
|
context: EffectContext,
|
||||||
|
SetDataVersionParams { version }: SetDataVersionParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let context = context.deref()?;
|
||||||
|
let package_id = &context.seed.id;
|
||||||
|
context
|
||||||
|
.seed
|
||||||
|
.ctx
|
||||||
|
.db
|
||||||
|
.mutate(|db| {
|
||||||
|
db.as_public_mut()
|
||||||
|
.as_package_data_mut()
|
||||||
|
.as_idx_mut(package_id)
|
||||||
|
.or_not_found(package_id)?
|
||||||
|
.as_data_version_mut()
|
||||||
|
.ser(&Some(version))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_data_version(context: EffectContext) -> Result<Option<VersionString>, Error> {
|
||||||
|
let context = context.deref()?;
|
||||||
|
let package_id = &context.seed.id;
|
||||||
|
context
|
||||||
|
.seed
|
||||||
|
.ctx
|
||||||
|
.db
|
||||||
|
.peek()
|
||||||
|
.await
|
||||||
|
.as_public()
|
||||||
|
.as_package_data()
|
||||||
|
.as_idx(package_id)
|
||||||
|
.or_not_found(package_id)?
|
||||||
|
.as_data_version()
|
||||||
|
.de()
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ impl ServiceMap {
|
|||||||
} else {
|
} else {
|
||||||
PackageState::Installing(installing)
|
PackageState::Installing(installing)
|
||||||
},
|
},
|
||||||
|
data_version: None,
|
||||||
status: Status {
|
status: Status {
|
||||||
configured: false,
|
configured: false,
|
||||||
main: MainStatus::Stopped,
|
main: MainStatus::Stopped,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { healthCheck, HealthCheckParams } from "./health/HealthCheck"
|
|||||||
import { checkPortListening } from "./health/checkFns/checkPortListening"
|
import { checkPortListening } from "./health/checkFns/checkPortListening"
|
||||||
import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
||||||
import { List } from "./config/builder/list"
|
import { List } from "./config/builder/list"
|
||||||
import { Migration } from "./inits/migrations/Migration"
|
import { VersionInfo, VersionOptions } from "./versionInfo/VersionInfo"
|
||||||
import { Install, InstallFn } from "./inits/setupInstall"
|
import { Install, InstallFn } from "./inits/setupInstall"
|
||||||
import { setupActions } from "./actions/setupActions"
|
import { setupActions } from "./actions/setupActions"
|
||||||
import { setupDependencyConfig } from "./dependencies/setupDependencyConfig"
|
import { setupDependencyConfig } from "./dependencies/setupDependencyConfig"
|
||||||
@@ -38,9 +38,9 @@ import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
|
|||||||
import { setupInit } from "./inits/setupInit"
|
import { setupInit } from "./inits/setupInit"
|
||||||
import {
|
import {
|
||||||
EnsureUniqueId,
|
EnsureUniqueId,
|
||||||
Migrations,
|
VersionGraph,
|
||||||
setupMigrations,
|
setupVersionGraph,
|
||||||
} from "./inits/migrations/setupMigrations"
|
} from "./versionInfo/setupVersionGraph"
|
||||||
import { Uninstall, UninstallFn, setupUninstall } from "./inits/setupUninstall"
|
import { Uninstall, UninstallFn, setupUninstall } from "./inits/setupUninstall"
|
||||||
import { setupMain } from "./mainFn"
|
import { setupMain } from "./mainFn"
|
||||||
import { defaultTrigger } from "./trigger/defaultTrigger"
|
import { defaultTrigger } from "./trigger/defaultTrigger"
|
||||||
@@ -319,7 +319,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
setupActions: (...createdActions: CreatedAction<any, any, any>[]) =>
|
setupActions: (...createdActions: CreatedAction<any, any, any>[]) =>
|
||||||
setupActions<Manifest, Store>(...createdActions),
|
setupActions<Manifest, Store>(...createdActions),
|
||||||
setupBackups: (...args: SetupBackupsParams<Manifest>) =>
|
setupBackups: (...args: SetupBackupsParams<Manifest>) =>
|
||||||
setupBackups<Manifest>(...args),
|
setupBackups<Manifest>(this.manifest, ...args),
|
||||||
setupConfig: <
|
setupConfig: <
|
||||||
ConfigType extends Config<any, Store> | Config<any, never>,
|
ConfigType extends Config<any, Store> | Config<any, never>,
|
||||||
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
|
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
|
||||||
@@ -388,7 +388,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setupInit: (
|
setupInit: (
|
||||||
migrations: Migrations<Manifest, Store>,
|
versions: VersionGraph<Manifest["version"]>,
|
||||||
install: Install<Manifest, Store>,
|
install: Install<Manifest, Store>,
|
||||||
uninstall: Uninstall<Manifest, Store>,
|
uninstall: Uninstall<Manifest, Store>,
|
||||||
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
|
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
|
||||||
@@ -399,7 +399,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
exposedStore: ExposedStorePaths,
|
exposedStore: ExposedStorePaths,
|
||||||
) =>
|
) =>
|
||||||
setupInit<Manifest, Store>(
|
setupInit<Manifest, Store>(
|
||||||
migrations,
|
versions,
|
||||||
install,
|
install,
|
||||||
uninstall,
|
uninstall,
|
||||||
setInterfaces,
|
setInterfaces,
|
||||||
@@ -420,15 +420,13 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
|
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
|
||||||
}) => Promise<Daemons<Manifest, any>>,
|
}) => Promise<Daemons<Manifest, any>>,
|
||||||
) => setupMain<Manifest, Store>(fn),
|
) => setupMain<Manifest, Store>(fn),
|
||||||
setupMigrations: <
|
setupVersionGraph: <
|
||||||
Migrations extends Array<Migration<Manifest, Store, any>>,
|
CurrentVersion extends string,
|
||||||
|
OtherVersions extends Array<VersionInfo<any>>,
|
||||||
>(
|
>(
|
||||||
...migrations: EnsureUniqueId<Migrations>
|
current: VersionInfo<CurrentVersion>,
|
||||||
) =>
|
...other: EnsureUniqueId<OtherVersions, OtherVersions, CurrentVersion>
|
||||||
setupMigrations<Manifest, Store, Migrations>(
|
) => setupVersionGraph<CurrentVersion, OtherVersions>(current, ...other),
|
||||||
this.manifest,
|
|
||||||
...migrations,
|
|
||||||
),
|
|
||||||
setupProperties:
|
setupProperties:
|
||||||
(
|
(
|
||||||
fn: (options: { effects: Effects }) => Promise<T.SdkPropertiesReturn>,
|
fn: (options: { effects: Effects }) => Promise<T.SdkPropertiesReturn>,
|
||||||
@@ -549,12 +547,9 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
>,
|
>,
|
||||||
) => List.dynamicText<Store>(getA),
|
) => List.dynamicText<Store>(getA),
|
||||||
},
|
},
|
||||||
Migration: {
|
VersionInfo: {
|
||||||
of: <Version extends string>(options: {
|
of: <Version extends string>(options: VersionOptions<Version>) =>
|
||||||
version: Version & ValidateExVer<Version>
|
VersionInfo.of<Version>(options),
|
||||||
up: (opts: { effects: Effects }) => Promise<void>
|
|
||||||
down: (opts: { effects: Effects }) => Promise<void>
|
|
||||||
}) => Migration.of<Manifest, Store, Version>(options),
|
|
||||||
},
|
},
|
||||||
StorePath: pathBuilder<Store>(),
|
StorePath: pathBuilder<Store>(),
|
||||||
Value: {
|
Value: {
|
||||||
@@ -755,15 +750,9 @@ export async function runCommand<Manifest extends T.Manifest>(
|
|||||||
},
|
},
|
||||||
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
|
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
|
||||||
const commands = splitCommand(command)
|
const commands = splitCommand(command)
|
||||||
const overlay = await Overlay.of(effects, image)
|
return Overlay.with(effects, image, options.mounts || [], (overlay) =>
|
||||||
try {
|
overlay.exec(commands),
|
||||||
for (let mount of options.mounts || []) {
|
)
|
||||||
await overlay.mount(mount.options, mount.path)
|
|
||||||
}
|
|
||||||
return await overlay.exec(commands)
|
|
||||||
} finally {
|
|
||||||
await overlay.destroy()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn {
|
function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type SetupBackupsParams<M extends T.Manifest> = Array<
|
|||||||
>
|
>
|
||||||
|
|
||||||
export function setupBackups<M extends T.Manifest>(
|
export function setupBackups<M extends T.Manifest>(
|
||||||
|
manifest: M,
|
||||||
...args: _<SetupBackupsParams<M>>
|
...args: _<SetupBackupsParams<M>>
|
||||||
) {
|
) {
|
||||||
const backups = Array<Backups<M>>()
|
const backups = Array<Backups<M>>()
|
||||||
@@ -36,6 +37,7 @@ export function setupBackups<M extends T.Manifest>(
|
|||||||
for (const backup of backups) {
|
for (const backup of backups) {
|
||||||
await backup.build(options.pathMaker).restoreBackup(options)
|
await backup.build(options.pathMaker).restoreBackup(options)
|
||||||
}
|
}
|
||||||
|
await options.effects.setDataVersion({ version: manifest.version })
|
||||||
}) as T.ExpectedExports.restoreBackup
|
}) as T.ExpectedExports.restoreBackup
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as P from "./exver"
|
|||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export type ValidateVersion<T extends String> =
|
export type ValidateVersion<T extends String> =
|
||||||
T extends `-${infer A}` ? never :
|
T extends `-${infer A}` ? never :
|
||||||
T extends `${infer A}-${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
|
T extends `${infer A}-${string}` ? ValidateVersion<A> :
|
||||||
T extends `${bigint}` ? unknown :
|
T extends `${bigint}` ? unknown :
|
||||||
T extends `${bigint}.${infer A}` ? ValidateVersion<A> :
|
T extends `${bigint}.${infer A}` ? ValidateVersion<A> :
|
||||||
never
|
never
|
||||||
@@ -16,9 +16,9 @@ export type ValidateExVer<T extends string> =
|
|||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export type ValidateExVers<T> =
|
export type ValidateExVers<T> =
|
||||||
T extends [] ? unknown :
|
T extends [] ? unknown[] :
|
||||||
T extends [infer A, ...infer B] ? ValidateExVer<A & string> & ValidateExVers<B> :
|
T extends [infer A, ...infer B] ? ValidateExVer<A & string> & ValidateExVers<B> :
|
||||||
never
|
never[]
|
||||||
|
|
||||||
type Anchor = {
|
type Anchor = {
|
||||||
type: "Anchor"
|
type: "Anchor"
|
||||||
@@ -426,6 +426,7 @@ function tests() {
|
|||||||
testTypeVersion("12.34.56")
|
testTypeVersion("12.34.56")
|
||||||
testTypeVersion("1.2-3")
|
testTypeVersion("1.2-3")
|
||||||
testTypeVersion("1-3")
|
testTypeVersion("1-3")
|
||||||
|
testTypeVersion("1-alpha")
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
testTypeVersion("-3")
|
testTypeVersion("-3")
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { ValidateExVer } from "../../exver"
|
|
||||||
import * as T from "../../types"
|
|
||||||
|
|
||||||
export class Migration<
|
|
||||||
Manifest extends T.Manifest,
|
|
||||||
Store,
|
|
||||||
Version extends string,
|
|
||||||
> {
|
|
||||||
constructor(
|
|
||||||
readonly options: {
|
|
||||||
version: Version & ValidateExVer<Version>
|
|
||||||
up: (opts: { effects: T.Effects }) => Promise<void>
|
|
||||||
down: (opts: { effects: T.Effects }) => Promise<void>
|
|
||||||
},
|
|
||||||
) {}
|
|
||||||
static of<
|
|
||||||
Manifest extends T.Manifest,
|
|
||||||
Store,
|
|
||||||
Version extends string,
|
|
||||||
>(options: {
|
|
||||||
version: Version & ValidateExVer<Version>
|
|
||||||
up: (opts: { effects: T.Effects }) => Promise<void>
|
|
||||||
down: (opts: { effects: T.Effects }) => Promise<void>
|
|
||||||
}) {
|
|
||||||
return new Migration<Manifest, Store, Version>(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
async up(opts: { effects: T.Effects }) {
|
|
||||||
this.up(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(opts: { effects: T.Effects }) {
|
|
||||||
this.down(opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { ExtendedVersion } from "../../exver"
|
|
||||||
|
|
||||||
import * as T from "../../types"
|
|
||||||
import { once } from "../../util/once"
|
|
||||||
import { Migration } from "./Migration"
|
|
||||||
|
|
||||||
export class Migrations<Manifest extends T.Manifest, Store> {
|
|
||||||
private constructor(
|
|
||||||
readonly manifest: T.Manifest,
|
|
||||||
readonly migrations: Array<Migration<Manifest, Store, any>>,
|
|
||||||
) {}
|
|
||||||
private sortedMigrations = once(() => {
|
|
||||||
const migrationsAsVersions = (
|
|
||||||
this.migrations as Array<Migration<Manifest, Store, any>>
|
|
||||||
)
|
|
||||||
.map((x) => [ExtendedVersion.parse(x.options.version), x] as const)
|
|
||||||
.filter(([v, _]) => v.flavor === this.currentVersion().flavor)
|
|
||||||
migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0]))
|
|
||||||
return migrationsAsVersions
|
|
||||||
})
|
|
||||||
private currentVersion = once(() =>
|
|
||||||
ExtendedVersion.parse(this.manifest.version),
|
|
||||||
)
|
|
||||||
static of<
|
|
||||||
Manifest extends T.Manifest,
|
|
||||||
Store,
|
|
||||||
Migrations extends Array<Migration<Manifest, Store, any>>,
|
|
||||||
>(manifest: T.Manifest, ...migrations: EnsureUniqueId<Migrations>) {
|
|
||||||
return new Migrations(
|
|
||||||
manifest,
|
|
||||||
migrations as Array<Migration<Manifest, Store, any>>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
async init({
|
|
||||||
effects,
|
|
||||||
previousVersion,
|
|
||||||
}: Parameters<T.ExpectedExports.init>[0]) {
|
|
||||||
if (!!previousVersion) {
|
|
||||||
const previousVersionExVer = ExtendedVersion.parse(previousVersion)
|
|
||||||
for (const [_, migration] of this.sortedMigrations()
|
|
||||||
.filter((x) => x[0].greaterThan(previousVersionExVer))
|
|
||||||
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
|
|
||||||
await migration.up({ effects })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async uninit({
|
|
||||||
effects,
|
|
||||||
nextVersion,
|
|
||||||
}: Parameters<T.ExpectedExports.uninit>[0]) {
|
|
||||||
if (!!nextVersion) {
|
|
||||||
const nextVersionExVer = ExtendedVersion.parse(nextVersion)
|
|
||||||
const reversed = [...this.sortedMigrations()].reverse()
|
|
||||||
for (const [_, migration] of reversed
|
|
||||||
.filter((x) => x[0].greaterThan(nextVersionExVer))
|
|
||||||
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
|
|
||||||
await migration.down({ effects })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupMigrations<
|
|
||||||
Manifest extends T.Manifest,
|
|
||||||
Store,
|
|
||||||
Migrations extends Array<Migration<Manifest, Store, any>>,
|
|
||||||
>(manifest: T.Manifest, ...migrations: EnsureUniqueId<Migrations>) {
|
|
||||||
return Migrations.of<Manifest, Store, Migrations>(manifest, ...migrations)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
export type EnsureUniqueId<A, B = A, ids = never> =
|
|
||||||
B extends [] ? A :
|
|
||||||
B extends [Migration<any, any, infer id>, ...infer Rest] ? (
|
|
||||||
id extends ids ? "One of the ids are not unique"[] :
|
|
||||||
EnsureUniqueId<A, Rest, id | ids>
|
|
||||||
) : "There exists a migration that is not a Migration"[]
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { DependenciesReceipt } from "../config/setupConfig"
|
import { DependenciesReceipt } from "../config/setupConfig"
|
||||||
|
import { ExtendedVersion, VersionRange } from "../exver"
|
||||||
import { SetInterfaces } from "../interfaces/setupInterfaces"
|
import { SetInterfaces } from "../interfaces/setupInterfaces"
|
||||||
|
|
||||||
import { ExposedStorePaths } from "../store/setupExposeStore"
|
import { ExposedStorePaths } from "../store/setupExposeStore"
|
||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
import { Migrations } from "./migrations/setupMigrations"
|
import { VersionGraph } from "../versionInfo/setupVersionGraph"
|
||||||
import { Install } from "./setupInstall"
|
import { Install } from "./setupInstall"
|
||||||
import { Uninstall } from "./setupUninstall"
|
import { Uninstall } from "./setupUninstall"
|
||||||
|
|
||||||
export function setupInit<Manifest extends T.Manifest, Store>(
|
export function setupInit<Manifest extends T.Manifest, Store>(
|
||||||
migrations: Migrations<Manifest, Store>,
|
versions: VersionGraph<Manifest["version"]>,
|
||||||
install: Install<Manifest, Store>,
|
install: Install<Manifest, Store>,
|
||||||
uninstall: Uninstall<Manifest, Store>,
|
uninstall: Uninstall<Manifest, Store>,
|
||||||
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
|
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
|
||||||
@@ -23,8 +24,19 @@ export function setupInit<Manifest extends T.Manifest, Store>(
|
|||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
init: async (opts) => {
|
init: async (opts) => {
|
||||||
await migrations.init(opts)
|
const prev = await opts.effects.getDataVersion()
|
||||||
await install.init(opts)
|
if (prev) {
|
||||||
|
await versions.migrate({
|
||||||
|
effects: opts.effects,
|
||||||
|
from: ExtendedVersion.parse(prev),
|
||||||
|
to: versions.currentVersion(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await install.install(opts)
|
||||||
|
await opts.effects.setDataVersion({
|
||||||
|
version: versions.current.options.version,
|
||||||
|
})
|
||||||
|
}
|
||||||
await setInterfaces({
|
await setInterfaces({
|
||||||
...opts,
|
...opts,
|
||||||
input: null,
|
input: null,
|
||||||
@@ -33,8 +45,18 @@ export function setupInit<Manifest extends T.Manifest, Store>(
|
|||||||
await setDependencies({ effects: opts.effects, input: null })
|
await setDependencies({ effects: opts.effects, input: null })
|
||||||
},
|
},
|
||||||
uninit: async (opts) => {
|
uninit: async (opts) => {
|
||||||
await migrations.uninit(opts)
|
if (opts.nextVersion) {
|
||||||
await uninstall.uninit(opts)
|
const prev = await opts.effects.getDataVersion()
|
||||||
|
if (prev) {
|
||||||
|
await versions.migrate({
|
||||||
|
effects: opts.effects,
|
||||||
|
from: ExtendedVersion.parse(prev),
|
||||||
|
to: ExtendedVersion.parse(opts.nextVersion),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await uninstall.uninstall(opts)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,10 @@ export class Install<Manifest extends T.Manifest, Store> {
|
|||||||
return new Install(fn)
|
return new Install(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
async init({
|
async install({ effects }: Parameters<T.ExpectedExports.init>[0]) {
|
||||||
effects,
|
await this.fn({
|
||||||
previousVersion,
|
effects,
|
||||||
}: Parameters<T.ExpectedExports.init>[0]) {
|
})
|
||||||
if (!previousVersion)
|
|
||||||
await this.fn({
|
|
||||||
effects,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class Uninstall<Manifest extends T.Manifest, Store> {
|
|||||||
return new Uninstall(fn)
|
return new Uninstall(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninit({
|
async uninstall({
|
||||||
effects,
|
effects,
|
||||||
nextVersion,
|
nextVersion,
|
||||||
}: Parameters<T.ExpectedExports.uninit>[0]) {
|
}: Parameters<T.ExpectedExports.uninit>[0]) {
|
||||||
|
|||||||
@@ -7,22 +7,11 @@ import {
|
|||||||
ImageSource,
|
ImageSource,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
|
|
||||||
export type SDKManifest<
|
export type SDKManifest = {
|
||||||
Version extends string,
|
|
||||||
Satisfies extends string[] = [],
|
|
||||||
> = {
|
|
||||||
/** The package identifier used by the OS. This must be unique amongst all other known packages */
|
/** The package identifier used by the OS. This must be unique amongst all other known packages */
|
||||||
readonly id: string
|
readonly id: string
|
||||||
/** A human readable service title */
|
/** A human readable service title */
|
||||||
readonly title: string
|
readonly title: string
|
||||||
/** Service version - accepts up to four digits, where the last confirms to revisions necessary for StartOs
|
|
||||||
* - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of
|
|
||||||
* the service
|
|
||||||
*/
|
|
||||||
readonly version: Version & ValidateExVer<Version>
|
|
||||||
readonly satisfies?: Satisfies & ValidateExVers<Satisfies>
|
|
||||||
/** Release notes for the update - can be a string, paragraph or URL */
|
|
||||||
readonly releaseNotes: string
|
|
||||||
/** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/
|
/** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/
|
||||||
readonly license: string // name of license
|
readonly license: string // name of license
|
||||||
/** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this),
|
/** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as T from "../types"
|
|||||||
import { ImageConfig, ImageId, VolumeId } from "../osBindings"
|
import { ImageConfig, ImageId, VolumeId } from "../osBindings"
|
||||||
import { SDKManifest, SDKImageConfig } from "./ManifestTypes"
|
import { SDKManifest, SDKImageConfig } from "./ManifestTypes"
|
||||||
import { SDKVersion } from "../StartSdk"
|
import { SDKVersion } from "../StartSdk"
|
||||||
|
import { VersionGraph } from "../versionInfo/setupVersionGraph"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an example of a function that takes a manifest and returns a new manifest with additional properties
|
* This is an example of a function that takes a manifest and returns a new manifest with additional properties
|
||||||
@@ -21,10 +22,12 @@ export function setupManifest<
|
|||||||
assets: AssetTypes[]
|
assets: AssetTypes[]
|
||||||
images: Record<ImagesTypes, SDKImageConfig>
|
images: Record<ImagesTypes, SDKImageConfig>
|
||||||
volumes: VolumesTypes[]
|
volumes: VolumesTypes[]
|
||||||
version: Version
|
|
||||||
},
|
},
|
||||||
Satisfies extends string[] = [],
|
Satisfies extends string[] = [],
|
||||||
>(manifest: SDKManifest<Version, Satisfies> & Manifest): Manifest & T.Manifest {
|
>(
|
||||||
|
manifest: SDKManifest & Manifest,
|
||||||
|
versions: VersionGraph<Version>,
|
||||||
|
): Manifest & T.Manifest {
|
||||||
const images = Object.entries(manifest.images).reduce(
|
const images = Object.entries(manifest.images).reduce(
|
||||||
(images, [k, v]) => {
|
(images, [k, v]) => {
|
||||||
v.arch = v.arch || ["aarch64", "x86_64"]
|
v.arch = v.arch || ["aarch64", "x86_64"]
|
||||||
@@ -39,7 +42,11 @@ export function setupManifest<
|
|||||||
...manifest,
|
...manifest,
|
||||||
gitHash: null,
|
gitHash: null,
|
||||||
osVersion: SDKVersion,
|
osVersion: SDKVersion,
|
||||||
satisfies: manifest.satisfies || [],
|
version: versions.current.options.version,
|
||||||
|
releaseNotes: versions.current.options.releaseNotes,
|
||||||
|
satisfies: versions.current.options.satisfies || [],
|
||||||
|
canMigrateTo: versions.canMigrateTo().toString(),
|
||||||
|
canMigrateFrom: versions.canMigrateFrom().toString(),
|
||||||
images,
|
images,
|
||||||
alerts: {
|
alerts: {
|
||||||
install: manifest.alerts?.install || null,
|
install: manifest.alerts?.install || null,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type HardwareRequirements = {
|
export type HardwareRequirements = {
|
||||||
device: { device?: string; processor?: string }
|
device: { display?: string; processor?: string }
|
||||||
ram: number | null
|
ram: number | null
|
||||||
arch: string[] | null
|
arch: string[] | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export type Manifest = {
|
|||||||
version: Version
|
version: Version
|
||||||
satisfies: Array<Version>
|
satisfies: Array<Version>
|
||||||
releaseNotes: string
|
releaseNotes: string
|
||||||
|
canMigrateTo: string
|
||||||
|
canMigrateFrom: string
|
||||||
license: string
|
license: string
|
||||||
wrapperRepo: string
|
wrapperRepo: string
|
||||||
upstreamRepo: string
|
upstreamRepo: string
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import type { PackageState } from "./PackageState"
|
|||||||
import type { ServiceInterface } from "./ServiceInterface"
|
import type { ServiceInterface } from "./ServiceInterface"
|
||||||
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
|
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
|
||||||
import type { Status } from "./Status"
|
import type { Status } from "./Status"
|
||||||
|
import type { Version } from "./Version"
|
||||||
|
|
||||||
export type PackageDataEntry = {
|
export type PackageDataEntry = {
|
||||||
stateInfo: PackageState
|
stateInfo: PackageState
|
||||||
|
dataVersion: Version | null
|
||||||
status: Status
|
status: Status
|
||||||
registry: string | null
|
registry: string | null
|
||||||
developerKey: string
|
developerKey: string
|
||||||
|
|||||||
3
sdk/lib/osBindings/SetDataVersionParams.ts
Normal file
3
sdk/lib/osBindings/SetDataVersionParams.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type SetDataVersionParams = { version: string }
|
||||||
@@ -132,6 +132,7 @@ export { SessionList } from "./SessionList"
|
|||||||
export { Sessions } from "./Sessions"
|
export { Sessions } from "./Sessions"
|
||||||
export { Session } from "./Session"
|
export { Session } from "./Session"
|
||||||
export { SetConfigured } from "./SetConfigured"
|
export { SetConfigured } from "./SetConfigured"
|
||||||
|
export { SetDataVersionParams } from "./SetDataVersionParams"
|
||||||
export { SetDependenciesParams } from "./SetDependenciesParams"
|
export { SetDependenciesParams } from "./SetDependenciesParams"
|
||||||
export { SetHealth } from "./SetHealth"
|
export { SetHealth } from "./SetHealth"
|
||||||
export { SetMainStatusStatus } from "./SetMainStatusStatus"
|
export { SetMainStatusStatus } from "./SetMainStatusStatus"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { Variants } from "../config/builder/variants"
|
|||||||
import { ValueSpec } from "../config/configTypes"
|
import { ValueSpec } from "../config/configTypes"
|
||||||
import { setupManifest } from "../manifest/setupManifest"
|
import { setupManifest } from "../manifest/setupManifest"
|
||||||
import { StartSdk } from "../StartSdk"
|
import { StartSdk } from "../StartSdk"
|
||||||
|
import { VersionGraph } from "../versionInfo/setupVersionGraph"
|
||||||
|
import { VersionInfo } from "../versionInfo/VersionInfo"
|
||||||
|
|
||||||
describe("builder tests", () => {
|
describe("builder tests", () => {
|
||||||
test("text", async () => {
|
test("text", async () => {
|
||||||
@@ -366,42 +368,48 @@ describe("values", () => {
|
|||||||
test("datetime", async () => {
|
test("datetime", async () => {
|
||||||
const sdk = StartSdk.of()
|
const sdk = StartSdk.of()
|
||||||
.withManifest(
|
.withManifest(
|
||||||
setupManifest({
|
setupManifest(
|
||||||
id: "testOutput",
|
{
|
||||||
title: "",
|
id: "testOutput",
|
||||||
version: "1.0.0:0",
|
title: "",
|
||||||
releaseNotes: "",
|
license: "",
|
||||||
license: "",
|
wrapperRepo: "",
|
||||||
replaces: [],
|
upstreamRepo: "",
|
||||||
wrapperRepo: "",
|
supportSite: "",
|
||||||
upstreamRepo: "",
|
marketingSite: "",
|
||||||
supportSite: "",
|
donationUrl: null,
|
||||||
marketingSite: "",
|
description: {
|
||||||
donationUrl: null,
|
short: "",
|
||||||
description: {
|
long: "",
|
||||||
short: "",
|
},
|
||||||
long: "",
|
containers: {},
|
||||||
},
|
images: {},
|
||||||
containers: {},
|
volumes: [],
|
||||||
images: {},
|
assets: [],
|
||||||
volumes: [],
|
alerts: {
|
||||||
assets: [],
|
install: null,
|
||||||
alerts: {
|
update: null,
|
||||||
install: null,
|
uninstall: null,
|
||||||
update: null,
|
restore: null,
|
||||||
uninstall: null,
|
start: null,
|
||||||
restore: null,
|
stop: null,
|
||||||
start: null,
|
},
|
||||||
stop: null,
|
dependencies: {
|
||||||
},
|
"remote-test": {
|
||||||
dependencies: {
|
description: "",
|
||||||
"remote-test": {
|
optional: true,
|
||||||
description: "",
|
s9pk: "https://example.com/remote-test.s9pk",
|
||||||
optional: true,
|
},
|
||||||
s9pk: "https://example.com/remote-test.s9pk",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
VersionGraph.of(
|
||||||
|
VersionInfo.of({
|
||||||
|
version: "1.0.0:0",
|
||||||
|
releaseNotes: "",
|
||||||
|
migrations: {},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.withStore<{ test: "a" }>()
|
.withStore<{ test: "a" }>()
|
||||||
.build(true)
|
.build(true)
|
||||||
|
|||||||
148
sdk/lib/test/graph.test.ts
Normal file
148
sdk/lib/test/graph.test.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { Graph } from "../util/graph"
|
||||||
|
|
||||||
|
describe("graph", () => {
|
||||||
|
{
|
||||||
|
{
|
||||||
|
test("findVertex", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[{ from: baz, metadata: "baz-qux" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const match = Array.from(graph.findVertex((v) => v.metadata === "qux"))
|
||||||
|
expect(match).toHaveLength(1)
|
||||||
|
expect(match[0]).toBe(qux)
|
||||||
|
})
|
||||||
|
test("shortestPathA", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[{ from: baz, metadata: "baz-qux" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
graph.addEdge("foo-qux", foo, qux)
|
||||||
|
expect(graph.shortestPath(foo, qux) || []).toHaveLength(1)
|
||||||
|
})
|
||||||
|
test("shortestPathB", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[{ from: baz, metadata: "baz-qux" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
graph.addEdge("bar-qux", bar, qux)
|
||||||
|
expect(graph.shortestPath(foo, qux) || []).toHaveLength(2)
|
||||||
|
})
|
||||||
|
test("shortestPathC", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[{ from: baz, metadata: "baz-qux" }],
|
||||||
|
[{ to: foo, metadata: "qux-foo" }],
|
||||||
|
)
|
||||||
|
expect(graph.shortestPath(foo, qux) || []).toHaveLength(3)
|
||||||
|
})
|
||||||
|
test("bfs", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[
|
||||||
|
{ from: foo, metadata: "foo-qux" },
|
||||||
|
{ from: baz, metadata: "baz-qux" },
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const bfs = Array.from(graph.breadthFirstSearch(foo))
|
||||||
|
expect(bfs).toHaveLength(4)
|
||||||
|
expect(bfs[0]).toBe(foo)
|
||||||
|
expect(bfs[1]).toBe(bar)
|
||||||
|
expect(bfs[2]).toBe(qux)
|
||||||
|
expect(bfs[3]).toBe(baz)
|
||||||
|
})
|
||||||
|
test("reverseBfs", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[
|
||||||
|
{ from: foo, metadata: "foo-qux" },
|
||||||
|
{ from: baz, metadata: "baz-qux" },
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const bfs = Array.from(graph.reverseBreadthFirstSearch(qux))
|
||||||
|
expect(bfs).toHaveLength(4)
|
||||||
|
expect(bfs[0]).toBe(qux)
|
||||||
|
expect(bfs[1]).toBe(foo)
|
||||||
|
expect(bfs[2]).toBe(baz)
|
||||||
|
expect(bfs[3]).toBe(bar)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,45 +1,56 @@
|
|||||||
import { StartSdk } from "../StartSdk"
|
import { StartSdk } from "../StartSdk"
|
||||||
import { setupManifest } from "../manifest/setupManifest"
|
import { setupManifest } from "../manifest/setupManifest"
|
||||||
|
import { VersionInfo } from "../versionInfo/VersionInfo"
|
||||||
|
import { VersionGraph } from "../versionInfo/setupVersionGraph"
|
||||||
|
|
||||||
export type Manifest = any
|
export type Manifest = any
|
||||||
export const sdk = StartSdk.of()
|
export const sdk = StartSdk.of()
|
||||||
.withManifest(
|
.withManifest(
|
||||||
setupManifest({
|
setupManifest(
|
||||||
id: "testOutput",
|
{
|
||||||
title: "",
|
id: "testOutput",
|
||||||
version: "1.0:0",
|
title: "",
|
||||||
releaseNotes: "",
|
license: "",
|
||||||
license: "",
|
replaces: [],
|
||||||
replaces: [],
|
wrapperRepo: "",
|
||||||
wrapperRepo: "",
|
upstreamRepo: "",
|
||||||
upstreamRepo: "",
|
supportSite: "",
|
||||||
supportSite: "",
|
marketingSite: "",
|
||||||
marketingSite: "",
|
donationUrl: null,
|
||||||
donationUrl: null,
|
description: {
|
||||||
description: {
|
short: "",
|
||||||
short: "",
|
long: "",
|
||||||
long: "",
|
},
|
||||||
},
|
containers: {},
|
||||||
containers: {},
|
images: {},
|
||||||
images: {},
|
volumes: [],
|
||||||
volumes: [],
|
assets: [],
|
||||||
assets: [],
|
alerts: {
|
||||||
alerts: {
|
install: null,
|
||||||
install: null,
|
update: null,
|
||||||
update: null,
|
uninstall: null,
|
||||||
uninstall: null,
|
restore: null,
|
||||||
restore: null,
|
start: null,
|
||||||
start: null,
|
stop: null,
|
||||||
stop: null,
|
},
|
||||||
},
|
dependencies: {
|
||||||
dependencies: {
|
"remote-test": {
|
||||||
"remote-test": {
|
description: "",
|
||||||
description: "",
|
optional: false,
|
||||||
optional: false,
|
s9pk: "https://example.com/remote-test.s9pk",
|
||||||
s9pk: "https://example.com/remote-test.s9pk",
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
VersionGraph.of(
|
||||||
|
VersionInfo.of({
|
||||||
|
version: "1.0.0:0",
|
||||||
|
releaseNotes: "",
|
||||||
|
migrations: {},
|
||||||
|
})
|
||||||
|
.satisfies("#other:1.0.0:0")
|
||||||
|
.satisfies("#other:2.0.0:0"),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.withStore<{ storeRoot: { storeLeaf: "value" } }>()
|
.withStore<{ storeRoot: { storeLeaf: "value" } }>()
|
||||||
.build(true)
|
.build(true)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CheckDependenciesParam,
|
CheckDependenciesParam,
|
||||||
ExecuteAction,
|
ExecuteAction,
|
||||||
GetConfiguredParams,
|
GetConfiguredParams,
|
||||||
|
SetDataVersionParams,
|
||||||
SetMainStatus,
|
SetMainStatus,
|
||||||
} from ".././osBindings"
|
} from ".././osBindings"
|
||||||
import { CreateOverlayedImageParams } from ".././osBindings"
|
import { CreateOverlayedImageParams } from ".././osBindings"
|
||||||
@@ -46,6 +47,8 @@ describe("startosTypeValidation ", () => {
|
|||||||
restart: undefined,
|
restart: undefined,
|
||||||
shutdown: undefined,
|
shutdown: undefined,
|
||||||
setConfigured: {} as SetConfigured,
|
setConfigured: {} as SetConfigured,
|
||||||
|
setDataVersion: {} as SetDataVersionParams,
|
||||||
|
getDataVersion: undefined,
|
||||||
setHealth: {} as SetHealth,
|
setHealth: {} as SetHealth,
|
||||||
exposeForDependents: {} as ExposeForDependentsParams,
|
exposeForDependents: {} as ExposeForDependentsParams,
|
||||||
getSslCertificate: {} as WithCallback<GetSslCertificateParams>,
|
getSslCertificate: {} as WithCallback<GetSslCertificateParams>,
|
||||||
|
|||||||
@@ -102,10 +102,7 @@ export namespace ExpectedExports {
|
|||||||
* Every time a package completes an install, this function is called before the main.
|
* Every time a package completes an install, this function is called before the main.
|
||||||
* Can be used to do migration like things.
|
* Can be used to do migration like things.
|
||||||
*/
|
*/
|
||||||
export type init = (options: {
|
export type init = (options: { effects: Effects }) => Promise<unknown>
|
||||||
effects: Effects
|
|
||||||
previousVersion: null | string
|
|
||||||
}) => Promise<unknown>
|
|
||||||
/** This will be ran during any time a package is uninstalled, for example during a update
|
/** This will be ran during any time a package is uninstalled, for example during a update
|
||||||
* this will be called.
|
* this will be called.
|
||||||
*/
|
*/
|
||||||
@@ -437,6 +434,10 @@ export type Effects = {
|
|||||||
value: ExtractStore
|
value: ExtractStore
|
||||||
}): Promise<void>
|
}): Promise<void>
|
||||||
}
|
}
|
||||||
|
/** sets the version that this service's data has been migrated to */
|
||||||
|
setDataVersion(options: { version: string }): Promise<void>
|
||||||
|
/** returns the version that this service's data has been migrated to */
|
||||||
|
getDataVersion(): Promise<string | null>
|
||||||
|
|
||||||
// system
|
// system
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,23 @@ export class Overlay implements ExecSpawnable {
|
|||||||
return new Overlay(effects, id, rootfs, guid)
|
return new Overlay(effects, id, rootfs, guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async with<T>(
|
||||||
|
effects: T.Effects,
|
||||||
|
image: { id: T.ImageId; sharedRun?: boolean },
|
||||||
|
mounts: { options: MountOptions; path: string }[],
|
||||||
|
fn: (overlay: Overlay) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
const overlay = await Overlay.of(effects, image)
|
||||||
|
try {
|
||||||
|
for (let mount of mounts) {
|
||||||
|
await overlay.mount(mount.options, mount.path)
|
||||||
|
}
|
||||||
|
return await fn(overlay)
|
||||||
|
} finally {
|
||||||
|
await overlay.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async mount(options: MountOptions, path: string): Promise<Overlay> {
|
async mount(options: MountOptions, path: string): Promise<Overlay> {
|
||||||
path = path.startsWith("/")
|
path = path.startsWith("/")
|
||||||
? `${this.rootfs}${path}`
|
? `${this.rootfs}${path}`
|
||||||
|
|||||||
244
sdk/lib/util/graph.ts
Normal file
244
sdk/lib/util/graph.ts
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import { boolean } from "ts-matches"
|
||||||
|
|
||||||
|
export type Vertex<VMetadata = void, EMetadata = void> = {
|
||||||
|
metadata: VMetadata
|
||||||
|
edges: Array<Edge<EMetadata, VMetadata>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Edge<EMetadata = void, VMetadata = void> = {
|
||||||
|
metadata: EMetadata
|
||||||
|
from: Vertex<VMetadata, EMetadata>
|
||||||
|
to: Vertex<VMetadata, EMetadata>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Graph<VMetadata = void, EMetadata = void> {
|
||||||
|
private readonly vertices: Array<Vertex<VMetadata, EMetadata>> = []
|
||||||
|
constructor() {}
|
||||||
|
addVertex(
|
||||||
|
metadata: VMetadata,
|
||||||
|
fromEdges: Array<Omit<Edge<EMetadata, VMetadata>, "to">>,
|
||||||
|
toEdges: Array<Omit<Edge<EMetadata, VMetadata>, "from">>,
|
||||||
|
): Vertex<VMetadata, EMetadata> {
|
||||||
|
const vertex: Vertex<VMetadata, EMetadata> = {
|
||||||
|
metadata,
|
||||||
|
edges: [],
|
||||||
|
}
|
||||||
|
for (let edge of fromEdges) {
|
||||||
|
const vEdge = {
|
||||||
|
metadata: edge.metadata,
|
||||||
|
from: edge.from,
|
||||||
|
to: vertex,
|
||||||
|
}
|
||||||
|
edge.from.edges.push(vEdge)
|
||||||
|
vertex.edges.push(vEdge)
|
||||||
|
}
|
||||||
|
for (let edge of toEdges) {
|
||||||
|
const vEdge = {
|
||||||
|
metadata: edge.metadata,
|
||||||
|
from: vertex,
|
||||||
|
to: edge.to,
|
||||||
|
}
|
||||||
|
edge.to.edges.push(vEdge)
|
||||||
|
vertex.edges.push(vEdge)
|
||||||
|
}
|
||||||
|
this.vertices.push(vertex)
|
||||||
|
return vertex
|
||||||
|
}
|
||||||
|
findVertex(
|
||||||
|
predicate: (vertex: Vertex<VMetadata, EMetadata>) => boolean,
|
||||||
|
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
||||||
|
const veritces = this.vertices
|
||||||
|
function* gen() {
|
||||||
|
for (let vertex of veritces) {
|
||||||
|
if (predicate(vertex)) {
|
||||||
|
yield vertex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gen()
|
||||||
|
}
|
||||||
|
addEdge(
|
||||||
|
metadata: EMetadata,
|
||||||
|
from: Vertex<VMetadata, EMetadata>,
|
||||||
|
to: Vertex<VMetadata, EMetadata>,
|
||||||
|
): Edge<EMetadata, VMetadata> {
|
||||||
|
const edge = {
|
||||||
|
metadata,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}
|
||||||
|
edge.from.edges.push(edge)
|
||||||
|
edge.to.edges.push(edge)
|
||||||
|
return edge
|
||||||
|
}
|
||||||
|
breadthFirstSearch(
|
||||||
|
from:
|
||||||
|
| Vertex<VMetadata, EMetadata>
|
||||||
|
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
||||||
|
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
||||||
|
const visited: Array<Vertex<VMetadata, EMetadata>> = []
|
||||||
|
function* rec(
|
||||||
|
vertex: Vertex<VMetadata, EMetadata>,
|
||||||
|
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
||||||
|
if (visited.includes(vertex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visited.push(vertex)
|
||||||
|
yield vertex
|
||||||
|
let generators = vertex.edges
|
||||||
|
.filter((e) => e.from === vertex)
|
||||||
|
.map((e) => rec(e.to))
|
||||||
|
while (generators.length) {
|
||||||
|
let prev = generators
|
||||||
|
generators = []
|
||||||
|
for (let gen of prev) {
|
||||||
|
const next = gen.next()
|
||||||
|
if (!next.done) {
|
||||||
|
generators.push(gen)
|
||||||
|
yield next.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from instanceof Function) {
|
||||||
|
let generators = this.vertices.filter(from).map(rec)
|
||||||
|
return (function* () {
|
||||||
|
while (generators.length) {
|
||||||
|
let prev = generators
|
||||||
|
generators = []
|
||||||
|
for (let gen of prev) {
|
||||||
|
const next = gen.next()
|
||||||
|
if (!next.done) {
|
||||||
|
generators.push(gen)
|
||||||
|
yield next.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
} else {
|
||||||
|
return rec(from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reverseBreadthFirstSearch(
|
||||||
|
to:
|
||||||
|
| Vertex<VMetadata, EMetadata>
|
||||||
|
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
||||||
|
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
||||||
|
const visited: Array<Vertex<VMetadata, EMetadata>> = []
|
||||||
|
function* rec(
|
||||||
|
vertex: Vertex<VMetadata, EMetadata>,
|
||||||
|
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
||||||
|
if (visited.includes(vertex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visited.push(vertex)
|
||||||
|
yield vertex
|
||||||
|
let generators = vertex.edges
|
||||||
|
.filter((e) => e.to === vertex)
|
||||||
|
.map((e) => rec(e.from))
|
||||||
|
while (generators.length) {
|
||||||
|
let prev = generators
|
||||||
|
generators = []
|
||||||
|
for (let gen of prev) {
|
||||||
|
const next = gen.next()
|
||||||
|
if (!next.done) {
|
||||||
|
generators.push(gen)
|
||||||
|
yield next.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to instanceof Function) {
|
||||||
|
let generators = this.vertices.filter(to).map(rec)
|
||||||
|
return (function* () {
|
||||||
|
while (generators.length) {
|
||||||
|
let prev = generators
|
||||||
|
generators = []
|
||||||
|
for (let gen of prev) {
|
||||||
|
const next = gen.next()
|
||||||
|
if (!next.done) {
|
||||||
|
generators.push(gen)
|
||||||
|
yield next.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
} else {
|
||||||
|
return rec(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shortestPath(
|
||||||
|
from:
|
||||||
|
| Vertex<VMetadata, EMetadata>
|
||||||
|
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
||||||
|
to:
|
||||||
|
| Vertex<VMetadata, EMetadata>
|
||||||
|
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
||||||
|
): Array<Edge<EMetadata, VMetadata>> | void {
|
||||||
|
const isDone =
|
||||||
|
to instanceof Function
|
||||||
|
? to
|
||||||
|
: (v: Vertex<VMetadata, EMetadata>) => v === to
|
||||||
|
const path: Array<Edge<EMetadata, VMetadata>> = []
|
||||||
|
const visited: Array<Vertex<VMetadata, EMetadata>> = []
|
||||||
|
function* check(
|
||||||
|
vertex: Vertex<VMetadata, EMetadata>,
|
||||||
|
path: Array<Edge<EMetadata, VMetadata>>,
|
||||||
|
): Generator<undefined, Array<Edge<EMetadata, VMetadata>> | undefined> {
|
||||||
|
if (isDone(vertex)) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if (visited.includes(vertex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visited.push(vertex)
|
||||||
|
yield
|
||||||
|
let generators = vertex.edges
|
||||||
|
.filter((e) => e.from === vertex)
|
||||||
|
.map((e) => check(e.to, [...path, e]))
|
||||||
|
while (generators.length) {
|
||||||
|
let prev = generators
|
||||||
|
generators = []
|
||||||
|
for (let gen of prev) {
|
||||||
|
const next = gen.next()
|
||||||
|
if (next.done === true) {
|
||||||
|
if (next.value) {
|
||||||
|
return next.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generators.push(gen)
|
||||||
|
yield
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from instanceof Function) {
|
||||||
|
let generators = this.vertices.filter(from).map((v) => check(v, []))
|
||||||
|
while (generators.length) {
|
||||||
|
let prev = generators
|
||||||
|
generators = []
|
||||||
|
for (let gen of prev) {
|
||||||
|
const next = gen.next()
|
||||||
|
if (next.done === true) {
|
||||||
|
if (next.value) {
|
||||||
|
return next.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generators.push(gen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const gen = check(from, [])
|
||||||
|
while (true) {
|
||||||
|
const next = gen.next()
|
||||||
|
if (next.done) {
|
||||||
|
return next.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
sdk/lib/versionInfo/VersionInfo.ts
Normal file
78
sdk/lib/versionInfo/VersionInfo.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { ValidateExVer } from "../exver"
|
||||||
|
import * as T from "../types"
|
||||||
|
|
||||||
|
export const IMPOSSIBLE = Symbol("IMPOSSIBLE")
|
||||||
|
|
||||||
|
export type VersionOptions<Version extends string> = {
|
||||||
|
/** The version being described */
|
||||||
|
version: Version & ValidateExVer<Version>
|
||||||
|
/** The release notes for this version */
|
||||||
|
releaseNotes: string
|
||||||
|
/** Data migrations for this version */
|
||||||
|
migrations: {
|
||||||
|
/**
|
||||||
|
* A migration from the previous version
|
||||||
|
* Leave blank to indicate no migration is necessary
|
||||||
|
* Set to `IMPOSSIBLE` to indicate migrating from the previous version is not possible
|
||||||
|
*/
|
||||||
|
up?: ((opts: { effects: T.Effects }) => Promise<void>) | typeof IMPOSSIBLE
|
||||||
|
/**
|
||||||
|
* A migration to the previous version
|
||||||
|
* Leave blank to indicate no migration is necessary
|
||||||
|
* Set to `IMPOSSIBLE` to indicate downgrades are prohibited
|
||||||
|
*/
|
||||||
|
down?: ((opts: { effects: T.Effects }) => Promise<void>) | typeof IMPOSSIBLE
|
||||||
|
/**
|
||||||
|
* Additional migrations, such as fast-forward migrations, or migrations from other flavors
|
||||||
|
*/
|
||||||
|
other?: Record<string, (opts: { effects: T.Effects }) => Promise<void>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VersionInfo<Version extends string> {
|
||||||
|
private _version: null | Version = null
|
||||||
|
private constructor(
|
||||||
|
readonly options: VersionOptions<Version> & { satisfies: string[] },
|
||||||
|
) {}
|
||||||
|
static of<Version extends string>(options: VersionOptions<Version>) {
|
||||||
|
return new VersionInfo<Version>({ ...options, satisfies: [] })
|
||||||
|
}
|
||||||
|
/** Specify a version that this version is 100% backwards compatible to */
|
||||||
|
satisfies<V extends string>(
|
||||||
|
version: V & ValidateExVer<V>,
|
||||||
|
): VersionInfo<Version> {
|
||||||
|
return new VersionInfo({
|
||||||
|
...this.options,
|
||||||
|
satisfies: [...this.options.satisfies, version],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __type_tests() {
|
||||||
|
const version: VersionInfo<"1.0.0:0"> = VersionInfo.of({
|
||||||
|
version: "1.0.0:0",
|
||||||
|
releaseNotes: "",
|
||||||
|
migrations: {},
|
||||||
|
})
|
||||||
|
.satisfies("#other:1.0.0:0")
|
||||||
|
.satisfies("#other:2.0.0:0")
|
||||||
|
// @ts-expect-error
|
||||||
|
.satisfies("#other:2.f.0:0")
|
||||||
|
|
||||||
|
let a: VersionInfo<"1.0.0:0"> = version
|
||||||
|
// @ts-expect-error
|
||||||
|
let b: VersionInfo<"1.0.0:3"> = version
|
||||||
|
|
||||||
|
VersionInfo.of({
|
||||||
|
// @ts-expect-error
|
||||||
|
version: "test",
|
||||||
|
releaseNotes: "",
|
||||||
|
migrations: {},
|
||||||
|
})
|
||||||
|
VersionInfo.of({
|
||||||
|
// @ts-expect-error
|
||||||
|
version: "test" as string,
|
||||||
|
releaseNotes: "",
|
||||||
|
migrations: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
210
sdk/lib/versionInfo/setupVersionGraph.ts
Normal file
210
sdk/lib/versionInfo/setupVersionGraph.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { ExtendedVersion, VersionRange } from "../exver"
|
||||||
|
|
||||||
|
import * as T from "../types"
|
||||||
|
import { Graph, Vertex } from "../util/graph"
|
||||||
|
import { once } from "../util/once"
|
||||||
|
import { IMPOSSIBLE, VersionInfo } from "./VersionInfo"
|
||||||
|
|
||||||
|
export class VersionGraph<CurrentVersion extends string> {
|
||||||
|
private readonly graph: () => Graph<
|
||||||
|
ExtendedVersion | VersionRange,
|
||||||
|
((opts: { effects: T.Effects }) => Promise<void>) | undefined
|
||||||
|
>
|
||||||
|
private constructor(
|
||||||
|
readonly current: VersionInfo<CurrentVersion>,
|
||||||
|
versions: Array<VersionInfo<any>>,
|
||||||
|
) {
|
||||||
|
this.graph = once(() => {
|
||||||
|
const graph = new Graph<
|
||||||
|
ExtendedVersion | VersionRange,
|
||||||
|
((opts: { effects: T.Effects }) => Promise<void>) | undefined
|
||||||
|
>()
|
||||||
|
const flavorMap: Record<
|
||||||
|
string,
|
||||||
|
[
|
||||||
|
ExtendedVersion,
|
||||||
|
VersionInfo<any>,
|
||||||
|
Vertex<
|
||||||
|
ExtendedVersion | VersionRange,
|
||||||
|
((opts: { effects: T.Effects }) => Promise<void>) | undefined
|
||||||
|
>,
|
||||||
|
][]
|
||||||
|
> = {}
|
||||||
|
for (let version of [current, ...versions]) {
|
||||||
|
const v = ExtendedVersion.parse(version.options.version)
|
||||||
|
const vertex = graph.addVertex(v, [], [])
|
||||||
|
const flavor = v.flavor || ""
|
||||||
|
if (!flavorMap[flavor]) {
|
||||||
|
flavorMap[flavor] = []
|
||||||
|
}
|
||||||
|
flavorMap[flavor].push([v, version, vertex])
|
||||||
|
}
|
||||||
|
for (let flavor in flavorMap) {
|
||||||
|
flavorMap[flavor].sort((a, b) => a[0].compareForSort(b[0]))
|
||||||
|
let prev:
|
||||||
|
| [
|
||||||
|
ExtendedVersion,
|
||||||
|
VersionInfo<any>,
|
||||||
|
Vertex<
|
||||||
|
ExtendedVersion | VersionRange,
|
||||||
|
(opts: { effects: T.Effects }) => Promise<void>
|
||||||
|
>,
|
||||||
|
]
|
||||||
|
| undefined = undefined
|
||||||
|
for (let [v, version, vertex] of flavorMap[flavor]) {
|
||||||
|
if (version.options.migrations.up !== IMPOSSIBLE) {
|
||||||
|
let range
|
||||||
|
if (prev) {
|
||||||
|
graph.addEdge(version.options.migrations.up, prev[2], vertex)
|
||||||
|
range = VersionRange.anchor(">=", prev[0]).and(
|
||||||
|
VersionRange.anchor("<", v),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
range = VersionRange.anchor("<", v)
|
||||||
|
}
|
||||||
|
const vRange = graph.addVertex(range, [], [])
|
||||||
|
graph.addEdge(version.options.migrations.up, vRange, vertex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.options.migrations.down !== IMPOSSIBLE) {
|
||||||
|
let range
|
||||||
|
if (prev) {
|
||||||
|
graph.addEdge(version.options.migrations.down, vertex, prev[2])
|
||||||
|
range = VersionRange.anchor(">=", prev[0]).and(
|
||||||
|
VersionRange.anchor("<", v),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
range = VersionRange.anchor("<", v)
|
||||||
|
}
|
||||||
|
const vRange = graph.addVertex(range, [], [])
|
||||||
|
graph.addEdge(version.options.migrations.down, vertex, vRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.options.migrations.other) {
|
||||||
|
for (let rangeStr in version.options.migrations.other) {
|
||||||
|
const range = VersionRange.parse(rangeStr)
|
||||||
|
const vRange = graph.addVertex(range, [], [])
|
||||||
|
graph.addEdge(
|
||||||
|
version.options.migrations.other[rangeStr],
|
||||||
|
vRange,
|
||||||
|
vertex,
|
||||||
|
)
|
||||||
|
for (let matching of graph.findVertex(
|
||||||
|
(v) =>
|
||||||
|
v.metadata instanceof ExtendedVersion &&
|
||||||
|
v.metadata.satisfies(range),
|
||||||
|
)) {
|
||||||
|
graph.addEdge(
|
||||||
|
version.options.migrations.other[rangeStr],
|
||||||
|
matching,
|
||||||
|
vertex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return graph
|
||||||
|
})
|
||||||
|
}
|
||||||
|
currentVersion = once(() =>
|
||||||
|
ExtendedVersion.parse(this.current.options.version),
|
||||||
|
)
|
||||||
|
static of<
|
||||||
|
CurrentVersion extends string,
|
||||||
|
OtherVersions extends Array<VersionInfo<any>>,
|
||||||
|
>(
|
||||||
|
currentVersion: VersionInfo<CurrentVersion>,
|
||||||
|
...other: EnsureUniqueId<OtherVersions, OtherVersions, CurrentVersion>
|
||||||
|
) {
|
||||||
|
return new VersionGraph(currentVersion, other as Array<VersionInfo<any>>)
|
||||||
|
}
|
||||||
|
async migrate({
|
||||||
|
effects,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}: {
|
||||||
|
effects: T.Effects
|
||||||
|
from: ExtendedVersion
|
||||||
|
to: ExtendedVersion
|
||||||
|
}) {
|
||||||
|
const graph = this.graph()
|
||||||
|
if (from && to) {
|
||||||
|
const path = graph.shortestPath(
|
||||||
|
(v) =>
|
||||||
|
(v.metadata instanceof VersionRange &&
|
||||||
|
v.metadata.satisfiedBy(from)) ||
|
||||||
|
(v.metadata instanceof ExtendedVersion && v.metadata.equals(from)),
|
||||||
|
(v) =>
|
||||||
|
(v.metadata instanceof VersionRange && v.metadata.satisfiedBy(to)) ||
|
||||||
|
(v.metadata instanceof ExtendedVersion && v.metadata.equals(to)),
|
||||||
|
)
|
||||||
|
if (path) {
|
||||||
|
for (let edge of path) {
|
||||||
|
if (edge.metadata) {
|
||||||
|
await edge.metadata({ effects })
|
||||||
|
}
|
||||||
|
await effects.setDataVersion({ version: edge.to.metadata.toString() })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
canMigrateFrom = once(() =>
|
||||||
|
Array.from(
|
||||||
|
this.graph().reverseBreadthFirstSearch(
|
||||||
|
(v) =>
|
||||||
|
(v.metadata instanceof VersionRange &&
|
||||||
|
v.metadata.satisfiedBy(this.currentVersion())) ||
|
||||||
|
(v.metadata instanceof ExtendedVersion &&
|
||||||
|
v.metadata.equals(this.currentVersion())),
|
||||||
|
),
|
||||||
|
).reduce(
|
||||||
|
(acc, x) =>
|
||||||
|
acc.or(
|
||||||
|
x.metadata instanceof VersionRange
|
||||||
|
? x.metadata
|
||||||
|
: VersionRange.anchor("=", x.metadata),
|
||||||
|
),
|
||||||
|
VersionRange.none(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
canMigrateTo = once(() =>
|
||||||
|
Array.from(
|
||||||
|
this.graph().breadthFirstSearch(
|
||||||
|
(v) =>
|
||||||
|
(v.metadata instanceof VersionRange &&
|
||||||
|
v.metadata.satisfiedBy(this.currentVersion())) ||
|
||||||
|
(v.metadata instanceof ExtendedVersion &&
|
||||||
|
v.metadata.equals(this.currentVersion())),
|
||||||
|
),
|
||||||
|
).reduce(
|
||||||
|
(acc, x) =>
|
||||||
|
acc.or(
|
||||||
|
x.metadata instanceof VersionRange
|
||||||
|
? x.metadata
|
||||||
|
: VersionRange.anchor("=", x.metadata),
|
||||||
|
),
|
||||||
|
VersionRange.none(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupVersionGraph<
|
||||||
|
CurrentVersion extends string,
|
||||||
|
OtherVersions extends Array<VersionInfo<any>>,
|
||||||
|
>(
|
||||||
|
current: VersionInfo<CurrentVersion>,
|
||||||
|
...other: EnsureUniqueId<OtherVersions, OtherVersions, CurrentVersion>
|
||||||
|
) {
|
||||||
|
return VersionGraph.of<CurrentVersion, OtherVersions>(current, ...other)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
export type EnsureUniqueId<A, B = A, OtherVersions = never> =
|
||||||
|
B extends [] ? A :
|
||||||
|
B extends [VersionInfo<infer Version>, ...infer Rest] ? (
|
||||||
|
Version extends OtherVersions ? "One or more versions are not unique"[] :
|
||||||
|
EnsureUniqueId<A, Rest, Version | OtherVersions>
|
||||||
|
) : "There exists a migration that is not a Migration"[]
|
||||||
@@ -88,6 +88,8 @@ export module Mock {
|
|||||||
title: 'Bitcoin Core',
|
title: 'Bitcoin Core',
|
||||||
version: '0.21.0:0',
|
version: '0.21.0:0',
|
||||||
satisfies: [],
|
satisfies: [],
|
||||||
|
canMigrateTo: '!',
|
||||||
|
canMigrateFrom: '*',
|
||||||
gitHash: 'abcdefgh',
|
gitHash: 'abcdefgh',
|
||||||
description: {
|
description: {
|
||||||
short: 'A Bitcoin full node by Bitcoin Core.',
|
short: 'A Bitcoin full node by Bitcoin Core.',
|
||||||
@@ -132,6 +134,8 @@ export module Mock {
|
|||||||
title: 'Lightning Network Daemon',
|
title: 'Lightning Network Daemon',
|
||||||
version: '0.11.1:0',
|
version: '0.11.1:0',
|
||||||
satisfies: [],
|
satisfies: [],
|
||||||
|
canMigrateTo: '!',
|
||||||
|
canMigrateFrom: '*',
|
||||||
gitHash: 'abcdefgh',
|
gitHash: 'abcdefgh',
|
||||||
description: {
|
description: {
|
||||||
short: 'A bolt spec compliant client.',
|
short: 'A bolt spec compliant client.',
|
||||||
@@ -188,6 +192,8 @@ export module Mock {
|
|||||||
title: 'Bitcoin Proxy',
|
title: 'Bitcoin Proxy',
|
||||||
version: '0.2.2:0',
|
version: '0.2.2:0',
|
||||||
satisfies: [],
|
satisfies: [],
|
||||||
|
canMigrateTo: '!',
|
||||||
|
canMigrateFrom: '*',
|
||||||
gitHash: 'lmnopqrx',
|
gitHash: 'lmnopqrx',
|
||||||
description: {
|
description: {
|
||||||
short: 'A super charger for your Bitcoin node.',
|
short: 'A super charger for your Bitcoin node.',
|
||||||
@@ -1684,6 +1690,7 @@ export module Mock {
|
|||||||
state: 'installed',
|
state: 'installed',
|
||||||
manifest: MockManifestBitcoind,
|
manifest: MockManifestBitcoind,
|
||||||
},
|
},
|
||||||
|
dataVersion: MockManifestBitcoind.version,
|
||||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
icon: '/assets/img/service-icons/bitcoind.svg',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
status: {
|
||||||
@@ -1860,6 +1867,7 @@ export module Mock {
|
|||||||
state: 'installed',
|
state: 'installed',
|
||||||
manifest: MockManifestBitcoinProxy,
|
manifest: MockManifestBitcoinProxy,
|
||||||
},
|
},
|
||||||
|
dataVersion: MockManifestBitcoinProxy.version,
|
||||||
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
|
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
status: {
|
||||||
@@ -1908,6 +1916,7 @@ export module Mock {
|
|||||||
state: 'installed',
|
state: 'installed',
|
||||||
manifest: MockManifestLnd,
|
manifest: MockManifestLnd,
|
||||||
},
|
},
|
||||||
|
dataVersion: MockManifestLnd.version,
|
||||||
icon: '/assets/img/service-icons/lnd.png',
|
icon: '/assets/img/service-icons/lnd.png',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export const mockPatchData: DataModel = {
|
|||||||
version: '0.20.0:0',
|
version: '0.20.0:0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dataVersion: '0.20.0:0',
|
||||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
icon: '/assets/img/service-icons/bitcoind.svg',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
status: {
|
||||||
@@ -295,6 +296,7 @@ export const mockPatchData: DataModel = {
|
|||||||
version: '0.11.0:0.0.1',
|
version: '0.11.0:0.0.1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dataVersion: '0.11.0:0.0.1',
|
||||||
icon: '/assets/img/service-icons/lnd.png',
|
icon: '/assets/img/service-icons/lnd.png',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
Reference in New Issue
Block a user