@highstate/contract 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,215 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { compact, decompact } from "./compaction"
3
+ import { HighstateSignature, objectRefSchema, objectWithIdSchema } from "./instance"
4
+
5
+ describe("compaction", () => {
6
+ it("roundtrips primitives", () => {
7
+ expect(decompact(compact(null))).toBe(null)
8
+ expect(decompact(compact(undefined))).toBe(undefined)
9
+ expect(decompact(compact(true))).toBe(true)
10
+ expect(decompact(compact(123))).toBe(123)
11
+ expect(decompact(compact("abc"))).toBe("abc")
12
+ })
13
+
14
+ it("compacts repeated object identities", () => {
15
+ const shared = { x: 1 }
16
+ const value = { a: shared, b: shared }
17
+
18
+ const compacted = compact(value)
19
+
20
+ const compactedRecord = compacted as Record<string, unknown>
21
+ const aResult = objectWithIdSchema.safeParse(compactedRecord.a)
22
+ const bResult = objectRefSchema.safeParse(compactedRecord.b)
23
+
24
+ expect(aResult.success).toBe(true)
25
+ expect(bResult.success).toBe(true)
26
+
27
+ const roundtripped = decompact(compacted)
28
+ expect(roundtripped).toEqual(value)
29
+ })
30
+
31
+ it("preserves object identity on decompact", () => {
32
+ const shared = { x: 1 }
33
+ const value = { a: shared, b: shared }
34
+
35
+ const roundtripped = decompact(compact(value)) as { a: unknown; b: unknown }
36
+ expect(roundtripped).toEqual(value)
37
+ expect(roundtripped.a).toBe(roundtripped.b)
38
+ })
39
+
40
+ it("roundtrips self-referential objects", () => {
41
+ const value: { self?: unknown } = {}
42
+ value.self = value
43
+
44
+ const compacted = compact(value)
45
+ const roundtripped = decompact(compacted) as { self: unknown }
46
+
47
+ expect(roundtripped.self).toBe(roundtripped)
48
+ })
49
+
50
+ it("roundtrips mutual references", () => {
51
+ const a: { name: string; other?: unknown } = { name: "a" }
52
+ const b: { name: string; other?: unknown } = { name: "b" }
53
+
54
+ a.other = b
55
+ b.other = a
56
+
57
+ const compacted = compact({ a, b })
58
+ const roundtripped = decompact(compacted) as { a: { other: unknown }; b: { other: unknown } }
59
+
60
+ expect(roundtripped.a.other).toBe(roundtripped.b)
61
+ expect(roundtripped.b.other).toBe(roundtripped.a)
62
+ })
63
+
64
+ it("does not transform children of defined objects", () => {
65
+ const shared = { x: 1 }
66
+ const value = { a: shared, b: shared }
67
+
68
+ const compacted = compact(value) as Record<string, unknown>
69
+ const aResult = objectWithIdSchema.safeParse(compacted.a)
70
+ expect(aResult.success).toBe(true)
71
+
72
+ const sharedInside = (aResult.data as { value: unknown }).value
73
+ expect(objectWithIdSchema.safeParse(sharedInside).success).toBe(false)
74
+ // Refs to already-defined outer objects are allowed inside the Id value.
75
+ // Nested Id definitions are not.
76
+ expect(sharedInside).toEqual(shared)
77
+ })
78
+
79
+ it("assigns references so outer occurrences win in bfs", () => {
80
+ const shared = { x: 1 }
81
+ const outer = { shared }
82
+ const inner = { shared }
83
+
84
+ // bfs from root sees outer first, so it must be the definition site.
85
+ const compacted = compact({ outer, inner }) as Record<string, unknown>
86
+
87
+ const outerResult = objectWithIdSchema.safeParse(
88
+ (compacted.outer as Record<string, unknown>).shared,
89
+ )
90
+ const innerResult = objectRefSchema.safeParse(
91
+ (compacted.inner as Record<string, unknown>).shared,
92
+ )
93
+
94
+ if (!outerResult.success || !innerResult.success) {
95
+ throw new Error("Test invariant violation")
96
+ }
97
+
98
+ expect(innerResult.data.id).toBe(outerResult.data.id)
99
+ })
100
+
101
+ it("throws on unresolved refs", () => {
102
+ const bad = {
103
+ [HighstateSignature.Ref]: true,
104
+ id: 123,
105
+ }
106
+
107
+ expect(() => decompact(bad)).toThrow(/Unresolved compacted ref id/i)
108
+ })
109
+
110
+ it("real world case: nested entity embedding with deduped base", () => {
111
+ const metadata = {
112
+ namespace: "default",
113
+ name: "gw",
114
+ uid: "uid-1",
115
+ labels: { app: "demo" },
116
+ annotations: { a: "b" },
117
+ }
118
+
119
+ const resourceBase = {
120
+ clusterId: "c1",
121
+ clusterName: "cluster",
122
+ type: "gateway",
123
+ metadata,
124
+ }
125
+
126
+ const scopedResource = {
127
+ metadata,
128
+ clusterId: resourceBase.clusterId,
129
+ clusterName: resourceBase.clusterName,
130
+ type: resourceBase.type,
131
+ "k8s.resource.v1": resourceBase,
132
+ }
133
+
134
+ const gateway = {
135
+ type: "gateway",
136
+ metadata,
137
+ clusterId: resourceBase.clusterId,
138
+ clusterName: resourceBase.clusterName,
139
+ "k8s.scoped-resource.v1": scopedResource,
140
+ "k8s.resource.v1": resourceBase,
141
+ }
142
+
143
+ const compacted = compact(gateway) as Record<string, unknown>
144
+
145
+ const metadataResult = objectWithIdSchema.safeParse(compacted.metadata)
146
+ expect(metadataResult.success).toBe(true)
147
+
148
+ const scopedResult = compacted["k8s.scoped-resource.v1"] as Record<string, unknown>
149
+ const scopedMetadataRef = objectRefSchema.safeParse(scopedResult.metadata)
150
+ expect(scopedMetadataRef.success).toBe(true)
151
+ if (!metadataResult.success || !scopedMetadataRef.success) {
152
+ throw new Error("Test invariant violation")
153
+ }
154
+
155
+ expect(scopedMetadataRef.data.id).toBe(metadataResult.data.id)
156
+
157
+ // Once an object is defined (wrapped with Id), its subtree must remain untransformed.
158
+ const definedMetadataValue = (metadataResult.data as { value: unknown }).value
159
+ expect(objectWithIdSchema.safeParse(definedMetadataValue).success).toBe(false)
160
+ // Refs to already-defined outer objects are allowed inside the Id value.
161
+ // Nested Id definitions are not.
162
+
163
+ expect(compacted).toMatchInlineSnapshot(`
164
+ {
165
+ "clusterId": "c1",
166
+ "clusterName": "cluster",
167
+ "k8s.resource.v1": {
168
+ "348d020e-0d9e-4ae7-9415-b91af99f5339": true,
169
+ "id": 2,
170
+ "value": {
171
+ "clusterId": "c1",
172
+ "clusterName": "cluster",
173
+ "metadata": {
174
+ "6d7f9da0-9cb6-496d-b72e-cf85ee4d9cf8": true,
175
+ "id": 1,
176
+ },
177
+ "type": "gateway",
178
+ },
179
+ },
180
+ "k8s.scoped-resource.v1": {
181
+ "clusterId": "c1",
182
+ "clusterName": "cluster",
183
+ "k8s.resource.v1": {
184
+ "6d7f9da0-9cb6-496d-b72e-cf85ee4d9cf8": true,
185
+ "id": 2,
186
+ },
187
+ "metadata": {
188
+ "6d7f9da0-9cb6-496d-b72e-cf85ee4d9cf8": true,
189
+ "id": 1,
190
+ },
191
+ "type": "gateway",
192
+ },
193
+ "metadata": {
194
+ "348d020e-0d9e-4ae7-9415-b91af99f5339": true,
195
+ "id": 1,
196
+ "value": {
197
+ "annotations": {
198
+ "a": "b",
199
+ },
200
+ "labels": {
201
+ "app": "demo",
202
+ },
203
+ "name": "gw",
204
+ "namespace": "default",
205
+ "uid": "uid-1",
206
+ },
207
+ },
208
+ "type": "gateway",
209
+ }
210
+ `)
211
+
212
+ const roundtripped = decompact(compacted)
213
+ expect(roundtripped).toEqual(gateway)
214
+ })
215
+ })
@@ -0,0 +1,381 @@
1
+ import { mapValues } from "remeda"
2
+ import { HighstateSignature, objectRefSchema, objectWithIdSchema } from "./instance"
3
+
4
+ export function compact<T>(value: T): unknown {
5
+ const counts = new WeakMap<object, number>()
6
+ const cyclic = new WeakSet<object>()
7
+ const expanded = new WeakSet<object>()
8
+ const inStack = new WeakSet<object>()
9
+ const stack: object[] = []
10
+
11
+ function countIdentities(current: unknown): void {
12
+ if (current === null || typeof current !== "object") {
13
+ return
14
+ }
15
+
16
+ counts.set(current, (counts.get(current) ?? 0) + 1)
17
+
18
+ if (inStack.has(current)) {
19
+ cyclic.add(current)
20
+
21
+ for (const entry of stack) {
22
+ cyclic.add(entry)
23
+ }
24
+ return
25
+ }
26
+
27
+ if (expanded.has(current)) {
28
+ return
29
+ }
30
+
31
+ expanded.add(current)
32
+ inStack.add(current)
33
+ stack.push(current)
34
+
35
+ if (Array.isArray(current)) {
36
+ try {
37
+ for (const item of current) {
38
+ countIdentities(item)
39
+ }
40
+ return
41
+ } finally {
42
+ stack.pop()
43
+ inStack.delete(current)
44
+ }
45
+ }
46
+
47
+ try {
48
+ for (const entryValue of Object.values(current)) {
49
+ countIdentities(entryValue)
50
+ }
51
+ } finally {
52
+ stack.pop()
53
+ inStack.delete(current)
54
+ }
55
+ }
56
+
57
+ countIdentities(value)
58
+
59
+ type DefinitionSite = { parent: object; key: string | number }
60
+
61
+ const ids = new WeakMap<object, number>()
62
+ const definitionSites = new WeakMap<object, DefinitionSite>()
63
+ let nextId = 1
64
+
65
+ function ensureId(current: object): number {
66
+ const existing = ids.get(current)
67
+ if (existing !== undefined) {
68
+ return existing
69
+ }
70
+
71
+ const allocated = nextId
72
+ nextId += 1
73
+ ids.set(current, allocated)
74
+ return allocated
75
+ }
76
+
77
+ function assignDefinitionSitesBfs(root: unknown): void {
78
+ if (root === null || typeof root !== "object") {
79
+ return
80
+ }
81
+
82
+ const queue: Array<{ parent: object; key: string | number; value: unknown }> = []
83
+ const expandedQueue = new WeakSet<object>()
84
+
85
+ if (Array.isArray(root)) {
86
+ for (let index = 0; index < root.length; index += 1) {
87
+ queue.push({ parent: root, key: index, value: root[index] })
88
+ }
89
+ } else {
90
+ for (const [key, entryValue] of Object.entries(root)) {
91
+ queue.push({ parent: root, key, value: entryValue })
92
+ }
93
+ }
94
+
95
+ while (queue.length > 0) {
96
+ const current = queue.shift()
97
+ if (current === undefined) {
98
+ continue
99
+ }
100
+
101
+ if (current.value === null || typeof current.value !== "object") {
102
+ continue
103
+ }
104
+
105
+ const occurrences = counts.get(current.value) ?? 0
106
+ if (occurrences > 1 && definitionSites.get(current.value) === undefined) {
107
+ definitionSites.set(current.value, { parent: current.parent, key: current.key })
108
+ ensureId(current.value)
109
+ }
110
+
111
+ if (expandedQueue.has(current.value)) {
112
+ continue
113
+ }
114
+
115
+ expandedQueue.add(current.value)
116
+
117
+ if (Array.isArray(current.value)) {
118
+ for (let index = 0; index < current.value.length; index += 1) {
119
+ queue.push({ parent: current.value, key: index, value: current.value[index] })
120
+ }
121
+ continue
122
+ }
123
+
124
+ for (const [key, entryValue] of Object.entries(current.value)) {
125
+ queue.push({ parent: current.value, key, value: entryValue })
126
+ }
127
+ }
128
+ }
129
+
130
+ assignDefinitionSitesBfs(value)
131
+
132
+ const emitted = new WeakSet<object>()
133
+
134
+ function buildTreeChildren(current: object, rootOfIdValue?: object): unknown {
135
+ if (Array.isArray(current)) {
136
+ return current.map(item => buildTree(item, rootOfIdValue))
137
+ }
138
+
139
+ return mapValues(current, entryValue => buildTree(entryValue, rootOfIdValue))
140
+ }
141
+
142
+ function buildTree(current: unknown, rootOfIdValue?: object): unknown {
143
+ if (current === null || typeof current !== "object") {
144
+ return current
145
+ }
146
+
147
+ // Inside an Id value, never emit nested Id wrappers.
148
+ // Match existing real-world snapshot: do not introduce refs for nested objects
149
+ // unless the nested object has already been defined earlier.
150
+ if (rootOfIdValue !== undefined && emitted.has(current)) {
151
+ const id = ids.get(current)
152
+ if (id === undefined) {
153
+ throw new Error("Compaction invariant violation: missing id for repeated object")
154
+ }
155
+
156
+ return {
157
+ [HighstateSignature.Ref]: true,
158
+ id,
159
+ }
160
+ }
161
+
162
+ // Cycles inside an Id value require defining the cyclic objects inline,
163
+ // otherwise we would infinitely recurse.
164
+ if (rootOfIdValue !== undefined && cyclic.has(current) && !emitted.has(current)) {
165
+ const id = ensureId(current)
166
+ emitted.add(current)
167
+
168
+ return {
169
+ [HighstateSignature.Id]: true,
170
+ id,
171
+ value: buildTreeChildren(current, current),
172
+ }
173
+ }
174
+
175
+ return buildTreeChildren(current, rootOfIdValue)
176
+ }
177
+
178
+ function buildAt(parent: object, key: string | number, current: unknown): unknown {
179
+ if (current === null || typeof current !== "object") {
180
+ return current
181
+ }
182
+
183
+ const occurrences = counts.get(current) ?? 0
184
+ if (occurrences <= 1) {
185
+ if (Array.isArray(current)) {
186
+ return current.map((item, index) => buildAt(current, index, item))
187
+ }
188
+
189
+ return mapValues(current, (entryValue, entryKey) => buildAt(current, entryKey, entryValue))
190
+ }
191
+
192
+ const site = definitionSites.get(current)
193
+ const shouldDefineHere = site?.parent === parent && site.key === key
194
+
195
+ const id = ids.get(current)
196
+ if (id === undefined) {
197
+ throw new Error("Compaction invariant violation: missing id for repeated object")
198
+ }
199
+
200
+ if (!shouldDefineHere || emitted.has(current)) {
201
+ return {
202
+ [HighstateSignature.Ref]: true,
203
+ id,
204
+ }
205
+ }
206
+
207
+ emitted.add(current)
208
+
209
+ return {
210
+ [HighstateSignature.Id]: true,
211
+ id,
212
+ value: buildTreeChildren(current, current),
213
+ }
214
+ }
215
+
216
+ // Prefer keeping the top-level value unwrapped for stability.
217
+ if (value === null || typeof value !== "object") {
218
+ return value
219
+ }
220
+
221
+ const topLevelOccurrences = counts.get(value) ?? 0
222
+ if (topLevelOccurrences > 1) {
223
+ const id = ensureId(value)
224
+ emitted.add(value)
225
+
226
+ return {
227
+ [HighstateSignature.Id]: true,
228
+ id,
229
+ value: buildTreeChildren(value, value),
230
+ }
231
+ }
232
+
233
+ if (Array.isArray(value)) {
234
+ return value.map((item, index) => buildAt(value, index, item))
235
+ }
236
+
237
+ return mapValues(value as Record<string, unknown>, (entryValue, entryKey) =>
238
+ buildAt(value as Record<string, unknown>, entryKey, entryValue),
239
+ )
240
+ }
241
+
242
+ export function decompact<T>(value: unknown): T {
243
+ const rawValuesById = new Map<number, unknown>()
244
+ const placeholdersById = new Map<number, unknown>()
245
+
246
+ function collect(current: unknown, visited: WeakSet<object>): void {
247
+ const result = objectWithIdSchema.safeParse(current)
248
+ if (result.success) {
249
+ const { id } = result.data
250
+ if (rawValuesById.has(id)) {
251
+ throw new Error(`Duplicate compacted id ${id}`)
252
+ }
253
+
254
+ rawValuesById.set(id, result.data.value)
255
+ collect(result.data.value, visited)
256
+ return
257
+ }
258
+
259
+ if (current === null || current === undefined || typeof current !== "object") {
260
+ return
261
+ }
262
+
263
+ if (visited.has(current)) {
264
+ return
265
+ }
266
+
267
+ visited.add(current)
268
+
269
+ if (Array.isArray(current)) {
270
+ for (const item of current) {
271
+ collect(item, visited)
272
+ }
273
+ return
274
+ }
275
+
276
+ for (const entryValue of Object.values(current)) {
277
+ collect(entryValue, visited)
278
+ }
279
+ }
280
+
281
+ function ensurePlaceholder(id: number): unknown {
282
+ const existing = placeholdersById.get(id)
283
+ if (existing !== undefined) {
284
+ return existing
285
+ }
286
+
287
+ const raw = rawValuesById.get(id)
288
+ if (raw === undefined) {
289
+ throw new Error(`Unresolved compacted ref id ${id}`)
290
+ }
291
+
292
+ let placeholder: unknown
293
+ if (raw !== null && typeof raw === "object") {
294
+ placeholder = Array.isArray(raw) ? [] : {}
295
+ } else {
296
+ placeholder = raw
297
+ }
298
+
299
+ placeholdersById.set(id, placeholder)
300
+ return placeholder
301
+ }
302
+
303
+ function resolve(current: unknown): unknown {
304
+ const refResult = objectRefSchema.safeParse(current)
305
+ if (refResult.success) {
306
+ return ensurePlaceholder(refResult.data.id)
307
+ }
308
+
309
+ const withIdResult = objectWithIdSchema.safeParse(current)
310
+ if (withIdResult.success) {
311
+ return ensurePlaceholder(withIdResult.data.id)
312
+ }
313
+
314
+ if (current === null || current === undefined || typeof current !== "object") {
315
+ return current
316
+ }
317
+
318
+ if (Array.isArray(current)) {
319
+ return current.map(item => resolve(item))
320
+ }
321
+
322
+ return mapValues(current, entryValue => resolve(entryValue))
323
+ }
324
+
325
+ function fillPlaceholder(id: number, visitedIds: Set<number>): void {
326
+ if (visitedIds.has(id)) {
327
+ return
328
+ }
329
+
330
+ visitedIds.add(id)
331
+
332
+ const raw = rawValuesById.get(id)
333
+ if (raw === undefined) {
334
+ throw new Error(`Unresolved compacted ref id ${id}`)
335
+ }
336
+
337
+ const placeholder = ensurePlaceholder(id)
338
+ if (placeholder === null || typeof placeholder !== "object") {
339
+ return
340
+ }
341
+
342
+ if (raw === null || typeof raw !== "object") {
343
+ throw new Error(`Compaction invariant violation: id ${id} points to non-object value`)
344
+ }
345
+
346
+ const resolved = resolve(raw)
347
+
348
+ if (Array.isArray(placeholder)) {
349
+ if (!Array.isArray(resolved)) {
350
+ throw new Error(`Compaction invariant violation: id ${id} array placeholder mismatch`)
351
+ }
352
+
353
+ placeholder.length = 0
354
+ for (const item of resolved) {
355
+ placeholder.push(item)
356
+ }
357
+ return
358
+ }
359
+
360
+ if (Array.isArray(resolved) || resolved === null || typeof resolved !== "object") {
361
+ throw new Error(`Compaction invariant violation: id ${id} object placeholder mismatch`)
362
+ }
363
+
364
+ for (const [key, entryValue] of Object.entries(resolved)) {
365
+ ;(placeholder as Record<string, unknown>)[key] = entryValue
366
+ }
367
+ }
368
+
369
+ collect(value, new WeakSet<object>())
370
+
371
+ for (const id of rawValuesById.keys()) {
372
+ ensurePlaceholder(id)
373
+ }
374
+
375
+ const visitedIds = new Set<number>()
376
+ for (const id of rawValuesById.keys()) {
377
+ fillPlaceholder(id, visitedIds)
378
+ }
379
+
380
+ return resolve(value) as T
381
+ }
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, it } from "vitest"
1
+ import { beforeEach, describe, expect, it } from "vitest"
2
2
  import { z } from "zod"
