@pulse-compute/runtime 0.0.0 → 1.0.0-beta.1
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.md +125 -2
- package/docs/API.md +539 -0
- package/docs/guides/routing.md +184 -0
- package/docs/preview-scope.md +142 -0
- package/package.json +38 -5
- package/src/host.d.ts +376 -0
- package/src/host.js +114 -0
- package/src/index.d.ts +189 -0
- package/src/index.js +11 -0
- package/src/internal/bindings.js +281 -0
- package/src/internal/body.js +254 -0
- package/src/internal/context.js +271 -0
- package/src/internal/effect-adapter.js +857 -0
- package/src/internal/errors.js +27 -0
- package/src/internal/event-emission.js +182 -0
- package/src/internal/event-execution.js +495 -0
- package/src/internal/fetch.js +221 -0
- package/src/internal/index.js +24 -0
- package/src/internal/logging.js +65 -0
- package/src/internal/package-runtime.js +597 -0
- package/src/internal/path.js +130 -0
- package/src/internal/redaction.js +231 -0
- package/src/internal/response.js +415 -0
- package/src/internal/router.js +459 -0
- package/src/internal/schema.js +340 -0
- package/src/package.d.ts +127 -0
- package/src/package.js +19 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PulseRuntimeContractError } = require('./errors.js');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MAX_BINDING_NAME_BYTES = 256;
|
|
6
|
+
const DEFAULT_MAX_BINDING_VALUE_BYTES = 65_536;
|
|
7
|
+
const DEFAULT_MAX_KV_NAMESPACE_BYTES = 256;
|
|
8
|
+
const DEFAULT_MAX_KV_KEY_BYTES = 1_024;
|
|
9
|
+
const DEFAULT_MAX_KV_VALUE_BYTES = 65_536;
|
|
10
|
+
const DEFAULT_MAX_KV_VALUE_DEPTH = 64;
|
|
11
|
+
const DEFAULT_MAX_KV_VALUE_ENTRIES = 10_000;
|
|
12
|
+
const textEncoder = new TextEncoder();
|
|
13
|
+
|
|
14
|
+
function byteLength(value) {
|
|
15
|
+
return textEncoder.encode(String(value)).byteLength;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function positiveLimit(label, value, fallback) {
|
|
19
|
+
const normalized = value === undefined ? fallback : Number(value);
|
|
20
|
+
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
21
|
+
throw new TypeError(`Pulse ${label} must be a positive safe integer.`);
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function contractError(code, message, detail) {
|
|
27
|
+
return new PulseRuntimeContractError(code, message, {
|
|
28
|
+
detail: detail === undefined ? undefined : Object.freeze({ ...detail })
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function boundedString(label, value, options) {
|
|
33
|
+
const {
|
|
34
|
+
invalidCode,
|
|
35
|
+
tooLargeCode,
|
|
36
|
+
maxBytes,
|
|
37
|
+
detail = {}
|
|
38
|
+
} = options;
|
|
39
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
40
|
+
throw contractError(invalidCode, `Pulse ${label} must be a non-empty string.`, {
|
|
41
|
+
...detail,
|
|
42
|
+
valueType: typeof value
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const bytes = byteLength(value);
|
|
46
|
+
if (bytes > maxBytes) {
|
|
47
|
+
throw contractError(tooLargeCode, `Pulse ${label} exceeds the ${maxBytes} byte limit.`, {
|
|
48
|
+
...detail,
|
|
49
|
+
bytes,
|
|
50
|
+
maxBytes
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeBindingName(kind, value, options = {}) {
|
|
57
|
+
const bindingKind = String(kind || 'binding');
|
|
58
|
+
return boundedString(`${bindingKind} name`, value, {
|
|
59
|
+
invalidCode: 'PULSE_BINDING_NAME_INVALID',
|
|
60
|
+
tooLargeCode: 'PULSE_BINDING_NAME_TOO_LARGE',
|
|
61
|
+
maxBytes: positiveLimit('maxBindingNameBytes', options.maxBindingNameBytes, DEFAULT_MAX_BINDING_NAME_BYTES),
|
|
62
|
+
detail: { bindingKind }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeKvNamespace(value, options = {}) {
|
|
67
|
+
return boundedString('KV namespace', value, {
|
|
68
|
+
invalidCode: 'PULSE_KV_NAMESPACE_INVALID',
|
|
69
|
+
tooLargeCode: 'PULSE_KV_NAMESPACE_TOO_LARGE',
|
|
70
|
+
maxBytes: positiveLimit('maxKvNamespaceBytes', options.maxKvNamespaceBytes, DEFAULT_MAX_KV_NAMESPACE_BYTES)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeKvKey(value, options = {}) {
|
|
75
|
+
return boundedString('KV key', value, {
|
|
76
|
+
invalidCode: 'PULSE_KV_KEY_INVALID',
|
|
77
|
+
tooLargeCode: 'PULSE_KV_KEY_TOO_LARGE',
|
|
78
|
+
maxBytes: positiveLimit('maxKvKeyBytes', options.maxKvKeyBytes, DEFAULT_MAX_KV_KEY_BYTES)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeBindingValueInternal(kind, value, options = {}) {
|
|
83
|
+
if (value === undefined) return undefined;
|
|
84
|
+
const bindingKind = String(kind || 'binding');
|
|
85
|
+
if (typeof value !== 'string') {
|
|
86
|
+
throw contractError(
|
|
87
|
+
'PULSE_BINDING_VALUE_INVALID',
|
|
88
|
+
`Pulse ${bindingKind} providers must return a string or undefined.`,
|
|
89
|
+
{ bindingKind, valueType: value === null ? 'null' : typeof value }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const maxBytes = positiveLimit('maxBindingValueBytes', options.maxBindingValueBytes, DEFAULT_MAX_BINDING_VALUE_BYTES);
|
|
93
|
+
const bytes = byteLength(value);
|
|
94
|
+
if (bytes > maxBytes) {
|
|
95
|
+
throw contractError(
|
|
96
|
+
'PULSE_BINDING_VALUE_TOO_LARGE',
|
|
97
|
+
`Pulse ${bindingKind} value exceeds the ${maxBytes} byte limit.`,
|
|
98
|
+
{ bindingKind, bytes, maxBytes }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function valueType(value) {
|
|
105
|
+
if (value === null) return 'null';
|
|
106
|
+
if (Array.isArray(value)) return 'array';
|
|
107
|
+
return typeof value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function kvValueError(message, path, value, extra = {}) {
|
|
111
|
+
return contractError('PULSE_KV_VALUE_INVALID', message, {
|
|
112
|
+
path,
|
|
113
|
+
valueType: valueType(value),
|
|
114
|
+
...extra
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function definePortableProperty(target, key, value) {
|
|
119
|
+
Object.defineProperty(target, key, {
|
|
120
|
+
enumerable: true,
|
|
121
|
+
configurable: false,
|
|
122
|
+
writable: false,
|
|
123
|
+
value
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clonePortableKvValue(value, state, path, depth) {
|
|
128
|
+
state.entries += 1;
|
|
129
|
+
if (state.entries > state.maxEntries) {
|
|
130
|
+
throw contractError(
|
|
131
|
+
'PULSE_KV_VALUE_ENTRIES_EXCEEDED',
|
|
132
|
+
`Pulse KV values may not exceed ${state.maxEntries} entries.`,
|
|
133
|
+
{ path, maxEntries: state.maxEntries }
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (depth > state.maxDepth) {
|
|
137
|
+
throw contractError(
|
|
138
|
+
'PULSE_KV_VALUE_DEPTH_EXCEEDED',
|
|
139
|
+
`Pulse KV values may not exceed ${state.maxDepth} nested levels.`,
|
|
140
|
+
{ path, maxDepth: state.maxDepth }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;
|
|
144
|
+
if (typeof value === 'number') {
|
|
145
|
+
if (!Number.isFinite(value)) throw kvValueError('Pulse KV numbers must be finite.', path, value);
|
|
146
|
+
return Object.is(value, -0) ? 0 : value;
|
|
147
|
+
}
|
|
148
|
+
if (value === undefined && state.allowUndefined && path === '$') return undefined;
|
|
149
|
+
if (value === undefined) throw kvValueError('Pulse KV values may not contain undefined.', path, value);
|
|
150
|
+
if (typeof value !== 'object') {
|
|
151
|
+
throw kvValueError('Pulse KV values must be JSON-compatible data.', path, value);
|
|
152
|
+
}
|
|
153
|
+
if (state.seen.has(value)) {
|
|
154
|
+
throw kvValueError('Pulse KV values may not contain cycles or repeated object references.', path, value);
|
|
155
|
+
}
|
|
156
|
+
state.seen.add(value);
|
|
157
|
+
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
160
|
+
const allowed = new Set(['length']);
|
|
161
|
+
for (let index = 0; index < value.length; index += 1) allowed.add(String(index));
|
|
162
|
+
for (const key of ownKeys) {
|
|
163
|
+
if (typeof key !== 'string' || !allowed.has(key)) {
|
|
164
|
+
throw kvValueError('Pulse KV arrays may only contain indexed data properties.', path, value, { key: String(key) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const output = [];
|
|
168
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
169
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) {
|
|
170
|
+
throw kvValueError('Pulse KV arrays may not contain sparse entries.', `${path}[${index}]`, undefined);
|
|
171
|
+
}
|
|
172
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
173
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
174
|
+
throw kvValueError('Pulse KV arrays may not contain accessors.', `${path}[${index}]`, value[index]);
|
|
175
|
+
}
|
|
176
|
+
definePortableProperty(output, String(index), clonePortableKvValue(descriptor.value, state, `${path}[${index}]`, depth + 1));
|
|
177
|
+
}
|
|
178
|
+
Object.defineProperty(output, 'length', { writable: false });
|
|
179
|
+
return Object.freeze(output);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const prototype = Object.getPrototypeOf(value);
|
|
183
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
184
|
+
throw kvValueError('Pulse KV objects must be ordinary object literals.', path, value, {
|
|
185
|
+
prototype: prototype && prototype.constructor && prototype.constructor.name || 'null'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const output = {};
|
|
189
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
190
|
+
if (typeof key !== 'string') {
|
|
191
|
+
throw kvValueError('Pulse KV objects may not contain symbol keys.', path, value);
|
|
192
|
+
}
|
|
193
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
194
|
+
if (!descriptor || !descriptor.enumerable || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
195
|
+
throw kvValueError('Pulse KV objects may only contain enumerable data properties.', `${path}.${key}`, value);
|
|
196
|
+
}
|
|
197
|
+
definePortableProperty(output, key, clonePortableKvValue(descriptor.value, state, `${path}.${key}`, depth + 1));
|
|
198
|
+
}
|
|
199
|
+
return Object.freeze(output);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeKvValue(value, options = {}) {
|
|
203
|
+
const allowUndefined = options.allowUndefined === true;
|
|
204
|
+
if (value === undefined && allowUndefined) return undefined;
|
|
205
|
+
const maxBytes = positiveLimit('maxKvValueBytes', options.maxKvValueBytes, DEFAULT_MAX_KV_VALUE_BYTES);
|
|
206
|
+
const maxDepth = positiveLimit('maxKvValueDepth', options.maxKvValueDepth ?? options.maxDepth, DEFAULT_MAX_KV_VALUE_DEPTH);
|
|
207
|
+
const maxEntries = positiveLimit('maxKvValueEntries', options.maxKvValueEntries ?? options.maxEntries, DEFAULT_MAX_KV_VALUE_ENTRIES);
|
|
208
|
+
const cloned = clonePortableKvValue(value, {
|
|
209
|
+
allowUndefined,
|
|
210
|
+
maxDepth,
|
|
211
|
+
maxEntries,
|
|
212
|
+
entries: 0,
|
|
213
|
+
seen: new WeakSet()
|
|
214
|
+
}, '$', 0);
|
|
215
|
+
const serialized = JSON.stringify(cloned);
|
|
216
|
+
const bytes = byteLength(serialized);
|
|
217
|
+
if (bytes > maxBytes) {
|
|
218
|
+
throw contractError(
|
|
219
|
+
'PULSE_KV_VALUE_TOO_LARGE',
|
|
220
|
+
`Pulse KV value exceeds the ${maxBytes} byte limit.`,
|
|
221
|
+
{ bytes, maxBytes }
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return cloned;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeBindingValue(capability, name, value, options = {}) {
|
|
228
|
+
const bindingKind = String(capability || 'binding').startsWith('secret') ? 'secret' : 'config';
|
|
229
|
+
// Name is intentionally validated separately from the returned value so error detail never includes it as a value.
|
|
230
|
+
normalizeBindingName(bindingKind, name, options);
|
|
231
|
+
return normalizeBindingValueResult(bindingKind, value, options);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeBindingValueResult(kind, value, options = {}) {
|
|
235
|
+
return normalizeBindingValueInternal(kind, value, options);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeKvAcknowledgement(value) {
|
|
239
|
+
if (typeof value !== 'boolean') {
|
|
240
|
+
throw contractError(
|
|
241
|
+
'PULSE_KV_ACK_INVALID',
|
|
242
|
+
'Pulse KV put providers must return a boolean acknowledgement.',
|
|
243
|
+
{ valueType: valueType(value) }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function cloneKvValue(value, options = {}) {
|
|
250
|
+
return normalizeKvValue(value, {
|
|
251
|
+
allowUndefined: options.allowUndefined === true,
|
|
252
|
+
maxKvValueBytes: options.maxKvValueBytes ?? options.maxBytes,
|
|
253
|
+
maxKvValueDepth: options.maxKvValueDepth ?? options.maxDepth,
|
|
254
|
+
maxKvValueEntries: options.maxKvValueEntries ?? options.maxEntries
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function normalizeKvPutResult(value) {
|
|
259
|
+
return normalizeKvAcknowledgement(value);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = Object.freeze({
|
|
263
|
+
DEFAULT_MAX_BINDING_NAME_BYTES,
|
|
264
|
+
DEFAULT_MAX_BINDING_VALUE_BYTES,
|
|
265
|
+
DEFAULT_MAX_KV_NAMESPACE_BYTES,
|
|
266
|
+
DEFAULT_MAX_KV_KEY_BYTES,
|
|
267
|
+
DEFAULT_MAX_KV_VALUE_BYTES,
|
|
268
|
+
DEFAULT_MAX_KV_VALUE_DEPTH,
|
|
269
|
+
DEFAULT_MAX_KV_VALUE_ENTRIES,
|
|
270
|
+
byteLength,
|
|
271
|
+
cloneKvValue,
|
|
272
|
+
normalizeBindingName,
|
|
273
|
+
normalizeBindingValue,
|
|
274
|
+
normalizeBindingValueResult,
|
|
275
|
+
normalizeKvAcknowledgement,
|
|
276
|
+
normalizeKvKey,
|
|
277
|
+
normalizeKvNamespace,
|
|
278
|
+
normalizeKvPutResult,
|
|
279
|
+
normalizeKvValue,
|
|
280
|
+
positiveLimit
|
|
281
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PulseRuntimeContractError } = require('./errors.js');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_STRUCTURED_BODY_BYTES = 65_536;
|
|
6
|
+
|
|
7
|
+
function normalizeBodyLimit(value, fallback = DEFAULT_STRUCTURED_BODY_BYTES) {
|
|
8
|
+
const limit = value === undefined ? fallback : Number(value);
|
|
9
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
10
|
+
throw new TypeError('Pulse structured body limits must be positive safe integers.');
|
|
11
|
+
}
|
|
12
|
+
return limit;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeContentType(value) {
|
|
16
|
+
return String(value || '').split(';')[0].trim().toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function classifyContentType(value) {
|
|
20
|
+
const normalized = normalizeContentType(value);
|
|
21
|
+
if (!normalized) return 'unknown';
|
|
22
|
+
if (normalized === 'application/json' || normalized.endsWith('+json')) return 'json';
|
|
23
|
+
if (normalized.startsWith('text/')) return 'text';
|
|
24
|
+
if ([
|
|
25
|
+
'application/javascript',
|
|
26
|
+
'application/xml',
|
|
27
|
+
'application/xhtml+xml',
|
|
28
|
+
'application/x-www-form-urlencoded'
|
|
29
|
+
].includes(normalized) || normalized.endsWith('+xml')) return 'text';
|
|
30
|
+
return 'opaque';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isPlainRecord(value) {
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
35
|
+
const prototype = Object.getPrototypeOf(value);
|
|
36
|
+
return prototype === Object.prototype || prototype === null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clonePlain(value) {
|
|
40
|
+
if (Array.isArray(value)) return value.map(clonePlain);
|
|
41
|
+
if (isPlainRecord(value)) {
|
|
42
|
+
const output = {};
|
|
43
|
+
for (const key of Object.keys(value)) {
|
|
44
|
+
Object.defineProperty(output, key, {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: clonePlain(value[key])
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function deepFreeze(value) {
|
|
57
|
+
if ((Array.isArray(value) || isPlainRecord(value)) && !Object.isFrozen(value)) {
|
|
58
|
+
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
59
|
+
Object.freeze(value);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function immutableClone(value) {
|
|
65
|
+
return deepFreeze(clonePlain(value));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function contentLength(headers) {
|
|
69
|
+
if (!headers || typeof headers.get !== 'function') return undefined;
|
|
70
|
+
const raw = headers.get('content-length');
|
|
71
|
+
if (raw == null || raw === '') return undefined;
|
|
72
|
+
const value = Number(raw);
|
|
73
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function bodyError(code, message, detail, cause) {
|
|
77
|
+
return new PulseRuntimeContractError(code, message, {
|
|
78
|
+
detail: Object.freeze({ ...(detail || {}) }),
|
|
79
|
+
cause
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function bodyUnavailable(label, detail, cause) {
|
|
84
|
+
return bodyError(
|
|
85
|
+
'PULSE_BODY_UNAVAILABLE',
|
|
86
|
+
`Pulse ${label} body is unavailable or was consumed outside its request-owned body projection.`,
|
|
87
|
+
detail,
|
|
88
|
+
cause
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function bodyTooLarge(label, bytes, maxBytes) {
|
|
93
|
+
return bodyError(
|
|
94
|
+
'PULSE_BODY_TOO_LARGE',
|
|
95
|
+
`Pulse ${label} body exceeds the ${maxBytes} byte structured-body limit.`,
|
|
96
|
+
{ bytes, maxBytes }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function opaqueInspection(label, contentType) {
|
|
101
|
+
return bodyError(
|
|
102
|
+
'PULSE_OPAQUE_BODY_INSPECTION',
|
|
103
|
+
`Pulse ${label} body is opaque and may only be returned as a host-owned response.`,
|
|
104
|
+
{ contentType: String(contentType || ''), contentClass: 'opaque' }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function bodyAborted(label, reason) {
|
|
109
|
+
if (reason instanceof PulseRuntimeContractError) return reason;
|
|
110
|
+
return bodyError(
|
|
111
|
+
'PULSE_RUNTIME_EFFECT_ABORTED',
|
|
112
|
+
`Pulse ${label} body read was aborted with its request.`,
|
|
113
|
+
{ reason: reason && reason.name ? reason.name : typeof reason },
|
|
114
|
+
reason
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function readBodyBytes(owner, maxBytes, label, signal) {
|
|
119
|
+
if (!owner || typeof owner !== 'object') throw bodyUnavailable(label, { reason: 'missing-owner' });
|
|
120
|
+
const declared = contentLength(owner.headers);
|
|
121
|
+
if (declared !== undefined && declared > maxBytes) throw bodyTooLarge(label, declared, maxBytes);
|
|
122
|
+
if (owner.bodyUsed) throw bodyUnavailable(label, { reason: 'already-consumed' });
|
|
123
|
+
if (signal && signal.aborted) throw bodyAborted(label, signal.reason);
|
|
124
|
+
if (owner.body == null) return new Uint8Array(0);
|
|
125
|
+
|
|
126
|
+
if (typeof owner.body.getReader !== 'function') {
|
|
127
|
+
if (typeof owner.arrayBuffer !== 'function') throw bodyUnavailable(label, { reason: 'reader-unavailable' });
|
|
128
|
+
let buffer;
|
|
129
|
+
try {
|
|
130
|
+
buffer = new Uint8Array(await owner.arrayBuffer());
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (signal && signal.aborted) throw bodyAborted(label, signal.reason);
|
|
133
|
+
throw bodyUnavailable(label, { reason: 'read-failed' }, error);
|
|
134
|
+
}
|
|
135
|
+
if (buffer.byteLength > maxBytes) throw bodyTooLarge(label, buffer.byteLength, maxBytes);
|
|
136
|
+
return buffer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const reader = owner.body.getReader();
|
|
140
|
+
const chunks = [];
|
|
141
|
+
let bytes = 0;
|
|
142
|
+
let removeAbortListener;
|
|
143
|
+
if (signal && typeof signal.addEventListener === 'function') {
|
|
144
|
+
const onAbort = () => {
|
|
145
|
+
Promise.resolve(reader.cancel(bodyAborted(label, signal.reason))).catch(() => undefined);
|
|
146
|
+
};
|
|
147
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
148
|
+
removeAbortListener = () => signal.removeEventListener('abort', onAbort);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
while (true) {
|
|
153
|
+
if (signal && signal.aborted) throw bodyAborted(label, signal.reason);
|
|
154
|
+
const result = await reader.read();
|
|
155
|
+
if (signal && signal.aborted) throw bodyAborted(label, signal.reason);
|
|
156
|
+
if (result.done) break;
|
|
157
|
+
const chunk = result.value instanceof Uint8Array ? result.value : new Uint8Array(result.value || 0);
|
|
158
|
+
bytes += chunk.byteLength;
|
|
159
|
+
if (bytes > maxBytes) {
|
|
160
|
+
const error = bodyTooLarge(label, bytes, maxBytes);
|
|
161
|
+
// Stop the request-owned structured projection as soon as the bound
|
|
162
|
+
// is crossed. Cancellation is best effort and must not mask the size error.
|
|
163
|
+
try { Promise.resolve(reader.cancel(error)).catch(() => undefined); } catch (_) { /* best effort */ }
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
chunks.push(chunk);
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error instanceof PulseRuntimeContractError) throw error;
|
|
170
|
+
if (signal && signal.aborted) throw bodyAborted(label, signal.reason);
|
|
171
|
+
throw bodyUnavailable(label, { reason: 'read-failed', bytes }, error);
|
|
172
|
+
} finally {
|
|
173
|
+
if (removeAbortListener) removeAbortListener();
|
|
174
|
+
try { reader.releaseLock(); } catch (_) { /* already released */ }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const output = new Uint8Array(bytes);
|
|
178
|
+
let offset = 0;
|
|
179
|
+
for (const chunk of chunks) {
|
|
180
|
+
output.set(chunk, offset);
|
|
181
|
+
offset += chunk.byteLength;
|
|
182
|
+
}
|
|
183
|
+
return output;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createStructuredBodyReader(owner, options = {}) {
|
|
187
|
+
const label = String(options.label || 'structured');
|
|
188
|
+
const maxBytes = normalizeBodyLimit(options.maxBytes);
|
|
189
|
+
const contentType = String(options.contentType || owner?.headers?.get?.('content-type') || '');
|
|
190
|
+
const contentClass = classifyContentType(contentType);
|
|
191
|
+
const forceStructured = options.forceStructured === true;
|
|
192
|
+
const signal = options.signal;
|
|
193
|
+
let bytesPromise;
|
|
194
|
+
let textPromise;
|
|
195
|
+
let jsonPromise;
|
|
196
|
+
|
|
197
|
+
function assertInspectable() {
|
|
198
|
+
if (!forceStructured && contentClass === 'opaque') throw opaqueInspection(label, contentType);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function bytes() {
|
|
202
|
+
assertInspectable();
|
|
203
|
+
if (!bytesPromise) bytesPromise = readBodyBytes(owner, maxBytes, label, signal);
|
|
204
|
+
return bytesPromise;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function text() {
|
|
208
|
+
assertInspectable();
|
|
209
|
+
if (!textPromise) textPromise = bytes().then((value) => new TextDecoder().decode(value));
|
|
210
|
+
return textPromise;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function json() {
|
|
214
|
+
assertInspectable();
|
|
215
|
+
if (!jsonPromise) {
|
|
216
|
+
jsonPromise = text().then((value) => {
|
|
217
|
+
try {
|
|
218
|
+
return immutableClone(JSON.parse(value));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw bodyError(
|
|
221
|
+
'PULSE_BODY_DECODE',
|
|
222
|
+
`Pulse ${label} body could not be decoded as JSON.`,
|
|
223
|
+
{ contentType, contentClass },
|
|
224
|
+
error
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return jsonPromise;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
label,
|
|
234
|
+
maxBytes,
|
|
235
|
+
contentType,
|
|
236
|
+
contentClass,
|
|
237
|
+
bodyClass: !forceStructured && contentClass === 'opaque' ? 'opaque' : 'structured',
|
|
238
|
+
bytes,
|
|
239
|
+
text,
|
|
240
|
+
json,
|
|
241
|
+
hasSnapshot() { return Boolean(bytesPromise); }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = Object.freeze({
|
|
246
|
+
DEFAULT_STRUCTURED_BODY_BYTES,
|
|
247
|
+
classifyContentType,
|
|
248
|
+
createStructuredBodyReader,
|
|
249
|
+
deepFreeze,
|
|
250
|
+
immutableClone,
|
|
251
|
+
normalizeBodyLimit,
|
|
252
|
+
normalizeContentType,
|
|
253
|
+
readBodyBytes
|
|
254
|
+
});
|