@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.
Files changed (125) hide show
  1. package/dist/chunk-NAAIDR4U.js +8499 -0
  2. package/dist/chunk-NAAIDR4U.js.map +1 -0
  3. package/dist/chunk-OU5OQBLB.js +74 -0
  4. package/dist/chunk-OU5OQBLB.js.map +1 -0
  5. package/dist/{chunk-WHALQHEZ.js → chunk-Y7DXREVO.js} +502 -774
  6. package/dist/chunk-Y7DXREVO.js.map +1 -0
  7. package/dist/highstate.manifest.json +4 -4
  8. package/dist/index.js +2979 -2233
  9. package/dist/index.js.map +1 -1
  10. package/dist/library/package-resolution-worker.js +7 -5
  11. package/dist/library/package-resolution-worker.js.map +1 -1
  12. package/dist/library/worker/main.js +40 -41
  13. package/dist/library/worker/main.js.map +1 -1
  14. package/dist/magic-string.es-5ABAC4JN.js +1292 -0
  15. package/dist/magic-string.es-5ABAC4JN.js.map +1 -0
  16. package/dist/shared/index.js +3 -216
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +9 -6
  19. package/src/artifact/encryption.ts +47 -7
  20. package/src/artifact/factory.ts +2 -2
  21. package/src/artifact/local.ts +2 -6
  22. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
  23. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
  24. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
  25. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
  26. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
  27. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
  28. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
  29. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
  30. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
  31. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
  32. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
  33. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
  34. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
  35. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
  36. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
  37. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
  38. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
  39. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
  40. package/src/business/artifact.ts +2 -1
  41. package/src/business/index.ts +1 -0
  42. package/src/business/instance-lock.ts +3 -2
  43. package/src/business/instance-state.ts +202 -60
  44. package/src/business/project-unlock.ts +41 -23
  45. package/src/business/project.ts +299 -0
  46. package/src/business/secret.test.ts +178 -0
  47. package/src/business/secret.ts +139 -45
  48. package/src/business/worker.test.ts +614 -0
  49. package/src/business/worker.ts +289 -52
  50. package/src/common/clock.ts +18 -0
  51. package/src/common/index.ts +3 -0
  52. package/src/common/random.ts +68 -0
  53. package/src/common/test/index.ts +2 -0
  54. package/src/common/test/render.ts +98 -0
  55. package/src/common/test/tracer.ts +359 -0
  56. package/src/config.ts +5 -1
  57. package/src/hotstate/manager.ts +8 -8
  58. package/src/hotstate/validation.ts +0 -1
  59. package/src/library/abstractions.ts +20 -11
  60. package/src/library/local.ts +6 -13
  61. package/src/library/worker/evaluator.ts +30 -34
  62. package/src/library/worker/loader.lite.ts +13 -0
  63. package/src/library/worker/main.ts +8 -8
  64. package/src/library/worker/protocol.ts +0 -11
  65. package/src/lock/index.ts +1 -0
  66. package/src/lock/manager.ts +17 -2
  67. package/src/lock/test.ts +108 -0
  68. package/src/orchestrator/manager.ts +17 -36
  69. package/src/orchestrator/operation-workset.ts +34 -37
  70. package/src/orchestrator/operation.ts +129 -74
  71. package/src/project/abstractions.ts +27 -51
  72. package/src/project/evaluation.ts +248 -0
  73. package/src/project/index.ts +1 -1
  74. package/src/project/local.ts +75 -127
  75. package/src/pubsub/manager.ts +21 -13
  76. package/src/runner/abstractions.ts +29 -9
  77. package/src/runner/artifact-env.ts +3 -3
  78. package/src/runner/local.ts +29 -19
  79. package/src/runner/pulumi.ts +4 -1
  80. package/src/services.ts +77 -24
  81. package/src/shared/models/backend/library.ts +4 -4
  82. package/src/shared/models/backend/project.ts +25 -6
  83. package/src/shared/models/backend/unlock-method.ts +1 -1
  84. package/src/shared/models/base.ts +1 -84
  85. package/src/shared/models/project/api-key.ts +5 -2
  86. package/src/shared/models/project/artifact.ts +3 -33
  87. package/src/shared/models/project/index.ts +1 -2
  88. package/src/shared/models/project/lock.ts +3 -3
  89. package/src/shared/models/project/model.ts +14 -0
  90. package/src/shared/models/project/operation.ts +3 -3
  91. package/src/shared/models/project/page.ts +3 -3
  92. package/src/shared/models/project/secret.ts +4 -18
  93. package/src/shared/models/project/service-account.ts +2 -2
  94. package/src/shared/models/project/state.ts +32 -15
  95. package/src/shared/models/project/terminal.ts +4 -5
  96. package/src/shared/models/project/trigger.ts +1 -1
  97. package/src/shared/models/project/unlock-method.ts +9 -2
  98. package/src/shared/models/project/worker.ts +9 -7
  99. package/src/shared/resolvers/graph-resolver.ts +41 -26
  100. package/src/shared/resolvers/input.ts +47 -5
  101. package/src/shared/resolvers/validation.ts +23 -7
  102. package/src/shared/utils/args.ts +25 -0
  103. package/src/shared/utils/index.ts +1 -0
  104. package/src/state/abstractions.ts +98 -259
  105. package/src/state/encryption.ts +39 -0
  106. package/src/state/index.ts +1 -0
  107. package/src/state/local/backend.ts +29 -222
  108. package/src/state/local/collection.ts +105 -86
  109. package/src/state/manager.ts +358 -287
  110. package/src/state/memory/backend.ts +70 -0
  111. package/src/state/memory/collection.ts +270 -0
  112. package/src/state/memory/index.ts +2 -0
  113. package/src/state/repository/repository.index.ts +1 -1
  114. package/src/state/repository/repository.ts +71 -22
  115. package/src/state/test.ts +457 -0
  116. package/src/unlock/abstractions.ts +49 -0
  117. package/src/unlock/index.ts +2 -0
  118. package/src/unlock/memory.ts +32 -0
  119. package/src/worker/manager.ts +28 -0
  120. package/dist/chunk-RCB4AFGD.js +0 -159
  121. package/dist/chunk-RCB4AFGD.js.map +0 -1
  122. package/dist/chunk-WHALQHEZ.js.map +0 -1
  123. package/src/project/manager.ts +0 -574
  124. package/src/shared/models/project/component.ts +0 -45
  125. 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,2 @@
1
+ export * from "./abstractions"
2
+ export * from "./memory"
@@ -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
+ }
@@ -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