3
3
  import { defineComponent } from "./component"
4
4
  import { defineEntity } from "./entity"
@@ -25,7 +25,7 @@ describe("defineComponent", () => {
25
25
 
26
26
  it("should return a component with args", () => {
27
27
  const virtualMachine = defineComponent({
28
- type: "proxmox.virtual_machine.v1",
28
+ type: "proxmox.virtual-machine.v1",
29
29
  args: {
30
30
  cores: z.number(),
31
31
  },
@@ -41,9 +41,21 @@ describe("defineComponent", () => {
41
41
  })
42
42
 
43
43
  it("should return a component with inputs", () => {
44
+ const resource = defineEntity({
45
+ type: "common.resource.v1",
46
+
47
+ schema: z.object({
48
+ id: z.string(),
49
+ }),
50
+ })
51
+
44
52
  const server = defineEntity({
45
53
  type: "common.server.v1",
46
54
 
55
+ includes: {
56
+ resource,
57
+ },
58
+
47
59
  schema: z.object({
48
60
  endpoint: z.string(),
49
61
  }),
@@ -55,6 +67,24 @@ describe("defineComponent", () => {
55
67
  },
56
68
  })
57
69
 
70
+ expect(server.model.directInclusions).toEqual([
71
+ {
72
+ field: "resource",
73
+ type: "common.resource.v1",
74
+ required: true,
75
+ multiple: false,
76
+ },
77
+ ])
78
+
79
+ const embeddedResult = server.schema.safeParse({
80
+ endpoint: "127.0.0.1",
81
+ resource: {
82
+ id: "test",
83
+ },
84
+ })
85
+
86
+ expect(embeddedResult.success).toBe(true)
87
+
58
88
  const cluster = defineComponent({
59
89
  type: "talos.cluster.v1",
60
90
  inputs: {
@@ -113,6 +143,52 @@ describe("defineComponent", () => {
113
143
  })
114
144
  })
115
145
 
146
+ it("should support entities with intersection schemas", () => {
147
+ const resource = defineEntity({
148
+ type: "common.resource.v1",
149
+
150
+ schema: z.object({
151
+ id: z.string(),
152
+ }),
153
+ })
154
+
155
+ const tagged = z.object({
156
+ tag: z.string(),
157
+ })
158
+
159
+ const server = defineEntity({
160
+ type: "common.server.v1",
161
+
162
+ includes: {
163
+ resource: { entity: resource, required: false, multiple: true },
164
+ },
165
+
166
+ schema: z.intersection(
167
+ tagged,
168
+ z.object({
169
+ endpoint: z.string(),
170
+ }),
171
+ ),
172
+ })
173
+
174
+ expect(server.model.directInclusions).toEqual([
175
+ {
176
+ field: "resource",
177
+ type: "common.resource.v1",
178
+ required: false,
179
+ multiple: true,
180
+ },
181
+ ])
182
+
183
+ expect(
184
+ server.schema.safeParse({
185
+ tag: "prod",
186
+ endpoint: "127.0.0.1",
187
+ "common.resource.v1": [{ id: "test" }],
188
+ }).success,
189
+ ).toBe(true)
190
+ })
191
+
116
192
  it("should return a component with args, inputs and outputs", () => {
117
193
  const server = defineEntity({
118
194
  type: "common.server.v1",