sendscript 1.0.6 → 2.0.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.
@@ -1,9 +1,5 @@
1
- import module from 'sendscript/module.mjs'
2
1
  import type * as mathTypes from './math.ts'
2
+ import Stringify from 'sendscript/stringify.mjs'
3
+ import references from 'sendscript/references.mjs'
3
4
 
4
- const math = module([
5
- 'add',
6
- 'square'
7
- ]) as typeof mathTypes
8
-
9
- export default math
5
+ export default references(['add', 'square']) as typeof mathTypes
package/index.test.mjs CHANGED
@@ -1,7 +1,14 @@
1
1
  import { test } from 'tap'
2
- import Sendscript from './index.mjs'
2
+ import Stringify from './stringify.mjs'
3
+ import references from './references.mjs'
4
+ import Parse from './parse.mjs'
3
5
 
4
- const module = {
6
+ const myModule = {
7
+ nested: {
8
+ again: {
9
+ T: () => true
10
+ }
11
+ },
5
12
  add: (a, b) => a + b,
6
13
  identity: (x) => x,
7
14
  concat: (a, b) => a.concat(b),
@@ -18,12 +25,23 @@ const module = {
18
25
  asyncAdd: async (a, b) => a + b,
19
26
  aPromise: Promise.resolve(42),
20
27
  delayedIdentity: async (x) => x,
21
- Function,
28
+ nullProto: () => {
29
+ const obj = Object.create(null)
30
+ obj.b = 'c'
31
+ return obj
32
+ },
22
33
  Promise
23
34
  }
24
35
 
25
- const sendscript = Sendscript(module)
26
- const { parse, stringify } = sendscript
36
+ const schema = Object.keys(myModule)
37
+ schema.push(['nested', [
38
+ ['again', ['T']]
39
+ ]])
40
+
41
+ const api = references(schema)
42
+ const stringify = Stringify()
43
+ const parse = Parse(schema, myModule)
44
+
27
45
  const run = (program) => parse(stringify(program))
28
46
 
29
47
  const RealPromise = Promise
@@ -35,7 +53,6 @@ test('should evaluate basic expressions correctly', async (t) => {
35
53
  resolve,
36
54
  delayedIdentity,
37
55
  noop,
38
- Function,
39
56
  Promise,
40
57
  instanceOf,
41
58
  asyncFn,
@@ -46,8 +63,24 @@ test('should evaluate basic expressions correctly', async (t) => {
46
63
  concat,
47
64
  identity,
48
65
  always,
49
- multiply3
50
- } = sendscript.module
66
+ multiply3,
67
+ nested
68
+ } = api
69
+
70
+ t.test('mix await without await', async t => {
71
+ const [one, two] = await run([await resolve(1), resolve(2)])
72
+
73
+ t.equal(one, 1)
74
+ t.ok(two instanceof RealPromise)
75
+
76
+ t.end()
77
+ })
78
+
79
+ t.test('calling nested function works', t => {
80
+ t.equal(run(nested.again.T()), true)
81
+
82
+ t.end()
83
+ })
51
84
 
52
85
  t.test('nested await works', async (t) => {
53
86
  // Async identity passthrough
@@ -110,9 +143,6 @@ test('should evaluate basic expressions correctly', async (t) => {
110
143
  t.strictSame(await run(asyncFn()), 'my-async-function')
111
144
  t.strictSame(await run(await resolve('my-promise')), 'my-promise')
112
145
  t.strictSame(run(instanceOf(resolve(asyncFn), Promise)), true)
113
- t.strictSame(
114
- await run(instanceOf(await resolve(asyncFn), Function)), true
115
- )
116
146
  t.strictSame(
117
147
  await run({ a: await resolve('b') }),
118
148
  { a: 'b' }
@@ -177,6 +207,16 @@ test('should evaluate basic expressions correctly', async (t) => {
177
207
  run(identity(['ref', 'hello'])),
178
208
  run(identity(toArray('ref', 'hello')))
179
209
  )
210
+ t.strictSame(
211
+ run(identity(['leaf', 1, 2, 3])),
212
+ ['leaf', 1, 2, 3]
213
+ )
214
+ t.end()
215
+ })
216
+
217
+ t.test('null-prototype object traversal', (t) => {
218
+ const { nullProto } = api
219
+ t.strictSame(run({ a: nullProto() }), { a: { b: 'c' } })
180
220
  t.end()
181
221
  })
182
222
 
@@ -195,3 +235,78 @@ test('should evaluate basic expressions correctly', async (t) => {
195
235
  t.end()
196
236
  })
197
237
  })
238
+
239
+ test('stringify: invalid children throws', t => {
240
+ const invalidSchema = [
241
+ ['math', 'add']
242
+ ]
243
+
244
+ t.throws(() => Parse(invalidSchema, {}))
245
+ t.throws(() => Parse([Symbol('no-allowed')], {}))
246
+ t.end()
247
+ })
248
+
249
+ test('forbidden/reflection access should be blocked', async (t) => {
250
+ function run (program) {
251
+ return parse(JSON.stringify(program))
252
+ }
253
+
254
+ t.test('cannot access constructor via ref', (t) => {
255
+ // try to reach the constructor of nested.again.T
256
+ t.throws(() => run(['ref', 'nested', 'again', 'T', 'constructor']))
257
+ t.end()
258
+ })
259
+
260
+ t.test('cannot access prototype property', (t) => {
261
+ // direct prototype traversal attempt
262
+ t.throws(() => run(['ref', 'nested', '__proto__']))
263
+ t.throws(() => run(['ref', 'nested', 'again', '__proto__']))
264
+ t.end()
265
+ })
266
+
267
+ t.test('cannot reach Function constructor', (t) => {
268
+ // many modules may accidentally expose Function; this should not be available via ref
269
+ t.throws(() => run(['ref', 'Function']))
270
+ t.end()
271
+ })
272
+
273
+ t.test('cannot call Function to execute code', (t) => {
274
+ // If Function were reachable, this would execute arbitrary code
275
+ const program = ['call', ['ref', 'Function'], [['leaf', '"return 1 + 2"']]]
276
+ t.throws(() => run(program))
277
+ t.end()
278
+ })
279
+
280
+ t.test('cannot access global Promise constructor via ref', (t) => {
281
+ t.throws(() => run(['ref', 'Promise', 'prototype', 'then']))
282
+ t.end()
283
+ })
284
+
285
+ t.test('cannot access Object.prototype methods', (t) => {
286
+ // toString is a common vector for prototype access
287
+ t.throws(() => run(['ref', 'toString']))
288
+ t.throws(() => run(['ref', 'nested', 'again', 'T', 'toString']))
289
+ t.end()
290
+ })
291
+
292
+ t.test('cannot reach process or global (if accidentally exposed)', (t) => {
293
+ // attempt common global names; parser should not resolve them
294
+ t.throws(() => run(['ref', 'process']))
295
+ t.throws(() => run(['ref', 'global']))
296
+ t.end()
297
+ })
298
+
299
+ t.test('cannot use constructor.constructor to reach Function', (t) => {
300
+ // attempt: nested.again.T.constructor.constructor
301
+ t.throws(() => run(['ref', 'nested', 'again', 'T', 'constructor', 'constructor']))
302
+ t.end()
303
+ })
304
+
305
+ t.test('throws when trying to get something that does not exist', (t) => {
306
+ // Ensure array that contains a ref-like array stays as data if not a SendScript ref wrapper:
307
+ t.throws(() => run(['ref', 'doesNotExist']))
308
+ t.end()
309
+ })
310
+
311
+ t.end()
312
+ })
@@ -0,0 +1,149 @@
1
+ import { test } from 'tap'
2
+ import { createRequire } from 'module'
3
+ import SuperJSON from 'superjson'
4
+
5
+ const require = createRequire(import.meta.url)
6
+ const { check, gen } = require('tape-check')
7
+
8
+ const leafSerializer = (value) => {
9
+ if (value === undefined) return JSON.stringify({ __sendscript_undefined__: true })
10
+ return JSON.stringify(SuperJSON.serialize(value))
11
+ }
12
+
13
+ const leafDeserializer = (text) => {
14
+ const parsed = JSON.parse(text)
15
+ if (parsed && parsed.__sendscript_undefined__ === true) return undefined
16
+ return SuperJSON.deserialize(parsed)
17
+ }
18
+
19
+ // Helper to compare values accounting for types that can't use ===
20
+ const valueEquals = (a, b) => {
21
+ if (a === b) return true
22
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
23
+ if (a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags
24
+ if (a instanceof Set && b instanceof Set) {
25
+ if (a.size !== b.size) return false
26
+ for (const item of a) {
27
+ if (!b.has(item)) return false
28
+ }
29
+ return true
30
+ }
31
+ if (a instanceof Map && b instanceof Map) {
32
+ if (a.size !== b.size) return false
33
+ for (const [key, val] of a) {
34
+ if (!b.has(key) || !valueEquals(val, b.get(key))) return false
35
+ }
36
+ return true
37
+ }
38
+ return JSON.stringify(a) === JSON.stringify(b)
39
+ }
40
+
41
+ // Helper to get a human-readable type name
42
+ const getTypeInfo = (value) => {
43
+ if (value === null) return 'null'
44
+ if (value === undefined) return 'undefined'
45
+ if (value instanceof Date) return 'Date'
46
+ if (value instanceof RegExp) return 'RegExp'
47
+ if (value instanceof Set) return 'Set'
48
+ if (value instanceof Map) return 'Map'
49
+ if (typeof value === 'bigint') return 'BigInt'
50
+ return typeof value
51
+ }
52
+
53
+ // Property 1: Round-trip - any value that can be serialized should deserialize to an equal value
54
+ test('property: round-trip serialization preserves value', check(
55
+ gen.any,
56
+ (t, value) => {
57
+ t.plan(1)
58
+ try {
59
+ const serialized = leafSerializer(value)
60
+ const deserialized = leafDeserializer(serialized)
61
+ const typeInfo = getTypeInfo(value)
62
+ t.ok(valueEquals(deserialized, value), `Round-trip preserved ${typeInfo}`)
63
+ } catch (e) {
64
+ // If serialization fails on a particular value, that's acceptable
65
+ // (not all values may be serializable)
66
+ const typeInfo = getTypeInfo(value)
67
+ t.pass(`Serialization of ${typeInfo} threw: ${e.message}`)
68
+ }
69
+ }
70
+ ))
71
+
72
+ // Property 2: Determinism - serializing the same value repeatedly produces identical results
73
+ test('property: serialization is deterministic', check(
74
+ gen.any,
75
+ (t, value) => {
76
+ t.plan(1)
77
+ try {
78
+ const serialized1 = leafSerializer(value)
79
+ const serialized2 = leafSerializer(value)
80
+ const typeInfo = getTypeInfo(value)
81
+ t.equal(serialized1, serialized2, `Serialization of ${typeInfo} is deterministic`)
82
+ } catch (e) {
83
+ const typeInfo = getTypeInfo(value)
84
+ t.pass(`Serialization threw for ${typeInfo}: ${e.message}`)
85
+ }
86
+ }
87
+ ))
88
+
89
+ // Property 3: Valid JSON - serialized output is always valid JSON
90
+ test('property: serialized output is valid JSON', check(
91
+ gen.any,
92
+ (t, value) => {
93
+ t.plan(1)
94
+ try {
95
+ const serialized = leafSerializer(value)
96
+ const parsed = JSON.parse(serialized)
97
+ t.ok(typeof parsed === 'object' || typeof parsed === 'string', 'Parsed JSON is an object or string')
98
+ } catch (e) {
99
+ t.fail(`Invalid JSON output for ${getTypeInfo(value)}: ${e.message}`)
100
+ }
101
+ }
102
+ ))
103
+
104
+ // Property 4: Undefined handling - undefined values are preserved through round-trip
105
+ test('property: undefined values are preserved through serialization', check(
106
+ gen.any,
107
+ (t, value) => {
108
+ t.plan(1)
109
+ if (value === undefined) {
110
+ const serialized = leafSerializer(value)
111
+ const deserialized = leafDeserializer(serialized)
112
+ t.equal(deserialized, undefined, 'Undefined preserved through serialization')
113
+ } else {
114
+ t.pass('Value was not undefined')
115
+ }
116
+ }
117
+ ))
118
+
119
+ // Property 5: Type distinctness - Different values should have different serializations (when possible)
120
+ test('property: different primitives have different serializations', check(
121
+ gen.primitive,
122
+ gen.primitive,
123
+ (t, val1, val2) => {
124
+ t.plan(1)
125
+ if (val1 !== val2 && !(Number.isNaN(val1) && Number.isNaN(val2))) {
126
+ const ser1 = leafSerializer(val1)
127
+ const ser2 = leafSerializer(val2)
128
+ t.not(ser1, ser2, `Different primitives ${getTypeInfo(val1)} and ${getTypeInfo(val2)} have different serializations`)
129
+ } else {
130
+ t.pass('Primitives are equal or both NaN')
131
+ }
132
+ }
133
+ ))
134
+
135
+ // Property 6: Idempotence of serialization - Re-parsing serialized value produces same serialization
136
+ test('property: serialization round-trip is stable', check(
137
+ gen.any,
138
+ (t, value) => {
139
+ t.plan(1)
140
+ try {
141
+ const ser1 = leafSerializer(value)
142
+ const deser1 = leafDeserializer(ser1)
143
+ const ser2 = leafSerializer(deser1)
144
+ t.equal(ser1, ser2, `Serialization is stable for ${getTypeInfo(value)}`)
145
+ } catch (e) {
146
+ t.pass(`Serialization error for ${getTypeInfo(value)}: ${e.message}`)
147
+ }
148
+ }
149
+ ))
@@ -0,0 +1,71 @@
1
+ import { test } from 'tap'
2
+ import SuperJSON from 'superjson'
3
+ import Parse from './parse.mjs'
4
+ import Stringify from './stringify.mjs'
5
+
6
+ const leafSerializer = (value) => {
7
+ if (value === undefined) return JSON.stringify({ __sendscript_undefined__: true })
8
+ return JSON.stringify(SuperJSON.serialize(value))
9
+ }
10
+
11
+ const leafDeserializer = (text) => {
12
+ const parsed = JSON.parse(text)
13
+ if (parsed && parsed.__sendscript_undefined__ === true) return undefined
14
+ return SuperJSON.deserialize(parsed)
15
+ }
16
+
17
+ const module = {
18
+ identity: (x) => x
19
+ }
20
+
21
+ const schema = Object.keys(module)
22
+
23
+ const run = (program, serializer, deserializer) => {
24
+ const parse = Parse(schema, module, deserializer)
25
+ const stringify = Stringify(serializer)
26
+
27
+ return parse(stringify(program))
28
+ }
29
+
30
+ test('custom leaf serializer/deserializer using superjson', async (t) => {
31
+ const value = {
32
+ date: new Date('2020-01-01T00:00:00.000Z'),
33
+ regex: /abc/gi,
34
+ big: BigInt('123456789012345678901234567890'),
35
+ undef: undefined,
36
+ nested: {
37
+ set: new Set([1, 2, 3]),
38
+ map: new Map([['a', 1], ['b', 2]])
39
+ }
40
+ }
41
+
42
+ const result = await run(value, leafSerializer, leafDeserializer)
43
+
44
+ t.ok(result.date instanceof Date)
45
+ t.equal(result.date.toISOString(), value.date.toISOString())
46
+
47
+ t.ok(result.regex instanceof RegExp)
48
+ t.equal(result.regex.source, 'abc')
49
+ t.equal(result.regex.flags, 'gi')
50
+
51
+ t.equal(result.big, value.big)
52
+
53
+ t.ok(Object.prototype.hasOwnProperty.call(result, 'undef'))
54
+ t.equal(result.undef, undefined)
55
+
56
+ t.ok(result.nested.set instanceof Set)
57
+ t.strictSame(Array.from(result.nested.set), [1, 2, 3])
58
+
59
+ t.ok(result.nested.map instanceof Map)
60
+ t.strictSame(Array.from(result.nested.map.entries()), [['a', 1], ['b', 2]])
61
+
62
+ t.end()
63
+ })
64
+
65
+ test('default leaf deserializer when not provided', async (t) => {
66
+ const value = { a: 1, b: 'hello' }
67
+ const result = await run(value)
68
+
69
+ t.strictSame(result, value)
70
+ t.end()
71
+ })
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "sendscript",
3
- "version": "1.0.6",
3
+ "version": "2.0.1",
4
4
  "description": "Blur the line between server and client code.",
5
5
  "module": true,
6
6
  "main": "index.mjs",
7
+ "bin": "./cli.mjs",
7
8
  "keywords": [
8
9
  "rpc",
9
10
  "json",
@@ -23,9 +24,13 @@
23
24
  "author": "Bas Huis",
24
25
  "license": "MIT",
25
26
  "devDependencies": {
26
- "tap": "^21.6.3"
27
+ "superjson": "^2.2.6",
28
+ "tap": "^21.6.3",
29
+ "tape-check": "^1.0.0-rc.0"
27
30
  },
28
31
  "dependencies": {
29
- "debug": "^4.4.3"
32
+ "debug": "^4.4.3",
33
+ "typedoc": "^0.28.18",
34
+ "typedoc-plugin-markdown": "^4.11.0"
30
35
  }
31
36
  }
package/parse.mjs CHANGED
@@ -1,43 +1,125 @@
1
1
  import Debug from './debug.mjs'
2
- import isNil from './is-nil.mjs'
3
2
  import { SendScriptReferenceError } from './error.mjs'
4
3
 
4
+ function flattenSchema (schema) {
5
+ const obj = {}
6
+
7
+ for (const item of schema) {
8
+ if (typeof item === 'string') {
9
+ // leaf function
10
+ obj[item] = true
11
+ } else if (Array.isArray(item)) {
12
+ const [name, children] = item
13
+ // TODO: Test this
14
+ if (!Array.isArray(children)) {
15
+ throw new Error(`Expected children array for namespace "${name}"`)
16
+ }
17
+ obj[name] = flattenSchema(children)
18
+ // TODO: Test this also
19
+ } else {
20
+ throw new Error('Schema items must be strings or [name, children] arrays')
21
+ }
22
+ }
23
+
24
+ return obj
25
+ }
26
+
5
27
  const debug = Debug.extend('parse')
6
28
 
7
- export default (env) =>
8
- function parse (program) {
9
- debug('program', program)
29
+ const isThenable = (value) => (
30
+ value != null && typeof value.then === 'function'
31
+ )
10
32
 
11
- const awaits = []
12
- const resolved = {}
33
+ const isAwaitPromise = Symbol('sendscript-await')
34
+ const undefinedSentinel = Symbol('sendscript-undefined')
13
35
 
14
- JSON.parse(program, (key, value) => {
15
- if (!Array.isArray(value)) return value
36
+ const isPlainObject = (value) => {
37
+ if (!value || typeof value !== 'object') return false
38
+ const proto = Object.getPrototypeOf(value)
39
+ return proto === Object.prototype || proto === null
40
+ }
16
41
 
17
- const [operator, ...rest] = value
42
+ const markAwait = (promise) => {
43
+ promise[isAwaitPromise] = true
44
+ return promise
45
+ }
18
46
 
19
- if (operator === 'await') {
20
- const [program, awaitId] = rest
21
-
22
- awaits.push(((program, awaitId) => async () => {
23
- resolved[awaitId] = await JSON.parse(
24
- JSON.stringify(program),
25
- reviver
26
- )
27
- })(program, awaitId))
28
- }
47
+ const isExplicitAwait = (value) => (
48
+ isThenable(value) && value[isAwaitPromise] === true
49
+ )
29
50
 
30
- return value
51
+ // Recursively resolve awaited values in a parsed tree
52
+ const resolveAwaitedValues = (value) => {
53
+ if (value === undefinedSentinel) return undefined
54
+
55
+ if (isThenable(value)) {
56
+ return isExplicitAwait(value)
57
+ ? markAwait(value.then(resolveAwaitedValues))
58
+ : value
59
+ }
60
+
61
+ if (Array.isArray(value)) {
62
+ let hasAwait = false
63
+ const result = value.map((item) => {
64
+ const resolved = resolveAwaitedValues(item)
65
+ if (isExplicitAwait(resolved)) hasAwait = true
66
+ return resolved
31
67
  })
32
68
 
33
- const spy = fn => (...args) => {
34
- const value = fn(...args)
35
- debug(args, ' => ', value)
36
- return value
69
+ if (!hasAwait) return result
70
+
71
+ const awaited = result.map((item, index) =>
72
+ isExplicitAwait(item)
73
+ ? Promise.resolve(item).then((resolved) => {
74
+ result[index] = resolved
75
+ return resolved
76
+ })
77
+ : item
78
+ )
79
+
80
+ return markAwait(Promise.all(awaited).then(() => result))
81
+ }
82
+
83
+ if (isPlainObject(value)) {
84
+ const result = {}
85
+ const promises = []
86
+
87
+ for (const key of Object.keys(value)) {
88
+ const resolved = resolveAwaitedValues(value[key])
89
+ if (isExplicitAwait(resolved)) {
90
+ promises.push(
91
+ Promise.resolve(resolved).then((resolvedValue) => {
92
+ result[key] = resolvedValue
93
+ })
94
+ )
95
+ } else {
96
+ result[key] = resolved
97
+ }
37
98
  }
38
99
 
100
+ if (!promises.length) return result
101
+ return markAwait(Promise.all(promises).then(() => result))
102
+ }
103
+
104
+ return value
105
+ }
106
+
107
+ const spy = (fn) => (...args) => {
108
+ const value = fn(...args)
109
+ debug(args, ' => ', value)
110
+ return value
111
+ }
112
+
113
+ const defaultLeafDeserializer = (text) => JSON.parse(text)
114
+
115
+ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
116
+ const schema = flattenSchema(schemaArg)
117
+
118
+ return function parse (program) {
119
+ debug('program', program)
120
+
39
121
  const reviver = spy((key, value) => {
40
- if (isNil(value)) return value
122
+ if (value === null) return value
41
123
 
42
124
  if (!Array.isArray(value)) {
43
125
  return value
@@ -45,48 +127,64 @@ export default (env) =>
45
127
 
46
128
  const [operator, ...rest] = value
47
129
 
48
- if (operator === 'await') {
49
- const [, awaitId] = rest
50
- debug('read awaits', resolved[awaitId], awaitId)
130
+ if (operator === 'leaf') {
131
+ const leafValue = deserialize(rest[0])
132
+ return leafValue === undefined ? undefinedSentinel : leafValue
133
+ }
51
134
 
52
- return resolved[awaitId]
135
+ if (operator === 'await') {
136
+ const [program] = rest
137
+ return markAwait(Promise.resolve(program))
53
138
  }
54
139
 
55
140
  if (Array.isArray(operator) && operator[0] === 'quote') {
56
141
  const [, quoted] = operator
57
-
58
142
  return [quoted, ...rest]
59
143
  }
60
144
 
61
145
  if (operator === 'call') {
62
146
  const [fn, args] = rest
63
-
64
- return fn(...args)
147
+ const resolvedFn = isExplicitAwait(fn) ? Promise.resolve(fn) : fn
148
+ const resolvedArgs = resolveAwaitedValues(args)
149
+
150
+ if (isExplicitAwait(resolvedFn) || isExplicitAwait(resolvedArgs)) {
151
+ const promiseFn = isExplicitAwait(resolvedFn)
152
+ ? Promise.resolve(resolvedFn)
153
+ : resolvedFn
154
+ const promiseArgs = isExplicitAwait(resolvedArgs)
155
+ ? Promise.resolve(resolvedArgs)
156
+ : resolvedArgs
157
+
158
+ return Promise.all([promiseFn, promiseArgs])
159
+ .then(([resolvedFnValue, resolvedArgsValue]) => resolvedFnValue(...resolvedArgsValue))
160
+ }
161
+
162
+ return fn(...resolvedArgs)
65
163
  }
66
164
 
67
165
  if (operator === 'ref') {
68
- const [name] = rest
69
-
70
- if (Object.hasOwn(env, name)) return env[name]
71
-
72
- throw new SendScriptReferenceError({ key, value })
166
+ const path = rest // e.g., ["math","add"]
167
+ let current = env
168
+ let schemaCurrent = schema
169
+
170
+ for (const segment of path) {
171
+ if (schemaCurrent && Object.hasOwn(schemaCurrent, segment)) {
172
+ current = current[segment]
173
+ schemaCurrent = schemaCurrent[segment]
174
+ } else {
175
+ throw new SendScriptReferenceError({ key, value })
176
+ }
177
+ }
178
+
179
+ return current
73
180
  }
74
181
 
75
182
  return value
76
183
  })
77
184
 
78
- if (awaits.length) {
79
- return sequential(awaits).then(() => {
80
- return JSON.parse(program, reviver)
81
- })
82
- }
185
+ const parsed = JSON.parse(program, reviver)
186
+ const result = resolveAwaitedValues(parsed)
83
187
 
84
- return JSON.parse(program, reviver)
188
+ return result
85
189
  }
86
-
87
- function sequential (promises) {
88
- return promises.reduce(
89
- (acc, curr) => acc.then(results => curr().then(res => [...results, res])),
90
- Promise.resolve([])
91
- )
92
190
  }