@highstate/backend 0.9.15 → 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 (187) 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-Y7DXREVO.js +1745 -0
  6. package/dist/chunk-Y7DXREVO.js.map +1 -0
  7. package/dist/highstate.manifest.json +4 -4
  8. package/dist/index.js +7227 -2501
  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 +76 -185
  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 -98
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +31 -10
  19. package/src/artifact/abstractions.ts +46 -0
  20. package/src/artifact/encryption.ts +109 -0
  21. package/src/artifact/factory.ts +36 -0
  22. package/src/artifact/index.ts +3 -0
  23. package/src/artifact/local.ts +138 -0
  24. package/src/business/__traces__/secret/update-instance-secrets/create-and-delete-secrets-simultaneously.md +356 -0
  25. package/src/business/__traces__/secret/update-instance-secrets/create-new-secrets-for-instance.md +274 -0
  26. package/src/business/__traces__/secret/update-instance-secrets/delete-existing-secrets.md +223 -0
  27. package/src/business/__traces__/secret/update-instance-secrets/no-op-when-no-changes.md +147 -0
  28. package/src/business/__traces__/secret/update-instance-secrets/update-existing-secrets.md +280 -0
  29. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration-when-other-exists.md +360 -0
  30. package/src/business/__traces__/worker/update-unit-registrations/add-new-unit-registration.md +215 -0
  31. package/src/business/__traces__/worker/update-unit-registrations/create-multiple-workers-with-different-identities.md +427 -0
  32. package/src/business/__traces__/worker/update-unit-registrations/handle-nonexistent-registration-id-gracefully.md +217 -0
  33. package/src/business/__traces__/worker/update-unit-registrations/no-op-when-no-changes.md +132 -0
  34. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-changes.md +454 -0
  35. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-when-image-version-changes.md +426 -0
  36. package/src/business/__traces__/worker/update-unit-registrations/recreate-worker-with-same-identity-reuses-service-account.md +372 -0
  37. package/src/business/__traces__/worker/update-unit-registrations/remove-one-of-multiple-unit-registrations.md +383 -0
  38. package/src/business/__traces__/worker/update-unit-registrations/remove-unit-registration.md +245 -0
  39. package/src/business/__traces__/worker/update-unit-registrations/update-existing-unit-registration-when-params-change.md +174 -0
  40. package/src/business/__traces__/worker/update-unit-registrations/update-params-and-image-simultaneously.md +432 -0
  41. package/src/business/__traces__/worker/update-unit-registrations/worker-with-multiple-registrations-not-deleted-when-one-removed.md +220 -0
  42. package/src/business/api-key.ts +65 -0
  43. package/src/business/artifact.ts +289 -0
  44. package/src/business/backend-unlock.ts +10 -0
  45. package/src/business/index.ts +10 -0
  46. package/src/business/instance-lock.ts +125 -0
  47. package/src/business/instance-state.ts +434 -0
  48. package/src/business/operation.ts +251 -0
  49. package/src/business/project-unlock.ts +260 -0
  50. package/src/business/project.ts +299 -0
  51. package/src/business/secret.test.ts +178 -0
  52. package/src/business/secret.ts +281 -0
  53. package/src/business/worker.test.ts +614 -0
  54. package/src/business/worker.ts +398 -0
  55. package/src/common/clock.ts +18 -0
  56. package/src/common/index.ts +5 -1
  57. package/src/common/performance.ts +44 -0
  58. package/src/common/random.ts +68 -0
  59. package/src/common/test/index.ts +2 -0
  60. package/src/common/test/render.ts +98 -0
  61. package/src/common/test/tracer.ts +359 -0
  62. package/src/common/tree.ts +33 -0
  63. package/src/common/utils.ts +40 -1
  64. package/src/config.ts +19 -11
  65. package/src/hotstate/abstractions.ts +48 -0
  66. package/src/hotstate/factory.ts +17 -0
  67. package/src/{secret → hotstate}/index.ts +1 -0
  68. package/src/hotstate/manager.ts +192 -0
  69. package/src/hotstate/memory.ts +100 -0
  70. package/src/hotstate/validation.ts +100 -0
  71. package/src/index.ts +2 -1
  72. package/src/library/abstractions.ts +24 -28
  73. package/src/library/factory.ts +2 -2
  74. package/src/library/local.ts +91 -111
  75. package/src/library/worker/evaluator.ts +36 -73
  76. package/src/library/worker/loader.lite.ts +54 -0
  77. package/src/library/worker/main.ts +15 -66
  78. package/src/library/worker/protocol.ts +6 -33
  79. package/src/lock/abstractions.ts +6 -0
  80. package/src/lock/factory.ts +15 -0
  81. package/src/lock/index.ts +4 -0
  82. package/src/lock/manager.ts +97 -0
  83. package/src/lock/memory.ts +19 -0
  84. package/src/lock/test.ts +108 -0
  85. package/src/orchestrator/manager.ts +118 -90
  86. package/src/orchestrator/operation-workset.ts +181 -93
  87. package/src/orchestrator/operation.ts +1021 -283
  88. package/src/project/abstractions.ts +27 -38
  89. package/src/project/evaluation.ts +248 -0
  90. package/src/project/factory.ts +1 -1
  91. package/src/project/index.ts +1 -2
  92. package/src/project/local.ts +107 -103
  93. package/src/pubsub/abstractions.ts +13 -0
  94. package/src/pubsub/factory.ts +19 -0
  95. package/src/{workspace → pubsub}/index.ts +1 -0
  96. package/src/pubsub/local.ts +36 -0
  97. package/src/pubsub/manager.ts +108 -0
  98. package/src/pubsub/validation.ts +33 -0
  99. package/src/runner/abstractions.ts +155 -68
  100. package/src/runner/artifact-env.ts +160 -0
  101. package/src/runner/factory.ts +20 -5
  102. package/src/runner/force-abort.ts +117 -0
  103. package/src/runner/local.ts +292 -372
  104. package/src/{common → runner}/pulumi.ts +89 -37
  105. package/src/services.ts +251 -40
  106. package/src/shared/index.ts +3 -11
  107. package/src/shared/models/backend/index.ts +3 -0
  108. package/src/shared/{library.ts → models/backend/library.ts} +4 -4
  109. package/src/shared/models/backend/project.ts +82 -0
  110. package/src/shared/models/backend/unlock-method.ts +20 -0
  111. package/src/shared/models/base.ts +68 -0
  112. package/src/shared/models/errors.ts +5 -0
  113. package/src/shared/models/index.ts +4 -0
  114. package/src/shared/models/project/api-key.ts +65 -0
  115. package/src/shared/models/project/artifact.ts +83 -0
  116. package/src/shared/models/project/index.ts +13 -0
  117. package/src/shared/models/project/lock.ts +91 -0
  118. package/src/shared/models/project/model.ts +14 -0
  119. package/src/shared/{operation.ts → models/project/operation.ts} +29 -8
  120. package/src/shared/models/project/page.ts +57 -0
  121. package/src/shared/models/project/secret.ts +98 -0
  122. package/src/shared/models/project/service-account.ts +22 -0
  123. package/src/shared/models/project/state.ts +449 -0
  124. package/src/shared/models/project/terminal.ts +98 -0
  125. package/src/shared/models/project/trigger.ts +56 -0
  126. package/src/shared/models/project/unlock-method.ts +38 -0
  127. package/src/shared/models/project/worker.ts +107 -0
  128. package/src/shared/resolvers/graph-resolver.ts +61 -18
  129. package/src/shared/resolvers/index.ts +5 -0
  130. package/src/shared/resolvers/input-hash.ts +53 -15
  131. package/src/shared/resolvers/input.ts +47 -13
  132. package/src/shared/resolvers/registry.ts +3 -2
  133. package/src/shared/resolvers/state.ts +2 -2
  134. package/src/shared/resolvers/validation.ts +82 -25
  135. package/src/shared/utils/args.ts +25 -0
  136. package/src/shared/{async-batcher.ts → utils/async-batcher.ts} +13 -1
  137. package/src/shared/utils/hash.ts +6 -0
  138. package/src/shared/utils/index.ts +4 -0
  139. package/src/shared/utils/promise-tracker.ts +23 -0
  140. package/src/state/abstractions.ts +199 -131
  141. package/src/state/encryption.ts +98 -0
  142. package/src/state/factory.ts +3 -5
  143. package/src/state/index.ts +4 -0
  144. package/src/state/keyring.ts +22 -0
  145. package/src/state/local/backend.ts +106 -0
  146. package/src/state/local/collection.ts +361 -0
  147. package/src/state/local/index.ts +2 -0
  148. package/src/state/manager.ts +875 -18
  149. package/src/state/memory/backend.ts +70 -0
  150. package/src/state/memory/collection.ts +270 -0
  151. package/src/state/memory/index.ts +2 -0
  152. package/src/state/repository/index.ts +2 -0
  153. package/src/state/repository/repository.index.ts +193 -0
  154. package/src/state/repository/repository.ts +507 -0
  155. package/src/state/test.ts +457 -0
  156. package/src/terminal/{shared.ts → abstractions.ts} +3 -3
  157. package/src/terminal/docker.ts +18 -14
  158. package/src/terminal/factory.ts +3 -3
  159. package/src/terminal/index.ts +1 -1
  160. package/src/terminal/manager.ts +131 -79
  161. package/src/terminal/run.sh.ts +21 -11
  162. package/src/unlock/abstractions.ts +49 -0
  163. package/src/unlock/index.ts +2 -0
  164. package/src/unlock/memory.ts +32 -0
  165. package/src/worker/abstractions.ts +42 -0
  166. package/src/worker/docker.ts +83 -0
  167. package/src/worker/factory.ts +20 -0
  168. package/src/worker/index.ts +3 -0
  169. package/src/worker/manager.ts +167 -0
  170. package/dist/chunk-KTGKNSKM.js +0 -979
  171. package/dist/chunk-KTGKNSKM.js.map +0 -1
  172. package/dist/chunk-WXDYCRTT.js +0 -234
  173. package/dist/chunk-WXDYCRTT.js.map +0 -1
  174. package/src/library/worker/loader.ts +0 -114
  175. package/src/preferences/shared.ts +0 -1
  176. package/src/project/lock.ts +0 -39
  177. package/src/project/manager.ts +0 -433
  178. package/src/secret/abstractions.ts +0 -59
  179. package/src/secret/factory.ts +0 -22
  180. package/src/secret/local.ts +0 -152
  181. package/src/shared/project.ts +0 -62
  182. package/src/shared/state.ts +0 -247
  183. package/src/shared/terminal.ts +0 -14
  184. package/src/state/local.ts +0 -612
  185. package/src/workspace/abstractions.ts +0 -41
  186. package/src/workspace/factory.ts +0 -14
  187. package/src/workspace/local.ts +0 -54
