dsh-ptc-plus 0.4.1 → 0.4.3
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/README.en.md +164 -78
- package/README.md +165 -79
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-icon.svg +5 -0
- package/client.js +854 -336
- package/compiler-core.cjs +161 -157
- package/compiler-core.cjs.map +3 -3
- package/docs/README.md +4 -1
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +8 -7
- package/docs/adr/0008-define-agent-operating-contract.md +2 -2
- package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
- package/docs/adr/0023-global-user-bindings.md +7 -7
- package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +9 -5
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
- package/docs/adr/README.md +4 -4
- package/docs/architecture.md +15 -10
- package/docs/binding-review-placement-design.md +1 -1
- package/docs/client-ui.md +9 -5
- package/docs/durability-design.md +12 -9
- package/docs/installation.md +27 -9
- package/docs/runtime-reference.md +23 -12
- package/docs/semantic-validation.md +7 -3
- package/docs/stateful-binding-updates-proposal.md +8 -4
- package/docs/user-bindings.en.md +10 -7
- package/docs/user-bindings.md +9 -6
- package/docs/value-wire.md +1 -1
- package/docs/verification-optimization-work.md +41 -0
- package/docs/verification.md +53 -2
- package/index.js +39 -34
- package/internal/async-local-storage-intrinsics.js +43 -0
- package/internal/binding-declaration-contract.js +8 -0
- package/internal/binding-source-analysis.js +381 -64
- package/internal/binding-update-policy.js +7 -1
- package/internal/callable-source-facts.js +37 -1
- package/internal/cell-analysis.js +85 -17
- package/internal/compiler-intrinsics.js +9 -43
- package/internal/compiler-module-links.js +7 -4
- package/internal/config-spec.js +1 -1
- package/internal/cordis-tools-owner.js +20 -10
- package/internal/dynamic-environment-integration.js +8 -4
- package/internal/dynamic-environment-runtime.js +24 -3
- package/internal/dynamic-native-calls.js +4 -1
- package/internal/dynamic-native-runtime.js +15 -1
- package/internal/edit-transport-owner.js +80 -20
- package/internal/execution-seam-compat.js +192 -30
- package/internal/failure-reporting.js +38 -30
- package/internal/isolated-worker.js +17 -10
- package/internal/kernel-worker.js +645 -393
- package/internal/message-sources.js +14 -0
- package/internal/module-compilation.js +7 -5
- package/internal/module-transform-contract.js +32 -2
- package/internal/record-utils.js +24 -15
- package/internal/repl-memory-projection.js +10 -0
- package/internal/repl-scope-normalizer.js +72 -35
- package/internal/repl-value-observer.js +145 -46
- package/internal/runtime-config.js +62 -2
- package/internal/runtime-intrinsics.js +104 -0
- package/internal/runtime-messages.js +6 -4
- package/internal/session-cell-executor.js +26 -2
- package/internal/session-journal-recovery.js +267 -67
- package/internal/session-journal-schema.js +30 -13
- package/internal/session-journal.js +485 -44
- package/internal/session-log-view.js +5 -4
- package/internal/session-runtime.js +46 -21
- package/internal/session-state.js +7 -29
- package/internal/settings-compat.js +66 -1
- package/internal/source-position-map.js +38 -0
- package/internal/stateful-root-compiler.js +3 -1
- package/internal/typescript-transform.js +4 -2
- package/internal/user-binding-console-worker.js +115 -41
- package/internal/user-binding-draft-projection.js +3 -2
- package/internal/user-bindings-owner.js +2 -1
- package/internal/user-bindings.js +16 -3
- package/internal/validated-parse-repair.js +6 -2
- package/internal/value-wire.js +189 -94
- package/internal/worker-client.js +99 -8
- package/internal/worker-cwd-virtualization.js +82 -54
- package/internal/worker-environment.js +20 -0
- package/internal/worker-output-capture.js +187 -0
- package/internal/worker-output-fence.js +26 -0
- package/internal/worker-realm-surfaces.js +197 -0
- package/internal/worker-repl-realm.js +140 -0
- package/package.json +37 -4
- package/src/client-activity.js +12 -2
- package/src/client-authoring-view.js +121 -62
- package/src/client-console.js +4 -2
- package/src/client-copy.js +4 -2
- package/src/client-feature-gates.js +7 -9
- package/src/client-host-compat.js +104 -27
- package/src/client-repl-view.js +3 -1
- package/src/client-styles.js +12 -1
- package/src/client-tool-view.js +3 -1
- package/src/client-transport.js +2 -2
- package/src/client-workbench.js +4 -3
- package/src/client.js +56 -17
package/internal/value-wire.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertFields, isRecord } from './record-utils.js'
|
|
2
|
+
import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
|
|
2
3
|
import {
|
|
3
4
|
VALUE_CODEC,
|
|
4
5
|
DEFAULT_VALUE_LIMITS,
|
|
@@ -12,12 +13,24 @@ import {
|
|
|
12
13
|
} from './value-wire-schema.js'
|
|
13
14
|
|
|
14
15
|
export { VALUE_CODEC, DEFAULT_VALUE_LIMITS } from './value-wire-schema.js'
|
|
16
|
+
const { Object, Reflect, Array, Map, Set, WeakMap, WeakSet, TypeError,
|
|
17
|
+
isArray, numberIsFinite, numberIsNaN, numberIsSafeInteger, toBigInt,
|
|
18
|
+
toNumber, toString, regexpTest, mapGet, mapHas, mapSet, setHas, setAdd,
|
|
19
|
+
setDelete, weakMapGet, weakMapSet, weakSetHas, weakSetAdd,
|
|
20
|
+
appendArray, popArray, join } = internal
|
|
15
21
|
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
22
|
+
const intrinsicFunctionToString = Function.prototype.toString
|
|
23
|
+
const reflectApply = Reflect.apply
|
|
24
|
+
const bufferByteLength = internal.bufferByteLength
|
|
25
|
+
const jsonStringify = JSON.stringify
|
|
16
26
|
|
|
17
27
|
function limitsOf(options = {}) {
|
|
18
28
|
const limits = { ...DEFAULT_VALUE_LIMITS, ...options }
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
const entries = Object.entries(limits)
|
|
30
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
31
|
+
const name = entries[index][0]
|
|
32
|
+
const value = entries[index][1]
|
|
33
|
+
if (!numberIsSafeInteger(value) || value < 1) throw new TypeError(`invalid PTC value limit ${name}`)
|
|
21
34
|
}
|
|
22
35
|
return limits
|
|
23
36
|
}
|
|
@@ -28,7 +41,7 @@ function hasIntrinsicConstructor(prototype, name) {
|
|
|
28
41
|
try {
|
|
29
42
|
return constructor.name === name
|
|
30
43
|
&& constructor.prototype === prototype
|
|
31
|
-
&&
|
|
44
|
+
&& reflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`
|
|
32
45
|
} catch {
|
|
33
46
|
return false
|
|
34
47
|
}
|
|
@@ -47,7 +60,7 @@ function plainObjectPrototype(value) {
|
|
|
47
60
|
|
|
48
61
|
function hasPlainArrayPrototype(value) {
|
|
49
62
|
const prototype = Object.getPrototypeOf(value)
|
|
50
|
-
if (!
|
|
63
|
+
if (!isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
|
|
51
64
|
const objectPrototype = Object.getPrototypeOf(prototype)
|
|
52
65
|
return objectPrototype !== null && typeof objectPrototype === 'object'
|
|
53
66
|
&& isIntrinsicObjectPrototype(objectPrototype)
|
|
@@ -58,7 +71,7 @@ function invalid(path, detail) {
|
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
function stringBytes(value) {
|
|
61
|
-
return
|
|
74
|
+
return bufferByteLength(value, 'utf8')
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
/** Encode one supported JavaScript graph into the project-owned JSON-safe wire. */
|
|
@@ -68,6 +81,7 @@ export function encodeValue(value, options = {}) {
|
|
|
68
81
|
const pending = []
|
|
69
82
|
const nodes = []
|
|
70
83
|
let edgeCount = 0
|
|
84
|
+
let arraySlotCount = 0
|
|
71
85
|
let textBytes = 0
|
|
72
86
|
|
|
73
87
|
const accountText = (text, path) => {
|
|
@@ -82,7 +96,7 @@ export function encodeValue(value, options = {}) {
|
|
|
82
96
|
return current
|
|
83
97
|
}
|
|
84
98
|
if (typeof current === 'number') {
|
|
85
|
-
if (
|
|
99
|
+
if (numberIsNaN(current)) return { tag: 'number', value: 'nan' }
|
|
86
100
|
if (current === Infinity) return { tag: 'number', value: 'infinity' }
|
|
87
101
|
if (current === -Infinity) return { tag: 'number', value: '-infinity' }
|
|
88
102
|
if (Object.is(current, -0)) return { tag: 'number', value: '-0' }
|
|
@@ -90,20 +104,20 @@ export function encodeValue(value, options = {}) {
|
|
|
90
104
|
}
|
|
91
105
|
if (current === undefined) return { tag: 'undefined' }
|
|
92
106
|
if (typeof current === 'bigint') {
|
|
93
|
-
const text =
|
|
107
|
+
const text = toString(current)
|
|
94
108
|
const digits = text[0] === '-' ? text.length - 1 : text.length
|
|
95
109
|
if (digits > limits.maxBigIntDigits) invalid(path, `BigInt exceeds ${limits.maxBigIntDigits} digits`)
|
|
96
110
|
accountText(text, path)
|
|
97
111
|
return { tag: 'bigint', value: text }
|
|
98
112
|
}
|
|
99
113
|
if (typeof current !== 'object') invalid(path, typeof current)
|
|
100
|
-
const prior = seen
|
|
114
|
+
const prior = weakMapGet(seen, current)
|
|
101
115
|
if (prior !== undefined) return { tag: 'reference', index: prior }
|
|
102
116
|
if (nodes.length >= limits.maxNodes) invalid(path, `node budget exceeds ${limits.maxNodes}`)
|
|
103
117
|
const index = nodes.length
|
|
104
|
-
seen
|
|
105
|
-
nodes
|
|
106
|
-
pending
|
|
118
|
+
weakMapSet(seen, current, index)
|
|
119
|
+
appendArray(nodes, undefined)
|
|
120
|
+
appendArray(pending, { value: current, index, path })
|
|
107
121
|
return { tag: 'reference', index }
|
|
108
122
|
}
|
|
109
123
|
|
|
@@ -111,24 +125,30 @@ export function encodeValue(value, options = {}) {
|
|
|
111
125
|
for (let cursor = 0; cursor < pending.length; cursor += 1) {
|
|
112
126
|
const item = pending[cursor]
|
|
113
127
|
const current = item.value
|
|
114
|
-
if (
|
|
128
|
+
if (isArray(current)) {
|
|
115
129
|
if (!hasPlainArrayPrototype(current)) invalid(item.path, 'non-plain array')
|
|
116
130
|
if (current.length > limits.maxArrayLength) invalid(item.path, `array length exceeds ${limits.maxArrayLength}`)
|
|
131
|
+
arraySlotCount += current.length
|
|
132
|
+
if (arraySlotCount > limits.maxArrayLength) {
|
|
133
|
+
invalid(item.path, `array slot budget exceeds ${limits.maxArrayLength}`)
|
|
134
|
+
}
|
|
117
135
|
const entries = []
|
|
118
|
-
|
|
136
|
+
const keys = Reflect.ownKeys(current)
|
|
137
|
+
for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
|
|
138
|
+
const key = keys[keyIndex]
|
|
119
139
|
if (key === 'length') continue
|
|
120
|
-
if (typeof key !== 'string' ||
|
|
140
|
+
if (typeof key !== 'string' || !regexpTest(/^(0|[1-9][0-9]*)$/, key)) {
|
|
121
141
|
invalid(item.path, 'array has a symbol or non-index property')
|
|
122
142
|
}
|
|
123
|
-
const index =
|
|
124
|
-
if (!
|
|
143
|
+
const index = toNumber(key)
|
|
144
|
+
if (!numberIsSafeInteger(index) || index >= current.length) invalid(`${item.path}[${key}]`, 'invalid array index')
|
|
125
145
|
const descriptor = Object.getOwnPropertyDescriptor(current, key)
|
|
126
146
|
if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
|
|
127
147
|
invalid(`${item.path}[${key}]`, 'array index must be an enumerable data property')
|
|
128
148
|
}
|
|
129
149
|
edgeCount += 1
|
|
130
150
|
if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
|
|
131
|
-
entries
|
|
151
|
+
appendArray(entries, [index, atom(descriptor.value, `${item.path}[${key}]`)])
|
|
132
152
|
}
|
|
133
153
|
nodes[item.index] = { type: 'array', length: current.length, entries }
|
|
134
154
|
continue
|
|
@@ -137,7 +157,9 @@ export function encodeValue(value, options = {}) {
|
|
|
137
157
|
const prototype = plainObjectPrototype(current)
|
|
138
158
|
if (prototype === undefined) invalid(item.path, 'non-plain object')
|
|
139
159
|
const entries = []
|
|
140
|
-
|
|
160
|
+
const keys = Reflect.ownKeys(current)
|
|
161
|
+
for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
|
|
162
|
+
const key = keys[keyIndex]
|
|
141
163
|
if (typeof key !== 'string') invalid(item.path, 'object has a symbol key')
|
|
142
164
|
const descriptor = Object.getOwnPropertyDescriptor(current, key)
|
|
143
165
|
if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
|
|
@@ -146,14 +168,14 @@ export function encodeValue(value, options = {}) {
|
|
|
146
168
|
accountText(key, `${item.path}.${key}`)
|
|
147
169
|
edgeCount += 1
|
|
148
170
|
if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
|
|
149
|
-
entries
|
|
171
|
+
appendArray(entries, [key, atom(descriptor.value, `${item.path}.${key}`)])
|
|
150
172
|
}
|
|
151
173
|
nodes[item.index] = { type: 'object', prototype, entries }
|
|
152
174
|
}
|
|
153
175
|
return { codec: VALUE_CODEC, root, nodes }
|
|
154
176
|
}
|
|
155
177
|
|
|
156
|
-
function
|
|
178
|
+
function validateAtom(atom, nodeCount, limits, budget, path) {
|
|
157
179
|
if (atom === null || typeof atom === 'boolean') return atom
|
|
158
180
|
if (typeof atom === 'string') {
|
|
159
181
|
budget.textBytes += stringBytes(atom)
|
|
@@ -161,114 +183,176 @@ function decodeAtom(atom, nodes, limits, budget, path) {
|
|
|
161
183
|
return atom
|
|
162
184
|
}
|
|
163
185
|
if (typeof atom === 'number') {
|
|
164
|
-
if (!
|
|
186
|
+
if (!numberIsFinite(atom) || Object.is(atom, -0)) throw new TypeError(`invalid PTC value number at ${path}`)
|
|
165
187
|
return atom
|
|
166
188
|
}
|
|
167
189
|
if (!isRecord(atom) || typeof atom.tag !== 'string') throw new TypeError(`invalid PTC value atom at ${path}`)
|
|
168
190
|
if (atom.tag === 'undefined') {
|
|
169
191
|
assertFields(atom, UNDEFINED_FIELDS, 'PTC undefined atom')
|
|
170
|
-
return undefined
|
|
192
|
+
return { tag: 'undefined' }
|
|
171
193
|
}
|
|
172
194
|
if (atom.tag === 'number') {
|
|
173
195
|
assertFields(atom, NUMBER_FIELDS, 'PTC number atom')
|
|
174
|
-
if (atom.value === 'nan'
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
196
|
+
if (atom.value === 'nan' || atom.value === 'infinity'
|
|
197
|
+
|| atom.value === '-infinity' || atom.value === '-0') {
|
|
198
|
+
return { tag: 'number', value: atom.value }
|
|
199
|
+
}
|
|
178
200
|
throw new TypeError('invalid PTC special number')
|
|
179
201
|
}
|
|
180
202
|
if (atom.tag === 'bigint') {
|
|
181
203
|
assertFields(atom, BIGINT_FIELDS, 'PTC BigInt atom')
|
|
182
|
-
if (typeof atom.value !== 'string' ||
|
|
204
|
+
if (typeof atom.value !== 'string' || !regexpTest(/^(0|-[1-9][0-9]*|[1-9][0-9]*)$/, atom.value)) {
|
|
183
205
|
throw new TypeError('invalid PTC BigInt')
|
|
184
206
|
}
|
|
185
207
|
const digits = atom.value[0] === '-' ? atom.value.length - 1 : atom.value.length
|
|
186
208
|
if (digits > limits.maxBigIntDigits) throw new TypeError('PTC BigInt digit budget exceeded')
|
|
187
209
|
budget.textBytes += stringBytes(atom.value)
|
|
188
210
|
if (budget.textBytes > limits.maxStringBytes) throw new TypeError('PTC value string budget exceeded')
|
|
189
|
-
return
|
|
211
|
+
return { tag: 'bigint', value: atom.value }
|
|
190
212
|
}
|
|
191
213
|
if (atom.tag === 'reference') {
|
|
192
214
|
assertFields(atom, REFERENCE_FIELDS, 'PTC reference atom')
|
|
193
|
-
if (!
|
|
215
|
+
if (!numberIsSafeInteger(atom.index) || atom.index < 0 || atom.index >= nodeCount) {
|
|
194
216
|
throw new TypeError('dangling PTC value reference')
|
|
195
217
|
}
|
|
196
|
-
|
|
197
|
-
|
|
218
|
+
if (atom.index >= budget.nextReference) {
|
|
219
|
+
if (atom.index !== budget.nextReference) throw new TypeError('non-canonical PTC value envelope')
|
|
220
|
+
budget.nextReference += 1
|
|
221
|
+
}
|
|
222
|
+
return { tag: 'reference', index: atom.index }
|
|
198
223
|
}
|
|
199
|
-
throw new TypeError(`unknown PTC value atom tag ${
|
|
224
|
+
throw new TypeError(`unknown PTC value atom tag ${jsonStringify(atom.tag)}`)
|
|
200
225
|
}
|
|
201
226
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
227
|
+
function canonicalArrayIndex(key) {
|
|
228
|
+
if (!regexpTest(/^(0|[1-9][0-9]*)$/, key)) return undefined
|
|
229
|
+
const index = toNumber(key)
|
|
230
|
+
return numberIsSafeInteger(index) && index < 0xffffffff && toString(index) === key ? index : undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const MAX_ARRAY_LENGTH = 0xffffffff
|
|
234
|
+
|
|
235
|
+
/** Validate and detach the complete envelope before allocating hydrated arrays. */
|
|
236
|
+
function validateCanonicalWire(wire, limits) {
|
|
205
237
|
assertFields(wire, ENVELOPE_FIELDS, 'PTC value envelope')
|
|
206
|
-
if (wire.codec !== VALUE_CODEC || !
|
|
238
|
+
if (wire.codec !== VALUE_CODEC || !isArray(wire.nodes)) throw new TypeError('invalid PTC value codec')
|
|
207
239
|
if (wire.nodes.length > limits.maxNodes) throw new TypeError('PTC value node budget exceeded')
|
|
208
|
-
const
|
|
240
|
+
const nodes = new Array(wire.nodes.length)
|
|
241
|
+
let arraySlots = 0
|
|
209
242
|
for (let index = 0; index < wire.nodes.length; index += 1) {
|
|
210
243
|
const node = wire.nodes[index]
|
|
211
244
|
if (!isRecord(node)) throw new TypeError(`invalid PTC value node ${index}`)
|
|
212
245
|
if (node.type === 'array') {
|
|
213
246
|
assertFields(node, ARRAY_NODE_FIELDS, `PTC array node ${index}`)
|
|
214
|
-
if (!
|
|
247
|
+
if (!numberIsSafeInteger(node.length) || node.length < 0
|
|
248
|
+
|| node.length > MAX_ARRAY_LENGTH || node.length > limits.maxArrayLength) {
|
|
215
249
|
throw new TypeError(`invalid PTC array length at node ${index}`)
|
|
216
250
|
}
|
|
217
|
-
if (!
|
|
218
|
-
|
|
251
|
+
if (!isArray(node.entries)) throw new TypeError(`invalid PTC array entries at node ${index}`)
|
|
252
|
+
arraySlots += node.length
|
|
253
|
+
if (arraySlots > limits.maxArrayLength) throw new TypeError('PTC value array slot budget exceeded')
|
|
254
|
+
nodes[index] = { type: 'array', length: node.length, entries: node.entries }
|
|
219
255
|
} else if (node.type === 'object') {
|
|
220
256
|
assertFields(node, OBJECT_NODE_FIELDS, `PTC object node ${index}`)
|
|
221
|
-
if (
|
|
257
|
+
if ((node.prototype !== 'object' && node.prototype !== 'null') || !isArray(node.entries)) {
|
|
222
258
|
throw new TypeError(`invalid PTC object node ${index}`)
|
|
223
259
|
}
|
|
224
|
-
|
|
260
|
+
nodes[index] = { type: 'object', prototype: node.prototype, entries: node.entries }
|
|
225
261
|
} else {
|
|
226
262
|
throw new TypeError(`unknown PTC value node type at ${index}`)
|
|
227
263
|
}
|
|
228
264
|
}
|
|
229
265
|
|
|
230
|
-
const budget = { edges: 0, textBytes: 0,
|
|
231
|
-
const root =
|
|
232
|
-
for (let nodeIndex = 0; nodeIndex <
|
|
233
|
-
const node =
|
|
234
|
-
const
|
|
266
|
+
const budget = { edges: 0, textBytes: 0, nextReference: 0 }
|
|
267
|
+
const root = validateAtom(wire.root, nodes.length, limits, budget, '$')
|
|
268
|
+
for (let nodeIndex = 0; nodeIndex < budget.nextReference; nodeIndex += 1) {
|
|
269
|
+
const node = nodes[nodeIndex]
|
|
270
|
+
const entries = []
|
|
235
271
|
if (node.type === 'array') {
|
|
236
272
|
let previous = -1
|
|
237
|
-
for (
|
|
238
|
-
|
|
273
|
+
for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
|
|
274
|
+
const entry = node.entries[entryIndex]
|
|
275
|
+
if (!isArray(entry) || entry.length !== 2 || !numberIsSafeInteger(entry[0])
|
|
239
276
|
|| entry[0] <= previous || entry[0] < 0 || entry[0] >= node.length) {
|
|
240
277
|
throw new TypeError(`invalid PTC array entry at node ${nodeIndex}`)
|
|
241
278
|
}
|
|
242
279
|
previous = entry[0]
|
|
243
280
|
budget.edges += 1
|
|
244
281
|
if (budget.edges > limits.maxEdges) throw new TypeError('PTC value edge budget exceeded')
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
enumerable: true, configurable: true, writable: true,
|
|
248
|
-
})
|
|
282
|
+
appendArray(entries, [entry[0], validateAtom(entry[1], nodes.length, limits, budget,
|
|
283
|
+
`$nodes[${nodeIndex}][${entry[0]}]`)])
|
|
249
284
|
}
|
|
285
|
+
nodes[nodeIndex] = { type: 'array', length: node.length, entries }
|
|
250
286
|
continue
|
|
251
287
|
}
|
|
252
288
|
const keys = new Set()
|
|
253
|
-
|
|
254
|
-
|
|
289
|
+
let previousIndex = -1
|
|
290
|
+
let ordinaryKeySeen = false
|
|
291
|
+
for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
|
|
292
|
+
const entry = node.entries[entryIndex]
|
|
293
|
+
if (!isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string' || setHas(keys, entry[0])) {
|
|
255
294
|
throw new TypeError(`invalid PTC object entry at node ${nodeIndex}`)
|
|
256
295
|
}
|
|
257
|
-
|
|
296
|
+
const index = canonicalArrayIndex(entry[0])
|
|
297
|
+
if (index === undefined) ordinaryKeySeen = true
|
|
298
|
+
else if (ordinaryKeySeen || index <= previousIndex) {
|
|
299
|
+
throw new TypeError('non-canonical PTC value envelope')
|
|
300
|
+
} else previousIndex = index
|
|
301
|
+
setAdd(keys, entry[0])
|
|
258
302
|
budget.textBytes += stringBytes(entry[0])
|
|
259
303
|
if (budget.textBytes > limits.maxStringBytes) throw new TypeError('PTC value string budget exceeded')
|
|
260
304
|
budget.edges += 1
|
|
261
305
|
if (budget.edges > limits.maxEdges) throw new TypeError('PTC value edge budget exceeded')
|
|
306
|
+
appendArray(entries, [entry[0], validateAtom(entry[1], nodes.length, limits, budget,
|
|
307
|
+
`$nodes[${nodeIndex}].${entry[0]}`)])
|
|
308
|
+
}
|
|
309
|
+
nodes[nodeIndex] = { type: 'object', prototype: node.prototype, entries }
|
|
310
|
+
}
|
|
311
|
+
if (budget.nextReference !== nodes.length) throw new TypeError('PTC value envelope contains unreachable nodes')
|
|
312
|
+
const normalized = { codec: VALUE_CODEC, root, nodes }
|
|
313
|
+
const serialization = jsonStringify(normalized)
|
|
314
|
+
if (serialization !== jsonStringify(wire)) throw new TypeError('non-canonical PTC value envelope')
|
|
315
|
+
return { wire: normalized, serialization }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function hydrateAtom(atom, nodes) {
|
|
319
|
+
if (atom === null || typeof atom === 'boolean' || typeof atom === 'string' || typeof atom === 'number') return atom
|
|
320
|
+
if (atom.tag === 'undefined') return undefined
|
|
321
|
+
if (atom.tag === 'number') {
|
|
322
|
+
if (atom.value === 'nan') return NaN
|
|
323
|
+
if (atom.value === 'infinity') return Infinity
|
|
324
|
+
if (atom.value === '-infinity') return -Infinity
|
|
325
|
+
return -0
|
|
326
|
+
}
|
|
327
|
+
if (atom.tag === 'bigint') return toBigInt(atom.value)
|
|
328
|
+
return nodes[atom.index]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Validate, hydrate, and own one canonical PTC value envelope without recursive stack growth. */
|
|
332
|
+
function decodeCanonicalValue(wire, options = {}) {
|
|
333
|
+
const limits = limitsOf(options)
|
|
334
|
+
const validated = validateCanonicalWire(wire, limits)
|
|
335
|
+
const targets = new Array(validated.wire.nodes.length)
|
|
336
|
+
for (let index = 0; index < validated.wire.nodes.length; index += 1) {
|
|
337
|
+
const node = validated.wire.nodes[index]
|
|
338
|
+
targets[index] = node.type === 'array' ? new Array(node.length)
|
|
339
|
+
: node.prototype === 'null' ? Object.create(null) : {}
|
|
340
|
+
}
|
|
341
|
+
const root = hydrateAtom(validated.wire.root, targets)
|
|
342
|
+
for (let nodeIndex = 0; nodeIndex < validated.wire.nodes.length; nodeIndex += 1) {
|
|
343
|
+
const node = validated.wire.nodes[nodeIndex]
|
|
344
|
+
const target = targets[nodeIndex]
|
|
345
|
+
for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
|
|
346
|
+
const entry = node.entries[entryIndex]
|
|
262
347
|
Object.defineProperty(target, entry[0], {
|
|
263
|
-
value:
|
|
348
|
+
value: hydrateAtom(entry[1], targets),
|
|
264
349
|
enumerable: true, configurable: true, writable: true,
|
|
265
350
|
})
|
|
266
351
|
}
|
|
267
352
|
}
|
|
268
|
-
if (budget.reachable.size !== wire.nodes.length) throw new TypeError('PTC value envelope contains unreachable nodes')
|
|
269
353
|
const canonical = encodeValue(root, limits)
|
|
270
|
-
const serialization =
|
|
271
|
-
if (serialization !==
|
|
354
|
+
const serialization = jsonStringify(canonical)
|
|
355
|
+
if (serialization !== validated.serialization) throw new TypeError('non-canonical PTC value envelope')
|
|
272
356
|
return { value: root, wire: canonical, serialization }
|
|
273
357
|
}
|
|
274
358
|
|
|
@@ -295,31 +379,32 @@ export function isPlainJsonTree(value) {
|
|
|
295
379
|
const pending = [value]
|
|
296
380
|
const seen = new WeakSet()
|
|
297
381
|
while (pending.length > 0) {
|
|
298
|
-
const current = pending
|
|
382
|
+
const current = popArray(pending)
|
|
299
383
|
if (current === null || typeof current === 'string' || typeof current === 'boolean') continue
|
|
300
|
-
if (typeof current === 'number' &&
|
|
384
|
+
if (typeof current === 'number' && numberIsFinite(current) && !Object.is(current, -0)) continue
|
|
301
385
|
if (typeof current !== 'object') return false
|
|
302
|
-
if (seen
|
|
303
|
-
seen
|
|
304
|
-
if (
|
|
386
|
+
if (weakSetHas(seen, current)) return false
|
|
387
|
+
weakSetAdd(seen, current)
|
|
388
|
+
if (isArray(current)) {
|
|
305
389
|
for (let index = 0; index < current.length; index += 1) {
|
|
306
390
|
if (!Object.hasOwn(current, index)) return false
|
|
307
|
-
pending
|
|
391
|
+
appendArray(pending, current[index])
|
|
308
392
|
}
|
|
309
393
|
} else {
|
|
310
|
-
|
|
394
|
+
const keys = Object.keys(current)
|
|
395
|
+
for (let index = 0; index < keys.length; index += 1) appendArray(pending, current[keys[index]])
|
|
311
396
|
}
|
|
312
397
|
}
|
|
313
398
|
return true
|
|
314
399
|
}
|
|
315
400
|
|
|
316
401
|
function renderKey(key) {
|
|
317
|
-
return IDENTIFIER
|
|
402
|
+
return regexpTest(IDENTIFIER, key) ? key : jsonStringify(key)
|
|
318
403
|
}
|
|
319
404
|
|
|
320
405
|
function atomText(atom) {
|
|
321
|
-
if (atom === null || typeof atom === 'boolean' || typeof atom === 'number') return
|
|
322
|
-
if (typeof atom === 'string') return
|
|
406
|
+
if (atom === null || typeof atom === 'boolean' || typeof atom === 'number') return toString(atom)
|
|
407
|
+
if (typeof atom === 'string') return jsonStringify(atom)
|
|
323
408
|
if (atom.tag === 'undefined') return 'undefined'
|
|
324
409
|
if (atom.tag === 'number') {
|
|
325
410
|
return atom.value === 'nan' ? 'NaN'
|
|
@@ -337,16 +422,20 @@ export function renderValueWire(wire, options = {}) {
|
|
|
337
422
|
}
|
|
338
423
|
|
|
339
424
|
function renderCanonicalValue(normalized, limits) {
|
|
340
|
-
const references = new Array(normalized.nodes.length)
|
|
425
|
+
const references = new Array(normalized.nodes.length)
|
|
426
|
+
for (let index = 0; index < references.length; index += 1) references[index] = 0
|
|
341
427
|
const count = (atom) => {
|
|
342
428
|
if (isRecord(atom) && atom.tag === 'reference') references[atom.index] += 1
|
|
343
429
|
}
|
|
344
430
|
count(normalized.root)
|
|
345
|
-
for (
|
|
431
|
+
for (let nodeIndex = 0; nodeIndex < normalized.nodes.length; nodeIndex += 1) {
|
|
432
|
+
const entries = normalized.nodes[nodeIndex].entries
|
|
433
|
+
for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 1) count(entries[entryIndex][1])
|
|
434
|
+
}
|
|
346
435
|
const labels = new Map()
|
|
347
436
|
let nextLabel = 0
|
|
348
437
|
for (let index = 0; index < references.length; index += 1) {
|
|
349
|
-
if (references[index] > 1) labels
|
|
438
|
+
if (references[index] > 1) mapSet(labels, index, ++nextLabel)
|
|
350
439
|
}
|
|
351
440
|
|
|
352
441
|
const chunks = []
|
|
@@ -354,19 +443,19 @@ function renderCanonicalValue(normalized, limits) {
|
|
|
354
443
|
const append = (text) => {
|
|
355
444
|
bytes += stringBytes(text)
|
|
356
445
|
if (bytes > limits.maxStringBytes) throw new TypeError(`rendered PTC value exceeds ${limits.maxStringBytes} bytes`)
|
|
357
|
-
chunks
|
|
446
|
+
appendArray(chunks, text)
|
|
358
447
|
}
|
|
359
448
|
const emitted = new Set()
|
|
360
449
|
const active = new Set()
|
|
361
450
|
const tasks = [{ kind: 'atom', atom: normalized.root }]
|
|
362
451
|
while (tasks.length > 0) {
|
|
363
|
-
const task = tasks
|
|
452
|
+
const task = popArray(tasks)
|
|
364
453
|
if (task.kind === 'text') {
|
|
365
454
|
append(task.text)
|
|
366
455
|
continue
|
|
367
456
|
}
|
|
368
457
|
if (task.kind === 'leave') {
|
|
369
|
-
active
|
|
458
|
+
setDelete(active, task.index)
|
|
370
459
|
continue
|
|
371
460
|
}
|
|
372
461
|
const primitive = atomText(task.atom)
|
|
@@ -375,38 +464,44 @@ function renderCanonicalValue(normalized, limits) {
|
|
|
375
464
|
continue
|
|
376
465
|
}
|
|
377
466
|
const index = task.atom.index
|
|
378
|
-
const label = labels
|
|
379
|
-
if (emitted
|
|
380
|
-
append(active
|
|
467
|
+
const label = mapGet(labels, index)
|
|
468
|
+
if (setHas(emitted, index)) {
|
|
469
|
+
append(setHas(active, index) ? `[Circular *${label}]` : `[Reference *${label}]`)
|
|
381
470
|
continue
|
|
382
471
|
}
|
|
383
|
-
emitted
|
|
384
|
-
active
|
|
472
|
+
setAdd(emitted, index)
|
|
473
|
+
setAdd(active, index)
|
|
385
474
|
if (label !== undefined) append(`<ref *${label}> `)
|
|
386
475
|
const node = normalized.nodes[index]
|
|
387
|
-
tasks
|
|
476
|
+
appendArray(tasks, { kind: 'leave', index })
|
|
388
477
|
if (node.type === 'array') {
|
|
389
478
|
append('[')
|
|
390
|
-
tasks
|
|
391
|
-
const entries = new Map(
|
|
392
|
-
|
|
479
|
+
appendArray(tasks, { kind: 'text', text: ']' })
|
|
480
|
+
const entries = new Map()
|
|
481
|
+
for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
|
|
482
|
+
const entry = node.entries[entryIndex]
|
|
483
|
+
mapSet(entries, entry[0], entry[1])
|
|
484
|
+
}
|
|
485
|
+
if (node.length > 0 && !mapHas(entries, node.length - 1)) appendArray(tasks, { kind: 'text', text: ',' })
|
|
393
486
|
for (let item = node.length - 1; item >= 0; item -= 1) {
|
|
394
|
-
if (item < node.length - 1) tasks
|
|
395
|
-
const value = entries
|
|
396
|
-
if (value !== undefined) tasks
|
|
487
|
+
if (item < node.length - 1) appendArray(tasks, { kind: 'text', text: ', ' })
|
|
488
|
+
const value = mapGet(entries, item)
|
|
489
|
+
if (value !== undefined) appendArray(tasks, { kind: 'atom', atom: value })
|
|
397
490
|
}
|
|
398
491
|
continue
|
|
399
492
|
}
|
|
400
493
|
append('{')
|
|
401
|
-
tasks
|
|
494
|
+
appendArray(tasks, { kind: 'text', text: '}' })
|
|
402
495
|
for (let entryIndex = node.entries.length - 1; entryIndex >= 0; entryIndex -= 1) {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
496
|
+
const entry = node.entries[entryIndex]
|
|
497
|
+
const key = entry[0]
|
|
498
|
+
const value = entry[1]
|
|
499
|
+
if (entryIndex < node.entries.length - 1) appendArray(tasks, { kind: 'text', text: ', ' })
|
|
500
|
+
appendArray(tasks, { kind: 'atom', atom: value })
|
|
501
|
+
appendArray(tasks, { kind: 'text', text: `${renderKey(key)}: ` })
|
|
407
502
|
}
|
|
408
503
|
}
|
|
409
|
-
return
|
|
504
|
+
return join(chunks, '')
|
|
410
505
|
}
|
|
411
506
|
|
|
412
507
|
/**
|