@sixb/core 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/index.js +21 -21
- package/dist/agents/index.js +33 -33
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/streams/index.js +2 -2
- package/dist/agents/streams/internal.js +2 -2
- package/dist/auth/index.js +3 -3
- package/dist/authorization/index.js +2 -2
- package/dist/bootstrap/index.js +36 -36
- package/dist/{chunk-dv4ct8mp.js → chunk-1jvq82rp.js} +3 -3
- package/dist/{chunk-16jenmzw.js → chunk-2tz5vnvz.js} +28 -28
- package/dist/{chunk-16jenmzw.js.map → chunk-2tz5vnvz.js.map} +1 -1
- package/dist/{chunk-728fp090.js → chunk-3c90jvfj.js} +2 -2
- package/dist/{chunk-vn3yh259.js → chunk-4gmng3vf.js} +1 -1
- package/dist/{chunk-hxgj4kzd.js → chunk-4jph1t3v.js} +7 -7
- package/dist/{chunk-znw0zvy4.js → chunk-4wn7bxxa.js} +25 -7
- package/dist/chunk-4wn7bxxa.js.map +12 -0
- package/dist/{chunk-3qpcrcj8.js → chunk-6aawea28.js} +2 -4
- package/dist/chunk-6aawea28.js.map +10 -0
- package/dist/{chunk-rzkr70gr.js → chunk-6fsr7y93.js} +5 -1
- package/dist/{chunk-rzkr70gr.js.map → chunk-6fsr7y93.js.map} +3 -3
- package/dist/{chunk-y487ywrv.js → chunk-6q7c68yx.js} +4 -4
- package/dist/{chunk-ypj2wqtj.js → chunk-70jmg2zx.js} +2 -2
- package/dist/{chunk-wn7mznxc.js → chunk-7jc21943.js} +3 -3
- package/dist/{chunk-rzcjp5m2.js → chunk-8fjmqjyx.js} +1 -1
- package/dist/{chunk-m8zkvep1.js → chunk-8w27xf9e.js} +1 -1
- package/dist/{chunk-z9h698s8.js → chunk-a4g5yjm0.js} +1 -1
- package/dist/{chunk-4ypbe95r.js → chunk-adbve9zt.js} +1 -1
- package/dist/{chunk-hj0n3c2p.js → chunk-dqkd3kdj.js} +40 -22
- package/dist/{chunk-hj0n3c2p.js.map → chunk-dqkd3kdj.js.map} +3 -3
- package/dist/{chunk-wnxabtn4.js → chunk-ex8s8wsk.js} +6 -6
- package/dist/{chunk-4tnrzqqh.js → chunk-fj1dvp37.js} +1 -1
- package/dist/{chunk-3fpgxpsq.js → chunk-fjw417jf.js} +127 -69
- package/dist/{chunk-3fpgxpsq.js.map → chunk-fjw417jf.js.map} +4 -4
- package/dist/{chunk-ht4a7mjg.js → chunk-g4rbm4a6.js} +42 -8
- package/dist/{chunk-ht4a7mjg.js.map → chunk-g4rbm4a6.js.map} +3 -3
- package/dist/{chunk-qma80ych.js → chunk-hn2r7z41.js} +3 -2
- package/dist/chunk-hn2r7z41.js.map +11 -0
- package/dist/{chunk-bdhfn0ww.js → chunk-kkqwzy6h.js} +1 -1
- package/dist/{chunk-ww185kt6.js → chunk-mmac1mp6.js} +2 -2
- package/dist/{chunk-jfdggg1p.js → chunk-n5t4mq6z.js} +5 -5
- package/dist/{chunk-3a338wrp.js → chunk-p0nbkys4.js} +1 -1
- package/dist/{chunk-kxea54yg.js → chunk-py2dw8cw.js} +3 -3
- package/dist/{chunk-e2c5yxfe.js → chunk-q3grgzp1.js} +2 -2
- package/dist/{chunk-6rzb1g6h.js → chunk-qne45xqq.js} +1 -1
- package/dist/{chunk-cbn96bve.js → chunk-r5sc1d2b.js} +2 -2
- package/dist/{chunk-y6vnvayj.js → chunk-vjcd8d27.js} +1 -1
- package/dist/{chunk-ma1mhh7w.js → chunk-wd34wnsy.js} +6 -6
- package/dist/{chunk-pvgpckm1.js → chunk-wem5vqjw.js} +7 -7
- package/dist/{chunk-gct3m1da.js → chunk-xr7y35yj.js} +3 -3
- package/dist/{chunk-q2xghyft.js → chunk-y0zmae99.js} +5 -5
- package/dist/{chunk-xgptywp0.js → chunk-y2aq6ag9.js} +1 -1
- package/dist/{chunk-yvf2jqj8.js → chunk-yfppyaty.js} +1 -1
- package/dist/{chunk-00pfcqh4.js → chunk-z708gemf.js} +129 -70
- package/dist/{chunk-00pfcqh4.js.map → chunk-z708gemf.js.map} +5 -5
- package/dist/deploy/define.d.ts +4 -0
- package/dist/deploy/define.d.ts.map +1 -0
- package/dist/deploy/index.d.ts +3 -0
- package/dist/deploy/index.d.ts.map +1 -0
- package/dist/deploy/index.js +10 -0
- package/dist/deploy/index.js.map +10 -0
- package/dist/deploy/internal.d.ts +3 -0
- package/dist/deploy/internal.d.ts.map +1 -0
- package/dist/deploy/internal.js +244 -0
- package/dist/deploy/internal.js.map +11 -0
- package/dist/deploy/services.d.ts +8 -0
- package/dist/deploy/services.d.ts.map +1 -0
- package/dist/deploy/types.d.ts +309 -0
- package/dist/deploy/types.d.ts.map +1 -0
- package/dist/deploy/validation.d.ts +7 -0
- package/dist/deploy/validation.d.ts.map +1 -0
- package/dist/error-reporting/internal.js +3 -3
- package/dist/errors/catalog.d.ts +4 -0
- package/dist/errors/catalog.d.ts.map +1 -1
- package/dist/errors/internal.js +1 -1
- package/dist/events/index.js +3 -3
- package/dist/execution/agent.js +32 -32
- package/dist/execution/primitive.js +28 -28
- package/dist/execution/request.js +30 -30
- package/dist/execution/request.js.map +1 -1
- package/dist/index.js +48 -48
- package/dist/index.js.map +1 -1
- package/dist/maintenance/ontology-maintenance.d.ts +2 -0
- package/dist/maintenance/ontology-maintenance.d.ts.map +1 -1
- package/dist/maintenance/types.d.ts +8 -2
- package/dist/maintenance/types.d.ts.map +1 -1
- package/dist/materialization/dataset-watermark.d.ts +6 -1
- package/dist/materialization/dataset-watermark.d.ts.map +1 -1
- package/dist/materialization/index.js +1 -1
- package/dist/materialization/model.d.ts +6 -0
- package/dist/materialization/model.d.ts.map +1 -1
- package/dist/materializer/index.js +17 -17
- package/dist/materializer/index.js.map +1 -1
- package/dist/materializer/projections/finish-run.d.ts.map +1 -1
- package/dist/materializer/projections/replace.d.ts.map +1 -1
- package/dist/materializer/projections/source-materialization.d.ts +6 -2
- package/dist/materializer/projections/source-materialization.d.ts.map +1 -1
- package/dist/models/execution/index.js +13 -13
- package/dist/objects/index.js +20 -20
- package/dist/objects/query/index.js +14 -14
- package/dist/pipelines/index.js +18 -18
- package/dist/projections/internal.js +15 -15
- package/dist/projections/types.d.ts +1 -1
- package/dist/projections/types.d.ts.map +1 -1
- package/dist/queues/in-memory.d.ts +1 -1
- package/dist/queues/in-memory.d.ts.map +1 -1
- package/dist/runtime/internal.js +18 -18
- package/dist/shares/internal.js +13 -13
- package/dist/shares/internal.js.map +1 -1
- package/dist/storage/action-runs/failure.js +2 -2
- package/dist/storage/ai-limits/enforcement.js +2 -2
- package/dist/storage/connector-connections/provider.js +1 -1
- package/dist/storage/file-upload-sessions/provider.js +4 -4
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +28 -28
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/internal.js +1 -1
- package/dist/storage/ontology/in-memory/sources.d.ts +6 -3
- package/dist/storage/ontology/in-memory/sources.d.ts.map +1 -1
- package/dist/storage/ontology/index.d.ts +1 -1
- package/dist/storage/ontology/index.d.ts.map +1 -1
- package/dist/storage/ontology/provider.d.ts +16 -3
- package/dist/storage/ontology/provider.d.ts.map +1 -1
- package/dist/storage/ontology/provider.js +8 -4
- package/dist/storage/ontology/provider.js.map +1 -1
- package/dist/storage/ontology/sources.d.ts +36 -8
- package/dist/storage/ontology/sources.d.ts.map +1 -1
- package/dist/storage/projection-runs/in-memory.d.ts +3 -1
- package/dist/storage/projection-runs/in-memory.d.ts.map +1 -1
- package/dist/storage/projection-runs/index.d.ts +1 -1
- package/dist/storage/projection-runs/index.d.ts.map +1 -1
- package/dist/storage/projection-runs/lifecycle.d.ts +12 -0
- package/dist/storage/projection-runs/lifecycle.d.ts.map +1 -1
- package/dist/storage/projection-runs/provider.js +10 -4
- package/dist/storage/projection-runs/provider.js.map +1 -1
- package/dist/storage/projection-runs/types.d.ts +32 -1
- package/dist/storage/projection-runs/types.d.ts.map +1 -1
- package/dist/storage/workflow-runs/provider.js +4 -4
- package/dist/syncs/index.js +19 -19
- package/dist/testing/index.js +296 -54
- package/dist/testing/index.js.map +4 -4
- package/dist/testing/ontology-storage-contract.d.ts.map +1 -1
- package/dist/testing/projection-run-storage-contract.d.ts.map +1 -1
- package/dist/webhooks/internal.js +14 -14
- package/dist/webhooks/internal.js.map +1 -1
- package/dist/workers/index.js +1 -1
- package/dist/workflows/index.js +33 -33
- package/package.json +13 -1
- package/src/deploy/define.ts +6 -0
- package/src/deploy/index.ts +41 -0
- package/src/deploy/internal.ts +7 -0
- package/src/deploy/services.ts +51 -0
- package/src/deploy/types.ts +338 -0
- package/src/deploy/validation.ts +261 -0
- package/src/errors/catalog.ts +4 -0
- package/src/maintenance/ontology-maintenance.ts +32 -2
- package/src/maintenance/types.ts +8 -2
- package/src/materialization/dataset-watermark.ts +6 -4
- package/src/materialization/model.ts +7 -0
- package/src/materializer/README.md +26 -6
- package/src/materializer/projections/finish-run.ts +129 -26
- package/src/materializer/projections/replace.ts +67 -49
- package/src/materializer/projections/source-materialization.ts +11 -6
- package/src/projections/types.ts +1 -0
- package/src/storage/index.ts +6 -1
- package/src/storage/ontology/in-memory/sources.ts +119 -55
- package/src/storage/ontology/index.ts +4 -1
- package/src/storage/ontology/provider.ts +64 -7
- package/src/storage/ontology/sources.ts +42 -8
- package/src/storage/projection-runs/in-memory.ts +41 -2
- package/src/storage/projection-runs/index.ts +2 -0
- package/src/storage/projection-runs/lifecycle.ts +61 -4
- package/src/storage/projection-runs/types.ts +41 -1
- package/src/testing/ontology-storage-contract.ts +141 -15
- package/src/testing/projection-run-storage-contract.ts +174 -1
- package/dist/chunk-3qpcrcj8.js.map +0 -10
- package/dist/chunk-qma80ych.js.map +0 -11
- package/dist/chunk-znw0zvy4.js.map +0 -12
- /package/dist/{chunk-dv4ct8mp.js.map → chunk-1jvq82rp.js.map} +0 -0
- /package/dist/{chunk-728fp090.js.map → chunk-3c90jvfj.js.map} +0 -0
- /package/dist/{chunk-vn3yh259.js.map → chunk-4gmng3vf.js.map} +0 -0
- /package/dist/{chunk-hxgj4kzd.js.map → chunk-4jph1t3v.js.map} +0 -0
- /package/dist/{chunk-y487ywrv.js.map → chunk-6q7c68yx.js.map} +0 -0
- /package/dist/{chunk-ypj2wqtj.js.map → chunk-70jmg2zx.js.map} +0 -0
- /package/dist/{chunk-wn7mznxc.js.map → chunk-7jc21943.js.map} +0 -0
- /package/dist/{chunk-rzcjp5m2.js.map → chunk-8fjmqjyx.js.map} +0 -0
- /package/dist/{chunk-m8zkvep1.js.map → chunk-8w27xf9e.js.map} +0 -0
- /package/dist/{chunk-z9h698s8.js.map → chunk-a4g5yjm0.js.map} +0 -0
- /package/dist/{chunk-4ypbe95r.js.map → chunk-adbve9zt.js.map} +0 -0
- /package/dist/{chunk-wnxabtn4.js.map → chunk-ex8s8wsk.js.map} +0 -0
- /package/dist/{chunk-4tnrzqqh.js.map → chunk-fj1dvp37.js.map} +0 -0
- /package/dist/{chunk-bdhfn0ww.js.map → chunk-kkqwzy6h.js.map} +0 -0
- /package/dist/{chunk-ww185kt6.js.map → chunk-mmac1mp6.js.map} +0 -0
- /package/dist/{chunk-jfdggg1p.js.map → chunk-n5t4mq6z.js.map} +0 -0
- /package/dist/{chunk-3a338wrp.js.map → chunk-p0nbkys4.js.map} +0 -0
- /package/dist/{chunk-kxea54yg.js.map → chunk-py2dw8cw.js.map} +0 -0
- /package/dist/{chunk-e2c5yxfe.js.map → chunk-q3grgzp1.js.map} +0 -0
- /package/dist/{chunk-6rzb1g6h.js.map → chunk-qne45xqq.js.map} +0 -0
- /package/dist/{chunk-cbn96bve.js.map → chunk-r5sc1d2b.js.map} +0 -0
- /package/dist/{chunk-y6vnvayj.js.map → chunk-vjcd8d27.js.map} +0 -0
- /package/dist/{chunk-ma1mhh7w.js.map → chunk-wd34wnsy.js.map} +0 -0
- /package/dist/{chunk-pvgpckm1.js.map → chunk-wem5vqjw.js.map} +0 -0
- /package/dist/{chunk-gct3m1da.js.map → chunk-xr7y35yj.js.map} +0 -0
- /package/dist/{chunk-q2xghyft.js.map → chunk-y0zmae99.js.map} +0 -0
- /package/dist/{chunk-xgptywp0.js.map → chunk-y2aq6ag9.js.map} +0 -0
- /package/dist/{chunk-yvf2jqj8.js.map → chunk-yfppyaty.js.map} +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deployment of this project: which services run, how browsers reach them, and the target that
|
|
3
|
+
* runs them. `sixb deploy` loads it from `sixb.deploy.ts`; the running app never reads it.
|
|
4
|
+
*/
|
|
5
|
+
export interface DeployConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Names the deployment on its target and in default domains. Lowercase letters, numbers, and
|
|
8
|
+
* hyphens.
|
|
9
|
+
*/
|
|
10
|
+
readonly name: string
|
|
11
|
+
/**
|
|
12
|
+
* Parent domain for HTTP services without their own `domain`: `<name>-<service>.<domain>`.
|
|
13
|
+
* Required unless every enabled HTTP service sets `domain`.
|
|
14
|
+
*/
|
|
15
|
+
readonly domain?: string
|
|
16
|
+
/** Where the services run, such as `new SshTarget({ host })` from `@sixb/deploy-ssh`. */
|
|
17
|
+
readonly target: DeployTarget
|
|
18
|
+
/**
|
|
19
|
+
* Environment for every service and release step. Committed with the project, so secrets
|
|
20
|
+
* belong in the target's `.env` instead.
|
|
21
|
+
*/
|
|
22
|
+
readonly env?: DeployEnv
|
|
23
|
+
/** Override or disable the Sixb services. Every service runs unless set to `false`. */
|
|
24
|
+
readonly services?: DeployServicesConfig
|
|
25
|
+
/** Long-running project scripts, supervised alongside the Sixb services. */
|
|
26
|
+
readonly processes?: Readonly<Record<string, DeployProcessDefinition>>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type DeployEnv = Readonly<Record<string, string>>
|
|
30
|
+
|
|
31
|
+
/** The services a browser or client reaches through a public domain. */
|
|
32
|
+
export type DeployHttpServiceName = "api" | "atlas" | "app"
|
|
33
|
+
|
|
34
|
+
/** The services that must run as exactly one process: a second would duplicate their work. */
|
|
35
|
+
export type DeploySingletonServiceName = "orchestrator" | "scheduler" | "rules"
|
|
36
|
+
|
|
37
|
+
export type DeployServiceName = DeployHttpServiceName | DeploySingletonServiceName | "workers"
|
|
38
|
+
|
|
39
|
+
export type DeployWorkerType = "sync" | "action" | "agent" | "pipeline" | "projection" | "workflow"
|
|
40
|
+
|
|
41
|
+
/** Action jobs run one at a time, so their concurrency is fixed. */
|
|
42
|
+
export type DeployConfigurableWorkerType = Exclude<DeployWorkerType, "action">
|
|
43
|
+
|
|
44
|
+
export interface DeployServicesConfig {
|
|
45
|
+
readonly api?: boolean | DeployHttpServiceConfig
|
|
46
|
+
readonly atlas?: boolean | DeployHttpServiceConfig
|
|
47
|
+
readonly app?: boolean | DeployHttpServiceConfig
|
|
48
|
+
readonly orchestrator?: boolean | DeploySingletonServiceConfig
|
|
49
|
+
readonly scheduler?: boolean | DeploySingletonServiceConfig
|
|
50
|
+
readonly rules?: boolean | DeploySingletonServiceConfig
|
|
51
|
+
readonly workers?: boolean | DeployWorkersConfig
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DeployHttpServiceConfig {
|
|
55
|
+
/** Public hostname. Defaults to `<name>-<service>.<domain>`. */
|
|
56
|
+
readonly domain?: string
|
|
57
|
+
/** Added to the deployment's `env` for this service only. */
|
|
58
|
+
readonly env?: DeployEnv
|
|
59
|
+
readonly process?: DeployProcessOptions
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DeploySingletonServiceConfig {
|
|
63
|
+
/** Added to the deployment's `env` for this service only. */
|
|
64
|
+
readonly env?: DeployEnv
|
|
65
|
+
readonly process?: DeployProcessOptions
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DeployWorkersConfig {
|
|
69
|
+
/**
|
|
70
|
+
* The worker types to run. `"all"`, the default, runs every type the project registers work
|
|
71
|
+
* for.
|
|
72
|
+
*/
|
|
73
|
+
readonly types?: readonly DeployWorkerType[] | "all"
|
|
74
|
+
/** Wall-clock budget for one agent turn, such as `"30s"`, `"10m"`, or `"1h"`. */
|
|
75
|
+
readonly agentTurnTimeout?: string
|
|
76
|
+
/** Jobs each worker type runs at once inside one process. */
|
|
77
|
+
readonly concurrency?: Readonly<Partial<Record<DeployConfigurableWorkerType, number>>>
|
|
78
|
+
/** Added to the deployment's `env` for the workers only. */
|
|
79
|
+
readonly env?: DeployEnv
|
|
80
|
+
readonly process?: DeployScalableProcessOptions
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DeployProcessDefinition {
|
|
84
|
+
/** Script to run with Bun, relative to the project directory. */
|
|
85
|
+
readonly entrypoint: string
|
|
86
|
+
readonly args?: readonly string[]
|
|
87
|
+
/** Added to the deployment's `env` for this process only. */
|
|
88
|
+
readonly env?: DeployEnv
|
|
89
|
+
readonly process?: DeployScalableProcessOptions
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DeployProcessOptions {
|
|
93
|
+
/** How long a stopping process may take before it is killed. Defaults to 10000. */
|
|
94
|
+
readonly killTimeoutMs?: number
|
|
95
|
+
/** Wait before restarting a process that exited, growing with each restart. Defaults to 1000. */
|
|
96
|
+
readonly restartDelayMs?: number
|
|
97
|
+
/** Restart the process when its memory passes this size, such as `"512M"` or `"2G"`. */
|
|
98
|
+
readonly maxMemory?: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface DeployScalableProcessOptions extends DeployProcessOptions {
|
|
102
|
+
/** How many copies of the process to run. Defaults to 1. */
|
|
103
|
+
readonly instances?: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Runs a deployment. `@sixb/deploy-ssh` provides the SSH target. Third-party targets are not
|
|
108
|
+
* supported in 0.1.x: this contract still grows with each thing `sixb deploy` learns to do.
|
|
109
|
+
*/
|
|
110
|
+
export interface DeployTarget {
|
|
111
|
+
/** The kind of target, shown in `sixb deploy` output, such as `"ssh"`. */
|
|
112
|
+
readonly kind: string
|
|
113
|
+
/** Where the deployment runs, shown in output, such as `"sixb@203.0.113.10"`. */
|
|
114
|
+
readonly location: string
|
|
115
|
+
/** The address an HTTP service listens on, where the target's proxy reaches it. */
|
|
116
|
+
listenAddress(service: DeployHttpServiceName): DeployListenAddress
|
|
117
|
+
/**
|
|
118
|
+
* Ships the source, runs the release, and resolves once its services answer. Rejects with the
|
|
119
|
+
* reason when a step fails; the steps it reported say how far it got.
|
|
120
|
+
*/
|
|
121
|
+
deploy(release: DeployRelease, source: DeploySource, context: DeployContext): Promise<void>
|
|
122
|
+
/** What the deployment runs right now, and which release it runs. */
|
|
123
|
+
status(context: DeployOperationContext): Promise<DeployStatus>
|
|
124
|
+
/** Writes recent log lines, then new ones as they arrive when `follow` is set. */
|
|
125
|
+
logs(options: DeployLogsOptions, context: DeployOperationContext): Promise<void>
|
|
126
|
+
/** Restarts, stops, or starts one service, or every service. */
|
|
127
|
+
control(
|
|
128
|
+
action: DeployControlAction,
|
|
129
|
+
service: string | undefined,
|
|
130
|
+
context: DeployOperationContext
|
|
131
|
+
): Promise<void>
|
|
132
|
+
/** What stands between the target and a working deploy of the release, changing nothing. */
|
|
133
|
+
check(release: DeployRelease, context: DeployCheckContext): Promise<readonly DeployCheck[]>
|
|
134
|
+
/** Prepares the target for the release, fixing what `check` reports as fixable. */
|
|
135
|
+
setup?(release: DeployRelease, context: DeploySetupContext): Promise<void>
|
|
136
|
+
/** Who may deploy and operate the deployment, for a target reached with keys. */
|
|
137
|
+
readonly access?: DeployAccess
|
|
138
|
+
/** Credentials for a CI job that deploys, for a target reached with keys. */
|
|
139
|
+
readonly ci?: DeployCi
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface DeployCheck {
|
|
143
|
+
readonly id: string
|
|
144
|
+
/** What was checked, such as `"SSH"` or `"api DNS"`. */
|
|
145
|
+
readonly label: string
|
|
146
|
+
/**
|
|
147
|
+
* `fixable`: `sixb deploy setup` fixes it. `manual`: only you can, as `remedy` says. Both stop
|
|
148
|
+
* a deploy; a `warning` does not.
|
|
149
|
+
*/
|
|
150
|
+
readonly status: "ok" | "fixable" | "manual" | "warning"
|
|
151
|
+
readonly detail: string
|
|
152
|
+
/** What to do about it, when the status is not `ok`. */
|
|
153
|
+
readonly remedy?: string
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface DeployCheckContext extends DeployOperationContext {
|
|
157
|
+
/** The project's directory inside its repository: `.` at the root. */
|
|
158
|
+
readonly projectPath: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface DeploySetupContext extends DeployCheckContext {
|
|
162
|
+
/** The account with sudo that prepares a server, for a target that needs one. */
|
|
163
|
+
readonly admin?: string
|
|
164
|
+
/** A public key file to authorize for deploys, instead of the one the target would pick. */
|
|
165
|
+
readonly key?: string
|
|
166
|
+
/** A line of progress for the person running setup. */
|
|
167
|
+
write(line: string): void
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface DeployAccess {
|
|
171
|
+
list(context: DeployOperationContext): Promise<readonly DeployAccessKey[]>
|
|
172
|
+
/** Authorizes a public key, given as its text (`ssh-ed25519 AAAA… comment`). */
|
|
173
|
+
add(key: string, context: DeployOperationContext): Promise<DeployAccessKey>
|
|
174
|
+
/** Revokes the keys whose fingerprint or comment matches, and returns them. */
|
|
175
|
+
remove(match: string, context: DeployOperationContext): Promise<readonly DeployAccessKey[]>
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface DeployAccessKey {
|
|
179
|
+
readonly type: string
|
|
180
|
+
readonly fingerprint: string
|
|
181
|
+
readonly comment: string
|
|
182
|
+
/** Whether the key is limited to running commands: no forwarding, no terminal. */
|
|
183
|
+
readonly restricted: boolean
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Lets a CI job deploy. `sixb deploy ci` stores what `create` returns as the CI system's secrets and
|
|
188
|
+
* writes the workflow around it.
|
|
189
|
+
*/
|
|
190
|
+
export interface DeployCi {
|
|
191
|
+
/**
|
|
192
|
+
* Creates a credential for one CI job, such as `"github:acme/shop"`, and authorizes it on the
|
|
193
|
+
* target next to the job's earlier ones. Running it again for the same job rotates the credential.
|
|
194
|
+
*/
|
|
195
|
+
create(job: string, context: DeployOperationContext): Promise<DeployCiCredential>
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface DeployCiCredential {
|
|
199
|
+
/** Describes the credential, such as its key's fingerprint. */
|
|
200
|
+
readonly description: string
|
|
201
|
+
/** What the CI job needs, by environment variable name. CI stores them as secrets. */
|
|
202
|
+
readonly secrets: Readonly<Record<string, string>>
|
|
203
|
+
/** Shell lines that install the credential on the CI runner, with `secrets` in the environment. */
|
|
204
|
+
readonly install: readonly string[]
|
|
205
|
+
/** Revokes the job's earlier credentials once CI holds this one, and returns them. */
|
|
206
|
+
retireOthers(): Promise<readonly DeployAccessKey[]>
|
|
207
|
+
/** Revokes this credential, when CI could not store it. */
|
|
208
|
+
revoke(): Promise<void>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface DeployListenAddress {
|
|
212
|
+
readonly host: string
|
|
213
|
+
readonly port: number
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Everything a deployment runs, resolved by `sixb deploy` from `sixb.deploy.ts`. A target decides
|
|
218
|
+
* where and how it runs; which processes exist, and the command each one starts with, come from
|
|
219
|
+
* Sixb.
|
|
220
|
+
*/
|
|
221
|
+
export interface DeployRelease {
|
|
222
|
+
readonly name: string
|
|
223
|
+
readonly target: { readonly kind: string; readonly location: string }
|
|
224
|
+
/** The Bun version the project runs on. */
|
|
225
|
+
readonly bunVersion: string
|
|
226
|
+
/** The environment every step and service starts from. */
|
|
227
|
+
readonly env: DeployEnv
|
|
228
|
+
readonly steps: {
|
|
229
|
+
/** Runs while the previous release still serves. */
|
|
230
|
+
readonly build: readonly DeployCommand[]
|
|
231
|
+
/** Runs after the services stop and before they start again. */
|
|
232
|
+
readonly beforeStart: readonly DeployCommand[]
|
|
233
|
+
}
|
|
234
|
+
readonly services: readonly DeployReleaseService[]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface DeployCommand {
|
|
238
|
+
/** `sixb` is the CLI the project installs; `bun` runs a project script. */
|
|
239
|
+
readonly program: "sixb" | "bun"
|
|
240
|
+
readonly args: readonly string[]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface DeployReleaseService {
|
|
244
|
+
readonly name: string
|
|
245
|
+
readonly kind: "http" | "singleton" | "workers" | "script"
|
|
246
|
+
readonly command: DeployCommand
|
|
247
|
+
/** The complete environment: the release's, then the service's own additions. */
|
|
248
|
+
readonly env: DeployEnv
|
|
249
|
+
readonly instances: number
|
|
250
|
+
readonly http?: DeployReleaseHttp
|
|
251
|
+
readonly process: DeployReleaseProcess
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface DeployReleaseHttp extends DeployListenAddress {
|
|
255
|
+
readonly domain: string
|
|
256
|
+
readonly publicOrigin: string
|
|
257
|
+
/** Answers once the service can take traffic, when the service has such a route. */
|
|
258
|
+
readonly readinessPath?: string
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface DeployReleaseProcess {
|
|
262
|
+
readonly killTimeoutMs: number
|
|
263
|
+
readonly restartDelayMs: number
|
|
264
|
+
readonly maxMemory?: string
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The committed files a deployment runs, read from git by `sixb deploy`. */
|
|
268
|
+
export interface DeploySource {
|
|
269
|
+
/** The commit being deployed. */
|
|
270
|
+
readonly commit: string
|
|
271
|
+
/** What the deployer asked for, such as `HEAD` or `main`. */
|
|
272
|
+
readonly ref: string
|
|
273
|
+
/** The project's directory inside the repository: `.` at its root. */
|
|
274
|
+
readonly projectPath: string
|
|
275
|
+
/** Tar archives of the committed files: the repository first, then each submodule. */
|
|
276
|
+
readonly archives: readonly DeploySourceArchive[]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface DeploySourceArchive {
|
|
280
|
+
/** Where the archive's files belong, relative to the repository root: `.` for the root. */
|
|
281
|
+
readonly path: string
|
|
282
|
+
open(): ReadableStream<Uint8Array>
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface DeployOperationContext {
|
|
286
|
+
/** The deployment's `name`, which a target may run several of. */
|
|
287
|
+
readonly name: string
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface DeployContext extends DeployOperationContext {
|
|
291
|
+
/** Who deployed, recorded with the release. */
|
|
292
|
+
readonly deployedBy: string
|
|
293
|
+
report(event: DeployEvent): void
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export type DeployEvent =
|
|
297
|
+
/** The steps the deployment will run, reported before the first one starts. */
|
|
298
|
+
| { readonly type: "steps"; readonly labels: readonly string[] }
|
|
299
|
+
| { readonly type: "step"; readonly index: number; readonly status: "running" | "done" }
|
|
300
|
+
| { readonly type: "output"; readonly line: string }
|
|
301
|
+
|
|
302
|
+
export interface DeployStatus {
|
|
303
|
+
/** The release the target last deployed, if any. */
|
|
304
|
+
readonly release: DeployRecord | null
|
|
305
|
+
/** Whether the target's process supervisor is running. */
|
|
306
|
+
readonly running: boolean
|
|
307
|
+
readonly processes: readonly DeployProcessStatus[]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface DeployRecord {
|
|
311
|
+
readonly commit: string
|
|
312
|
+
readonly ref: string
|
|
313
|
+
readonly deployedAt: string
|
|
314
|
+
readonly deployedBy: string
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export interface DeployProcessStatus {
|
|
318
|
+
readonly service: string
|
|
319
|
+
readonly instance: number
|
|
320
|
+
readonly status: "starting" | "running" | "stopping" | "stopped" | "exited"
|
|
321
|
+
readonly pid?: number
|
|
322
|
+
readonly restarts: number
|
|
323
|
+
readonly startedAt?: string
|
|
324
|
+
readonly cpuPercent?: number
|
|
325
|
+
readonly memoryBytes?: number
|
|
326
|
+
readonly lastError?: string
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface DeployLogsOptions {
|
|
330
|
+
/** One service's logs, or every service's. */
|
|
331
|
+
readonly service?: string
|
|
332
|
+
/** How many recent lines to write first. */
|
|
333
|
+
readonly tail: number
|
|
334
|
+
readonly follow: boolean
|
|
335
|
+
write(line: string): void
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export type DeployControlAction = "restart" | "start" | "stop"
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEPLOY_CONFIGURABLE_WORKER_TYPES,
|
|
3
|
+
DEPLOY_SERVICES,
|
|
4
|
+
DEPLOY_WORKER_TYPES,
|
|
5
|
+
isDeployHttpService,
|
|
6
|
+
isDeploySingletonService,
|
|
7
|
+
} from "./services"
|
|
8
|
+
import type { DeployConfig } from "./types"
|
|
9
|
+
|
|
10
|
+
type UnknownRecord = Record<string, unknown>
|
|
11
|
+
|
|
12
|
+
const NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
|
|
13
|
+
const DOMAIN_LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
|
|
14
|
+
const DOMAIN_PATTERN = new RegExp(`^${DOMAIN_LABEL}(?:\\.${DOMAIN_LABEL})*$`, "i")
|
|
15
|
+
const ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/
|
|
16
|
+
const MEMORY_PATTERN = /^\d+(?:\.\d+)?\s*(?:b|k|kb|m|mb|g|gb)?$/i
|
|
17
|
+
/** Longest delay a timer honors; anything larger fires immediately. */
|
|
18
|
+
const MAX_TIMER_MS = 2_147_483_647
|
|
19
|
+
|
|
20
|
+
const PROCESS_OPTIONS = ["killTimeoutMs", "restartDelayMs", "maxMemory"] as const
|
|
21
|
+
const SCALABLE_PROCESS_OPTIONS = [...PROCESS_OPTIONS, "instances"] as const
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks a loaded `sixb.deploy.ts` default export and returns it typed. Validates shape and
|
|
25
|
+
* values only; `sixb deploy` resolves defaults and checks what depends on the target.
|
|
26
|
+
*/
|
|
27
|
+
export function validateDeployConfig(value: unknown): DeployConfig {
|
|
28
|
+
const config = record(value, "The deploy config")
|
|
29
|
+
knownKeys(config, ["name", "domain", "target", "env", "services", "processes"], "")
|
|
30
|
+
|
|
31
|
+
name(config.name, "name")
|
|
32
|
+
if (config.domain !== undefined) domain(config.domain, "domain")
|
|
33
|
+
target(config.target)
|
|
34
|
+
if (config.env !== undefined) env(config.env, "env")
|
|
35
|
+
if (config.services !== undefined) services(config.services)
|
|
36
|
+
if (config.processes !== undefined) processes(config.processes)
|
|
37
|
+
|
|
38
|
+
return value as DeployConfig
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function target(value: unknown): void {
|
|
42
|
+
const hint = "Use a deploy target, such as `new SshTarget({ host })` from @sixb/deploy-ssh."
|
|
43
|
+
if (typeof value !== "object" || value === null) {
|
|
44
|
+
throw new Error(`[SixbDeploy] target is required. ${hint}`)
|
|
45
|
+
}
|
|
46
|
+
const candidate = value as UnknownRecord
|
|
47
|
+
const methods = ["listenAddress", "deploy", "status", "logs", "control", "check"]
|
|
48
|
+
if (
|
|
49
|
+
typeof candidate.kind !== "string" ||
|
|
50
|
+
typeof candidate.location !== "string" ||
|
|
51
|
+
methods.some((method) => typeof candidate[method] !== "function")
|
|
52
|
+
) {
|
|
53
|
+
throw new Error(`[SixbDeploy] target is not a deploy target. ${hint}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function services(value: unknown): void {
|
|
58
|
+
const config = record(value, "services")
|
|
59
|
+
knownKeys(config, DEPLOY_SERVICES, "services")
|
|
60
|
+
|
|
61
|
+
for (const service of DEPLOY_SERVICES) {
|
|
62
|
+
const override = config[service]
|
|
63
|
+
if (override === undefined || typeof override === "boolean") continue
|
|
64
|
+
const path = `services.${service}`
|
|
65
|
+
const options = record(override, path)
|
|
66
|
+
|
|
67
|
+
if (isDeployHttpService(service)) {
|
|
68
|
+
knownKeys(options, ["domain", "env", "process"], path, {
|
|
69
|
+
port: "Ports belong to the target, such as `new SshTarget({ ports: { api: 3012 } })`.",
|
|
70
|
+
})
|
|
71
|
+
if (options.domain !== undefined) domain(options.domain, `${path}.domain`)
|
|
72
|
+
if (options.process !== undefined) {
|
|
73
|
+
processOptions(options.process, `${path}.process`, PROCESS_OPTIONS, {
|
|
74
|
+
instances: "HTTP services run one process each.",
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
} else if (isDeploySingletonService(service)) {
|
|
78
|
+
knownKeys(options, ["env", "process"], path)
|
|
79
|
+
if (options.process !== undefined) {
|
|
80
|
+
processOptions(options.process, `${path}.process`, PROCESS_OPTIONS, {
|
|
81
|
+
instances: `The ${service} runs exactly one process: a second would duplicate its work.`,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
knownKeys(options, ["types", "agentTurnTimeout", "concurrency", "env", "process"], path)
|
|
86
|
+
if (options.types !== undefined) workerTypes(options.types, `${path}.types`)
|
|
87
|
+
if (options.agentTurnTimeout !== undefined) {
|
|
88
|
+
string(options.agentTurnTimeout, `${path}.agentTurnTimeout`)
|
|
89
|
+
}
|
|
90
|
+
if (options.concurrency !== undefined) {
|
|
91
|
+
workerConcurrency(options.concurrency, `${path}.concurrency`)
|
|
92
|
+
}
|
|
93
|
+
if (options.process !== undefined) {
|
|
94
|
+
processOptions(options.process, `${path}.process`, SCALABLE_PROCESS_OPTIONS)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (options.env !== undefined) env(options.env, `${path}.env`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function processes(value: unknown): void {
|
|
103
|
+
const config = record(value, "processes")
|
|
104
|
+
|
|
105
|
+
for (const [processName, definition] of Object.entries(config)) {
|
|
106
|
+
const path = `processes.${processName}`
|
|
107
|
+
name(processName, path)
|
|
108
|
+
if ((DEPLOY_SERVICES as readonly string[]).includes(processName)) {
|
|
109
|
+
throw new Error(`[SixbDeploy] ${path} uses the name of a Sixb service. Rename the process.`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const options = record(definition, path)
|
|
113
|
+
knownKeys(options, ["entrypoint", "args", "env", "process"], path)
|
|
114
|
+
entrypoint(options.entrypoint, `${path}.entrypoint`)
|
|
115
|
+
if (options.args !== undefined) {
|
|
116
|
+
if (!Array.isArray(options.args) || options.args.some((arg) => typeof arg !== "string")) {
|
|
117
|
+
throw new Error(`[SixbDeploy] ${path}.args must be an array of strings.`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (options.env !== undefined) env(options.env, `${path}.env`)
|
|
121
|
+
if (options.process !== undefined) {
|
|
122
|
+
processOptions(options.process, `${path}.process`, SCALABLE_PROCESS_OPTIONS)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function workerTypes(value: unknown, path: string): void {
|
|
128
|
+
if (value === "all") return
|
|
129
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
130
|
+
throw new Error(`[SixbDeploy] ${path} must be "all" or a non-empty list of worker types.`)
|
|
131
|
+
}
|
|
132
|
+
const seen = new Set<string>()
|
|
133
|
+
for (const workerType of value) {
|
|
134
|
+
if (!(DEPLOY_WORKER_TYPES as readonly unknown[]).includes(workerType)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`[SixbDeploy] ${path} has unknown worker type ${JSON.stringify(workerType)}. ` +
|
|
137
|
+
`Available: ${DEPLOY_WORKER_TYPES.join(", ")}.`
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
if (seen.has(workerType)) {
|
|
141
|
+
throw new Error(`[SixbDeploy] ${path} lists ${workerType} more than once.`)
|
|
142
|
+
}
|
|
143
|
+
seen.add(workerType)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function workerConcurrency(value: unknown, path: string): void {
|
|
148
|
+
const config = record(value, path)
|
|
149
|
+
for (const [workerType, concurrency] of Object.entries(config)) {
|
|
150
|
+
if (workerType === "action") {
|
|
151
|
+
throw new Error(`[SixbDeploy] ${path}.action cannot be set: action jobs run one at a time.`)
|
|
152
|
+
}
|
|
153
|
+
if (!(DEPLOY_CONFIGURABLE_WORKER_TYPES as readonly string[]).includes(workerType)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`[SixbDeploy] ${path} has unknown worker type "${workerType}". ` +
|
|
156
|
+
`Available: ${DEPLOY_CONFIGURABLE_WORKER_TYPES.join(", ")}.`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
positiveInteger(concurrency, `${path}.${workerType}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function processOptions(
|
|
164
|
+
value: unknown,
|
|
165
|
+
path: string,
|
|
166
|
+
allowed: readonly string[],
|
|
167
|
+
hints: Readonly<Record<string, string>> = {}
|
|
168
|
+
): void {
|
|
169
|
+
const options = record(value, path)
|
|
170
|
+
knownKeys(options, allowed, path, hints)
|
|
171
|
+
if (options.instances !== undefined) positiveInteger(options.instances, `${path}.instances`)
|
|
172
|
+
if (options.killTimeoutMs !== undefined) {
|
|
173
|
+
positiveInteger(options.killTimeoutMs, `${path}.killTimeoutMs`, MAX_TIMER_MS)
|
|
174
|
+
}
|
|
175
|
+
if (options.restartDelayMs !== undefined) {
|
|
176
|
+
positiveInteger(options.restartDelayMs, `${path}.restartDelayMs`, MAX_TIMER_MS)
|
|
177
|
+
}
|
|
178
|
+
if (options.maxMemory !== undefined) {
|
|
179
|
+
if (typeof options.maxMemory !== "string" || !MEMORY_PATTERN.test(options.maxMemory.trim())) {
|
|
180
|
+
throw new Error(`[SixbDeploy] ${path}.maxMemory must be a size such as "512M" or "2G".`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function entrypoint(value: unknown, path: string): void {
|
|
186
|
+
string(value, path)
|
|
187
|
+
const segments = value.split(/[\\/]/)
|
|
188
|
+
if (value.startsWith("/") || segments.includes("..")) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`[SixbDeploy] ${path} must be a path inside the project, relative to its directory.`
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function env(value: unknown, path: string): void {
|
|
196
|
+
const variables = record(value, path)
|
|
197
|
+
for (const [key, variable] of Object.entries(variables)) {
|
|
198
|
+
if (!ENV_NAME_PATTERN.test(key)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`[SixbDeploy] ${path}.${key} is not a valid environment variable name. ` +
|
|
201
|
+
"Use uppercase letters, digits, and underscores."
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
if (typeof variable !== "string") {
|
|
205
|
+
throw new Error(`[SixbDeploy] ${path}.${key} must be a string.`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function name(value: unknown, path: string): void {
|
|
211
|
+
string(value, path)
|
|
212
|
+
if (!NAME_PATTERN.test(value) || value.length > 63) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`[SixbDeploy] ${path} must use lowercase letters, numbers, and hyphens, ` +
|
|
215
|
+
"start and end with a letter or number, and be at most 63 characters."
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function domain(value: unknown, path: string): void {
|
|
221
|
+
string(value, path)
|
|
222
|
+
if (value.includes("://") || value.includes("/")) {
|
|
223
|
+
throw new Error(`[SixbDeploy] ${path} must be a hostname such as app.example.com, not a URL.`)
|
|
224
|
+
}
|
|
225
|
+
if (value.length > 253 || !DOMAIN_PATTERN.test(value)) {
|
|
226
|
+
throw new Error(`[SixbDeploy] ${path} must be a valid hostname such as app.example.com.`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function positiveInteger(value: unknown, path: string, maximum = Number.MAX_SAFE_INTEGER): void {
|
|
231
|
+
if (!Number.isInteger(value) || (value as number) < 1 || (value as number) > maximum) {
|
|
232
|
+
throw new Error(`[SixbDeploy] ${path} must be a positive integer.`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function string(value: unknown, path: string): asserts value is string {
|
|
237
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
238
|
+
throw new Error(`[SixbDeploy] ${path} must be a non-empty string.`)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function record(value: unknown, path: string): UnknownRecord {
|
|
243
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
244
|
+
throw new Error(`[SixbDeploy] ${path} must be an object.`)
|
|
245
|
+
}
|
|
246
|
+
return value as UnknownRecord
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function knownKeys(
|
|
250
|
+
value: UnknownRecord,
|
|
251
|
+
allowed: readonly string[],
|
|
252
|
+
path: string,
|
|
253
|
+
hints: Readonly<Record<string, string>> = {}
|
|
254
|
+
): void {
|
|
255
|
+
for (const key of Object.keys(value)) {
|
|
256
|
+
if (allowed.includes(key)) continue
|
|
257
|
+
const setting = path ? `${path}.${key}` : key
|
|
258
|
+
const hint = hints[key] ?? `Available: ${allowed.join(", ")}.`
|
|
259
|
+
throw new Error(`[SixbDeploy] Unknown deploy setting '${setting}'. ${hint}`)
|
|
260
|
+
}
|
|
261
|
+
}
|
package/src/errors/catalog.ts
CHANGED
|
@@ -146,6 +146,10 @@ export const SIXB_ERROR_DEFINITIONS = {
|
|
|
146
146
|
publicMessage: "The embedding call was interrupted; its outcome and charges may be unknown.",
|
|
147
147
|
retryable: false,
|
|
148
148
|
},
|
|
149
|
+
"storage.unavailable": {
|
|
150
|
+
publicMessage: "Storage is temporarily unavailable.",
|
|
151
|
+
retryable: true,
|
|
152
|
+
},
|
|
149
153
|
"sync.execution_failed": {
|
|
150
154
|
publicMessage: "Sync execution failed.",
|
|
151
155
|
retryable: false,
|
|
@@ -24,6 +24,9 @@ const DEGRADED_FAILURE_THRESHOLD = 2
|
|
|
24
24
|
// run ahead of the database's; the sweep leaves sessions alone until this long past expiry.
|
|
25
25
|
const ABANDONED_UPLOAD_SWEEP_DELAY_MS = 5 * 60_000
|
|
26
26
|
const ABANDONED_UPLOAD_ABORT_GRACE_MS = 15 * 60_000
|
|
27
|
+
// One abandoned candidate of a large projection holds hundreds of thousands of rows. Batches this
|
|
28
|
+
// size keep each delete short while clearing such a candidate within a pass.
|
|
29
|
+
const ABANDONED_SOURCE_PURGE_BATCH = 50_000
|
|
27
30
|
|
|
28
31
|
interface OntologyMaintenanceDependencies {
|
|
29
32
|
readonly projectId: string
|
|
@@ -188,6 +191,11 @@ export class OntologyMaintenance {
|
|
|
188
191
|
failures,
|
|
189
192
|
{ rowsDeleted: 0, materializationsDeleted: 0 }
|
|
190
193
|
)
|
|
194
|
+
const abandonedSources = await captureFailure(
|
|
195
|
+
() => this.purgeAbandonedSources(started),
|
|
196
|
+
failures,
|
|
197
|
+
{ rowsDeleted: 0, materializationsDeleted: 0 }
|
|
198
|
+
)
|
|
191
199
|
const uploads = this.storage.fileUploadSessions
|
|
192
200
|
if (uploads) {
|
|
193
201
|
// Retire abandoned uploads first: the store never reaps one that is still pending.
|
|
@@ -197,8 +205,30 @@ export class OntologyMaintenance {
|
|
|
197
205
|
|
|
198
206
|
return {
|
|
199
207
|
publishedOutboxRowsDeleted,
|
|
200
|
-
terminalSourceRowsDeleted: terminalSources.rowsDeleted,
|
|
201
|
-
terminalSourceMaterializationsDeleted:
|
|
208
|
+
terminalSourceRowsDeleted: terminalSources.rowsDeleted + abandonedSources.rowsDeleted,
|
|
209
|
+
terminalSourceMaterializationsDeleted:
|
|
210
|
+
terminalSources.materializationsDeleted + abandonedSources.materializationsDeleted,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Deletes abandoned candidates batch after batch, until none is left or the pass runs long. */
|
|
215
|
+
private async purgeAbandonedSources(
|
|
216
|
+
started: Date
|
|
217
|
+
): Promise<{ readonly rowsDeleted: number; readonly materializationsDeleted: number }> {
|
|
218
|
+
let rowsDeleted = 0
|
|
219
|
+
let materializationsDeleted = 0
|
|
220
|
+
while (true) {
|
|
221
|
+
const purged = await this.storage.ontology.sources.purgeAbandoned({
|
|
222
|
+
projectId: this.projectId,
|
|
223
|
+
limit: ABANDONED_SOURCE_PURGE_BATCH,
|
|
224
|
+
})
|
|
225
|
+
rowsDeleted += purged.rowsDeleted
|
|
226
|
+
materializationsDeleted += purged.materializationsDeleted
|
|
227
|
+
const exhausted =
|
|
228
|
+
purged.rowsDeleted + purged.materializationsDeleted < ABANDONED_SOURCE_PURGE_BATCH
|
|
229
|
+
if (exhausted || this.now().getTime() - started.getTime() >= this.intervalMs) {
|
|
230
|
+
return { rowsDeleted, materializationsDeleted }
|
|
231
|
+
}
|
|
202
232
|
}
|
|
203
233
|
}
|
|
204
234
|
|
package/src/maintenance/types.ts
CHANGED
|
@@ -3,9 +3,15 @@ export interface OntologyMaintenanceOptions {
|
|
|
3
3
|
readonly intervalMs?: number
|
|
4
4
|
/** Published outbox retention. Defaults to 24 hours. */
|
|
5
5
|
readonly publishedOutboxRetentionMs?: number
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Retention of superseded source versions. Defaults to 24 hours. Abandoned candidates have none:
|
|
8
|
+
* nothing reads them, so the next pass deletes them whole.
|
|
9
|
+
*/
|
|
7
10
|
readonly terminalSourceRetentionMs?: number
|
|
8
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Maximum deletions attempted per cleanup domain and pass. Defaults to 1,000. Abandoned
|
|
13
|
+
* candidates are deleted in larger batches until none is left or the pass reaches `intervalMs`.
|
|
14
|
+
*/
|
|
9
15
|
readonly cleanupLimit?: number
|
|
10
16
|
/** Warn after this shutdown grace period, then wait for active operations. Defaults to 30 seconds. */
|
|
11
17
|
readonly shutdownTimeoutMs?: number
|