mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
backend changes
This commit is contained in:
@@ -35,13 +35,13 @@ const SOCKET_PATH = "/media/startos/rpc/host.sock"
|
||||
let hostSystemId = 0
|
||||
|
||||
export type EffectContext = {
|
||||
procedureId: string | null
|
||||
eventId: string | null
|
||||
callbacks?: CallbackHolder
|
||||
constRetry?: () => void
|
||||
}
|
||||
|
||||
const rpcRoundFor =
|
||||
(procedureId: string | null) =>
|
||||
(eventId: string | null) =>
|
||||
<K extends T.EffectMethod | "clearCallbacks">(
|
||||
method: K,
|
||||
params: Record<string, unknown>,
|
||||
@@ -52,7 +52,7 @@ const rpcRoundFor =
|
||||
JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params: { ...params, procedureId: procedureId || undefined },
|
||||
params: { ...params, eventId: eventId ?? undefined },
|
||||
}) + "\n",
|
||||
)
|
||||
})
|
||||
@@ -103,8 +103,9 @@ const rpcRoundFor =
|
||||
}
|
||||
|
||||
export function makeEffects(context: EffectContext): Effects {
|
||||
const rpcRound = rpcRoundFor(context.procedureId)
|
||||
const rpcRound = rpcRoundFor(context.eventId)
|
||||
const self: Effects = {
|
||||
eventId: context.eventId,
|
||||
child: (name) =>
|
||||
makeEffects({ ...context, callbacks: context.callbacks?.child(name) }),
|
||||
constRetry: context.constRetry,
|
||||
|
||||
@@ -242,11 +242,11 @@ export class RpcListener {
|
||||
.when(runType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const { input, timeout, id: procedureId } = params
|
||||
const { input, timeout, id: eventId } = params
|
||||
const result = this.getResult(
|
||||
procedure,
|
||||
system,
|
||||
procedureId,
|
||||
eventId,
|
||||
timeout,
|
||||
input,
|
||||
)
|
||||
@@ -256,11 +256,11 @@ export class RpcListener {
|
||||
.when(sandboxRunType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const { input, timeout, id: procedureId } = params
|
||||
const { input, timeout, id: eventId } = params
|
||||
const result = this.getResult(
|
||||
procedure,
|
||||
system,
|
||||
procedureId,
|
||||
eventId,
|
||||
timeout,
|
||||
input,
|
||||
)
|
||||
@@ -275,7 +275,7 @@ export class RpcListener {
|
||||
const callbacks =
|
||||
this.callbacks?.getChild("main") || this.callbacks?.child("main")
|
||||
const effects = makeEffects({
|
||||
procedureId: null,
|
||||
eventId: null,
|
||||
callbacks,
|
||||
})
|
||||
return handleRpc(
|
||||
@@ -304,7 +304,7 @@ export class RpcListener {
|
||||
}
|
||||
await this._system.exit(
|
||||
makeEffects({
|
||||
procedureId: params.id,
|
||||
eventId: params.id,
|
||||
}),
|
||||
target,
|
||||
)
|
||||
@@ -320,14 +320,14 @@ export class RpcListener {
|
||||
const system = await this.getDependencies.system()
|
||||
this.callbacks = new CallbackHolder(
|
||||
makeEffects({
|
||||
procedureId: params.id,
|
||||
eventId: params.id,
|
||||
}),
|
||||
)
|
||||
const callbacks = this.callbacks.child("init")
|
||||
console.error("Initializing...")
|
||||
await system.init(
|
||||
makeEffects({
|
||||
procedureId: params.id,
|
||||
eventId: params.id,
|
||||
callbacks,
|
||||
}),
|
||||
params.kind,
|
||||
@@ -399,7 +399,7 @@ export class RpcListener {
|
||||
private getResult(
|
||||
procedure: typeof jsonPath._TYPE,
|
||||
system: System,
|
||||
procedureId: string,
|
||||
eventId: string,
|
||||
timeout: number | null | undefined,
|
||||
input: any,
|
||||
) {
|
||||
@@ -410,7 +410,7 @@ export class RpcListener {
|
||||
}
|
||||
const callbacks = this.callbacks?.child(procedure)
|
||||
const effects = makeEffects({
|
||||
procedureId,
|
||||
eventId,
|
||||
callbacks,
|
||||
})
|
||||
|
||||
|
||||
@@ -509,13 +509,18 @@ export class SystemForEmbassy implements System {
|
||||
): Promise<T.ActionInput | null> {
|
||||
if (actionId === "config") {
|
||||
const config = await this.getConfig(effects, timeoutMs)
|
||||
return { spec: config.spec, value: config.config }
|
||||
return {
|
||||
eventId: effects.eventId!,
|
||||
spec: config.spec,
|
||||
value: config.config,
|
||||
}
|
||||
} else if (actionId === "properties") {
|
||||
return null
|
||||
} else {
|
||||
const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"]
|
||||
if (!oldSpec) return null
|
||||
return {
|
||||
eventId: effects.eventId!,
|
||||
spec: transformConfigSpec(oldSpec as OldConfigSpec),
|
||||
value: null,
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActionInput {
|
||||
pub event_id: Guid,
|
||||
#[ts(type = "Record<string, unknown>")]
|
||||
pub spec: Value,
|
||||
#[ts(type = "Record<string, unknown> | null")]
|
||||
@@ -270,6 +271,7 @@ pub fn display_action_result<T: Serialize>(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RunActionParams {
|
||||
pub package_id: PackageId,
|
||||
pub event_id: Option<Guid>,
|
||||
pub action_id: ActionId,
|
||||
#[ts(optional, type = "any")]
|
||||
pub input: Option<Value>,
|
||||
@@ -278,6 +280,7 @@ pub struct RunActionParams {
|
||||
#[derive(Parser)]
|
||||
struct CliRunActionParams {
|
||||
pub package_id: PackageId,
|
||||
pub event_id: Option<Guid>,
|
||||
pub action_id: ActionId,
|
||||
#[command(flatten)]
|
||||
pub input: StdinDeserializable<Option<Value>>,
|
||||
@@ -286,12 +289,14 @@ impl From<CliRunActionParams> for RunActionParams {
|
||||
fn from(
|
||||
CliRunActionParams {
|
||||
package_id,
|
||||
event_id,
|
||||
action_id,
|
||||
input,
|
||||
}: CliRunActionParams,
|
||||
) -> Self {
|
||||
Self {
|
||||
package_id,
|
||||
event_id,
|
||||
action_id,
|
||||
input: input.0,
|
||||
}
|
||||
@@ -331,6 +336,7 @@ pub async fn run_action(
|
||||
ctx: RpcContext,
|
||||
RunActionParams {
|
||||
package_id,
|
||||
event_id,
|
||||
action_id,
|
||||
input,
|
||||
}: RunActionParams,
|
||||
@@ -340,7 +346,11 @@ pub async fn run_action(
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(lazy_format!("Manager for {}", package_id))?
|
||||
.run_action(Guid::new(), action_id, input.unwrap_or_default())
|
||||
.run_action(
|
||||
event_id.unwrap_or_default(),
|
||||
action_id,
|
||||
input.unwrap_or_default(),
|
||||
)
|
||||
.await
|
||||
.map(|res| res.map(ActionResult::upcast))
|
||||
}
|
||||
|
||||
@@ -198,13 +198,12 @@ pub struct NetworkInfo {
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct NetworkInterfaceInfo {
|
||||
pub inbound: Option<bool>,
|
||||
pub outbound: Option<bool>,
|
||||
pub public: Option<bool>,
|
||||
pub ip_info: Option<IpInfo>,
|
||||
}
|
||||
impl NetworkInterfaceInfo {
|
||||
pub fn inbound(&self) -> bool {
|
||||
self.inbound.unwrap_or_else(|| {
|
||||
pub fn public(&self) -> bool {
|
||||
self.public.unwrap_or_else(|| {
|
||||
!self.ip_info.as_ref().map_or(true, |ip_info| {
|
||||
let ip4s = ip_info
|
||||
.subnets
|
||||
|
||||
@@ -169,7 +169,7 @@ impl LanPortForwardController {
|
||||
(
|
||||
iface.clone(),
|
||||
(
|
||||
info.inbound(),
|
||||
info.public(),
|
||||
info.ip_info.as_ref().map_or(Vec::new(), |i| {
|
||||
i.subnets
|
||||
.iter()
|
||||
@@ -205,7 +205,7 @@ impl LanPortForwardController {
|
||||
ip_info
|
||||
.iter()
|
||||
.map(|(iface, info)| (iface.clone(), (
|
||||
info.inbound(),
|
||||
info.public(),
|
||||
info.ip_info.as_ref().map_or(Vec::new(), |i| {
|
||||
i.subnets
|
||||
.iter()
|
||||
|
||||
@@ -335,7 +335,7 @@ impl NetServiceData {
|
||||
for (interface, public, ip_info) in
|
||||
net_ifaces.iter().filter_map(|(interface, info)| {
|
||||
if let Some(ip_info) = &info.ip_info {
|
||||
Some((interface, info.inbound(), ip_info))
|
||||
Some((interface, info.public(), ip_info))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
|
||||
info.ip_info.as_ref()
|
||||
.and_then(|ip_info| ip_info.device_type)
|
||||
.map_or_else(|| "UNKNOWN".to_owned(), |ty| format!("{ty:?}")),
|
||||
info.inbound(),
|
||||
info.public(),
|
||||
info.ip_info.as_ref().map_or_else(
|
||||
|| "<DISCONNECTED>".to_owned(),
|
||||
|ip_info| ip_info.subnets
|
||||
@@ -585,21 +585,24 @@ async fn watch_ip(
|
||||
None
|
||||
};
|
||||
|
||||
write_to.send_if_modified(|m| {
|
||||
let (inbound, outbound) = m
|
||||
.get(&iface)
|
||||
.map_or((None, None), |i| (i.inbound, i.outbound));
|
||||
m.insert(
|
||||
iface.clone(),
|
||||
NetworkInterfaceInfo {
|
||||
inbound,
|
||||
outbound,
|
||||
ip_info: ip_info.clone(),
|
||||
},
|
||||
)
|
||||
.filter(|old| &old.ip_info == &ip_info)
|
||||
.is_none()
|
||||
});
|
||||
write_to.send_if_modified(
|
||||
|m: &mut BTreeMap<
|
||||
InternedString,
|
||||
NetworkInterfaceInfo,
|
||||
>| {
|
||||
let public =
|
||||
m.get(&iface).map_or(None, |i| i.public);
|
||||
m.insert(
|
||||
iface.clone(),
|
||||
NetworkInterfaceInfo {
|
||||
public,
|
||||
ip_info: ip_info.clone(),
|
||||
},
|
||||
)
|
||||
.filter(|old| &old.ip_info == &ip_info)
|
||||
.is_none()
|
||||
},
|
||||
);
|
||||
|
||||
Ok::<_, Error>(())
|
||||
})
|
||||
@@ -856,7 +859,7 @@ impl NetworkInterfaceController {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
.inbound,
|
||||
.public,
|
||||
public,
|
||||
);
|
||||
prev != public
|
||||
@@ -968,8 +971,7 @@ impl ListenerMap {
|
||||
) -> Result<(), Error> {
|
||||
let mut keep = BTreeSet::<SocketAddr>::new();
|
||||
for info in ip_info.values().chain([&NetworkInterfaceInfo {
|
||||
inbound: Some(false),
|
||||
outbound: Some(false),
|
||||
public: Some(false),
|
||||
ip_info: Some(IpInfo {
|
||||
name: "lo".into(),
|
||||
scope_id: 1,
|
||||
@@ -984,7 +986,7 @@ impl ListenerMap {
|
||||
ntp_servers: Default::default(),
|
||||
}),
|
||||
}]) {
|
||||
if public || !info.inbound() {
|
||||
if public || !info.public() {
|
||||
if let Some(ip_info) = &info.ip_info {
|
||||
for ipnet in &ip_info.subnets {
|
||||
let addr = match ipnet.addr() {
|
||||
@@ -1003,7 +1005,7 @@ impl ListenerMap {
|
||||
};
|
||||
keep.insert(addr);
|
||||
if let Some((_, is_public, wan_ip)) = self.listeners.get_mut(&addr) {
|
||||
*is_public = info.inbound();
|
||||
*is_public = info.public();
|
||||
*wan_ip = info.ip_info.as_ref().and_then(|i| i.wan_ip);
|
||||
continue;
|
||||
}
|
||||
@@ -1021,7 +1023,7 @@ impl ListenerMap {
|
||||
.into(),
|
||||
)
|
||||
.with_kind(ErrorKind::Network)?,
|
||||
info.inbound(),
|
||||
info.public(),
|
||||
info.ip_info.as_ref().and_then(|i| i.wan_ip),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -110,10 +110,6 @@ pub async fn list(
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<NotificationWithId>, Error>>()?;
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_unread_notification_count_mut()
|
||||
.ser(&0)?;
|
||||
Ok(notifs)
|
||||
}
|
||||
Some(before) => {
|
||||
@@ -195,22 +191,23 @@ pub async fn mark_seen(
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let mut diff = 0;
|
||||
let n = db.as_private_mut().as_notifications_mut();
|
||||
for id in ids {
|
||||
if !n
|
||||
.as_idx_mut(&id)
|
||||
n.as_idx_mut(&id)
|
||||
.or_not_found(lazy_format!("Notification #{id}"))?
|
||||
.as_seen_mut()
|
||||
.replace(&true)?
|
||||
{
|
||||
diff += 1;
|
||||
.ser(&true)?;
|
||||
}
|
||||
let mut unread = 0;
|
||||
for (_, n) in n.as_entries()? {
|
||||
if !n.as_seen().de()? {
|
||||
unread += 1;
|
||||
}
|
||||
}
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_unread_notification_count_mut()
|
||||
.mutate(|n| Ok(*n -= diff))?;
|
||||
.ser(&unread)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -223,22 +220,23 @@ pub async fn mark_seen_before(
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let mut diff = 0;
|
||||
let n = db.as_private_mut().as_notifications_mut();
|
||||
for id in n.keys()?.range(..before) {
|
||||
if !n
|
||||
.as_idx_mut(&id)
|
||||
n.as_idx_mut(&id)
|
||||
.or_not_found(lazy_format!("Notification #{id}"))?
|
||||
.as_seen_mut()
|
||||
.replace(&true)?
|
||||
{
|
||||
diff += 1;
|
||||
.ser(&true)?;
|
||||
}
|
||||
let mut unread = 0;
|
||||
for (_, n) in n.as_entries()? {
|
||||
if !n.as_seen().de()? {
|
||||
unread += 1;
|
||||
}
|
||||
}
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_unread_notification_count_mut()
|
||||
.mutate(|n| Ok(*n -= diff))?;
|
||||
.ser(&unread)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -251,21 +249,23 @@ pub async fn mark_unseen(
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let mut diff = 0;
|
||||
let n = db.as_private_mut().as_notifications_mut();
|
||||
for id in ids {
|
||||
if n.as_idx_mut(&id)
|
||||
n.as_idx_mut(&id)
|
||||
.or_not_found(lazy_format!("Notification #{id}"))?
|
||||
.as_seen_mut()
|
||||
.replace(&false)?
|
||||
{
|
||||
diff += 1;
|
||||
.ser(&false)?;
|
||||
}
|
||||
let mut unread = 0;
|
||||
for (_, n) in n.as_entries()? {
|
||||
if !n.as_seen().de()? {
|
||||
unread += 1;
|
||||
}
|
||||
}
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_unread_notification_count_mut()
|
||||
.mutate(|n| Ok(*n += diff))?;
|
||||
.ser(&unread)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -115,7 +115,7 @@ pub fn update_tasks(
|
||||
}
|
||||
|
||||
pub(super) struct RunAction {
|
||||
id: ActionId,
|
||||
action_id: ActionId,
|
||||
input: Value,
|
||||
}
|
||||
impl Handler<RunAction> for ServiceActor {
|
||||
@@ -127,7 +127,7 @@ impl Handler<RunAction> for ServiceActor {
|
||||
&mut self,
|
||||
id: Guid,
|
||||
RunAction {
|
||||
id: ref action_id,
|
||||
ref action_id,
|
||||
input,
|
||||
}: RunAction,
|
||||
jobs: &BackgroundJobQueue,
|
||||
@@ -145,7 +145,7 @@ impl Handler<RunAction> for ServiceActor {
|
||||
.into_idx(package_id)
|
||||
.or_not_found(package_id)?
|
||||
.into_actions()
|
||||
.into_idx(&action_id)
|
||||
.into_idx(action_id)
|
||||
.or_not_found(lazy_format!("{package_id} action {action_id}"))?
|
||||
.de()?;
|
||||
if matches!(&action.visibility, ActionVisibility::Disabled(_)) {
|
||||
@@ -226,14 +226,6 @@ impl Service {
|
||||
action_id: ActionId,
|
||||
input: Value,
|
||||
) -> Result<Option<ActionResult>, Error> {
|
||||
self.actor
|
||||
.send(
|
||||
id,
|
||||
RunAction {
|
||||
id: action_id,
|
||||
input,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
self.actor.send(id, RunAction { action_id, input }).await?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,21 +21,15 @@ pub async fn rebuild(context: EffectContext) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn restart(
|
||||
context: EffectContext,
|
||||
ProcedureId { procedure_id }: ProcedureId,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn restart(context: EffectContext, EventId { event_id }: EventId) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
context.restart(procedure_id, false).await?;
|
||||
context.restart(event_id, false).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(
|
||||
context: EffectContext,
|
||||
ProcedureId { procedure_id }: ProcedureId,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn shutdown(context: EffectContext, EventId { event_id }: EventId) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
context.stop(procedure_id, false).await?;
|
||||
context.stop(event_id, false).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ pub(super) use crate::service::effects::context::EffectContext;
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ProcedureId {
|
||||
pub struct EventId {
|
||||
#[serde(default)]
|
||||
#[arg(default_value_t, long)]
|
||||
pub procedure_id: Guid,
|
||||
pub event_id: Guid,
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@ pub struct PersistentContainer {
|
||||
pub(super) lxc_container: OnceCell<LxcContainer>,
|
||||
pub(super) rpc_client: UnixRpcClient,
|
||||
pub(super) rpc_server: watch::Sender<Option<(NonDetachingJoinHandle<()>, ShutdownHandle)>>,
|
||||
// procedures: Mutex<Vec<(ProcedureName, ProcedureId)>>,
|
||||
js_mount: MountGuard,
|
||||
volumes: BTreeMap<VolumeId, MountGuard>,
|
||||
assets: Vec<MountGuard>,
|
||||
|
||||
@@ -202,6 +202,7 @@ pub async fn cli_update_system(
|
||||
prev.overall.set_complete();
|
||||
progress.update(&prev);
|
||||
}
|
||||
println!("Update complete. Restart your server to apply the update.")
|
||||
} else {
|
||||
println!("Updating to v{v}...")
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
/** Used to reach out from the pure js runtime */
|
||||
|
||||
export type Effects = {
|
||||
readonly eventId: string | null
|
||||
child: (name: string) => Effects
|
||||
constRetry?: () => void
|
||||
isInContext: boolean
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ExtractInputSpecType, InputSpec, LazyBuild } from "./inputSpec"
|
||||
import { InputSpec, LazyBuild } from "./inputSpec"
|
||||
import { List } from "./list"
|
||||
import { UnionRes, UnionResStaticValidatedAs, Variants } from "./variants"
|
||||
import {
|
||||
FilePath,
|
||||
Pattern,
|
||||
RandomString,
|
||||
ValueSpec,
|
||||
@@ -27,6 +26,12 @@ import {
|
||||
} from "ts-matches"
|
||||
import { DeepPartial } from "../../../types"
|
||||
|
||||
export const fileInfoParser = object({
|
||||
path: string,
|
||||
commitment: object({ hash: string, size: number }),
|
||||
})
|
||||
export type FileInfo = typeof fileInfoParser._TYPE
|
||||
|
||||
type AsRequired<T, Required extends boolean> = Required extends true
|
||||
? T
|
||||
: T | null
|
||||
@@ -891,47 +896,54 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
}
|
||||
}, spec.validator)
|
||||
}
|
||||
// static file<Store, Required extends boolean>(a: {
|
||||
// name: string
|
||||
// description?: string | null
|
||||
// extensions: string[]
|
||||
// required: Required
|
||||
// }) {
|
||||
// const buildValue = {
|
||||
// type: "file" as const,
|
||||
// description: null,
|
||||
// warning: null,
|
||||
// ...a,
|
||||
// }
|
||||
// return new Value<AsRequired<FilePath, Required>, Store>(
|
||||
// () => ({
|
||||
// ...buildValue,
|
||||
// }),
|
||||
// asRequiredParser(object({ filePath: string }), a),
|
||||
// )
|
||||
// }
|
||||
// static dynamicFile<Store>(
|
||||
// a: LazyBuild<
|
||||
// Store,
|
||||
// {
|
||||
// name: string
|
||||
// description?: string | null
|
||||
// warning?: string | null
|
||||
// extensions: string[]
|
||||
// required: boolean
|
||||
// }
|
||||
// >,
|
||||
// ) {
|
||||
// return new Value<FilePath | null, Store>(
|
||||
// async (options) => ({
|
||||
// type: "file" as const,
|
||||
// description: null,
|
||||
// warning: null,
|
||||
// ...(await a(options)),
|
||||
// }),
|
||||
// object({ filePath: string }).nullable(),
|
||||
// )
|
||||
// }
|
||||
static file<Required extends boolean>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
extensions: string[]
|
||||
required: Required
|
||||
}) {
|
||||
const buildValue = {
|
||||
type: "file" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
}
|
||||
return new Value<AsRequired<FileInfo, Required>>(
|
||||
() => ({
|
||||
spec: {
|
||||
...buildValue,
|
||||
},
|
||||
validator: asRequiredParser(fileInfoParser, a),
|
||||
}),
|
||||
asRequiredParser(fileInfoParser, a),
|
||||
)
|
||||
}
|
||||
static dynamicFile<Required extends boolean>(
|
||||
a: LazyBuild<{
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
extensions: string[]
|
||||
required: Required
|
||||
}>,
|
||||
) {
|
||||
return new Value<AsRequired<FileInfo, Required>, FileInfo | null>(
|
||||
async (options) => {
|
||||
const spec = {
|
||||
type: "file" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...(await a(options)),
|
||||
}
|
||||
return {
|
||||
spec,
|
||||
validator: asRequiredParser(fileInfoParser, spec),
|
||||
}
|
||||
},
|
||||
fileInfoParser.nullable(),
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented.
|
||||
* @example
|
||||
|
||||
@@ -66,9 +66,6 @@ export type ValueSpecTextarea = {
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
export type FilePath = {
|
||||
filePath: string
|
||||
}
|
||||
export type ValueSpecNumber = {
|
||||
type: "number"
|
||||
min: number | null
|
||||
|
||||
@@ -5,9 +5,11 @@ import { once } from "../util"
|
||||
import { InitScript } from "../inits"
|
||||
import { Parser } from "ts-matches"
|
||||
|
||||
type MaybeInputSpec<Type> = {} extends Type ? null : InputSpec<Type>
|
||||
export type Run<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
input: A
|
||||
spec: T.inputSpecTypes.InputSpec
|
||||
}) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined>
|
||||
export type GetInput<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
@@ -47,11 +49,14 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
implements ActionInfo<Id, Type>
|
||||
{
|
||||
readonly _INPUT: Type = null as any as Type
|
||||
private cachedParser?: Parser<unknown, Type>
|
||||
private prevInputSpec: Record<
|
||||
string,
|
||||
{ spec: T.inputSpecTypes.InputSpec; validator: Parser<unknown, Type> }
|
||||
> = {}
|
||||
private constructor(
|
||||
readonly id: Id,
|
||||
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
|
||||
private readonly inputSpec: InputSpec<Type>,
|
||||
private readonly inputSpec: MaybeInputSpec<Type>,
|
||||
private readonly getInputFn: GetInput<Type>,
|
||||
private readonly runFn: Run<Type>,
|
||||
) {}
|
||||
@@ -81,7 +86,7 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
return new Action(
|
||||
id,
|
||||
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })),
|
||||
InputSpec.of({}),
|
||||
null,
|
||||
async () => null,
|
||||
run,
|
||||
)
|
||||
@@ -100,10 +105,15 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
return metadata
|
||||
}
|
||||
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
|
||||
const built = await this.inputSpec.build(options)
|
||||
this.cachedParser = built.validator
|
||||
let spec = {}
|
||||
if (this.inputSpec) {
|
||||
const built = await this.inputSpec.build(options)
|
||||
this.prevInputSpec[options.effects.eventId!] = built
|
||||
spec = built.spec
|
||||
}
|
||||
return {
|
||||
spec: built.spec,
|
||||
eventId: options.effects.eventId!,
|
||||
spec,
|
||||
value:
|
||||
((await this.getInputFn(options)) as
|
||||
| Record<string, unknown>
|
||||
@@ -115,15 +125,23 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
effects: T.Effects
|
||||
input: Type
|
||||
}): Promise<T.ActionResult | null> {
|
||||
const parser =
|
||||
this.cachedParser ?? (await this.inputSpec.build(options)).validator
|
||||
let spec = {}
|
||||
if (this.inputSpec) {
|
||||
const prevInputSpec = this.prevInputSpec[options.effects.eventId!]
|
||||
if (!prevInputSpec) {
|
||||
throw new Error(
|
||||
`getActionInput has not been called for EventID ${options.effects.eventId}`,
|
||||
)
|
||||
}
|
||||
options.input = prevInputSpec.validator.unsafeCast(options.input)
|
||||
spec = prevInputSpec.spec
|
||||
}
|
||||
return (
|
||||
(await this.runFn({
|
||||
effects: options.effects,
|
||||
input: this.cachedParser
|
||||
? this.cachedParser.unsafeCast(options.input)
|
||||
: options.input,
|
||||
})) || null
|
||||
input: options.input,
|
||||
spec,
|
||||
})) ?? null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Guid } from "./Guid"
|
||||
|
||||
export type ActionInput = {
|
||||
eventId: Guid
|
||||
spec: Record<string, unknown>
|
||||
value: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Guid } from "./Guid"
|
||||
|
||||
export type ProcedureId = { procedureId: Guid }
|
||||
export type EventId = { eventId: Guid }
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { IpInfo } from "./IpInfo"
|
||||
|
||||
export type NetworkInterfaceInfo = {
|
||||
inbound: boolean | null
|
||||
outbound: boolean | null
|
||||
public: boolean | null
|
||||
ipInfo: IpInfo | null
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ export { Duration } from "./Duration"
|
||||
export { EchoParams } from "./EchoParams"
|
||||
export { EditSignerParams } from "./EditSignerParams"
|
||||
export { EncryptedWire } from "./EncryptedWire"
|
||||
export { EventId } from "./EventId"
|
||||
export { ExportActionParams } from "./ExportActionParams"
|
||||
export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams"
|
||||
export { FileType } from "./FileType"
|
||||
@@ -155,7 +156,6 @@ export { PackageVersionInfo } from "./PackageVersionInfo"
|
||||
export { PasswordType } from "./PasswordType"
|
||||
export { PathOrUrl } from "./PathOrUrl"
|
||||
export { Percentage } from "./Percentage"
|
||||
export { ProcedureId } from "./ProcedureId"
|
||||
export { Progress } from "./Progress"
|
||||
export { ProgressUnits } from "./ProgressUnits"
|
||||
export { Public } from "./Public"
|
||||
|
||||
@@ -46,6 +46,7 @@ type EffectsTypeChecker<T extends StringObject = Effects> = {
|
||||
describe("startosTypeValidation ", () => {
|
||||
test(`checking the params match`, () => {
|
||||
typeEquality<EffectsTypeChecker>({
|
||||
eventId: {} as never,
|
||||
child: "",
|
||||
isInContext: {} as never,
|
||||
onLeaveContext: () => {},
|
||||
|
||||
@@ -104,7 +104,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
|
||||
// prettier-ignore
|
||||
type StartSdkEffectWrapper = {
|
||||
[K in keyof Omit<Effects, NestedEffects | InterfaceEffects | MainUsedEffects | CallbackEffects | AlreadyExposed>]: (effects: Effects, ...args: Parameters<Effects[K]>) => ReturnType<Effects[K]>
|
||||
[K in keyof Omit<Effects, "eventId" | NestedEffects | InterfaceEffects | MainUsedEffects | CallbackEffects | AlreadyExposed>]: (effects: Effects, ...args: Parameters<Effects[K]>) => ReturnType<Effects[K]>
|
||||
}
|
||||
const startSdkEffectWrapper: StartSdkEffectWrapper = {
|
||||
restart: (effects, ...args) => effects.restart(...args),
|
||||
|
||||
@@ -133,6 +133,7 @@ export class ActionInputModal {
|
||||
readonly warning = this.context.data.actionInfo.metadata.warning
|
||||
readonly pkgInfo = this.context.data.pkgInfo
|
||||
readonly requestInfo = this.context.data.requestInfo
|
||||
eventId: string | null = null
|
||||
|
||||
buttons: ActionButton<any>[] = [
|
||||
{
|
||||
@@ -151,6 +152,7 @@ export class ActionInputModal {
|
||||
).pipe(
|
||||
map(res => {
|
||||
const originalValue = res.value || {}
|
||||
this.eventId = res.eventId
|
||||
|
||||
return {
|
||||
spec: res.spec,
|
||||
@@ -174,7 +176,12 @@ export class ActionInputModal {
|
||||
|
||||
async execute(input: object) {
|
||||
if (await this.checkConflicts(input)) {
|
||||
await this.actionService.execute(this.pkgInfo.id, this.actionId, input)
|
||||
await this.actionService.execute(
|
||||
this.pkgInfo.id,
|
||||
this.eventId,
|
||||
this.actionId,
|
||||
input,
|
||||
)
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,9 @@ export class ActionService {
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.execute(pkgInfo.id, actionInfo.id))
|
||||
.subscribe(() => this.execute(pkgInfo.id, null, actionInfo.id))
|
||||
} else {
|
||||
this.execute(pkgInfo.id, actionInfo.id)
|
||||
this.execute(pkgInfo.id, null, actionInfo.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -96,14 +96,20 @@ export class ActionService {
|
||||
}
|
||||
}
|
||||
|
||||
async execute(packageId: string, actionId: string, input?: object) {
|
||||
async execute(
|
||||
packageId: string,
|
||||
eventId: string | null,
|
||||
actionId: string,
|
||||
input?: object,
|
||||
) {
|
||||
const loader = this.loader.open('Loading').subscribe()
|
||||
|
||||
try {
|
||||
const res = await this.api.runAction({
|
||||
packageId,
|
||||
eventId,
|
||||
actionId,
|
||||
input: input || null,
|
||||
input: input ?? null,
|
||||
})
|
||||
|
||||
if (!res) return
|
||||
|
||||
@@ -347,12 +347,14 @@ export namespace RR {
|
||||
|
||||
export type GetActionInputReq = { packageId: string; actionId: string } // package.action.get-input
|
||||
export type GetActionInputRes = {
|
||||
eventId: string
|
||||
spec: IST.InputSpec
|
||||
value: object | null
|
||||
}
|
||||
|
||||
export type ActionReq = {
|
||||
packageId: string
|
||||
eventId: string | null
|
||||
actionId: string
|
||||
input: object | null
|
||||
} // package.action.run
|
||||
|
||||
@@ -1108,6 +1108,7 @@ export class MockApiService extends ApiService {
|
||||
): Promise<RR.GetActionInputRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
eventId: 'ANZXNWIFRTTBZ6T52KQPZILIQQODDHXQ',
|
||||
value: Mock.MockConfig,
|
||||
spec: await Mock.getActionInputSpec(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user