@highstate/backend 0.9.16 → 0.9.18
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/chunk-NAAIDR4U.js +8499 -0
- package/dist/chunk-NAAIDR4U.js.map +1 -0
- package/dist/chunk-OU5OQBLB.js +74 -0
- package/dist/chunk-OU5OQBLB.js.map +1 -0
- package/dist/{chunk-WHALQHEZ.js → chunk-Y7DXREVO.js} +502 -774
- package/dist/chunk-Y7DXREVO.js.map +1 -0
- package/dist/highstate.manifest.json +4 -4
- package/dist/index.js +2979 -2233
- package/dist/index.js.map +1 -1
- package/dist/library/package-resolution-worker.js +7 -5
- package/dist/library/package-resolution-worker.js.map +1 -1
- package/dist/library/worker/main.js +40 -41
- package/dist/library/worker/main.js.map +1 -1
- package/dist/magic-string.es-5ABAC4JN.js +1292 -0
- package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
- package/dist/shared/index.js +3 -216
- package/dist/shared/index.js.map +1 -1
- package/package.json +9 -6
- package/src/artifact/encryption.ts +47 -7
- package/src/artifact/factory.ts +2 -2
- package/src/artifact/local.ts +2 -6
- package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
- package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
- package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
- package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
- package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
- package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
- package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
- package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
- package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
- package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
- package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
- package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
- package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
- package/src/business/artifact.ts +2 -1
- package/src/business/index.ts +1 -0
- package/src/business/instance-lock.ts +3 -2
- package/src/business/instance-state.ts +202 -60
- package/src/business/project-unlock.ts +41 -23
- package/src/business/project.ts +299 -0
- package/src/business/secret.test.ts +178 -0
- package/src/business/secret.ts +139 -45
- package/src/business/worker.test.ts +614 -0
- package/src/business/worker.ts +289 -52
- package/src/common/clock.ts +18 -0
- package/src/common/index.ts +3 -0
- package/src/common/random.ts +68 -0
- package/src/common/test/index.ts +2 -0
- package/src/common/test/render.ts +98 -0
- package/src/common/test/tracer.ts +359 -0
- package/src/config.ts +5 -1
- package/src/hotstate/manager.ts +8 -8
- package/src/hotstate/validation.ts +0 -1
- package/src/library/abstractions.ts +20 -11
- package/src/library/local.ts +6 -13
- package/src/library/worker/evaluator.ts +30 -34
- package/src/library/worker/loader.lite.ts +13 -0
- package/src/library/worker/main.ts +8 -8
- package/src/library/worker/protocol.ts +0 -11
- package/src/lock/index.ts +1 -0
- package/src/lock/manager.ts +17 -2
- package/src/lock/test.ts +108 -0
- package/src/orchestrator/manager.ts +17 -36
- package/src/orchestrator/operation-workset.ts +34 -37
- package/src/orchestrator/operation.ts +129 -74
- package/src/project/abstractions.ts +27 -51
- package/src/project/evaluation.ts +248 -0
- package/src/project/index.ts +1 -1
- package/src/project/local.ts +75 -127
- package/src/pubsub/manager.ts +21 -13
- package/src/runner/abstractions.ts +29 -9
- package/src/runner/artifact-env.ts +3 -3
- package/src/runner/local.ts +29 -19
- package/src/runner/pulumi.ts +4 -1
- package/src/services.ts +77 -24
- package/src/shared/models/backend/library.ts +4 -4
- package/src/shared/models/backend/project.ts +25 -6
- package/src/shared/models/backend/unlock-method.ts +1 -1
- package/src/shared/models/base.ts +1 -84
- package/src/shared/models/project/api-key.ts +5 -2
- package/src/shared/models/project/artifact.ts +3 -33
- package/src/shared/models/project/index.ts +1 -2
- package/src/shared/models/project/lock.ts +3 -3
- package/src/shared/models/project/model.ts +14 -0
- package/src/shared/models/project/operation.ts +3 -3
- package/src/shared/models/project/page.ts +3 -3
- package/src/shared/models/project/secret.ts +4 -18
- package/src/shared/models/project/service-account.ts +2 -2
- package/src/shared/models/project/state.ts +32 -15
- package/src/shared/models/project/terminal.ts +4 -5
- package/src/shared/models/project/trigger.ts +1 -1
- package/src/shared/models/project/unlock-method.ts +9 -2
- package/src/shared/models/project/worker.ts +9 -7
- package/src/shared/resolvers/graph-resolver.ts +41 -26
- package/src/shared/resolvers/input.ts +47 -5
- package/src/shared/resolvers/validation.ts +23 -7
- package/src/shared/utils/args.ts +25 -0
- package/src/shared/utils/index.ts +1 -0
- package/src/state/abstractions.ts +98 -259
- package/src/state/encryption.ts +39 -0
- package/src/state/index.ts +1 -0
- package/src/state/local/backend.ts +29 -222
- package/src/state/local/collection.ts +105 -86
- package/src/state/manager.ts +358 -287
- package/src/state/memory/backend.ts +70 -0
- package/src/state/memory/collection.ts +270 -0
- package/src/state/memory/index.ts +2 -0
- package/src/state/repository/repository.index.ts +1 -1
- package/src/state/repository/repository.ts +71 -22
- package/src/state/test.ts +457 -0
- package/src/unlock/abstractions.ts +49 -0
- package/src/unlock/index.ts +2 -0
- package/src/unlock/memory.ts +32 -0
- package/src/worker/manager.ts +28 -0
- package/dist/chunk-RCB4AFGD.js +0 -159
- package/dist/chunk-RCB4AFGD.js.map +0 -1
- package/dist/chunk-WHALQHEZ.js.map +0 -1
- package/src/project/manager.ts +0 -574
- package/src/shared/models/project/component.ts +0 -45
- package/src/shared/models/project/instance.ts +0 -74
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import type { Fixtures } from "@vitest/runner"
|
|
2
|
+
import type { StateBatch, StateSnapshot, CollectionQuery } from "./abstractions"
|
|
3
|
+
import type { EncryptionBackend, KeyObfuscationBackend } from "./encryption"
|
|
4
|
+
import type { StateCollection } from "./abstractions"
|
|
5
|
+
import type { CollectionQueryResult } from "../shared"
|
|
6
|
+
import { z } from "zod"
|
|
7
|
+
import * as md from "ts-markdown-builder"
|
|
8
|
+
import {
|
|
9
|
+
type TraceEntry,
|
|
10
|
+
type TestTracer,
|
|
11
|
+
type TestBaseFixtures,
|
|
12
|
+
linkTraceEntry,
|
|
13
|
+
renderMdValue,
|
|
14
|
+
renderTraceEntry,
|
|
15
|
+
} from "../common"
|
|
16
|
+
import { createTestHotStateManager, HotStateManager } from "../hotstate"
|
|
17
|
+
import { MemoryProjectUnlockBackend, type ProjectUnlockBackend } from "../unlock"
|
|
18
|
+
import { StateManager } from "./manager"
|
|
19
|
+
import { PassThroughEncryptionBackend, PassThroughKeyObfuscationBackend } from "./encryption"
|
|
20
|
+
import { MemoryStateBackend, MemoryStateBatch, MemoryStateSnapshot } from "./memory"
|
|
21
|
+
import { StateRepository } from "./repository"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Formats multiple key-value pairs as individual blocks.
|
|
25
|
+
*/
|
|
26
|
+
function formatKeyValuePairs(entries: [string, unknown][]): string {
|
|
27
|
+
return md.joinBlocks(
|
|
28
|
+
entries.flatMap(([key, value]) => {
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
return `${md.bold("Key:")} ${md.code(`"${key}"`)}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
//
|
|
35
|
+
`${md.bold("Key:")} ${md.code(`"${key}"`)}`,
|
|
36
|
+
md.bold("Value:"),
|
|
37
|
+
renderMdValue(value),
|
|
38
|
+
]
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class StateReadEntry implements TraceEntry {
|
|
44
|
+
constructor(
|
|
45
|
+
readonly id: number,
|
|
46
|
+
private readonly operation: string,
|
|
47
|
+
private readonly collection: string,
|
|
48
|
+
private readonly keys: string[] | undefined,
|
|
49
|
+
private readonly result: unknown,
|
|
50
|
+
private readonly snapshot?: StateSnapshot,
|
|
51
|
+
) {
|
|
52
|
+
if (snapshot instanceof MemoryStateSnapshot) {
|
|
53
|
+
snapshot.usageEntries.push(this)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render(): string {
|
|
58
|
+
const fields: Record<string, unknown> = {
|
|
59
|
+
collection: this.collection,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.keys) {
|
|
63
|
+
fields.keys = this.keys
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fields.result = this.result
|
|
67
|
+
|
|
68
|
+
return renderTraceEntry({
|
|
69
|
+
icon: "📖",
|
|
70
|
+
title: this.operation,
|
|
71
|
+
fields: {
|
|
72
|
+
collection: {
|
|
73
|
+
value: md.code(this.collection),
|
|
74
|
+
raw: true,
|
|
75
|
+
},
|
|
76
|
+
keys: {
|
|
77
|
+
value: this.keys,
|
|
78
|
+
},
|
|
79
|
+
result: {
|
|
80
|
+
value: this.result,
|
|
81
|
+
alwaysRender: true,
|
|
82
|
+
},
|
|
83
|
+
snapshotId: {
|
|
84
|
+
value:
|
|
85
|
+
this.snapshot instanceof MemoryStateSnapshot
|
|
86
|
+
? `${md.code(this.snapshot.id.toString())}, captured at ${linkTraceEntry(this.snapshot.traceEntry)}`
|
|
87
|
+
: undefined,
|
|
88
|
+
raw: true,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class StateWriteEntry implements TraceEntry {
|
|
96
|
+
constructor(
|
|
97
|
+
readonly id: number,
|
|
98
|
+
private readonly operation: string,
|
|
99
|
+
private readonly collection: string,
|
|
100
|
+
private readonly entries: [string, unknown][],
|
|
101
|
+
private readonly batch?: StateBatch,
|
|
102
|
+
) {
|
|
103
|
+
if (batch instanceof MemoryStateBatch) {
|
|
104
|
+
batch.opEntries.push(this)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render(): string {
|
|
109
|
+
const icon = this.operation.startsWith("delete") || this.operation === "clear" ? "🗑️" : "💾"
|
|
110
|
+
|
|
111
|
+
const batchInfo =
|
|
112
|
+
this.batch instanceof MemoryStateBatch
|
|
113
|
+
? this.batch.traceEntry
|
|
114
|
+
? `${md.code(this.batch.id.toString())}, written at ${linkTraceEntry(this.batch.traceEntry)}`
|
|
115
|
+
: md.code(this.batch.id.toString())
|
|
116
|
+
: undefined
|
|
117
|
+
|
|
118
|
+
return renderTraceEntry({
|
|
119
|
+
icon,
|
|
120
|
+
title: this.operation,
|
|
121
|
+
fields: {
|
|
122
|
+
collection: {
|
|
123
|
+
value: md.code(this.collection),
|
|
124
|
+
raw: true,
|
|
125
|
+
},
|
|
126
|
+
batchId: {
|
|
127
|
+
value: batchInfo,
|
|
128
|
+
raw: true,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
body: formatKeyValuePairs(this.entries),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class TestStateRepository<TSchema extends z.ZodType> {
|
|
137
|
+
private readonly collectionName: Promise<string>
|
|
138
|
+
|
|
139
|
+
constructor(
|
|
140
|
+
collection: StateCollection,
|
|
141
|
+
private readonly repository: StateRepository<TSchema>,
|
|
142
|
+
private readonly tracer: TestTracer,
|
|
143
|
+
) {
|
|
144
|
+
this.collectionName = collection.namespace
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async get(key: string, snapshot?: StateSnapshot): Promise<z.infer<TSchema> | undefined> {
|
|
148
|
+
const result = await this.repository.get(key, snapshot)
|
|
149
|
+
|
|
150
|
+
const entry = new StateReadEntry(
|
|
151
|
+
this.tracer.nextEntryId(),
|
|
152
|
+
"get",
|
|
153
|
+
await this.collectionName,
|
|
154
|
+
[key],
|
|
155
|
+
result,
|
|
156
|
+
snapshot,
|
|
157
|
+
)
|
|
158
|
+
this.tracer.addEntry(entry)
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getMany(keys: string[], snapshot?: StateSnapshot): Promise<[string, z.infer<TSchema>][]> {
|
|
164
|
+
const result = await this.repository.getMany(keys, snapshot)
|
|
165
|
+
|
|
166
|
+
const entry = new StateReadEntry(
|
|
167
|
+
this.tracer.nextEntryId(),
|
|
168
|
+
"getMany",
|
|
169
|
+
await this.collectionName,
|
|
170
|
+
keys,
|
|
171
|
+
result,
|
|
172
|
+
snapshot,
|
|
173
|
+
)
|
|
174
|
+
this.tracer.addEntry(entry)
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async getManyRecord(
|
|
180
|
+
keys: string[],
|
|
181
|
+
snapshot?: StateSnapshot,
|
|
182
|
+
): Promise<Record<string, z.infer<TSchema>>> {
|
|
183
|
+
const result = await this.repository.getManyRecord(keys, snapshot)
|
|
184
|
+
|
|
185
|
+
const entry = new StateReadEntry(
|
|
186
|
+
this.tracer.nextEntryId(),
|
|
187
|
+
"getManyRecord",
|
|
188
|
+
await this.collectionName,
|
|
189
|
+
keys,
|
|
190
|
+
result,
|
|
191
|
+
snapshot,
|
|
192
|
+
)
|
|
193
|
+
this.tracer.addEntry(entry)
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async getManyItems(ids: string[], snapshot?: StateSnapshot): Promise<z.infer<TSchema>[]> {
|
|
199
|
+
const result = await this.repository.getManyItems(ids, snapshot)
|
|
200
|
+
|
|
201
|
+
const entry = new StateReadEntry(
|
|
202
|
+
this.tracer.nextEntryId(),
|
|
203
|
+
"getManyItems",
|
|
204
|
+
await this.collectionName,
|
|
205
|
+
ids,
|
|
206
|
+
result,
|
|
207
|
+
snapshot,
|
|
208
|
+
)
|
|
209
|
+
this.tracer.addEntry(entry)
|
|
210
|
+
|
|
211
|
+
return result
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getAll(snapshot?: StateSnapshot): Promise<Array<[string, z.infer<TSchema>]>> {
|
|
215
|
+
const result = await this.repository.getAll(snapshot)
|
|
216
|
+
|
|
217
|
+
const entry = new StateReadEntry(
|
|
218
|
+
this.tracer.nextEntryId(),
|
|
219
|
+
"getAll",
|
|
220
|
+
await this.collectionName,
|
|
221
|
+
undefined,
|
|
222
|
+
result,
|
|
223
|
+
snapshot,
|
|
224
|
+
)
|
|
225
|
+
this.tracer.addEntry(entry)
|
|
226
|
+
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async getAllRecord(snapshot?: StateSnapshot): Promise<Record<string, z.infer<TSchema>>> {
|
|
231
|
+
const result = await this.repository.getAllRecord(snapshot)
|
|
232
|
+
|
|
233
|
+
const entry = new StateReadEntry(
|
|
234
|
+
this.tracer.nextEntryId(),
|
|
235
|
+
"getAllRecord",
|
|
236
|
+
await this.collectionName,
|
|
237
|
+
undefined,
|
|
238
|
+
result,
|
|
239
|
+
snapshot,
|
|
240
|
+
)
|
|
241
|
+
this.tracer.addEntry(entry)
|
|
242
|
+
|
|
243
|
+
return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async getAllItems(snapshot?: StateSnapshot): Promise<z.infer<TSchema>[]> {
|
|
247
|
+
const result = await this.repository.getAllItems(snapshot)
|
|
248
|
+
|
|
249
|
+
const entry = new StateReadEntry(
|
|
250
|
+
this.tracer.nextEntryId(),
|
|
251
|
+
"getAllItems",
|
|
252
|
+
await this.collectionName,
|
|
253
|
+
undefined,
|
|
254
|
+
result,
|
|
255
|
+
snapshot,
|
|
256
|
+
)
|
|
257
|
+
this.tracer.addEntry(entry)
|
|
258
|
+
|
|
259
|
+
return result
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async put(key: string, value: z.infer<TSchema>, batch?: StateBatch): Promise<void> {
|
|
263
|
+
await this.repository.put(key, value, batch)
|
|
264
|
+
|
|
265
|
+
const entry = new StateWriteEntry(
|
|
266
|
+
this.tracer.nextEntryId(),
|
|
267
|
+
"put",
|
|
268
|
+
await this.collectionName,
|
|
269
|
+
[[key, value]],
|
|
270
|
+
batch,
|
|
271
|
+
)
|
|
272
|
+
this.tracer.addEntry(entry)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async putMany(entries: [string, z.infer<TSchema>][], batch?: StateBatch): Promise<void> {
|
|
276
|
+
await this.repository.putMany(entries, batch)
|
|
277
|
+
|
|
278
|
+
const entry = new StateWriteEntry(
|
|
279
|
+
this.tracer.nextEntryId(),
|
|
280
|
+
"putMany",
|
|
281
|
+
await this.collectionName,
|
|
282
|
+
entries,
|
|
283
|
+
batch,
|
|
284
|
+
)
|
|
285
|
+
this.tracer.addEntry(entry)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async putItem(item: z.infer<TSchema> & { id: string }, batch?: StateBatch): Promise<void> {
|
|
289
|
+
await this.repository.putItem(item, batch)
|
|
290
|
+
|
|
291
|
+
const entry = new StateWriteEntry(
|
|
292
|
+
this.tracer.nextEntryId(),
|
|
293
|
+
"putItem",
|
|
294
|
+
await this.collectionName,
|
|
295
|
+
[[item.id, item]],
|
|
296
|
+
batch,
|
|
297
|
+
)
|
|
298
|
+
this.tracer.addEntry(entry)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async putManyItems(
|
|
302
|
+
items: (z.infer<TSchema> & { id: string })[],
|
|
303
|
+
batch?: StateBatch,
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
await this.repository.putManyItems(items, batch)
|
|
306
|
+
|
|
307
|
+
const entries = items.map(item => [item.id, item] as [string, unknown])
|
|
308
|
+
const entry = new StateWriteEntry(
|
|
309
|
+
this.tracer.nextEntryId(),
|
|
310
|
+
"putManyItems",
|
|
311
|
+
await this.collectionName,
|
|
312
|
+
entries,
|
|
313
|
+
batch,
|
|
314
|
+
)
|
|
315
|
+
this.tracer.addEntry(entry)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async delete(key: string, batch?: StateBatch): Promise<void> {
|
|
319
|
+
await this.repository.delete(key, batch)
|
|
320
|
+
|
|
321
|
+
const entry = new StateWriteEntry(
|
|
322
|
+
this.tracer.nextEntryId(),
|
|
323
|
+
"delete",
|
|
324
|
+
await this.collectionName,
|
|
325
|
+
[[key, undefined]],
|
|
326
|
+
batch,
|
|
327
|
+
)
|
|
328
|
+
this.tracer.addEntry(entry)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async deleteMany(keys: string[], batch?: StateBatch): Promise<void> {
|
|
332
|
+
await this.repository.deleteMany(keys, batch)
|
|
333
|
+
|
|
334
|
+
const entry = new StateWriteEntry(
|
|
335
|
+
this.tracer.nextEntryId(),
|
|
336
|
+
"deleteMany",
|
|
337
|
+
await this.collectionName,
|
|
338
|
+
keys.map(key => [key, undefined]),
|
|
339
|
+
batch,
|
|
340
|
+
)
|
|
341
|
+
this.tracer.addEntry(entry)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async clear(): Promise<void> {
|
|
345
|
+
await this.repository.clear()
|
|
346
|
+
|
|
347
|
+
const entry = new StateWriteEntry(
|
|
348
|
+
this.tracer.nextEntryId(),
|
|
349
|
+
"clear",
|
|
350
|
+
await this.collectionName,
|
|
351
|
+
[],
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
this.tracer.addEntry(entry)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async query(
|
|
358
|
+
query: CollectionQuery,
|
|
359
|
+
snapshot?: StateSnapshot,
|
|
360
|
+
): Promise<CollectionQueryResult<[string, z.infer<TSchema>]>> {
|
|
361
|
+
const result = await this.repository.query(query, snapshot)
|
|
362
|
+
|
|
363
|
+
const entry = new StateReadEntry(
|
|
364
|
+
this.tracer.nextEntryId(),
|
|
365
|
+
"query",
|
|
366
|
+
await this.collectionName,
|
|
367
|
+
undefined,
|
|
368
|
+
result,
|
|
369
|
+
snapshot,
|
|
370
|
+
)
|
|
371
|
+
this.tracer.addEntry(entry)
|
|
372
|
+
|
|
373
|
+
return result
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async queryItems(
|
|
377
|
+
query: CollectionQuery,
|
|
378
|
+
snapshot?: StateSnapshot,
|
|
379
|
+
): Promise<CollectionQueryResult<z.infer<TSchema>>> {
|
|
380
|
+
const result = await this.repository.queryItems(query, snapshot)
|
|
381
|
+
|
|
382
|
+
const entry = new StateReadEntry(
|
|
383
|
+
this.tracer.nextEntryId(),
|
|
384
|
+
"queryItems",
|
|
385
|
+
await this.collectionName,
|
|
386
|
+
undefined,
|
|
387
|
+
result,
|
|
388
|
+
snapshot,
|
|
389
|
+
)
|
|
390
|
+
this.tracer.addEntry(entry)
|
|
391
|
+
|
|
392
|
+
return result
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export const stateFixtures: Fixtures<
|
|
397
|
+
{
|
|
398
|
+
projectUnlockBackend: ProjectUnlockBackend
|
|
399
|
+
hotStateManager: HotStateManager
|
|
400
|
+
stateManager: StateManager
|
|
401
|
+
},
|
|
402
|
+
TestBaseFixtures
|
|
403
|
+
> = {
|
|
404
|
+
// eslint-disable-next-line no-empty-pattern
|
|
405
|
+
projectUnlockBackend: async ({}, use) => {
|
|
406
|
+
const projectUnlockBackend = new MemoryProjectUnlockBackend()
|
|
407
|
+
|
|
408
|
+
// always "unlock" the "test" project
|
|
409
|
+
await projectUnlockBackend.unlockProject("test", "test", "test")
|
|
410
|
+
|
|
411
|
+
await use(projectUnlockBackend)
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
// eslint-disable-next-line no-empty-pattern
|
|
415
|
+
hotStateManager: async ({}, use) => {
|
|
416
|
+
const hotStateManager = createTestHotStateManager()
|
|
417
|
+
|
|
418
|
+
await use(hotStateManager)
|
|
419
|
+
},
|
|
420
|
+
stateManager: async ({ tracer, clock, projectUnlockBackend }, use) => {
|
|
421
|
+
const backend = new MemoryStateBackend(tracer)
|
|
422
|
+
|
|
423
|
+
const createRepository = <TSchema extends z.ZodType>(
|
|
424
|
+
collection: StateCollection,
|
|
425
|
+
schema: TSchema,
|
|
426
|
+
encryptionBackend: EncryptionBackend,
|
|
427
|
+
keyObfuscationBackend: KeyObfuscationBackend,
|
|
428
|
+
) => {
|
|
429
|
+
const baseRepository = new StateRepository(
|
|
430
|
+
collection,
|
|
431
|
+
schema,
|
|
432
|
+
encryptionBackend,
|
|
433
|
+
keyObfuscationBackend,
|
|
434
|
+
clock,
|
|
435
|
+
tracer.logger,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
return new TestStateRepository(
|
|
439
|
+
collection,
|
|
440
|
+
baseRepository,
|
|
441
|
+
tracer,
|
|
442
|
+
) as unknown as StateRepository<TSchema>
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const stateManager = new StateManager(
|
|
446
|
+
PassThroughEncryptionBackend.instance,
|
|
447
|
+
PassThroughKeyObfuscationBackend.instance,
|
|
448
|
+
backend,
|
|
449
|
+
projectUnlockBackend,
|
|
450
|
+
clock,
|
|
451
|
+
tracer.logger,
|
|
452
|
+
createRepository,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
await use(stateManager)
|
|
456
|
+
},
|
|
457
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
|
|
3
|
+
export const unlockedProjectInfo = z.object({
|
|
4
|
+
/**
|
|
5
|
+
* The base64-encoded master key for the project.
|
|
6
|
+
*/
|
|
7
|
+
masterKey: z.base64(),
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The namespace for the project, used to obfuscate keys.
|
|
11
|
+
*/
|
|
12
|
+
namespace: z.string(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export type UnlockedProjectInfo = z.infer<typeof unlockedProjectInfo>
|
|
16
|
+
|
|
17
|
+
export interface ProjectUnlockBackend {
|
|
18
|
+
/**
|
|
19
|
+
* Checks if the project is unlocked.
|
|
20
|
+
*
|
|
21
|
+
* @param projectId The ID of the project to check.
|
|
22
|
+
* @return A promise that resolves to true if the project is unlocked, false otherwise.
|
|
23
|
+
*/
|
|
24
|
+
checkProjectUnlocked(projectId: string): Promise<boolean>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Gets the unlocked project information.
|
|
28
|
+
*
|
|
29
|
+
* @param projectId The ID of the project to get the information for.
|
|
30
|
+
* @return A promise that resolves to the unlocked project information, or null if the project is not unlocked.
|
|
31
|
+
*/
|
|
32
|
+
getUnlockedProjectInfo(projectId: string): Promise<UnlockedProjectInfo | null>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Unlocks the project with the given master key and namespace.
|
|
36
|
+
*
|
|
37
|
+
* @param projectId The ID of the project to unlock.
|
|
38
|
+
* @param masterKey The base64-encoded master key for the project.
|
|
39
|
+
* @param namespace The namespace for the project, used to obfuscate keys.
|
|
40
|
+
*/
|
|
41
|
+
unlockProject(projectId: string, masterKey: string, namespace: string): Promise<void>
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Locks the project, removing its unlocked state.
|
|
45
|
+
*
|
|
46
|
+
* @param projectId The ID of the project to lock.
|
|
47
|
+
*/
|
|
48
|
+
lockProject(projectId: string): Promise<void>
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ProjectUnlockBackend, UnlockedProjectInfo } from "./abstractions"
|
|
2
|
+
|
|
3
|
+
export class MemoryProjectUnlockBackend implements ProjectUnlockBackend {
|
|
4
|
+
private readonly projects = new Map<string, UnlockedProjectInfo>()
|
|
5
|
+
|
|
6
|
+
checkProjectUnlocked(projectId: string): Promise<boolean> {
|
|
7
|
+
return Promise.resolve(this.projects.has(projectId))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getUnlockedProjectInfo(projectId: string): Promise<UnlockedProjectInfo | null> {
|
|
11
|
+
const info = this.projects.get(projectId)
|
|
12
|
+
|
|
13
|
+
return Promise.resolve(info || null)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
lockProject(projectId: string): Promise<void> {
|
|
17
|
+
this.projects.delete(projectId)
|
|
18
|
+
|
|
19
|
+
return Promise.resolve()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
unlockProject(projectId: string, masterKey: string, namespace: string): Promise<void> {
|
|
23
|
+
const info: UnlockedProjectInfo = {
|
|
24
|
+
masterKey,
|
|
25
|
+
namespace,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.projects.set(projectId, info)
|
|
29
|
+
|
|
30
|
+
return Promise.resolve()
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/worker/manager.ts
CHANGED
|
@@ -30,7 +30,15 @@ export class WorkerManager {
|
|
|
30
30
|
)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
private readonly workerAbortControllers = new Map<string, AbortController>()
|
|
34
|
+
|
|
33
35
|
async startWorker(projectId: string, workerId: string, restart = false): Promise<void> {
|
|
36
|
+
if (this.workerAbortControllers.get(workerId)?.signal.aborted) {
|
|
37
|
+
// if the worker was aborted due to worker deletion, clear logs and exit
|
|
38
|
+
await this.stateManager.getWorkerLogRepository(projectId, workerId).clear()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
await this.lockManager.acquire(["worker", workerId], async () => {
|
|
35
43
|
const worker = await this.stateManager.getWorkerRepository(projectId).get(workerId)
|
|
36
44
|
if (!worker) {
|
|
@@ -80,6 +88,9 @@ export class WorkerManager {
|
|
|
80
88
|
logBatcher.call(log)
|
|
81
89
|
})
|
|
82
90
|
|
|
91
|
+
const abortController = new AbortController()
|
|
92
|
+
this.workerAbortControllers.set(workerId, abortController)
|
|
93
|
+
|
|
83
94
|
void this.workerBackend
|
|
84
95
|
.run({
|
|
85
96
|
projectId,
|
|
@@ -87,6 +98,7 @@ export class WorkerManager {
|
|
|
87
98
|
apiPath: this.config.HIGHSTATE_WORKER_API_PATH,
|
|
88
99
|
apiKey: apiKey.token,
|
|
89
100
|
stdout,
|
|
101
|
+
signal: abortController.signal,
|
|
90
102
|
})
|
|
91
103
|
// regardless the exit reason, we want to restart the worker if it has remaining attempts
|
|
92
104
|
.finally(() => void this.startWorker(projectId, worker.id, true))
|
|
@@ -122,6 +134,22 @@ export class WorkerManager {
|
|
|
122
134
|
})
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
stopWorker(projectId: string, workerId: string): void {
|
|
138
|
+
const abortController = this.workerAbortControllers.get(workerId)
|
|
139
|
+
if (!abortController) {
|
|
140
|
+
this.logger.warn(
|
|
141
|
+
{ projectId, workerId },
|
|
142
|
+
`worker "%s" is not running in project "%s", cannot stop`,
|
|
143
|
+
workerId,
|
|
144
|
+
projectId,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
abortController.abort()
|
|
151
|
+
}
|
|
152
|
+
|
|
125
153
|
private async launchActiveWorkers(projectId: string): Promise<void> {
|
|
126
154
|
const workers = await this.stateManager.getWorkerRepository(projectId).getAllItems()
|
|
127
155
|
|