circuitjson-toolkit 1.4.1 → 1.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.
@@ -0,0 +1,201 @@
1
+ import { TOOLKIT_WORKER_PROTOCOL } from './ToolkitWorkerProtocol.mjs'
2
+
3
+ const REFLECT_APPLY = Reflect.apply
4
+ const REFLECT_CONSTRUCT = Reflect.construct
5
+ const REFLECT_OWN_KEYS = Reflect.ownKeys
6
+ const GET_PROTOTYPE = Object.getPrototypeOf
7
+ const GET_DESCRIPTORS = Object.getOwnPropertyDescriptors
8
+ const HAS_OWN = Object.hasOwn
9
+ const OBJECT_PROTOTYPE = Object.prototype
10
+ const WEAK_MAP_GET = WeakMap.prototype.get
11
+ const WEAK_MAP_SET = WeakMap.prototype.set
12
+ const WEAK_MAP_DELETE = WeakMap.prototype.delete
13
+ const WORKER_CONSTRUCTOR = globalThis.Worker
14
+ const WORKER_POST = WORKER_CONSTRUCTOR?.prototype?.postMessage
15
+ const WORKER_TERMINATE = WORKER_CONSTRUCTOR?.prototype?.terminate
16
+ const ADD_EVENT_LISTENER = EventTarget.prototype.addEventListener
17
+ const REMOVE_EVENT_LISTENER = EventTarget.prototype.removeEventListener
18
+ const MESSAGE_DATA = Object.getOwnPropertyDescriptor(
19
+ MessageEvent.prototype,
20
+ 'data'
21
+ )?.get
22
+ const EVENT_CURRENT_TARGET = Object.getOwnPropertyDescriptor(
23
+ Event.prototype,
24
+ 'currentTarget'
25
+ )?.get
26
+ const EVENT_TRUSTED =
27
+ Object.getOwnPropertyDescriptor(new Event('message'), 'isTrusted')?.get ||
28
+ Object.getOwnPropertyDescriptor(Event.prototype, 'isTrusted')?.get
29
+ const ERROR_DATA =
30
+ typeof ErrorEvent === 'function'
31
+ ? Object.getOwnPropertyDescriptor(ErrorEvent.prototype, 'error')?.get
32
+ : null
33
+ const RECEIVED_RESULTS = new WeakMap()
34
+
35
+ /**
36
+ * Keeps native workers and their unexposed structured-clone results private.
37
+ * A custom factory cannot mint the one-use result capabilities created here.
38
+ */
39
+ export class NativeParserWorkerTransport {
40
+ #worker
41
+ #listeners = new Map()
42
+ #nativeListeners = new Map()
43
+
44
+ /**
45
+ * Constructs an isolated transport using the captured native constructor.
46
+ * @param {string | URL} url Worker module URL.
47
+ */
48
+ constructor(url) {
49
+ this.#worker = REFLECT_CONSTRUCT(WORKER_CONSTRUCTOR, [
50
+ url,
51
+ { type: 'module' }
52
+ ])
53
+ for (const type of ['message', 'error', 'messageerror']) {
54
+ const listener = (event) => this.#receive(type, event)
55
+ this.#nativeListeners.set(type, listener)
56
+ REFLECT_APPLY(ADD_EVENT_LISTENER, this.#worker, [type, listener])
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Uses private native transport only while the captured host is unchanged.
62
+ * Replaced or emulated Worker constructors retain generic validation.
63
+ * @param {string | URL} url Worker module URL.
64
+ * @returns {object} Browser-compatible worker transport.
65
+ */
66
+ static create(url) {
67
+ const Constructor = globalThis.Worker
68
+ if (
69
+ Constructor !== WORKER_CONSTRUCTOR ||
70
+ typeof WORKER_POST !== 'function' ||
71
+ typeof WORKER_TERMINATE !== 'function' ||
72
+ !MESSAGE_DATA ||
73
+ !EVENT_CURRENT_TARGET ||
74
+ !EVENT_TRUSTED
75
+ ) {
76
+ return REFLECT_CONSTRUCT(Constructor, [url, { type: 'module' }])
77
+ }
78
+ return new NativeParserWorkerTransport(url)
79
+ }
80
+
81
+ /**
82
+ * Consumes an opaque result exactly once. The returned data has never been
83
+ * exposed outside this transport; exposing it here destroys its capability.
84
+ * @param {unknown} token Result candidate.
85
+ * @returns {{ value: unknown } | null} Privately received data, if proven.
86
+ */
87
+ static consumeResult(token) {
88
+ const received = REFLECT_APPLY(WEAK_MAP_GET, RECEIVED_RESULTS, [token])
89
+ if (!received) return null
90
+ REFLECT_APPLY(WEAK_MAP_DELETE, RECEIVED_RESULTS, [token])
91
+ return received
92
+ }
93
+
94
+ /** @param {string} type Event type. @param {Function} listener Listener. @returns {void} */
95
+ addEventListener(type, listener) {
96
+ if (!this.#listeners.has(type)) this.#listeners.set(type, new Set())
97
+ this.#listeners.get(type).add(listener)
98
+ }
99
+
100
+ /** @param {string} type Event type. @param {Function} listener Listener. @returns {void} */
101
+ removeEventListener(type, listener) {
102
+ this.#listeners.get(type)?.delete(listener)
103
+ }
104
+
105
+ /** @param {unknown} message Request. @param {Transferable[]} [transfer] Transfer list. @returns {void} */
106
+ postMessage(message, transfer = []) {
107
+ REFLECT_APPLY(WORKER_POST, this.#worker, [message, transfer])
108
+ }
109
+
110
+ /** Detaches native listeners and terminates the private worker. @returns {void} */
111
+ terminate() {
112
+ for (const [type, listener] of this.#nativeListeners) {
113
+ REFLECT_APPLY(REMOVE_EVENT_LISTENER, this.#worker, [type, listener])
114
+ }
115
+ this.#nativeListeners.clear()
116
+ this.#listeners.clear()
117
+ REFLECT_APPLY(WORKER_TERMINATE, this.#worker, [])
118
+ }
119
+
120
+ /**
121
+ * Hides the native target and result graph before invoking any consumer.
122
+ * Synthetic events and invalid envelopes never receive a capability.
123
+ * @param {string} type Event type.
124
+ * @param {Event} event Native event.
125
+ * @returns {void}
126
+ */
127
+ #receive(type, event) {
128
+ let forwarded = {}
129
+ if (type === 'message') {
130
+ const data = REFLECT_APPLY(MESSAGE_DATA, event, [])
131
+ forwarded = { data }
132
+ if (
133
+ REFLECT_APPLY(EVENT_TRUSTED, event, []) === true &&
134
+ REFLECT_APPLY(EVENT_CURRENT_TARGET, event, []) === this.#worker
135
+ ) {
136
+ try {
137
+ const message =
138
+ NativeParserWorkerTransport.#resultMessage(data)
139
+ if (message) {
140
+ const token = Object.freeze(Object.create(null))
141
+ REFLECT_APPLY(WEAK_MAP_SET, RECEIVED_RESULTS, [
142
+ token,
143
+ { value: message.value }
144
+ ])
145
+ message.value = token
146
+ forwarded = { data: message }
147
+ }
148
+ } catch {
149
+ // The ordinary client boundary reports malformed envelopes.
150
+ }
151
+ }
152
+ } else if (type === 'error') {
153
+ forwarded = {
154
+ error: ERROR_DATA ? REFLECT_APPLY(ERROR_DATA, event, []) : null
155
+ }
156
+ }
157
+ for (const listener of this.#listeners.get(type) || []) {
158
+ REFLECT_APPLY(listener, undefined, [forwarded])
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Captures only exact native result headers without calling replaceable
164
+ * exported helpers that could introduce caller-owned values into the graph.
165
+ * @param {unknown} data Privately received native event data.
166
+ * @returns {object | null} Result envelope safe to hide behind a capability.
167
+ */
168
+ static #resultMessage(data) {
169
+ if (!data || typeof data !== 'object') return null
170
+ const prototype = GET_PROTOTYPE(data)
171
+ if (prototype !== OBJECT_PROTOTYPE && prototype !== null) return null
172
+ const descriptors = GET_DESCRIPTORS(data)
173
+ if (REFLECT_OWN_KEYS(descriptors).length !== 4) return null
174
+ for (const key of ['protocol', 'type', 'requestId', 'value']) {
175
+ const descriptor = descriptors[key]
176
+ if (
177
+ !descriptor ||
178
+ !HAS_OWN(descriptor, 'value') ||
179
+ descriptor.enumerable !== true
180
+ ) {
181
+ return null
182
+ }
183
+ }
184
+ const protocol = descriptors.protocol.value
185
+ const type = descriptors.type.value
186
+ const requestId = descriptors.requestId.value
187
+ if (
188
+ protocol !== TOOLKIT_WORKER_PROTOCOL ||
189
+ type !== 'result' ||
190
+ typeof requestId !== 'string' ||
191
+ !requestId ||
192
+ requestId.length > 256
193
+ ) {
194
+ return null
195
+ }
196
+ return { protocol, type, requestId, value: descriptors.value.value }
197
+ }
198
+ }
199
+
200
+ Object.freeze(NativeParserWorkerTransport.prototype)
201
+ Object.freeze(NativeParserWorkerTransport)
@@ -3,6 +3,7 @@ import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
3
3
  import { TOOLKIT_WORKER_PROTOCOL } from './ToolkitWorkerProtocol.mjs'
4
4
  import { WorkerRequestData } from './WorkerRequestData.mjs'
5
5
  import { WorkerResponseData } from './WorkerResponseData.mjs'
6
+ import { NativeParserWorkerTransport } from './NativeParserWorkerTransport.mjs'
6
7
 
7
8
  const ABORTED_GETTER = Object.getOwnPropertyDescriptor(
8
9
  AbortSignal.prototype,
@@ -73,14 +74,22 @@ export class ParserWorkerClient {
73
74
  }
74
75
  }
75
76
 
76
- /**
77
- * Returns the process-local default client, creating it lazily.
78
- * @returns {ParserWorkerClient} Default worker client.
79
- */
77
+ /** @returns {ParserWorkerClient} Lazily-created process-local default client. */
80
78
  static defaultClient() {
81
79
  return ParserWorkerClient.#defaultClientFor(null)
82
80
  }
83
81
 
82
+ /**
83
+ * Creates a client whose native worker and received graphs remain private.
84
+ * @param {string | URL} workerUrl Worker module URL.
85
+ * @returns {ParserWorkerClient} Client using native clone provenance.
86
+ */
87
+ static fromWorkerUrl(workerUrl) {
88
+ return new ParserWorkerClient({
89
+ createWorker: () => NativeParserWorkerTransport.create(workerUrl)
90
+ })
91
+ }
92
+
84
93
  /**
85
94
  * Returns the process-local default client for one internal attempt.
86
95
  * @param {object | null} attemptToken Internal attempt identity.
@@ -91,26 +100,15 @@ export class ParserWorkerClient {
91
100
  throw ParserWorkerClient.#unavailableError(attemptToken)
92
101
  }
93
102
  if (!ParserWorkerClient.#defaultClient) {
94
- ParserWorkerClient.#defaultClient = new ParserWorkerClient({
95
- createWorker: () => {
96
- const WorkerConstructor = globalThis.Worker
97
- return Reflect.construct(WorkerConstructor, [
98
- new URL(
99
- '../../workers/parser.worker.mjs',
100
- import.meta.url
101
- ),
102
- { type: 'module' }
103
- ])
104
- }
105
- })
103
+ ParserWorkerClient.#defaultClient =
104
+ ParserWorkerClient.fromWorkerUrl(
105
+ new URL('../../workers/parser.worker.mjs', import.meta.url)
106
+ )
106
107
  }
107
108
  return ParserWorkerClient.#defaultClient
108
109
  }
109
110
 
110
- /**
111
- * Disposes the process-local default worker client when one exists.
112
- * @returns {void}
113
- */
111
+ /** Disposes the process-local default worker client. @returns {void} */
114
112
  static disposeDefault() {
115
113
  ParserWorkerClient.#defaultClient?.dispose()
116
114
  ParserWorkerClient.#defaultClient = null
@@ -0,0 +1,201 @@
1
+ const ARRAY_BUFFER_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
2
+ ArrayBuffer.prototype,
3
+ 'byteLength'
4
+ )?.get
5
+ const ARRAY_BUFFER_RESIZABLE_GETTER = Object.getOwnPropertyDescriptor(
6
+ ArrayBuffer.prototype,
7
+ 'resizable'
8
+ )?.get
9
+ const SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER =
10
+ typeof SharedArrayBuffer === 'function'
11
+ ? Object.getOwnPropertyDescriptor(
12
+ SharedArrayBuffer.prototype,
13
+ 'byteLength'
14
+ )?.get
15
+ : null
16
+ const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype)
17
+ const TYPED_ARRAY_BUFFER_GETTER = Object.getOwnPropertyDescriptor(
18
+ TYPED_ARRAY_PROTOTYPE,
19
+ 'buffer'
20
+ )?.get
21
+ const TYPED_ARRAY_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
22
+ TYPED_ARRAY_PROTOTYPE,
23
+ 'byteLength'
24
+ )?.get
25
+ const TYPED_ARRAY_BYTE_OFFSET_GETTER = Object.getOwnPropertyDescriptor(
26
+ TYPED_ARRAY_PROTOTYPE,
27
+ 'byteOffset'
28
+ )?.get
29
+ const TYPED_ARRAY_TAG_GETTER = Object.getOwnPropertyDescriptor(
30
+ TYPED_ARRAY_PROTOTYPE,
31
+ Symbol.toStringTag
32
+ )?.get
33
+ const DATA_VIEW_BUFFER_GETTER = Object.getOwnPropertyDescriptor(
34
+ DataView.prototype,
35
+ 'buffer'
36
+ )?.get
37
+ const DATA_VIEW_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
38
+ DataView.prototype,
39
+ 'byteLength'
40
+ )?.get
41
+ const DATA_VIEW_BYTE_OFFSET_GETTER = Object.getOwnPropertyDescriptor(
42
+ DataView.prototype,
43
+ 'byteOffset'
44
+ )?.get
45
+ const DATA_VIEW_CONSTRUCTOR = DataView
46
+ const UINT8_ARRAY_CONSTRUCTOR = Uint8Array
47
+ const UINT8_ARRAY_SET = Uint8Array.prototype.set
48
+ const ARRAY_BUFFER_IS_VIEW = ArrayBuffer.isView
49
+ const OBJECT_GET_PROTOTYPE_OF = Object.getPrototypeOf
50
+ const OBJECT_PROTOTYPE = Object.prototype
51
+ const TYPED_ARRAYS = new Map(
52
+ [
53
+ Int8Array,
54
+ Uint8Array,
55
+ Uint8ClampedArray,
56
+ Int16Array,
57
+ Uint16Array,
58
+ Int32Array,
59
+ Uint32Array,
60
+ Float32Array,
61
+ Float64Array,
62
+ typeof BigInt64Array === 'function' ? BigInt64Array : null,
63
+ typeof BigUint64Array === 'function' ? BigUint64Array : null
64
+ ]
65
+ .filter(Boolean)
66
+ .map((Constructor) => [Constructor.name, Constructor])
67
+ )
68
+
69
+ /**
70
+ * Inspects and isolates worker binary data through captured intrinsic slots.
71
+ */
72
+ export class WorkerBinaryData {
73
+ /**
74
+ * Classifies records only in a privately received native clone graph.
75
+ * Generic inputs must still use exact brands, even with these prototypes.
76
+ * @param {object} value Proven native-clone value.
77
+ * @returns {boolean} Whether it has a normalized plain-record prototype.
78
+ */
79
+ static isStandardRecord(value) {
80
+ const prototype = OBJECT_GET_PROTOTYPE_OF(value)
81
+ return prototype === OBJECT_PROTOTYPE || prototype === null
82
+ }
83
+
84
+ /**
85
+ * Recreates one intrinsic view on an exact isolated backing buffer.
86
+ * @param {{ byteLength: number, tag: string }} view View fields.
87
+ * @param {ArrayBuffer} buffer Exact copied buffer.
88
+ * @returns {ArrayBufferView} Recreated view.
89
+ */
90
+ static recreateView(view, buffer) {
91
+ if (view.tag === 'DataView') return new DATA_VIEW_CONSTRUCTOR(buffer)
92
+ const Constructor = TYPED_ARRAYS.get(view.tag)
93
+ if (!Constructor || view.byteLength % Constructor.BYTES_PER_ELEMENT) {
94
+ throw new TypeError('Worker request binary view is unsupported.')
95
+ }
96
+ return new Constructor(
97
+ buffer,
98
+ 0,
99
+ view.byteLength / Constructor.BYTES_PER_ELEMENT
100
+ )
101
+ }
102
+
103
+ /**
104
+ * Reads genuine typed-array or DataView internal slots.
105
+ * @param {unknown} value View candidate.
106
+ * @returns {{ buffer: ArrayBufferLike, byteOffset: number, byteLength: number, tag: string } | null} Intrinsic view fields.
107
+ */
108
+ static view(value) {
109
+ if (!ARRAY_BUFFER_IS_VIEW(value)) return null
110
+ try {
111
+ const tag = TYPED_ARRAY_TAG_GETTER?.call(value)
112
+ if (TYPED_ARRAYS.has(tag)) {
113
+ return {
114
+ buffer: TYPED_ARRAY_BUFFER_GETTER.call(value),
115
+ byteOffset: TYPED_ARRAY_BYTE_OFFSET_GETTER.call(value),
116
+ byteLength: TYPED_ARRAY_BYTE_LENGTH_GETTER.call(value),
117
+ tag
118
+ }
119
+ }
120
+ if (
121
+ DATA_VIEW_BUFFER_GETTER &&
122
+ DATA_VIEW_BYTE_OFFSET_GETTER &&
123
+ DATA_VIEW_BYTE_LENGTH_GETTER
124
+ ) {
125
+ const buffer = DATA_VIEW_BUFFER_GETTER.call(value)
126
+ return {
127
+ buffer,
128
+ byteOffset: DATA_VIEW_BYTE_OFFSET_GETTER.call(value),
129
+ byteLength: DATA_VIEW_BYTE_LENGTH_GETTER.call(value),
130
+ tag: 'DataView'
131
+ }
132
+ }
133
+ } catch {
134
+ return null
135
+ }
136
+ return null
137
+ }
138
+
139
+ /** @param {unknown} value Candidate. @returns {number | null} Byte length. */
140
+ static bufferLength(value) {
141
+ if (!ARRAY_BUFFER_BYTE_LENGTH_GETTER) return null
142
+ try {
143
+ return ARRAY_BUFFER_BYTE_LENGTH_GETTER.call(value)
144
+ } catch {
145
+ return null
146
+ }
147
+ }
148
+
149
+ /** @param {unknown} value Candidate. @returns {number | null} Byte length. */
150
+ static sharedBufferLength(value) {
151
+ if (!SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER) return null
152
+ try {
153
+ return SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER.call(value)
154
+ } catch {
155
+ return null
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Returns whether a genuine ArrayBuffer can change size after accounting.
161
+ * @param {unknown} value Buffer candidate.
162
+ * @returns {boolean} Whether the buffer is resizable.
163
+ */
164
+ static isResizableBuffer(value) {
165
+ if (!ARRAY_BUFFER_RESIZABLE_GETTER) return false
166
+ try {
167
+ return ARRAY_BUFFER_RESIZABLE_GETTER.call(value) === true
168
+ } catch {
169
+ return false
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Copies one already-accounted intrinsic byte range into a fixed buffer.
175
+ * The explicit range prevents a growable backing store from widening the
176
+ * copy between its limit check and snapshot.
177
+ * @param {ArrayBufferLike} buffer Source buffer.
178
+ * @param {number} byteOffset Captured byte offset.
179
+ * @param {number} byteLength Captured byte length.
180
+ * @returns {ArrayBuffer} Fixed owned snapshot.
181
+ */
182
+ static copyBuffer(buffer, byteOffset, byteLength) {
183
+ try {
184
+ const source = new UINT8_ARRAY_CONSTRUCTOR(
185
+ buffer,
186
+ byteOffset,
187
+ byteLength
188
+ )
189
+ const copy = new UINT8_ARRAY_CONSTRUCTOR(byteLength)
190
+ UINT8_ARRAY_SET.call(copy, source)
191
+ return TYPED_ARRAY_BUFFER_GETTER.call(copy)
192
+ } catch {
193
+ throw new TypeError(
194
+ 'Worker request binary data changed while it was copied.'
195
+ )
196
+ }
197
+ }
198
+ }
199
+
200
+ Object.freeze(WorkerBinaryData.prototype)
201
+ Object.freeze(WorkerBinaryData)