@highstate/contract 0.18.0 → 0.20.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.
- package/dist/index.js +682 -426
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/component.spec.ts +912 -3
- package/src/component.ts +524 -89
- package/src/cuidv2d.spec.ts +39 -0
- package/src/cuidv2d.ts +54 -0
- package/src/entity.ts +321 -81
- package/src/evaluation.ts +62 -12
- package/src/index.ts +9 -1
- package/src/instance-input.ts +219 -0
- package/src/instance.ts +312 -63
- package/src/json-schema.ts +115 -0
- package/src/pulumi.ts +45 -16
- package/src/unit.ts +26 -10
- package/src/compaction.spec.ts +0 -215
- package/src/compaction.ts +0 -381
package/src/unit.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
type ComponentInputSpec,
|
|
14
14
|
type ComponentModel,
|
|
15
15
|
type ComponentOptions,
|
|
16
|
+
type ComponentOutputOptions,
|
|
17
|
+
type ComponentOutputOptionsToSpec,
|
|
16
18
|
componentArgumentSchema,
|
|
17
19
|
componentModelSchema,
|
|
18
20
|
defineComponent,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
type ToFullComponentArgumentOptions,
|
|
23
25
|
toFullComponentArgumentOptions,
|
|
24
26
|
} from "./component"
|
|
27
|
+
import { boundaryInput } from "./evaluation"
|
|
25
28
|
|
|
26
29
|
export const componentSecretSchema = componentArgumentSchema.extend({
|
|
27
30
|
/**
|
|
@@ -48,7 +51,7 @@ type UnitOptions<
|
|
|
48
51
|
TType extends VersionedName,
|
|
49
52
|
TArgs extends Record<string, ComponentArgumentOptions>,
|
|
50
53
|
TInputs extends Record<string, ComponentInputOptions>,
|
|
51
|
-
TOutputs extends Record<string,
|
|
54
|
+
TOutputs extends Record<string, ComponentOutputOptions<TInputs>>,
|
|
52
55
|
TSecrets extends Record<string, ComponentSecretOptions>,
|
|
53
56
|
> = Omit<ComponentOptions<TType, TArgs, TInputs, TOutputs>, "create"> & {
|
|
54
57
|
source: UnitSource
|
|
@@ -97,7 +100,12 @@ export type Unit<
|
|
|
97
100
|
TInputs extends Record<string, ComponentInputSpec> = Record<string, never>,
|
|
98
101
|
TOutputs extends Record<string, ComponentInputSpec> = Record<string, never>,
|
|
99
102
|
TSecrets extends Record<string, unknown> = Record<string, never>,
|
|
100
|
-
|
|
103
|
+
TInputOptions extends Record<string, ComponentInputOptions> = Record<string, never>,
|
|
104
|
+
TOutputOptions extends Record<string, ComponentOutputOptions<TInputOptions>> = Record<
|
|
105
|
+
string,
|
|
106
|
+
never
|
|
107
|
+
>,
|
|
108
|
+
> = Component<TType, TArgs, TInputs, TOutputs, "unit", TInputOptions, TOutputOptions> & {
|
|
101
109
|
/**
|
|
102
110
|
* Holds the type of the unit secrets.
|
|
103
111
|
*
|
|
@@ -115,7 +123,7 @@ export function defineUnit<
|
|
|
115
123
|
TType extends VersionedName = VersionedName,
|
|
116
124
|
TArgs extends Record<string, ComponentArgumentOptions> = Record<string, never>,
|
|
117
125
|
TInputs extends Record<string, ComponentInputOptions> = Record<string, never>,
|
|
118
|
-
TOutputs extends Record<string,
|
|
126
|
+
TOutputs extends Record<string, ComponentOutputOptions<TInputs>> = Record<string, never>,
|
|
119
127
|
TSecrets extends Record<string, ComponentSecretOptions> = Record<string, never>,
|
|
120
128
|
>(
|
|
121
129
|
options: UnitOptions<TType, TArgs, TInputs, TOutputs, TSecrets>,
|
|
@@ -123,26 +131,34 @@ export function defineUnit<
|
|
|
123
131
|
TType,
|
|
124
132
|
{ [K in keyof TArgs]: ComponentArgumentOptionsToSchema<TArgs[K]> },
|
|
125
133
|
{ [K in keyof TInputs]: ComponentInputOptionsToSpec<TInputs[K]> },
|
|
126
|
-
{ [K in keyof TOutputs]:
|
|
127
|
-
{ [K in keyof TSecrets]: ComponentArgumentOptionsToSchema<TSecrets[K]> }
|
|
134
|
+
{ [K in keyof TOutputs]: ComponentOutputOptionsToSpec<TOutputs[K], TInputs> },
|
|
135
|
+
{ [K in keyof TSecrets]: ComponentArgumentOptionsToSchema<TSecrets[K]> },
|
|
136
|
+
TInputs,
|
|
137
|
+
TOutputs
|
|
128
138
|
> {
|
|
129
139
|
if (!options.source) {
|
|
130
140
|
throw new Error("Unit source is required")
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
const component = defineComponent<TType, TArgs, TInputs, TOutputs>({
|
|
143
|
+
const component = defineComponent<TType, TArgs, TInputs, TOutputs, "unit">({
|
|
134
144
|
...options,
|
|
135
145
|
[kind]: "unit",
|
|
136
146
|
|
|
137
147
|
create({ id }) {
|
|
138
|
-
const outputs: Record<
|
|
148
|
+
const outputs: Record<
|
|
149
|
+
string,
|
|
150
|
+
InstanceInput & { provided: true; [boundaryInput]: InstanceInput }
|
|
151
|
+
> = {}
|
|
139
152
|
for (const key in options.outputs ?? {}) {
|
|
140
|
-
outputs[key] =
|
|
141
|
-
|
|
153
|
+
outputs[key] = {
|
|
154
|
+
provided: true,
|
|
155
|
+
instanceId: id,
|
|
156
|
+
output: key,
|
|
157
|
+
[boundaryInput]: {
|
|
142
158
|
instanceId: id,
|
|
143
159
|
output: key,
|
|
144
160
|
},
|
|
145
|
-
|
|
161
|
+
}
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
return outputs as any
|
package/src/compaction.spec.ts
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
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
|
-
})
|
package/src/compaction.ts
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
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
|
-
}
|