@highstate/backend 0.9.14 → 0.9.16
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-RCB4AFGD.js +159 -0
- package/dist/chunk-RCB4AFGD.js.map +1 -0
- package/dist/chunk-WHALQHEZ.js +2017 -0
- package/dist/chunk-WHALQHEZ.js.map +1 -0
- package/dist/highstate.manifest.json +3 -3
- package/dist/index.js +6146 -2174
- package/dist/index.js.map +1 -1
- package/dist/library/worker/main.js +51 -159
- package/dist/library/worker/main.js.map +1 -1
- package/dist/shared/index.js +159 -43
- package/package.json +25 -7
- package/src/artifact/abstractions.ts +46 -0
- package/src/artifact/encryption.ts +69 -0
- package/src/artifact/factory.ts +36 -0
- package/src/artifact/index.ts +3 -0
- package/src/artifact/local.ts +142 -0
- package/src/business/api-key.ts +65 -0
- package/src/business/artifact.ts +288 -0
- package/src/business/backend-unlock.ts +10 -0
- package/src/business/index.ts +9 -0
- package/src/business/instance-lock.ts +124 -0
- package/src/business/instance-state.ts +292 -0
- package/src/business/operation.ts +251 -0
- package/src/business/project-unlock.ts +242 -0
- package/src/business/secret.ts +187 -0
- package/src/business/worker.ts +161 -0
- package/src/common/index.ts +2 -1
- package/src/common/performance.ts +44 -0
- package/src/common/tree.ts +33 -0
- package/src/common/utils.ts +40 -1
- package/src/config.ts +14 -10
- package/src/hotstate/abstractions.ts +48 -0
- package/src/hotstate/factory.ts +17 -0
- package/src/{secret → hotstate}/index.ts +1 -0
- package/src/hotstate/manager.ts +192 -0
- package/src/hotstate/memory.ts +100 -0
- package/src/hotstate/validation.ts +101 -0
- package/src/index.ts +2 -1
- package/src/library/abstractions.ts +10 -23
- package/src/library/factory.ts +2 -2
- package/src/library/local.ts +89 -102
- package/src/library/worker/evaluator.ts +14 -47
- package/src/library/worker/loader.lite.ts +41 -0
- package/src/library/worker/main.ts +14 -65
- package/src/library/worker/protocol.ts +8 -24
- package/src/lock/abstractions.ts +6 -0
- package/src/lock/factory.ts +15 -0
- package/src/{workspace → lock}/index.ts +1 -0
- package/src/lock/manager.ts +82 -0
- package/src/lock/memory.ts +19 -0
- package/src/orchestrator/manager.ts +131 -82
- package/src/orchestrator/operation-workset.ts +188 -77
- package/src/orchestrator/operation.ts +975 -284
- package/src/project/abstractions.ts +20 -7
- package/src/project/factory.ts +1 -1
- package/src/project/index.ts +0 -1
- package/src/project/local.ts +73 -17
- package/src/project/manager.ts +272 -131
- package/src/pubsub/abstractions.ts +13 -0
- package/src/pubsub/factory.ts +19 -0
- package/src/pubsub/index.ts +3 -0
- package/src/pubsub/local.ts +36 -0
- package/src/pubsub/manager.ts +100 -0
- package/src/pubsub/validation.ts +33 -0
- package/src/runner/abstractions.ts +135 -68
- package/src/runner/artifact-env.ts +160 -0
- package/src/runner/factory.ts +20 -5
- package/src/runner/force-abort.ts +117 -0
- package/src/runner/local.ts +281 -372
- package/src/{common → runner}/pulumi.ts +86 -37
- package/src/services.ts +193 -35
- package/src/shared/index.ts +3 -11
- package/src/shared/models/backend/index.ts +3 -0
- package/src/shared/models/backend/project.ts +63 -0
- package/src/shared/models/backend/unlock-method.ts +20 -0
- package/src/shared/models/base.ts +151 -0
- package/src/shared/models/errors.ts +5 -0
- package/src/shared/models/index.ts +4 -0
- package/src/shared/models/project/api-key.ts +62 -0
- package/src/shared/models/project/artifact.ts +113 -0
- package/src/shared/models/project/component.ts +45 -0
- package/src/shared/models/project/index.ts +14 -0
- package/src/shared/{project.ts → models/project/instance.ts} +12 -0
- package/src/shared/models/project/lock.ts +91 -0
- package/src/shared/{operation.ts → models/project/operation.ts} +43 -8
- package/src/shared/models/project/page.ts +57 -0
- package/src/shared/models/project/secret.ts +112 -0
- package/src/shared/models/project/service-account.ts +22 -0
- package/src/shared/models/project/state.ts +432 -0
- package/src/shared/models/project/terminal.ts +99 -0
- package/src/shared/models/project/trigger.ts +56 -0
- package/src/shared/models/project/unlock-method.ts +31 -0
- package/src/shared/models/project/worker.ts +105 -0
- package/src/shared/resolvers/graph-resolver.ts +74 -13
- package/src/shared/resolvers/index.ts +5 -0
- package/src/shared/resolvers/input-hash.ts +53 -15
- package/src/shared/resolvers/input.ts +1 -9
- package/src/shared/resolvers/registry.ts +7 -2
- package/src/shared/resolvers/state.ts +12 -0
- package/src/shared/resolvers/validation.ts +61 -20
- package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
- package/src/shared/utils/hash.ts +6 -0
- package/src/shared/utils/index.ts +3 -0
- package/src/shared/utils/promise-tracker.ts +23 -0
- package/src/state/abstractions.ts +330 -101
- package/src/state/encryption.ts +59 -0
- package/src/state/factory.ts +3 -5
- package/src/state/index.ts +3 -0
- package/src/state/keyring.ts +22 -0
- package/src/state/local/backend.ts +299 -0
- package/src/state/local/collection.ts +342 -0
- package/src/state/local/index.ts +2 -0
- package/src/state/manager.ts +804 -18
- package/src/state/repository/index.ts +2 -0
- package/src/state/repository/repository.index.ts +193 -0
- package/src/state/repository/repository.ts +458 -0
- package/src/terminal/{shared.ts → abstractions.ts} +3 -3
- package/src/terminal/docker.ts +18 -14
- package/src/terminal/factory.ts +3 -3
- package/src/terminal/index.ts +1 -1
- package/src/terminal/manager.ts +134 -80
- package/src/terminal/run.sh.ts +22 -10
- package/src/worker/abstractions.ts +42 -0
- package/src/worker/docker.ts +83 -0
- package/src/worker/factory.ts +20 -0
- package/src/worker/index.ts +3 -0
- package/src/worker/manager.ts +139 -0
- package/dist/chunk-C2TJAQAD.js +0 -937
- package/dist/chunk-C2TJAQAD.js.map +0 -1
- package/dist/chunk-WXDYCRTT.js +0 -234
- package/dist/chunk-WXDYCRTT.js.map +0 -1
- package/src/library/worker/loader.ts +0 -114
- package/src/preferences/shared.ts +0 -1
- package/src/project/lock.ts +0 -39
- package/src/secret/abstractions.ts +0 -59
- package/src/secret/factory.ts +0 -22
- package/src/secret/local.ts +0 -152
- package/src/shared/state.ts +0 -270
- package/src/shared/terminal.ts +0 -13
- package/src/state/local.ts +0 -612
- package/src/workspace/abstractions.ts +0 -41
- package/src/workspace/factory.ts +0 -14
- package/src/workspace/local.ts +0 -54
- /package/src/shared/{library.ts → models/backend/library.ts} +0 -0
package/src/state/factory.ts
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import type { StateBackend } from "./abstractions"
|
|
2
2
|
import type { Logger } from "pino"
|
|
3
|
-
import type { LocalPulumiHost } from "../common"
|
|
4
3
|
import { z } from "zod"
|
|
5
4
|
import { LocalStateBackend, localStateBackendConfig } from "./local"
|
|
6
5
|
|
|
7
6
|
export const stateBackendConfig = z.object({
|
|
8
|
-
|
|
7
|
+
HIGHSTATE_STATE_BACKEND_TYPE: z.enum(["local"]).default("local"),
|
|
9
8
|
...localStateBackendConfig.shape,
|
|
10
9
|
})
|
|
11
10
|
|
|
12
11
|
export function createStateBackend(
|
|
13
12
|
config: z.infer<typeof stateBackendConfig>,
|
|
14
|
-
localPulumiHost: LocalPulumiHost,
|
|
15
13
|
logger: Logger,
|
|
16
14
|
): Promise<StateBackend> {
|
|
17
|
-
switch (config.
|
|
15
|
+
switch (config.HIGHSTATE_STATE_BACKEND_TYPE) {
|
|
18
16
|
case "local": {
|
|
19
|
-
return LocalStateBackend.create(config,
|
|
17
|
+
return LocalStateBackend.create(config, logger)
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
20
|
}
|
package/src/state/index.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Logger } from "pino"
|
|
2
|
+
import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring"
|
|
3
|
+
import { generateIdentity } from "age-encryption"
|
|
4
|
+
|
|
5
|
+
const serviceName = "io.highstate.backend"
|
|
6
|
+
const accountName = "identity"
|
|
7
|
+
|
|
8
|
+
export async function getOrCreateBackendIdentity(logger: Logger): Promise<string> {
|
|
9
|
+
const credentials = await findCredentialsAsync(serviceName)
|
|
10
|
+
const entry = credentials.find(entry => entry.account === accountName)
|
|
11
|
+
|
|
12
|
+
if (entry) {
|
|
13
|
+
logger.debug("found existing backend identity in keyring")
|
|
14
|
+
return entry.password
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const newIdentity = await generateIdentity()
|
|
18
|
+
await new AsyncEntry(serviceName, accountName).setPassword(newIdentity)
|
|
19
|
+
|
|
20
|
+
logger.info("created new backend identity and stored it in the OS keyring")
|
|
21
|
+
return newIdentity
|
|
22
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { AbstractLevel } from "abstract-level"
|
|
2
|
+
import type { Logger } from "pino"
|
|
3
|
+
import { resolve } from "node:path"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import {
|
|
6
|
+
CollectionBackend,
|
|
7
|
+
type StateBackend,
|
|
8
|
+
type StateBatch,
|
|
9
|
+
type StateCollection,
|
|
10
|
+
type StateSnapshot,
|
|
11
|
+
} from "../abstractions"
|
|
12
|
+
import { LocalStateCollection } from "./collection"
|
|
13
|
+
|
|
14
|
+
export const localStateBackendConfig = z.object({
|
|
15
|
+
HIGHSTATE_STATE_BACKEND_LOCAL_DIR: z.string().optional(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A state backend that stores the state in a local LevelDB database.
|
|
20
|
+
*
|
|
21
|
+
* The default state location is `~/.highstate/state.db`.
|
|
22
|
+
*
|
|
23
|
+
* Storage Structure:
|
|
24
|
+
*
|
|
25
|
+
* ## System Collections
|
|
26
|
+
* - `backend/id` - static id for the backend
|
|
27
|
+
* - `backend/encryptedMasterKey` - encrypted master key for the backend
|
|
28
|
+
* - `backend/projects/{projectId}` - project metadata
|
|
29
|
+
* - `backend/projectMasterKeys/{projectId}` - encrypted master keys for data encryption/decryption
|
|
30
|
+
* - `backend/projectHashNamespaces/{projectId}` - UUID namespaces for ID hashing per project
|
|
31
|
+
* - `backend/projectUnlockSuites/{projectId}` - unlock suites for project unlocking
|
|
32
|
+
*
|
|
33
|
+
* ## Project Collections
|
|
34
|
+
* - `projects/{projectId}/instanceStates/{instanceId}` - instance states
|
|
35
|
+
* - `projects/{projectId}/instanceLocks/{instanceId}` - instance locks for operation coordination
|
|
36
|
+
* - `projects/{projectId}/terminalSessions/{sessionId}` - terminal sessions (sessionId is UUIDv7)
|
|
37
|
+
* - `projects/{projectId}/activeTerminalSessions` - index of active terminal sessions
|
|
38
|
+
* - `projects/{projectId}/instances/{instanceId}/terminalSessions/{sessionId}` - terminal session index per instance
|
|
39
|
+
* - `projects/{projectId}/terminals/{terminalId}` - project terminals
|
|
40
|
+
* - `projects/{projectId}/terminalSpecs/{terminalId}` - terminal spec data
|
|
41
|
+
* - `projects/{projectId}/pages/{pageId}` - project pages
|
|
42
|
+
* - `projects/{projectId}/pageContents/{pageId}` - page content data
|
|
43
|
+
* - `projects/{projectId}/artifacts/{artifactId}` - project artifacts
|
|
44
|
+
* - `projects/{projectId}/artifactHashIndex/{hash}` - artifact hash to ID mapping
|
|
45
|
+
* - `projects/{projectId}/triggers/{triggerId}` - instance triggers
|
|
46
|
+
* - `projects/{projectId}/operations/{operationId}` - project operations
|
|
47
|
+
* - `projects/{projectId}/activeOperations/{operationId}` - index of active operations
|
|
48
|
+
* - `projects/{projectId}/unlockMethods/{methodId}` - unlock methods for the project
|
|
49
|
+
* - `projects/{projectId}/compositeInstances/{instanceId}` - evaluated composite instances
|
|
50
|
+
* - `projects/{projectId}/secrets/{secretId}` - project secrets
|
|
51
|
+
* - `projects/{projectId}/secretContents/{secretId}` - secret content data
|
|
52
|
+
* - `projects/{projectId}/serviceAccounts/{serviceAccountId}` - project service accounts
|
|
53
|
+
* - `projects/{projectId}/apiKeys/{apiKeyId}` - project API keys
|
|
54
|
+
* - `projects/{projectId}/apiKeyTokenIndex/{tokenHash}` - maps API key token hashes to API key IDs
|
|
55
|
+
* - `projects/{projectId}/workers/{workerId}` - project workers
|
|
56
|
+
* - `projects/{projectId}/workerRegistrations/{registrationId}` - worker registrations
|
|
57
|
+
* - `projects/{projectId}/workers/{workerId}/registrations/{registrationId}` - index of registrations per worker
|
|
58
|
+
* - `projects/{projectId}/workers/{workerId}/logs/{logId}` - worker logs (logId is UUIDv7)
|
|
59
|
+
*
|
|
60
|
+
* ## Operation Collections
|
|
61
|
+
* - `projects/{projectId}/operations/{operationId}/logs/{logId}` - operation logs (logId is UUIDv7)
|
|
62
|
+
* - `projects/{projectId}/operations/{operationId}/instances/{instanceId}/logs/{logId}` - instance-specific log index
|
|
63
|
+
*
|
|
64
|
+
* ## Terminal Collections
|
|
65
|
+
* - `projects/{projectId}/terminalSessions/{sessionId}/lines/{lineId}` - terminal session history (lineId is UUIDv7)
|
|
66
|
+
*
|
|
67
|
+
* ## Collection Metadata
|
|
68
|
+
* Each collection sublevel contains a special `_count` key that tracks the total number of documents
|
|
69
|
+
* in that collection for efficient pagination and querying without full iteration.
|
|
70
|
+
*/
|
|
71
|
+
export class LocalStateBackend implements StateBackend {
|
|
72
|
+
constructor(private readonly db: AbstractLevel<string, unknown>) {}
|
|
73
|
+
|
|
74
|
+
async getEncryptedBackendMasterKey(): Promise<string | null> {
|
|
75
|
+
const encryptedMasterKey = await this.db.get("backend/encryptedMasterKey")
|
|
76
|
+
|
|
77
|
+
return encryptedMasterKey ?? null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async setEncryptedBackendMasterKey(encryptedMasterKey: string): Promise<void> {
|
|
81
|
+
await this.db.put("backend/encryptedMasterKey", encryptedMasterKey)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getStaticBackendId(): Promise<string | null> {
|
|
85
|
+
const ns = await this.db.get("backend/id")
|
|
86
|
+
|
|
87
|
+
return ns ?? null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async setStaticBackendId(namespace: string): Promise<void> {
|
|
91
|
+
await this.db.put("backend/id", namespace)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
createStateBatch(): StateBatch {
|
|
95
|
+
const batch = this.db.batch()
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
[CollectionBackend]: batch,
|
|
99
|
+
[Symbol.asyncDispose]: () => batch.close(),
|
|
100
|
+
write: () => batch.write(),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
createStateSnapshot(): StateSnapshot {
|
|
105
|
+
const snapshot = this.db.snapshot()
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
[CollectionBackend]: snapshot,
|
|
109
|
+
[Symbol.asyncDispose]: () => snapshot.close(),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
createProjectCollection(): StateCollection {
|
|
114
|
+
return LocalStateCollection.create(this.db, "backend/projects")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
createProjectMasterKeyCollection(): StateCollection {
|
|
118
|
+
return LocalStateCollection.create(this.db, "backend/projectMasterKeys")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
createProjectIdHashNamespaceCollection(): StateCollection {
|
|
122
|
+
return LocalStateCollection.create(this.db, "backend/projectHashNamespaces")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
createProjectUnlockSuiteCollection(): StateCollection {
|
|
126
|
+
return LocalStateCollection.create(this.db, "backend/projectUnlockSuites")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
createTerminalSessionCollection(projectId: string): StateCollection {
|
|
130
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/terminalSessions`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
createActiveTerminalSessionIndexCollection(projectId: string): StateCollection {
|
|
134
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/activeTerminalSessions`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
createInstanceTerminalSessionIndexCollection(
|
|
138
|
+
projectId: string,
|
|
139
|
+
instanceId: string,
|
|
140
|
+
): StateCollection {
|
|
141
|
+
return LocalStateCollection.create(
|
|
142
|
+
this.db,
|
|
143
|
+
`projects/${projectId}/instances/${instanceId}/terminalSessions`,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
createTerminalCollection(projectId: string): StateCollection {
|
|
148
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/terminals`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
createTerminalSpecCollection(projectId: string): StateCollection {
|
|
152
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/terminalSpecs`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
createServiceAccountCollection(projectId: string): StateCollection {
|
|
156
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/serviceAccounts`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
createApiKeyCollection(projectId: string): StateCollection {
|
|
160
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/apiKeys`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
createApiKeyTokenIndexCollection(projectId: string): StateCollection {
|
|
164
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/apiKeyTokenIndex`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
createWorkerCollection(projectId: string): StateCollection {
|
|
168
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/workers`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
createWorkerRegistrationCollection(projectId: string): StateCollection {
|
|
172
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/workerRegistrations`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
createWorkerRegistrationIndexCollection(projectId: string, workerId: string): StateCollection {
|
|
176
|
+
return LocalStateCollection.create(
|
|
177
|
+
this.db,
|
|
178
|
+
`projects/${projectId}/workers/${workerId}/registrations`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
createWorkerLogCollection(projectId: string, workerId: string): StateCollection {
|
|
183
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/workers/${workerId}/logs`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
createPageCollection(projectId: string): StateCollection {
|
|
187
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/pages`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
createPageContentCollection(projectId: string): StateCollection {
|
|
191
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/pageContents`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
createArtifactCollection(projectId: string): StateCollection {
|
|
195
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/artifacts`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
createArtifactHashIndexCollection(projectId: string): StateCollection {
|
|
199
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/artifactHashIndex`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
createTriggerCollection(projectId: string): StateCollection {
|
|
203
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/triggers`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
createUnlockMethodCollection(projectId: string): StateCollection {
|
|
207
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/unlockMethods`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
createOperationCollection(projectId: string): StateCollection {
|
|
211
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/operations`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
createActiveOperationIndexCollection(projectId: string): StateCollection {
|
|
215
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/activeOperations`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
createSecretCollection(projectId: string): StateCollection {
|
|
219
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/secrets`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
createSecretContentCollection(projectId: string): StateCollection {
|
|
223
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/secretContents`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
createSecretIndexCollection(projectId: string): StateCollection {
|
|
227
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/secretIndex`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
createOperationLogCollection(projectId: string, operationId: string): StateCollection {
|
|
231
|
+
return LocalStateCollection.create(
|
|
232
|
+
this.db,
|
|
233
|
+
`projects/${projectId}/operations/${operationId}/logs`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
createInstanceLogIndexCollection(
|
|
238
|
+
projectId: string,
|
|
239
|
+
operationId: string,
|
|
240
|
+
instanceId: string,
|
|
241
|
+
): StateCollection {
|
|
242
|
+
return LocalStateCollection.create(
|
|
243
|
+
this.db,
|
|
244
|
+
`projects/${projectId}/operations/${operationId}/instances/${instanceId}/logs`,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
createTerminalSessionLineCollection(projectId: string, sessionId: string): StateCollection {
|
|
249
|
+
return LocalStateCollection.create(
|
|
250
|
+
this.db,
|
|
251
|
+
`projects/${projectId}/terminalSessions/${sessionId}/lines`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
createCompositeInstanceCollection(projectId: string): StateCollection {
|
|
256
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/compositeInstances`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
createInstanceStateCollection(projectId: string): StateCollection {
|
|
260
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/instanceStates`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
createInstanceLockCollection(projectId: string): StateCollection {
|
|
264
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/instanceLocks`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
createUserLayoutCollection(): StateCollection {
|
|
268
|
+
return LocalStateCollection.create(this.db, "backend/userLayouts")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
createProjectViewportCollection(projectId: string): StateCollection {
|
|
272
|
+
return LocalStateCollection.create(this.db, `projects/${projectId}/viewports`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
createInstanceViewportCollection(projectId: string, instanceId: string): StateCollection {
|
|
276
|
+
return LocalStateCollection.create(
|
|
277
|
+
this.db,
|
|
278
|
+
`projects/${projectId}/instances/${instanceId}/viewports`,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
static async create(
|
|
283
|
+
config: z.infer<typeof localStateBackendConfig>,
|
|
284
|
+
logger: Logger,
|
|
285
|
+
): Promise<StateBackend> {
|
|
286
|
+
const childLogger = logger.child({ backend: "StateBackend", service: "LocalStateBackend" })
|
|
287
|
+
|
|
288
|
+
let location = config.HIGHSTATE_STATE_BACKEND_LOCAL_DIR
|
|
289
|
+
if (!location) {
|
|
290
|
+
location = resolve(process.env.HOME!, ".highstate", "state.db")
|
|
291
|
+
childLogger.debug({ location }, "using default local state backend location")
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const { ClassicLevel } = await import("classic-level")
|
|
295
|
+
const db = new ClassicLevel<string, string>(location)
|
|
296
|
+
|
|
297
|
+
return new LocalStateBackend(db as unknown as AbstractLevel<string, unknown>)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AbstractChainedBatch,
|
|
3
|
+
AbstractIteratorOptions,
|
|
4
|
+
AbstractLevel,
|
|
5
|
+
AbstractSnapshot,
|
|
6
|
+
AbstractSublevel,
|
|
7
|
+
} from "abstract-level"
|
|
8
|
+
import type { CollectionQueryResult } from "../../shared"
|
|
9
|
+
import { BetterLock } from "better-lock"
|
|
10
|
+
import {
|
|
11
|
+
type StateCollection,
|
|
12
|
+
type CollectionQuery,
|
|
13
|
+
type StateSnapshot,
|
|
14
|
+
type StateBatch,
|
|
15
|
+
CollectionBackend,
|
|
16
|
+
} from "../abstractions"
|
|
17
|
+
|
|
18
|
+
type Sublevel = AbstractSublevel<
|
|
19
|
+
AbstractLevel<string, unknown>,
|
|
20
|
+
string | Buffer<ArrayBufferLike> | Uint8Array<ArrayBufferLike>,
|
|
21
|
+
string,
|
|
22
|
+
Uint8Array<ArrayBufferLike>
|
|
23
|
+
>
|
|
24
|
+
|
|
25
|
+
type ChainedBatch = AbstractChainedBatch<AbstractLevel<string, unknown>, string, Uint8Array>
|
|
26
|
+
|
|
27
|
+
function getSnapshot(snapshot: StateSnapshot | undefined) {
|
|
28
|
+
if (snapshot && CollectionBackend in snapshot) {
|
|
29
|
+
return snapshot[CollectionBackend] as AbstractSnapshot
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getBatch(batch: StateBatch | undefined) {
|
|
36
|
+
if (batch && CollectionBackend in batch) {
|
|
37
|
+
return batch[CollectionBackend] as AbstractChainedBatch<
|
|
38
|
+
AbstractLevel<string, unknown>,
|
|
39
|
+
string,
|
|
40
|
+
Uint8Array
|
|
41
|
+
>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getOrCreateBatch(db: AbstractLevel<string, unknown>, batch?: StateBatch) {
|
|
48
|
+
const explicitBatch = getBatch(batch)
|
|
49
|
+
if (explicitBatch) {
|
|
50
|
+
return { chainedBatch: explicitBatch, explicit: true }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const chainedBatch = db.batch() as unknown as AbstractChainedBatch<
|
|
54
|
+
AbstractLevel<string, unknown>,
|
|
55
|
+
string,
|
|
56
|
+
Uint8Array
|
|
57
|
+
>
|
|
58
|
+
|
|
59
|
+
return { chainedBatch, explicit: false }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* LocalStateCollection implements StateCollection using LevelDB sublevel for binary data storage.
|
|
64
|
+
* It operates on raw binary data without any knowledge of the data structure.
|
|
65
|
+
*/
|
|
66
|
+
export class LocalStateCollection implements StateCollection {
|
|
67
|
+
private static readonly countKey = "_count"
|
|
68
|
+
|
|
69
|
+
// the lock for operations that modify the collection by first reading the item
|
|
70
|
+
// used to update the count key atomically
|
|
71
|
+
private readonly readWriteLock = new BetterLock()
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
private readonly db: AbstractLevel<string, unknown>,
|
|
75
|
+
private readonly sublevel: Sublevel,
|
|
76
|
+
) {}
|
|
77
|
+
|
|
78
|
+
async get(key: string, snapshot?: StateSnapshot): Promise<Uint8Array | undefined> {
|
|
79
|
+
return await this.sublevel.get(key, { snapshot: getSnapshot(snapshot) })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getMany(keys: string[], snapshot?: StateSnapshot): Promise<(Uint8Array | undefined)[]> {
|
|
83
|
+
return await this.sublevel.getMany(keys, { snapshot: getSnapshot(snapshot) })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getAll(snapshot?: StateSnapshot): Promise<Array<[string, Uint8Array]>> {
|
|
87
|
+
const result: Array<[string, Uint8Array]> = []
|
|
88
|
+
for await (const [key, value] of this.sublevel.iterator({ snapshot: getSnapshot(snapshot) })) {
|
|
89
|
+
// skip count key
|
|
90
|
+
if (key === LocalStateCollection.countKey) continue
|
|
91
|
+
|
|
92
|
+
result.push([key, value])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async put(key: string, value: Uint8Array, batch?: StateBatch): Promise<void> {
|
|
99
|
+
await this.readWriteLock.acquire(key, async () => {
|
|
100
|
+
// check if this is a new item
|
|
101
|
+
const existing = await this.sublevel.get(key)
|
|
102
|
+
const isNewItem = !existing
|
|
103
|
+
|
|
104
|
+
const { chainedBatch, explicit } = getOrCreateBatch(this.db, batch)
|
|
105
|
+
|
|
106
|
+
chainedBatch.put(key, value, { sublevel: this.sublevel })
|
|
107
|
+
|
|
108
|
+
// update count if this is a new item
|
|
109
|
+
if (isNewItem) {
|
|
110
|
+
await this.incrementCount(1, chainedBatch)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!explicit) {
|
|
114
|
+
// if we created a new batch, we need to commit it
|
|
115
|
+
await chainedBatch.write()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async putMany(entries: Array<[string, Uint8Array]>, batch?: StateBatch): Promise<void> {
|
|
121
|
+
const keys = entries.map(([key]) => key)
|
|
122
|
+
|
|
123
|
+
await this.readWriteLock.acquire(keys, async () => {
|
|
124
|
+
// check which items are new
|
|
125
|
+
let newItemsCount = 0
|
|
126
|
+
|
|
127
|
+
for (const [key] of entries) {
|
|
128
|
+
const existing = await this.sublevel.get(key)
|
|
129
|
+
if (!existing) {
|
|
130
|
+
newItemsCount++
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { chainedBatch, explicit } = getOrCreateBatch(this.db, batch)
|
|
135
|
+
|
|
136
|
+
for (const [key, value] of entries) {
|
|
137
|
+
chainedBatch.put(key, value, { sublevel: this.sublevel })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// update count for new items
|
|
141
|
+
if (newItemsCount > 0) {
|
|
142
|
+
await this.incrementCount(newItemsCount, chainedBatch)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!explicit) {
|
|
146
|
+
// if we created a new batch, we need to commit it
|
|
147
|
+
await chainedBatch.write()
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async delete(key: string, batch?: StateBatch): Promise<void> {
|
|
153
|
+
await this.readWriteLock.acquire(key, async () => {
|
|
154
|
+
// check if item exists before deleting
|
|
155
|
+
const existing = await this.sublevel.get(key)
|
|
156
|
+
|
|
157
|
+
const { chainedBatch, explicit } = getOrCreateBatch(this.db, batch)
|
|
158
|
+
|
|
159
|
+
chainedBatch.del(key, { sublevel: this.sublevel })
|
|
160
|
+
|
|
161
|
+
// update count if item existed
|
|
162
|
+
if (existing) {
|
|
163
|
+
await this.incrementCount(-1, chainedBatch)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!explicit) {
|
|
167
|
+
// if we created a new batch, we need to commit it
|
|
168
|
+
await chainedBatch.write()
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async deleteMany(keys: string[], batch?: StateBatch): Promise<void> {
|
|
174
|
+
await this.readWriteLock.acquire(keys, async () => {
|
|
175
|
+
// check which items exist before deleting
|
|
176
|
+
const existingValues = await this.sublevel.getMany(keys)
|
|
177
|
+
let deletedItemsCount = 0
|
|
178
|
+
|
|
179
|
+
for (const value of existingValues) {
|
|
180
|
+
if (value) {
|
|
181
|
+
deletedItemsCount++
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { chainedBatch, explicit } = getOrCreateBatch(this.db, batch)
|
|
186
|
+
|
|
187
|
+
for (const id of keys) {
|
|
188
|
+
chainedBatch.del(id, { sublevel: this.sublevel })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// update count for deleted items
|
|
192
|
+
if (deletedItemsCount > 0) {
|
|
193
|
+
await this.incrementCount(-deletedItemsCount, chainedBatch)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!explicit) {
|
|
197
|
+
// if we created a new batch, we need to commit it
|
|
198
|
+
await chainedBatch.write()
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async query(
|
|
204
|
+
query: CollectionQuery,
|
|
205
|
+
snapshot?: StateSnapshot,
|
|
206
|
+
): Promise<CollectionQueryResult<[string, Uint8Array]>> {
|
|
207
|
+
const { cursor, count = 20, sort = "asc" } = query
|
|
208
|
+
const limit = Math.min(count, 100) // cap at 100
|
|
209
|
+
|
|
210
|
+
const items: [string, Uint8Array][] = []
|
|
211
|
+
let nextCursor: string | undefined
|
|
212
|
+
|
|
213
|
+
const iteratorOptions: AbstractIteratorOptions<string, Uint8Array> = {
|
|
214
|
+
limit: limit + 1, // +1 to account "_count" key
|
|
215
|
+
reverse: sort === "desc",
|
|
216
|
+
snapshot: getSnapshot(snapshot),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (cursor && sort === "asc") {
|
|
220
|
+
iteratorOptions.gt = cursor // start after the cursor
|
|
221
|
+
} else if (cursor && sort === "desc") {
|
|
222
|
+
iteratorOptions.lt = cursor // start before the cursor
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for await (const [key, value] of this.sublevel.iterator(iteratorOptions)) {
|
|
226
|
+
// skip count key
|
|
227
|
+
if (key === LocalStateCollection.countKey) continue
|
|
228
|
+
|
|
229
|
+
items.push([key, value])
|
|
230
|
+
|
|
231
|
+
// if we've collected enough documents, the last key becomes the next cursor
|
|
232
|
+
if (items.length === limit) {
|
|
233
|
+
nextCursor = key
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// get total count for the collection
|
|
239
|
+
const total = await this.getTotalCount(snapshot)
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
items,
|
|
243
|
+
total,
|
|
244
|
+
nextCursor,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async getTotalCount(snapshot?: StateSnapshot): Promise<number> {
|
|
249
|
+
const countData = await this.sublevel.get(LocalStateCollection.countKey, {
|
|
250
|
+
snapshot: getSnapshot(snapshot),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (!countData) {
|
|
254
|
+
// if count doesn't exist, recalculate it
|
|
255
|
+
return await this.readWriteLock.acquire(LocalStateCollection.countKey, async () => {
|
|
256
|
+
return await this.recalculateCount()
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// decode the count data and return it as an integer
|
|
261
|
+
// if the data is not a valid number, recalculate it
|
|
262
|
+
const count = new TextDecoder().decode(countData)
|
|
263
|
+
const countValue = parseInt(count, 10)
|
|
264
|
+
if (isNaN(countValue)) {
|
|
265
|
+
return await this.readWriteLock.acquire(LocalStateCollection.countKey, async () => {
|
|
266
|
+
return await this.recalculateCount()
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return countValue
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Helper method to increment the count by a specific amount.
|
|
275
|
+
*
|
|
276
|
+
* MUST be used within a lock to ensure atomicity.
|
|
277
|
+
*/
|
|
278
|
+
private async incrementCount(amount: number, batch: ChainedBatch): Promise<void> {
|
|
279
|
+
await this.readWriteLock.acquire(LocalStateCollection.countKey, async () => {
|
|
280
|
+
const currentCount = await this.sublevel.get(LocalStateCollection.countKey)
|
|
281
|
+
if (!currentCount) {
|
|
282
|
+
await this.recalculateCount()
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const count = new TextDecoder().decode(currentCount)
|
|
287
|
+
const currentCountValue = parseInt(count, 10)
|
|
288
|
+
if (isNaN(currentCountValue)) {
|
|
289
|
+
await this.recalculateCount()
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const newCount = currentCountValue + amount
|
|
294
|
+
const countData = new TextEncoder().encode(newCount.toString())
|
|
295
|
+
|
|
296
|
+
batch.put(LocalStateCollection.countKey, countData, { sublevel: this.sublevel })
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Helper method to recalculate the count from scratch by iterating through all keys.
|
|
302
|
+
*/
|
|
303
|
+
private async recalculateCount(): Promise<number> {
|
|
304
|
+
// calculate the count by iterating through all keys
|
|
305
|
+
let count = 0
|
|
306
|
+
for await (const key of this.sublevel.keys()) {
|
|
307
|
+
if (key !== LocalStateCollection.countKey) {
|
|
308
|
+
count++
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// store the calculated count
|
|
313
|
+
const countData = new TextEncoder().encode(count.toString())
|
|
314
|
+
await this.sublevel.put(LocalStateCollection.countKey, countData)
|
|
315
|
+
|
|
316
|
+
return count
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
iterate(snapshot?: StateSnapshot): AsyncIterable<[string, Uint8Array]> {
|
|
320
|
+
const sublevel = this.sublevel
|
|
321
|
+
const snapshotOption = getSnapshot(snapshot)
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<[string, Uint8Array], void, unknown> {
|
|
325
|
+
for await (const [key, value] of sublevel.iterator({ snapshot: snapshotOption })) {
|
|
326
|
+
// skip count key
|
|
327
|
+
if (key === LocalStateCollection.countKey) continue
|
|
328
|
+
yield [key, value] as [string, Uint8Array]
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Creates a LocalStateCollection for the specified path using binary encoding.
|
|
336
|
+
*/
|
|
337
|
+
static create(db: AbstractLevel<string, unknown>, path: string): LocalStateCollection {
|
|
338
|
+
const sublevel = db.sublevel<string, Uint8Array>(path, { valueEncoding: "buffer" })
|
|
339
|
+
|
|
340
|
+
return new LocalStateCollection(db, sublevel as Sublevel)
|
|
341
|
+
}
|
|
342
|
+
}
|