@@ -0,0 +1,70 @@
1
+ import type { TestTracer } from "../../common"
2
+ import {
3
+ type StateBackend,
4
+ type StateBatch,
5
+ type StateSnapshot,
6
+ type StateCollection,
7
+ type CollectionMap,
8
+ } from "../abstractions"
9
+ import { MemoryStateCollection, MemoryStateBatch, MemoryStateSnapshot } from "./collection"
10
+
11
+ /**
12
+ * A memory-based state backend implementation for testing purposes.
13
+ */
14
+ export class MemoryStateBackend implements StateBackend {
15
+ private collections = new Map<string, MemoryStateCollection>()
16
+ private batchIdCounter = 0
17
+ private snapshotIdCounter = 0
18
+
19
+ constructor(private readonly tracer: TestTracer) {}
20
+
21
+ getEncryptedBackendMasterKey(): Promise<string | undefined> {
22
+ throw new Error("Method not implemented.")
23
+ }
24
+
25
+ setEncryptedBackendMasterKey(): Promise<void> {
26
+ throw new Error("Method not implemented.")
27
+ }
28
+
29
+ getEncryptedBackendNamespace(): Promise<Uint8Array | undefined> {
30
+ throw new Error("Method not implemented.")
31
+ }
32
+
33
+ setEncryptedBackendNamespace(): Promise<void> {
34
+ throw new Error("Method not implemented.")
35
+ }
36
+
37
+ createStateBatch(): StateBatch {
38
+ this.batchIdCounter += 1
39
+ return new MemoryStateBatch(this.batchIdCounter, this.tracer)
40
+ }
41
+
42
+ createStateSnapshot(): StateSnapshot {
43
+ this.snapshotIdCounter += 1
44
+ // Create a snapshot of all existing collections
45
+ const allCollections = Array.from(this.collections.values())
46
+ return new MemoryStateSnapshot(allCollections, this.snapshotIdCounter, this.tracer)
47
+ }
48
+
49
+ createCollection<TName extends keyof CollectionMap>(
50
+ name: TName,
51
+ params: CollectionMap[TName],
52
+ ): StateCollection {
53
+ const fullName = [name, ...Object.values(params)].join(":")
54
+
55
+ const existingCollection = this.collections.get(fullName)
56
+ if (existingCollection) {
57
+ return existingCollection
58
+ }
59
+
60
+ const collection = new MemoryStateCollection(
61
+ name,
62
+ params,
63
+ Promise.resolve(fullName), // this should be a UUIDv5 namespace, but for testing we will use the full name
64
+ this.tracer,
65
+ )
66
+ this.collections.set(fullName, collection)
67
+
68
+ return collection
69
+ }
70
+ }
@@ -0,0 +1,270 @@
1
+ import type { CollectionQueryResult } from "../../shared"
2
+ import { linkTraceEntry, type TestTracer, type TraceEntry } from "../../common"
3
+ import {
4
+ CollectionBackend,
5
+ type StateCollection,
6
+ type StateBatch,
7
+ type StateSnapshot,
8
+ type CollectionQuery,
9
+ } from "../abstractions"
10
+
11
+ /**
12
+ * A memory-based state collection implementation for testing purposes.
13
+ */
14
+ export class MemoryStateCollection implements StateCollection {
15
+ private data = new Map<string, Uint8Array>()
16
+
17
+ constructor(
18
+ readonly name: string,
19
+ readonly params: Record<string, string>,
20
+ readonly namespace: Promise<string>,
21
+ readonly tracer: TestTracer,
22
+ ) {}
23
+
24
+ get(id: string, snapshot?: StateSnapshot): Promise<Uint8Array | undefined> {
25
+ if (snapshot) {
26
+ const memorySnapshot = snapshot as unknown as MemoryStateSnapshot
27
+ const snapshotData = memorySnapshot.getCollection(this)
28
+
29
+ return Promise.resolve(snapshotData.get(id))
30
+ }
31
+
32
+ return Promise.resolve(this.data.get(id))
33
+ }
34
+
35
+ getMany(ids: string[], snapshot?: StateSnapshot): Promise<(Uint8Array | undefined)[]> {
36
+ const source = snapshot
37
+ ? (snapshot as unknown as MemoryStateSnapshot).getCollection(this)
38
+ : this.data
39
+
40
+ return Promise.resolve(ids.map(id => source.get(id)))
41
+ }
42
+
43
+ getAll(snapshot?: StateSnapshot): Promise<[string, Uint8Array][]> {
44
+ const source = snapshot
45
+ ? (snapshot as unknown as MemoryStateSnapshot).getCollection(this)
46
+ : this.data
47
+
48
+ return Promise.resolve(Array.from(source.entries()))
49
+ }
50
+
51
+ // eslint-disable-next-line @typescript-eslint/require-await
52
+ async query(
53
+ query: CollectionQuery,
54
+ snapshot?: StateSnapshot,
55
+ ): Promise<CollectionQueryResult<[string, Uint8Array]>> {
56
+ const source = snapshot
57
+ ? (snapshot as unknown as MemoryStateSnapshot).getCollection(this)
58
+ : this.data
59
+
60
+ let entries = Array.from(source.entries())
61
+
62
+ // apply cursor filtering
63
+ if (query.cursor) {
64
+ if (query.sort === "desc") {
65
+ entries = entries.filter(([id]) => id < query.cursor!)
66
+ } else {
67
+ entries = entries.filter(([id]) => id > query.cursor!)
68
+ }
69
+ }
70
+
71
+ // apply sorting
72
+ entries.sort(([a], [b]) => (query.sort === "desc" ? b.localeCompare(a) : a.localeCompare(b)))
73
+
74
+ // apply limit
75
+ const count = Math.min(query.count ?? 20, 100)
76
+ const limitedEntries = entries.slice(0, count)
77
+
78
+ return {
79
+ items: limitedEntries,
80
+ total: source.size,
81
+ nextCursor:
82
+ limitedEntries.length > 0 ? limitedEntries[limitedEntries.length - 1][0] : undefined,
83
+ }
84
+ }
85
+
86
+ getTotalCount(snapshot?: StateSnapshot): Promise<number> {
87
+ const source = snapshot
88
+ ? (snapshot as unknown as MemoryStateSnapshot).getCollection(this)
89
+ : this.data
90
+
91
+ return Promise.resolve(source.size)
92
+ }
93
+
94
+ // eslint-disable-next-line @typescript-eslint/require-await
95
+ async *iterate(snapshot?: StateSnapshot): AsyncIterable<[string, Uint8Array]> {
96
+ const source = snapshot
97
+ ? (snapshot as unknown as MemoryStateSnapshot).getCollection(this)
98
+ : this.data
99
+
100
+ for (const entry of source.entries()) {
101
+ yield entry
102
+ }
103
+ }
104
+
105
+ put(id: string, value: Uint8Array, batch?: StateBatch): Promise<void> {
106
+ if (batch) {
107
+ const memoryBatch = batch as MemoryStateBatch
108
+ memoryBatch[CollectionBackend].push({ type: "put", collection: this, id, value })
109
+ return Promise.resolve()
110
+ }
111
+
112
+ this.data.set(id, value)
113
+ return Promise.resolve()
114
+ }
115
+
116
+ putMany(entries: Array<[string, Uint8Array]>, batch?: StateBatch): Promise<void> {
117
+ if (batch) {
118
+ const memoryBatch = batch as MemoryStateBatch
119
+ for (const [id, value] of entries) {
120
+ memoryBatch[CollectionBackend].push({ type: "put", collection: this, id, value })
121
+ }
122
+ return Promise.resolve()
123
+ }
124
+
125
+ for (const [id, value] of entries) {
126
+ this.data.set(id, value)
127
+ }
128
+ return Promise.resolve()
129
+ }
130
+
131
+ delete(id: string, batch?: StateBatch): Promise<void> {
132
+ if (batch) {
133
+ const memoryBatch = batch as MemoryStateBatch
134
+ memoryBatch[CollectionBackend].push({ type: "delete", collection: this, id })
135
+ return Promise.resolve()
136
+ }
137
+
138
+ this.data.delete(id)
139
+ return Promise.resolve()
140
+ }
141
+
142
+ deleteMany(ids: string[], batch?: StateBatch): Promise<void> {
143
+ if (batch) {
144
+ const memoryBatch = batch as MemoryStateBatch
145
+ for (const id of ids) {
146
+ memoryBatch[CollectionBackend].push({ type: "delete", collection: this, id })
147
+ }
148
+ return Promise.resolve()
149
+ }
150
+
151
+ for (const id of ids) {
152
+ this.data.delete(id)
153
+ }
154
+ return Promise.resolve()
155
+ }
156
+
157
+ clear(): Promise<void> {
158
+ this.data.clear()
159
+ return Promise.resolve()
160
+ }
161
+ }
162
+
163
+ export interface MemoryBatchOperation {
164
+ type: "put" | "delete"
165
+ collection: MemoryStateCollection
166
+ id: string
167
+ value?: Uint8Array
168
+ }
169
+
170
+ class StateBatchEntry implements TraceEntry {
171
+ constructor(
172
+ readonly id: number,
173
+ private readonly opEntries: TraceEntry[],
174
+ ) {}
175
+
176
+ render(): string {
177
+ return `💾 write ` + this.opEntries.map(linkTraceEntry).join(", ")
178
+ }
179
+ }
180
+
181
+ class StateSnapshotEntry implements TraceEntry {
182
+ constructor(
183
+ readonly id: number,
184
+ readonly usageEntries: TraceEntry[],
185
+ ) {}
186
+
187
+ render(): string {
188
+ if (this.usageEntries.length === 0) {
189
+ return `📸 snapshot`
190
+ }
191
+
192
+ return `📸 snapshot, used by ` + this.usageEntries.map(linkTraceEntry).join(", ")
193
+ }
194
+ }
195
+
196
+ export class MemoryStateBatch implements StateBatch {
197
+ [CollectionBackend]: MemoryBatchOperation[] = []
198
+
199
+ constructor(
200
+ readonly id: number,
201
+ private readonly tracer: TestTracer,
202
+ ) {}
203
+
204
+ // for testing purposes
205
+ readonly opEntries: TraceEntry[] = []
206
+ traceEntry?: TraceEntry
207
+
208
+ disposed = false
209
+
210
+ write(): Promise<void> {
211
+ if (this.disposed) {
212
+ return Promise.resolve()
213
+ }
214
+
215
+ for (const operation of this[CollectionBackend]) {
216
+ if (operation.type === "put") {
217
+ operation.collection["data"].set(operation.id, operation.value!)
218
+ } else if (operation.type === "delete") {
219
+ operation.collection["data"].delete(operation.id)
220
+ }
221
+ }
222
+
223
+ this.traceEntry = new StateBatchEntry(this.tracer.nextEntryId(), this.opEntries)
224
+ this.tracer.addEntry(this.traceEntry)
225
+
226
+ this.disposed = true
227
+ return Promise.resolve()
228
+ }
229
+
230
+ [Symbol.asyncDispose](): Promise<void> {
231
+ if (this.disposed) {
232
+ return Promise.resolve()
233
+ }
234
+
235
+ this.disposed = true
236
+ return Promise.resolve()
237
+ }
238
+ }
239
+
240
+ export class MemoryStateSnapshot implements StateSnapshot {
241
+ [CollectionBackend] = new Map<MemoryStateCollection, Map<string, Uint8Array>>()
242
+
243
+ readonly traceEntry: TraceEntry
244
+ readonly usageEntries: TraceEntry[] = []
245
+
246
+ constructor(
247
+ collections: MemoryStateCollection[],
248
+ readonly id: number,
249
+ readonly tracer: TestTracer,
250
+ ) {
251
+ // create snapshots of all collections at this point in time
252
+ for (const collection of collections) {
253
+ this[CollectionBackend].set(collection, new Map(collection["data"]))
254
+ }
255
+
256
+ this.traceEntry = new StateSnapshotEntry(tracer.nextEntryId(), this.usageEntries)
257
+ tracer.addEntry(this.traceEntry)
258
+ }
259
+
260
+ getCollection(collection: MemoryStateCollection): Map<string, Uint8Array> {
261
+ const snapshot = this[CollectionBackend].get(collection)
262
+
263
+ return snapshot ?? new Map<string, Uint8Array>()
264
+ }
265
+
266
+ [Symbol.asyncDispose](): Promise<void> {
267
+ this[CollectionBackend].clear()
268
+ return Promise.resolve()
269
+ }
270
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./backend"
2
+ export * from "./collection"
@@ -0,0 +1,2 @@
1
+ export * from "./repository"
2
+ export * from "./repository.index"
@@ -0,0 +1,193 @@
1
+ import type { Logger } from "pino"
2
+ import type { StateRepository } from "./repository"
3
+ import type { CollectionQuery, StateSnapshot } from "../abstractions"
4
+ import type { CollectionQueryResult } from "../../shared"
5
+ import type { StateManager } from "../manager"
6
+ import { type z } from "zod"
7
+
8
+ /**
9
+ * The index value that indicates an object key is the same as the index key.
10
+ *
11
+ * NOTE: DO NOT use this feature when the index repository has key hashing enabled.
12
+ */
13
+ export const SAME_KEY = ""
14
+
15
+ /**
16
+ * StateIndexRepository provides read-only operations that combine two StateRepository instances.
17
+ *
18
+ * The index repository maps index keys to object keys, allowing indirect access to objects in the main repository.
19
+ */
20
+ export class StateIndexRepository<TSchema extends z.ZodType> {
21
+ constructor(
22
+ private readonly stateManager: StateManager,
23
+ public readonly repository: StateRepository<TSchema>,
24
+ public readonly indexRepository: StateRepository<z.ZodString>,
25
+ private readonly logger: Logger,
26
+ ) {}
27
+
28
+ /**
29
+ * Retrieves a single item by its index key.
30
+ */
31
+ async get(indexKey: string): Promise<z.infer<TSchema> | undefined> {
32
+ const snapshot = this.stateManager.snapshot()
33
+
34
+ // fetch the object key from the index repository
35
+ const objectKey = await this.indexRepository.get(indexKey, snapshot)
36
+ if (!objectKey) {
37
+ return undefined
38
+ }
39
+
40
+ // if index value is empty string, use the index key itself
41
+ const resolvedObjectId = objectKey === SAME_KEY ? indexKey : objectKey
42
+
43
+ const item = await this.repository.get(resolvedObjectId, snapshot)
44
+ if (item === undefined) {
45
+ this.logger.warn({ indexKey, resolvedObjectId }, "index points to non-existent object")
46
+ return undefined
47
+ }
48
+
49
+ return item
50
+ }
51
+
52
+ /**
53
+ * Retrieves multiple items by their index keys, returning an array of records.
54
+ *
55
+ * NOTE: the keys of the records are the index keys, not the object keys.
56
+ */
57
+ async getMany(indexKeys: string[]): Promise<[string, z.infer<TSchema>][]> {
58
+ if (indexKeys.length === 0) return []
59
+
60
+ const snapshot = this.stateManager.snapshot()
61
+
62
+ // get all index records for the provided keys
63
+ const indexRecords = await this.indexRepository.getMany(indexKeys, snapshot)
64
+ if (indexRecords.length === 0) return []
65
+
66
+ // resolve index records to get the actual objects
67
+ return this.resolveIndexEntries(indexRecords, snapshot)
68
+ }
69
+
70
+ /**
71
+ * Retrieves multiple items by their index keys, returning a record of objects.
72
+ *
73
+ * NOTE: the keys of the record are the index keys, not the object keys.
74
+ */
75
+ async getManyRecord(indexKeys: string[]): Promise<Record<string, z.infer<TSchema> | undefined>> {
76
+ const records = await this.getMany(indexKeys)
77
+
78
+ return Object.fromEntries(records)
79
+ }
80
+
81
+ /**
82
+ * Retrieves multiple items by their index keys, returning an array of objects.
83
+ */
84
+ async getManyItems(indexKeys: string[]): Promise<z.infer<TSchema>[]> {
85
+ const records = await this.getMany(indexKeys)
86
+
87
+ return Object.values(records)
88
+ .filter(item => item !== null)
89
+ .map(([, item]) => item)
90
+ }
91
+
92
+ /**
93
+ * Retrieves all items that have index entries, returning an array of records.
94
+ *
95
+ * NOTE: the keys of the records are the index keys, not the object keys.
96
+ */
97
+ async getAll(): Promise<Array<[string, z.infer<TSchema>]>> {
98
+ const snapshot = this.stateManager.snapshot()
99
+
100
+ // get all index records
101
+ const indexEntries = await this.indexRepository.getAll(snapshot)
102
+ if (indexEntries.length === 0) return []
103
+
104
+ // resolve index entries to get the actual objects
105
+ return this.resolveIndexEntries(indexEntries, snapshot)
106
+ }
107
+
108
+ /**
109
+ * Retrieves all items that have index entries, returning a record of objects.
110
+ *
111
+ * NOTE: the keys of the record are the index keys, not the object keys.
112
+ */
113
+ async getAllRecord(): Promise<Record<string, z.infer<TSchema> | null>> {
114
+ const allEntries = await this.getAll()
115
+
116
+ // convert the array of entries to a record
117
+ return Object.fromEntries(allEntries)
118
+ }
119
+
120
+ /**
121
+ * Retrieves all items that have index entries.
122
+ * Iterates through the index and returns all matched objects.
123
+ */
124
+ async getAllItems(): Promise<z.infer<TSchema>[]> {
125
+ const allEntries = await this.getAll()
126
+
127
+ return allEntries.map(([, item]) => item)
128
+ }
129
+
130
+ /**
131
+ * Queries the index repository and retrieves items from the main repository.
132
+ */
133
+ async query(query: CollectionQuery): Promise<CollectionQueryResult<[string, z.infer<TSchema>]>> {
134
+ const snapshot = this.stateManager.snapshot()
135
+
136
+ // first, resolve the query to get the index keys
137
+ const result = await this.indexRepository.query(query, snapshot)
138
+
139
+ // then resolve the index entries to get the actual objects
140
+ const items = await this.resolveIndexEntries(result.items, snapshot)
141
+
142
+ return {
143
+ total: result.total,
144
+ items,
145
+ nextCursor: result.nextCursor,
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Queries the index repository and retrieves items from the main repository, returning an array of objects.
151
+ */
152
+ async queryItems(query: CollectionQuery): Promise<CollectionQueryResult<z.infer<TSchema>>> {
153
+ const result = await this.query(query)
154
+
155
+ // extract the items from the result
156
+ return {
157
+ total: result.total,
158
+ items: result.items.map(([, item]) => item),
159
+ nextCursor: result.nextCursor,
160
+ }
161
+ }
162
+
163
+ private async resolveIndexEntries(
164
+ indexEntries: [string, string][],
165
+ snapshot: StateSnapshot,
166
+ ): Promise<[string, z.infer<TSchema>][]> {
167
+ // collect all object keys that were found
168
+ const objectKeys: string[] = []
169
+ for (const [indexKey, objectId] of indexEntries) {
170
+ // if index value is empty string, use the index key itself
171
+ const resolvedObjectId = objectId === SAME_KEY ? indexKey : objectId
172
+ objectKeys.push(resolvedObjectId)
173
+ }
174
+
175
+ // get objects from main repository
176
+ const objectRecord = await this.repository.getManyRecord(objectKeys, snapshot)
177
+ const result: [string, z.infer<TSchema>][] = []
178
+
179
+ for (const [indexKey, objectKey] of indexEntries) {
180
+ const resolvedObjectId = objectKey === SAME_KEY ? indexKey : objectKey
181
+ const item = objectRecord[resolvedObjectId]
182
+
183
+ if (item === undefined) {
184
+ this.logger.warn({ indexKey, resolvedObjectId }, "index points to non-existent object")
185
+ continue
186
+ }
187
+
188
+ result.push([indexKey, item])
189
+ }
190
+
191
+ return result
192
+ }
193
+ }