@greendrake/service-state 0.3.0 → 0.5.0
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/README.md +24 -15
- package/package.json +4 -4
- package/src/main.ts +16 -6
- package/src/server.ts +140 -29
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @greendrake/service-state
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Backend services an operator can turn on and off, each a state machine both halves of a dashboard share, any number of them over one socket. The root entry is the contract — the states, the status, the method and push names, the transition table — and is pure types and data with no runtime dependency, so the frontend importing it pulls in nothing. `./server` is the machine behind it.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
OFF --start--> STARTING --> ON ON --stop--> STOPPING --> OFF
|
|
@@ -24,37 +24,46 @@ The `./server` entry needs `@greendrake/rpc-server`, which is an optional peer d
|
|
|
24
24
|
import { SERVICE_STATE_EVENT, type ServiceStateMethods, type ServiceStatePushes, type ServiceStatus } from '@greendrake/service-state'
|
|
25
25
|
|
|
26
26
|
interface ServiceStatus {
|
|
27
|
+
service: string // which service this is the status of
|
|
27
28
|
state: 'OFF' | 'STARTING' | 'ON' | 'STOPPING' | 'ERROR'
|
|
28
29
|
error: string | null // what went wrong, in ERROR; null everywhere else
|
|
29
30
|
changed_at: number // epoch ms
|
|
30
31
|
}
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
`ServiceStateMethods` is the four methods (`service.status`, `service.start`, `service.stop`, `service.refresh`), each
|
|
34
|
-
|
|
35
|
-
`@greendrake/dash`'s `ServiceStateControl` renders all of this; a dashboard that wants the control needs no code of its own beyond wiring it to its client and transport.
|
|
34
|
+
`ServiceStateMethods` is the four methods (`service.status`, `service.start`, `service.stop`, `service.refresh`), each taking a `ServiceRef` — `{ service }`, the name of the service it is about — and answering its `ServiceStatus`; hand it to a typed client. `ServiceStatePushes` types the `service.state` event, which carries a status to **every** connected socket: a service has one state, and everyone watching it is watching the same thing. The status names its service, so a socket watching several tells their pushes apart by it.
|
|
36
35
|
|
|
37
36
|
## The machine
|
|
38
37
|
|
|
39
38
|
```ts
|
|
40
39
|
import { createServiceStateMachine, serviceStateMethods } from '@greendrake/service-state/server'
|
|
41
40
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
const unit = (name: string) => ({
|
|
42
|
+
start: () => systemd.start(name),
|
|
43
|
+
stop: () => systemd.stop(name),
|
|
44
|
+
check: () => systemd.isActive(name).then(on => (on ? 'ON' : 'OFF'))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Each machine is named for the wire; the registry — any EventSink, and
|
|
48
|
+
// @greendrake/rpc-server's ConnRegistry is one — is where its pushes go; and
|
|
49
|
+
// the last argument is how old a reading may get before the machine takes
|
|
50
|
+
// another, in milliseconds.
|
|
51
|
+
const machines = await Promise.all([createServiceStateMachine('web', unit('web.service'), registry, 30_000), createServiceStateMachine('worker', unit('worker.service'), registry, 30_000)])
|
|
52
|
+
|
|
53
|
+
const methods = { ...serviceStateMethods(machines), ...whateverElse }
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
// On shutdown, stop the re-reads.
|
|
56
|
+
machines.forEach(machine => machine.close())
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
The factory reads the service before returning, so the machine never reports a state it has not verified — one that assumed `OFF` at boot would have every dashboard showing `OFF` for a service that is up.
|
|
59
|
+
The name is what a call's `{ service }` reaches the machine by and what its every status carries. The factory reads the service before returning, so the machine never reports a state it has not verified — one that assumed `OFF` at boot would have every dashboard showing `OFF` for a service that is up.
|
|
60
|
+
|
|
61
|
+
Nor does it keep reporting one it verified long ago. Nothing tells the machine when a service is changed by other hands — a unit stopped from a shell, a schedule that turns it off overnight — so it reads the service again whenever its last reading is `maxAgeMs` old, and pushes what changed: a dashboard left open follows the service, and one opened after a quiet spell is shown what the service is, not what it was. `status` answers from the latest reading without waiting on the service. A transition that lands counts as a reading, and so does a `refresh`; one asked while a read is under way joins it, unless a transition has begun since that read went out — which makes it a reading of the past, so a fresh one goes out instead. A transition under way and `ERROR` are the machine's own word rather than a reading, and are not read past — `ERROR` until somebody asks for a `refresh`, because reading past it would take the why away before anyone saw it. A transition begun while a read is out goes ahead, and the read's answer, which describes the service before it, is dropped. `close()` stops the re-reads, for a process shutting down.
|
|
62
|
+
|
|
63
|
+
`check` must settle promptly: a `refresh` waits on it, and the next re-read is timed from when it settles, so a check that never settles leaves the state as it last stood.
|
|
55
64
|
|
|
56
65
|
`start` and `stop` **return as soon as the transition has begun**, with the in-flight status. Where it ends up is the machine's next state, pushed to everyone — not the caller's answer, because everyone watching needs it equally and one of them happening to have asked changes nothing. A hook that rejects lands the machine in `ERROR` carrying its message.
|
|
57
66
|
|
|
58
67
|
One transition at a time, and each only from the state it is defined for. Anything else — `start` while already `ON`, `refresh` mid-transition — is refused with `SERVICE_BUSY`. A client showing the state never triggers it, because the state it is showing offers no such button.
|
|
59
68
|
|
|
60
|
-
`serviceStateMethods(
|
|
69
|
+
`serviceStateMethods(machines, auth = 'admin')` returns the four `MethodDef`s over every machine given, so one dispatch table — and one socket — serves them all. A call's argument is checked for its shape before any machine is reached (`INVALID_ARGUMENT` on `service`); one naming a service the table does not serve is refused with `SERVICE_NOT_FOUND` — a `_NOT_FOUND` code, which clients following `@greendrake/rpc-server`'s convention treat as not found. Two machines under one name are refused as the table is built. Admin-only by default: turning a service off is not a read.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greendrake/service-state",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A backend service as a state machine (OFF/STARTING/ON/STOPPING/ERROR): the wire contract both halves share, and the server-side machine behind it",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eugene Greendrake <eugene@greendrake.nz>",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"prettier": "@greendrake/dev-config/prettier",
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@greendrake/rpc-server": "0.
|
|
41
|
+
"@greendrake/rpc-server": "0.5.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"@greendrake/rpc-server": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@greendrake/dev-config": "0.
|
|
50
|
-
"@greendrake/rpc-server": "0.
|
|
49
|
+
"@greendrake/dev-config": "0.5.0",
|
|
50
|
+
"@greendrake/rpc-server": "0.5.0",
|
|
51
51
|
"@types/bun": "^1.3.5",
|
|
52
52
|
"typescript": "6.0.3"
|
|
53
53
|
}
|
package/src/main.ts
CHANGED
|
@@ -7,8 +7,17 @@ export const SERVICE_STATES = ['OFF', 'STARTING', 'ON', 'STOPPING', 'ERROR'] as
|
|
|
7
7
|
|
|
8
8
|
export type ServiceState = (typeof SERVICE_STATES)[number]
|
|
9
9
|
|
|
10
|
+
// Which service a call is about. Every method takes it, so one socket serves
|
|
11
|
+
// any number of services side by side.
|
|
12
|
+
export interface ServiceRef {
|
|
13
|
+
service: string
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
// Wire shape: snake_case, as the envelope's own fields are.
|
|
11
17
|
export interface ServiceStatus {
|
|
18
|
+
// The service this is the status of — what a push is told apart by, since
|
|
19
|
+
// every service's pushes arrive on the same socket.
|
|
20
|
+
service: string
|
|
12
21
|
state: ServiceState
|
|
13
22
|
// What went wrong, in ERROR. Null in every other state — a stale message
|
|
14
23
|
// outliving the failure it describes is worse than none.
|
|
@@ -39,14 +48,15 @@ export type ServiceCommand = keyof typeof TRANSITIONS
|
|
|
39
48
|
export const SERVICE_STATE_EVENT = 'service.state'
|
|
40
49
|
|
|
41
50
|
// The methods a service-state backend serves. A dashboard hands this to its
|
|
42
|
-
//
|
|
51
|
+
// typed client; the server derives its definitions from it.
|
|
43
52
|
export interface ServiceStateMethods {
|
|
44
|
-
'service.status': () => ServiceStatus
|
|
45
|
-
'service.start': () => ServiceStatus
|
|
46
|
-
'service.stop': () => ServiceStatus
|
|
47
|
-
'service.refresh': () => ServiceStatus
|
|
53
|
+
'service.status': (ref: ServiceRef) => ServiceStatus
|
|
54
|
+
'service.start': (ref: ServiceRef) => ServiceStatus
|
|
55
|
+
'service.stop': (ref: ServiceRef) => ServiceStatus
|
|
56
|
+
'service.refresh': (ref: ServiceRef) => ServiceStatus
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
// Every state change reaches every connected socket: a service has one state,
|
|
51
|
-
// and everyone watching it is watching the same thing.
|
|
60
|
+
// and everyone watching it is watching the same thing. The status names its
|
|
61
|
+
// service, so a socket watching several follows each by its name.
|
|
52
62
|
export type ServiceStatePushes = { [SERVICE_STATE_EVENT]: ServiceStatus }
|
package/src/server.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { ApiError } from '@greendrake/rpc-server'
|
|
2
2
|
import type { AuthRequirement, EventSink, MethodDef } from '@greendrake/rpc-server'
|
|
3
3
|
import { SERVICE_STATE_EVENT, TRANSITIONS } from './main'
|
|
4
|
-
import type { ServiceCommand, ServiceState, ServiceStateMethods, ServiceStatus } from './main'
|
|
4
|
+
import type { ServiceCommand, ServiceRef, ServiceState, ServiceStateMethods, ServiceStatus } from './main'
|
|
5
5
|
|
|
6
6
|
// What a concrete service plugs in. `start` and `stop` resolve once the
|
|
7
7
|
// service is up or down; rejecting puts the machine in ERROR with the
|
|
8
8
|
// rejection's message. `check` reports what the service is actually doing
|
|
9
|
-
// right now, read from the service rather than remembered
|
|
9
|
+
// right now, read from the service rather than remembered, and must settle
|
|
10
|
+
// promptly: a refresh waits on it, and the next re-read is timed from when it
|
|
11
|
+
// settles, so a check that never does leaves the state as it last stood.
|
|
10
12
|
export interface ServiceImplementation {
|
|
11
13
|
start(): Promise<void>
|
|
12
14
|
stop(): Promise<void>
|
|
@@ -14,6 +16,11 @@ export interface ServiceImplementation {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface ServiceStateMachine {
|
|
19
|
+
// What the service is called on the wire: what a call names to reach it,
|
|
20
|
+
// and what each of its pushes carries.
|
|
21
|
+
readonly name: string
|
|
22
|
+
// What the service was last read as, or what its last transition left it
|
|
23
|
+
// as — kept current by the machine's own re-reads (see the factory).
|
|
17
24
|
status(): ServiceStatus
|
|
18
25
|
// Return as soon as the transition has begun, with the in-flight status.
|
|
19
26
|
// Where it ends up is the machine's next state, pushed to everyone — not
|
|
@@ -21,8 +28,12 @@ export interface ServiceStateMachine {
|
|
|
21
28
|
// of them happening to have asked changes nothing.
|
|
22
29
|
start(): ServiceStatus
|
|
23
30
|
stop(): ServiceStatus
|
|
24
|
-
// Re-read the service and adopt what it reports
|
|
31
|
+
// Re-read the service and adopt what it reports, joining a read already
|
|
32
|
+
// under way — one begun since the last transition — rather than asking
|
|
33
|
+
// the service again.
|
|
25
34
|
refresh(): Promise<ServiceStatus>
|
|
35
|
+
// Stop re-reading the service, for a process shutting down.
|
|
36
|
+
close(): void
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
// The command a state is in no position to take. One code covers both cases —
|
|
@@ -31,20 +42,46 @@ export interface ServiceStateMachine {
|
|
|
31
42
|
// what a client that ignored the state gets told.
|
|
32
43
|
const BUSY = 'SERVICE_BUSY'
|
|
33
44
|
|
|
45
|
+
// A call naming a service the table does not serve. A `_NOT_FOUND` code, so
|
|
46
|
+
// clients following rpc-server's convention treat it as not found.
|
|
47
|
+
const NOT_FOUND = 'SERVICE_NOT_FOUND'
|
|
48
|
+
|
|
34
49
|
// The service-state machine over a concrete implementation. Reads the service
|
|
35
50
|
// before returning, so it never reports a state it has not verified — a
|
|
36
51
|
// machine that assumed OFF at boot would have the dashboard show OFF for a
|
|
37
|
-
// service that is up.
|
|
38
|
-
|
|
52
|
+
// service that is up. Nor does it later: nothing tells the machine when a
|
|
53
|
+
// service is changed by other hands — a unit stopped from a shell, a schedule
|
|
54
|
+
// that turns it off overnight — so it reads the service again whenever its
|
|
55
|
+
// last reading is `maxAgeMs` old, and pushes what changed to everyone
|
|
56
|
+
// watching, open dashboards included. A transition that lands counts as a
|
|
57
|
+
// reading. A transition under way and ERROR are the machine's own word rather
|
|
58
|
+
// than a reading, and are not read past — ERROR until somebody asks for a
|
|
59
|
+
// refresh, because reading past it would take the why away before anyone saw
|
|
60
|
+
// it.
|
|
61
|
+
export const createServiceStateMachine = async (name: string, service: ServiceImplementation, sink: EventSink, maxAgeMs: number): Promise<ServiceStateMachine> => {
|
|
39
62
|
let current: ServiceStatus = {
|
|
63
|
+
service: name,
|
|
40
64
|
state: 'OFF',
|
|
41
65
|
error: null,
|
|
42
66
|
changed_at: Date.now()
|
|
43
67
|
}
|
|
44
|
-
let
|
|
68
|
+
let transitioning = false
|
|
69
|
+
// Counts the transitions begun. A read that began before the latest one
|
|
70
|
+
// reports what the service was before it, so its answer is dropped rather
|
|
71
|
+
// than adopted over the transition's.
|
|
72
|
+
let transitions = 0
|
|
73
|
+
// The read under way and the transition count it began under. A refresh
|
|
74
|
+
// or re-read asked while it is out joins it rather than asking the service
|
|
75
|
+
// again — unless a transition has begun since, which makes it a reading of
|
|
76
|
+
// the past: then a fresh read goes out, and the old one is dropped when it
|
|
77
|
+
// settles.
|
|
78
|
+
let reading: { promise: Promise<ServiceStatus>; under: number } | undefined
|
|
79
|
+
let nextRead: ReturnType<typeof setTimeout> | undefined
|
|
80
|
+
let closed = false
|
|
45
81
|
|
|
46
82
|
const set = (state: ServiceState, error: string | null): ServiceStatus => {
|
|
47
83
|
current = {
|
|
84
|
+
service: name,
|
|
48
85
|
state,
|
|
49
86
|
error,
|
|
50
87
|
changed_at: Date.now()
|
|
@@ -57,12 +94,23 @@ export const createServiceStateMachine = async (service: ServiceImplementation,
|
|
|
57
94
|
// a poll against a steady service is silent.
|
|
58
95
|
const adopt = (state: ServiceState, error: string | null): ServiceStatus => (state === current.state && error === current.error ? current : set(state, error))
|
|
59
96
|
|
|
97
|
+
// The next re-read, `maxAgeMs` after the reading just taken. None in ERROR,
|
|
98
|
+
// which stands until somebody refreshes.
|
|
99
|
+
const schedule = (): void => {
|
|
100
|
+
clearTimeout(nextRead)
|
|
101
|
+
if (!closed && current.state !== 'ERROR') {
|
|
102
|
+
nextRead = setTimeout(() => void read(), maxAgeMs)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
60
106
|
const run = (command: ServiceCommand): ServiceStatus => {
|
|
61
107
|
const { from, during, to } = TRANSITIONS[command]
|
|
62
|
-
if (
|
|
108
|
+
if (transitioning || current.state !== from) {
|
|
63
109
|
throw new ApiError(BUSY)
|
|
64
110
|
}
|
|
65
|
-
|
|
111
|
+
transitioning = true
|
|
112
|
+
transitions += 1
|
|
113
|
+
clearTimeout(nextRead)
|
|
66
114
|
const answer = set(during, null)
|
|
67
115
|
void service[command]()
|
|
68
116
|
.then(
|
|
@@ -70,30 +118,61 @@ export const createServiceStateMachine = async (service: ServiceImplementation,
|
|
|
70
118
|
(e: Error) => set('ERROR', e.message)
|
|
71
119
|
)
|
|
72
120
|
.finally(() => {
|
|
73
|
-
|
|
121
|
+
transitioning = false
|
|
122
|
+
schedule()
|
|
74
123
|
})
|
|
75
124
|
return answer
|
|
76
125
|
}
|
|
77
126
|
|
|
127
|
+
const read = (): Promise<ServiceStatus> => {
|
|
128
|
+
if (reading?.under === transitions) {
|
|
129
|
+
return reading.promise
|
|
130
|
+
}
|
|
131
|
+
clearTimeout(nextRead)
|
|
132
|
+
const under = transitions
|
|
133
|
+
// A transition begun since the read went out has the last word — its
|
|
134
|
+
// own landing schedules the next read — and the read's answer, from
|
|
135
|
+
// before it, is dropped.
|
|
136
|
+
const land = (state: ServiceState, error: string | null): ServiceStatus => {
|
|
137
|
+
if (under !== transitions) {
|
|
138
|
+
return current
|
|
139
|
+
}
|
|
140
|
+
const status = adopt(state, error)
|
|
141
|
+
schedule()
|
|
142
|
+
return status
|
|
143
|
+
}
|
|
144
|
+
const promise = service
|
|
145
|
+
.check()
|
|
146
|
+
.then(
|
|
147
|
+
state => land(state, null),
|
|
148
|
+
(e: Error) => land('ERROR', e.message)
|
|
149
|
+
)
|
|
150
|
+
.finally(() => {
|
|
151
|
+
if (reading?.promise === promise) {
|
|
152
|
+
reading = undefined
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
reading = { promise, under }
|
|
156
|
+
return promise
|
|
157
|
+
}
|
|
158
|
+
|
|
78
159
|
const refresh = async (): Promise<ServiceStatus> => {
|
|
79
|
-
if (
|
|
160
|
+
if (transitioning) {
|
|
80
161
|
throw new ApiError(BUSY)
|
|
81
162
|
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
return adopt(await service.check(), null)
|
|
85
|
-
} catch (e) {
|
|
86
|
-
return adopt('ERROR', (e as Error).message)
|
|
87
|
-
} finally {
|
|
88
|
-
inFlight = false
|
|
89
|
-
}
|
|
163
|
+
return read()
|
|
90
164
|
}
|
|
91
165
|
|
|
92
166
|
const machine: ServiceStateMachine = {
|
|
167
|
+
name,
|
|
93
168
|
status: () => current,
|
|
94
169
|
start: () => run('start'),
|
|
95
170
|
stop: () => run('stop'),
|
|
96
|
-
refresh
|
|
171
|
+
refresh,
|
|
172
|
+
close: () => {
|
|
173
|
+
closed = true
|
|
174
|
+
clearTimeout(nextRead)
|
|
175
|
+
}
|
|
97
176
|
}
|
|
98
177
|
await machine.refresh()
|
|
99
178
|
return machine
|
|
@@ -101,16 +180,48 @@ export const createServiceStateMachine = async (service: ServiceImplementation,
|
|
|
101
180
|
|
|
102
181
|
// Derived from the client-facing contract rather than restated beside it: a
|
|
103
182
|
// method added there has to be defined here before this compiles.
|
|
104
|
-
export type ServiceStateMethodDefs = { [K in keyof ServiceStateMethods]: MethodDef<
|
|
183
|
+
export type ServiceStateMethodDefs = { [K in keyof ServiceStateMethods]: MethodDef<Parameters<ServiceStateMethods[K]>[0], ReturnType<ServiceStateMethods[K]>> }
|
|
184
|
+
|
|
185
|
+
// The argument every method takes, checked for its shape. Whether it names a
|
|
186
|
+
// service the table serves is the table's to say, as SERVICE_NOT_FOUND. Written
|
|
187
|
+
// against Standard Schema directly, so this package carries no validator.
|
|
188
|
+
const SERVICE_REF: NonNullable<MethodDef<ServiceRef>['args']> = {
|
|
189
|
+
'~standard': {
|
|
190
|
+
version: 1,
|
|
191
|
+
vendor: 'greendrake',
|
|
192
|
+
validate: value => (typeof value === 'object' && value !== null && 'service' in value && typeof value.service === 'string' ? { value: { service: value.service } } : { issues: [{ message: 'Expected the name of a service', path: ['service'] }] })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
105
195
|
|
|
106
|
-
// The four methods, ready to merge into a dispatch
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
196
|
+
// The four methods over every machine given, ready to merge into a dispatch
|
|
197
|
+
// table: one socket serves them all, each call naming its service. Admin-only
|
|
198
|
+
// by default: turning a service off is not a read.
|
|
199
|
+
export const serviceStateMethods = (machines: readonly ServiceStateMachine[], auth: AuthRequirement = 'admin'): ServiceStateMethodDefs => {
|
|
200
|
+
const byName = new Map(machines.map(machine => [machine.name, machine]))
|
|
201
|
+
if (byName.size !== machines.length) {
|
|
202
|
+
throw new Error(`serviceStateMethods: two machines share a name among ${machines.map(machine => machine.name).join(', ')}`)
|
|
203
|
+
}
|
|
204
|
+
// Each method is the same three steps — the argument checked, the service
|
|
205
|
+
// it names found, one thing asked of that service's machine — and differs
|
|
206
|
+
// only in what is asked.
|
|
207
|
+
const asking = (ask: (machine: ServiceStateMachine) => ServiceStatus | Promise<ServiceStatus>): MethodDef<ServiceRef, ServiceStatus> => ({
|
|
208
|
+
auth,
|
|
209
|
+
args: SERVICE_REF,
|
|
210
|
+
handler: (_ctx, { service }) => {
|
|
211
|
+
const machine = byName.get(service)
|
|
212
|
+
if (!machine) {
|
|
213
|
+
throw new ApiError(NOT_FOUND)
|
|
214
|
+
}
|
|
215
|
+
return ask(machine)
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
return {
|
|
219
|
+
'service.status': asking(machine => machine.status()),
|
|
220
|
+
'service.start': asking(machine => machine.start()),
|
|
221
|
+
'service.stop': asking(machine => machine.stop()),
|
|
222
|
+
'service.refresh': asking(machine => machine.refresh())
|
|
223
|
+
}
|
|
224
|
+
}
|
|
114
225
|
|
|
115
226
|
export { SERVICE_STATE_EVENT, SERVICE_STATES, TRANSITIONS } from './main'
|
|
116
|
-
export type { ServiceCommand, ServiceState, ServiceStateMethods, ServiceStatePushes, ServiceStatus } from './main'
|
|
227
|
+
export type { ServiceCommand, ServiceRef, ServiceState, ServiceStateMethods, ServiceStatePushes, ServiceStatus } from './main'
|