sendscript 2.3.1 → 2.3.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/.prettierrc +6 -0
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +28 -4
- package/README.md +203 -128
- package/README.mz +82 -87
- package/example/client.socket.io.mjs +2 -4
- package/example/math.mjs +1 -1
- package/example/typescript/docs/functions/add.md +1 -1
- package/example/typescript/docs/functions/square.md +1 -1
- package/package.json +1 -1
- package/parse.mjs +51 -29
- package/references.mjs +57 -6
- package/stringify.mjs +45 -15
package/README.mz
CHANGED
|
@@ -11,23 +11,25 @@ Write JS code that you can run on servers, browsers or other clients.
|
|
|
11
11
|
|
|
12
12
|
## Introduction
|
|
13
13
|
|
|
14
|
-
There has been interest in improving APIs by allowing aggregations in a
|
|
15
|
-
|
|
14
|
+
There has been interest in improving APIs by allowing aggregations in a single
|
|
15
|
+
request. Examples include
|
|
16
16
|
|
|
17
|
-
- [JSON-RPC](https://json-rpc.dev/) which allows you to do multiple requests
|
|
18
|
-
|
|
17
|
+
- [JSON-RPC](https://json-rpc.dev/) which allows you to do multiple requests but
|
|
18
|
+
it does not allow you to compose the return value of one endpoint to be the
|
|
19
19
|
input/arguments of another.
|
|
20
20
|
|
|
21
|
-
- [GraphQL](https://graphql.org/) is very cool but also introduces a new
|
|
22
|
-
tooling that is required to wield it.
|
|
21
|
+
- [GraphQL](https://graphql.org/) is very cool but also introduces a new
|
|
22
|
+
languages and the tooling that is required to wield it.
|
|
23
23
|
|
|
24
|
-
What SendScript attempts is to allow for very expressive queries and mutations
|
|
25
|
-
that read and write like ordinary JS. That means that the
|
|
26
|
-
that are sent to the server from a client can also
|
|
27
|
-
|
|
28
|
-
using more advanced
|
|
24
|
+
What SendScript attempts is to allow for very expressive queries and mutations
|
|
25
|
+
to be performed that read and write like ordinary JS. That means that the
|
|
26
|
+
queries and complete programs that are sent to the server from a client can also
|
|
27
|
+
just run on the server as is. The only limitation being the serialization which
|
|
28
|
+
by default is limited by JSON and could be extended by using more advanced
|
|
29
|
+
(de)serialization libraries.
|
|
29
30
|
|
|
30
|
-
SendScript produces an intermediate JSON representation of the program. Let's
|
|
31
|
+
SendScript produces an intermediate JSON representation of the program. Let's
|
|
32
|
+
see what that looks like.
|
|
31
33
|
|
|
32
34
|
```js|json node --input-type=module | tee /tmp/sendscript.json
|
|
33
35
|
import Stringify from 'sendscript/stringify.mjs'
|
|
@@ -60,22 +62,26 @@ console.log(parse(program))
|
|
|
60
62
|
SendScript does more than a simple function call. It supports function
|
|
61
63
|
composition and even await.
|
|
62
64
|
|
|
63
|
-
This package is nothing more than the absolute core of sendscript. It
|
|
64
|
-
includes:
|
|
65
|
+
This package is nothing more than the absolute core of sendscript. It includes:
|
|
65
66
|
|
|
66
67
|
- The `references` function to create stubs to write the programs.
|
|
67
68
|
- `stringify` which takes the program and returns a JSON string.
|
|
68
|
-
- `parse` which takes the `stringify` JSON string and a real module and returns
|
|
69
|
+
- `parse` which takes the `stringify` JSON string and a real module and returns
|
|
70
|
+
the result.
|
|
69
71
|
|
|
70
|
-
The naming could use more love and there are many things to solve either in the
|
|
71
|
-
Things like supporting more complex (de)serializers, errors
|
|
72
|
-
sendscript programs. Contact me if I have
|
|
72
|
+
The naming could use more love and there are many things to solve either in the
|
|
73
|
+
core or around it. Things like supporting more complex (de)serializers, errors
|
|
74
|
+
and maybe mixing client functions with sendscript programs. Contact me if I have
|
|
75
|
+
piqued your interest.
|
|
73
76
|
|
|
74
77
|
---
|
|
75
78
|
|
|
76
|
-
SendScript leaves it up to you to choose HTTP, web-sockets or any other
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
SendScript leaves it up to you to choose HTTP, web-sockets or any other method
|
|
80
|
+
of communication between servers and clients that best fits your needs.
|
|
81
|
+
|
|
82
|
+
## Reference
|
|
83
|
+
|
|
84
|
+
<!-- Reference -->
|
|
79
85
|
|
|
80
86
|
## Socket example
|
|
81
87
|
|
|
@@ -89,7 +95,7 @@ We write a simple module.
|
|
|
89
95
|
// ./example/math.mjs
|
|
90
96
|
|
|
91
97
|
export const add = (a, b) => a + b
|
|
92
|
-
export const square = a => a * a
|
|
98
|
+
export const square = (a) => a * a
|
|
93
99
|
```
|
|
94
100
|
|
|
95
101
|
### Server
|
|
@@ -141,12 +147,10 @@ const stringify = Stringify()
|
|
|
141
147
|
const port = process.env.PORT || 3000
|
|
142
148
|
const client = socketClient(`http://localhost:${port}`)
|
|
143
149
|
|
|
144
|
-
const send = program => {
|
|
150
|
+
const send = (program) => {
|
|
145
151
|
return new Promise((resolve, reject) => {
|
|
146
152
|
client.emit('message', stringify(program), (error, result) => {
|
|
147
|
-
error
|
|
148
|
-
? reject(error)
|
|
149
|
-
: resolve(result)
|
|
153
|
+
error ? reject(error) : resolve(result)
|
|
150
154
|
})
|
|
151
155
|
})
|
|
152
156
|
}
|
|
@@ -179,9 +183,11 @@ pkill sendscript
|
|
|
179
183
|
|
|
180
184
|
## Repl
|
|
181
185
|
|
|
182
|
-
Sendscript ships with a barebones (no-dependencies) node-repl script. One can
|
|
186
|
+
Sendscript ships with a barebones (no-dependencies) node-repl script. One can
|
|
187
|
+
run it by simply typing `sendscript` in their console.
|
|
183
188
|
|
|
184
|
-
> Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for
|
|
189
|
+
> Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for
|
|
190
|
+
> printingonly sendscript logs.
|
|
185
191
|
|
|
186
192
|
## Promises
|
|
187
193
|
|
|
@@ -193,27 +199,33 @@ Supported since vs `v2.3`.
|
|
|
193
199
|
const getOrCreatePost = send(createPost(title).catch(createPost(title)))
|
|
194
200
|
```
|
|
195
201
|
|
|
196
|
-
You will likely need to define better helpers that makes it safer to handle
|
|
197
|
-
|
|
202
|
+
You will likely need to define better helpers that makes it safer to handle
|
|
203
|
+
rejections and work with promises. It is however sensible to have this basic
|
|
204
|
+
behavior for the sendscript DSL and parser.
|
|
198
205
|
|
|
199
206
|
### await
|
|
200
207
|
|
|
201
|
-
SendScript supports async/await seamlessly within a single request. This avoids
|
|
208
|
+
SendScript supports async/await seamlessly within a single request. This avoids
|
|
209
|
+
the performance pitfalls of waterfall-style messaging, which can be especially
|
|
210
|
+
slow on high-latency networks.
|
|
202
211
|
|
|
203
|
-
While it's possible to chain promises manually or use utility functions, native
|
|
212
|
+
While it's possible to chain promises manually or use utility functions, native
|
|
213
|
+
async/await support makes your code more readable, modern, and easier to reason
|
|
214
|
+
about — aligning SendScript with today’s JavaScript best practices.
|
|
204
215
|
|
|
205
216
|
```js
|
|
206
217
|
const userId = 'user-123'
|
|
207
218
|
const program = {
|
|
208
219
|
unread: await fetchUnreadMessages(userId),
|
|
209
220
|
emptyTrash: await emptyTrash(userId),
|
|
210
|
-
archived: await archiveMessages(selectMessages({ old: true }))
|
|
221
|
+
archived: await archiveMessages(selectMessages({ old: true })),
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
const result = await send(program)
|
|
214
225
|
```
|
|
215
226
|
|
|
216
|
-
This operation is done in a single round-trip. The result is an object with the
|
|
227
|
+
This operation is done in a single round-trip. The result is an object with the
|
|
228
|
+
defined properties and returned values.
|
|
217
229
|
|
|
218
230
|
## TypeScript
|
|
219
231
|
|
|
@@ -246,15 +258,16 @@ npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./e
|
|
|
246
258
|
|
|
247
259
|
You can see the docs [here](./example/typescript/docs/globals.md)
|
|
248
260
|
|
|
249
|
-
> [!NOTE]
|
|
250
|
-
>
|
|
251
|
-
>
|
|
252
|
-
> Values are subject to serialization and deserialization.
|
|
253
|
-
|
|
261
|
+
> [!NOTE] Although type coercion on the client side can improve the development
|
|
262
|
+
> experience, it does not represent the actual type. Values are subject to
|
|
263
|
+
> serialization and deserialization.
|
|
254
264
|
|
|
255
265
|
## Schema and Nested Modules
|
|
256
266
|
|
|
257
|
-
Sendscript allows you to define your API as a **nested object of functions**,
|
|
267
|
+
Sendscript allows you to define your API as a **nested object of functions**,
|
|
268
|
+
making it easy to organize your DSL into modules and submodules. Each function
|
|
269
|
+
is instrumented so that when serialized, it produces a structured reference that
|
|
270
|
+
can be safely sent and executed elsewhere.
|
|
258
271
|
|
|
259
272
|
### Defining a Nested Module
|
|
260
273
|
|
|
@@ -286,17 +299,13 @@ Functions are referenced via their **path in the module tree**:
|
|
|
286
299
|
```js
|
|
287
300
|
const { math, vector } = references(schema)
|
|
288
301
|
|
|
289
|
-
math.add(
|
|
290
|
-
1,
|
|
291
|
-
vector.length(
|
|
292
|
-
vector.multiply([1,2], 3)
|
|
293
|
-
)
|
|
294
|
-
)
|
|
302
|
+
math.add(1, vector.length(vector.multiply([1, 2], 3)))
|
|
295
303
|
```
|
|
296
304
|
|
|
297
305
|
## Validation (using Zod)
|
|
298
306
|
|
|
299
|
-
SendScript focuses on program serialization and execution. For runtime input
|
|
307
|
+
SendScript focuses on program serialization and execution. For runtime input
|
|
308
|
+
validation, you can use [Zod](https://zod.dev).
|
|
300
309
|
|
|
301
310
|
### Validating structured input
|
|
302
311
|
|
|
@@ -304,7 +313,7 @@ SendScript focuses on program serialization and execution. For runtime input val
|
|
|
304
313
|
const userSchema = z.object({
|
|
305
314
|
id: z.string().uuid(),
|
|
306
315
|
name: z.string(),
|
|
307
|
-
roles: z.array(z.string())
|
|
316
|
+
roles: z.array(z.string()),
|
|
308
317
|
})
|
|
309
318
|
|
|
310
319
|
export function createUser(user) {
|
|
@@ -322,13 +331,19 @@ export function createUser(user) {
|
|
|
322
331
|
|
|
323
332
|
## Leaf Serializer
|
|
324
333
|
|
|
325
|
-
By default, SendScript uses JSON for serialization, which limits support to
|
|
334
|
+
By default, SendScript uses JSON for serialization, which limits support to
|
|
335
|
+
primitives and plain objects/arrays. To support richer JavaScript types like
|
|
336
|
+
`Date`, `RegExp`, `BigInt`, `Map`, `Set`, and `undefined`, you can provide
|
|
337
|
+
custom serialization functions.
|
|
326
338
|
|
|
327
|
-
The `stringify` function accepts an optional `leafSerializer` parameter, and
|
|
339
|
+
The `stringify` function accepts an optional `leafSerializer` parameter, and
|
|
340
|
+
`parse` accepts an optional `leafDeserializer` parameter. These functions
|
|
341
|
+
control how non-SendScript values (leaves) are encoded and decoded.
|
|
328
342
|
|
|
329
343
|
### Example with superjson
|
|
330
344
|
|
|
331
|
-
Here's how to use [superjson](https://github.com/blitz-js/superjson) to support
|
|
345
|
+
Here's how to use [superjson](https://github.com/blitz-js/superjson) to support
|
|
346
|
+
extended types:
|
|
332
347
|
|
|
333
348
|
```js
|
|
334
349
|
import SuperJSON from 'superjson'
|
|
@@ -358,7 +373,10 @@ const program = {
|
|
|
358
373
|
pattern: /foo/gi,
|
|
359
374
|
count: BigInt('9007199254740992'),
|
|
360
375
|
items: new Set([1, 2, 3]),
|
|
361
|
-
mapping: new Map([
|
|
376
|
+
mapping: new Map([
|
|
377
|
+
['a', 1],
|
|
378
|
+
['b', 2],
|
|
379
|
+
]),
|
|
362
380
|
}
|
|
363
381
|
|
|
364
382
|
// Serialize with custom leaf serializer
|
|
@@ -368,8 +386,8 @@ const json = stringify(processData(program))
|
|
|
368
386
|
const env = {
|
|
369
387
|
processData: (data) => ({
|
|
370
388
|
success: true,
|
|
371
|
-
received: data
|
|
372
|
-
})
|
|
389
|
+
received: data,
|
|
390
|
+
}),
|
|
373
391
|
}
|
|
374
392
|
|
|
375
393
|
// Parse with custom leaf deserializer
|
|
@@ -378,24 +396,8 @@ const parse = Parse(schema, env, leadDeserializer)
|
|
|
378
396
|
const result = parse(json)
|
|
379
397
|
```
|
|
380
398
|
|
|
381
|
-
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous
|
|
382
|
-
|
|
383
|
-
## Tests
|
|
384
|
-
|
|
385
|
-
Tests with 100% code coverage.
|
|
386
|
-
|
|
387
|
-
```bash bash
|
|
388
|
-
npm t -- -R silent
|
|
389
|
-
npm t -- report text-summary
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
## Formatting
|
|
393
|
-
|
|
394
|
-
Standard because no config.
|
|
395
|
-
|
|
396
|
-
```bash bash
|
|
397
|
-
npx standard
|
|
398
|
-
```
|
|
399
|
+
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous
|
|
400
|
+
and safe from colliding with SendScript operators.
|
|
399
401
|
|
|
400
402
|
## Changelog
|
|
401
403
|
|
|
@@ -406,24 +408,17 @@ The [changelog][changelog] is generated using the useful
|
|
|
406
408
|
npx auto-changelog -p
|
|
407
409
|
```
|
|
408
410
|
|
|
409
|
-
## Dependencies
|
|
410
|
-
|
|
411
|
-
Check if packages are up to date on release.
|
|
412
|
-
|
|
413
|
-
```bash bash
|
|
414
|
-
npm outdated && echo 'No outdated packages found'
|
|
415
|
-
```
|
|
416
|
-
|
|
417
411
|
## License
|
|
418
412
|
|
|
419
413
|
See the [LICENSE.txt][license] file for details.
|
|
420
414
|
|
|
421
|
-
##
|
|
415
|
+
## Issues
|
|
422
416
|
|
|
423
|
-
|
|
417
|
+
See [issues][issues] for roadmap and known bugs.
|
|
424
418
|
|
|
425
|
-
[license]
|
|
426
|
-
[socket.io]:https://socket.io/
|
|
427
|
-
[changelog]
|
|
428
|
-
[auto-changelog]:https://www.npmjs.com/package/auto-changelog
|
|
429
|
-
[typedoc]:https://github.com/TypeStrong/typedoc
|
|
419
|
+
[license]: ./LICENSE.txt
|
|
420
|
+
[socket.io]: https://socket.io/
|
|
421
|
+
[changelog]: ./CHANGELOG.md
|
|
422
|
+
[auto-changelog]: https://www.npmjs.com/package/auto-changelog
|
|
423
|
+
[typedoc]: https://github.com/TypeStrong/typedoc
|
|
424
|
+
[issues]: https://github.com/bas080/sendscript/issues
|
|
@@ -11,12 +11,10 @@ const stringify = Stringify()
|
|
|
11
11
|
const port = process.env.PORT || 3000
|
|
12
12
|
const client = socketClient(`http://localhost:${port}`)
|
|
13
13
|
|
|
14
|
-
const send = program => {
|
|
14
|
+
const send = (program) => {
|
|
15
15
|
return new Promise((resolve, reject) => {
|
|
16
16
|
client.emit('message', stringify(program), (error, result) => {
|
|
17
|
-
error
|
|
18
|
-
? reject(error)
|
|
19
|
-
: resolve(result)
|
|
17
|
+
error ? reject(error) : resolve(result)
|
|
20
18
|
})
|
|
21
19
|
})
|
|
22
20
|
}
|
package/example/math.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> **add**(`a`, `b`): `number`
|
|
10
10
|
|
|
11
|
-
Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/
|
|
11
|
+
Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/08eb266b06d8998e6849dbc5c9738fbe782ada18/example/typescript/math.ts#L1)
|
|
12
12
|
|
|
13
13
|
## Parameters
|
|
14
14
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> **square**(`a`): `number`
|
|
10
10
|
|
|
11
|
-
Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/
|
|
11
|
+
Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/08eb266b06d8998e6849dbc5c9738fbe782ada18/example/typescript/math.ts#L2)
|
|
12
12
|
|
|
13
13
|
## Parameters
|
|
14
14
|
|
package/package.json
CHANGED
package/parse.mjs
CHANGED
|
@@ -6,13 +6,14 @@ function flattenSchema (schema) {
|
|
|
6
6
|
|
|
7
7
|
for (const item of schema) {
|
|
8
8
|
if (typeof item === 'string') {
|
|
9
|
-
// leaf function
|
|
10
9
|
obj[item] = true
|
|
11
10
|
} else if (Array.isArray(item)) {
|
|
12
11
|
const [name, children] = item
|
|
12
|
+
|
|
13
13
|
if (!Array.isArray(children)) {
|
|
14
14
|
throw new Error(`Expected children array for namespace "${name}"`)
|
|
15
15
|
}
|
|
16
|
+
|
|
16
17
|
obj[name] = flattenSchema(children)
|
|
17
18
|
} else {
|
|
18
19
|
throw new Error('Schema items must be strings or [name, children] arrays')
|
|
@@ -38,7 +39,6 @@ const spy = (type, fn) => (...args) => {
|
|
|
38
39
|
return value
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
// Recursively resolve awaited values in a parsed tree
|
|
42
42
|
const evaluate = spy('eval', (value, awaits = []) => {
|
|
43
43
|
if (value === undefinedSentinel) return undefined
|
|
44
44
|
|
|
@@ -47,49 +47,41 @@ const evaluate = spy('eval', (value, awaits = []) => {
|
|
|
47
47
|
|
|
48
48
|
if (operator === 'await') {
|
|
49
49
|
const [index] = rest
|
|
50
|
-
// No need to check index. It is closely tied to the program.
|
|
51
|
-
// if (typeof index !== 'number' || index < 0 || index >= awaitsResolved.length) {
|
|
52
|
-
// throw new Error(`Invalid await index: ${index}`);
|
|
53
|
-
// }
|
|
54
50
|
return awaits[index]
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
if (operator === 'then') {
|
|
58
54
|
const [v, onResolve, onReject] = rest
|
|
59
|
-
return evaluate(v, awaits).then(
|
|
55
|
+
return evaluate(v, awaits).then(
|
|
56
|
+
evaluate(onResolve, awaits),
|
|
57
|
+
evaluate(onReject, awaits)
|
|
58
|
+
)
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
if (operator === 'call') {
|
|
63
|
-
// Step 1: evaluate the function itself
|
|
64
62
|
let [fn, args] = rest
|
|
65
63
|
fn = evaluate(fn, awaits)
|
|
66
64
|
|
|
67
|
-
// Step 2: evaluate each argument AFTER fn is ready
|
|
68
65
|
for (let i = 0; i < args.length; i++) {
|
|
69
66
|
args[i] = evaluate(args[i], awaits)
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
// Step 3: call the function
|
|
73
69
|
return fn(...args)
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
if (operator === 'quote') {
|
|
77
73
|
const [quoted] = rest
|
|
78
|
-
return quoted
|
|
74
|
+
return quoted
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (let index = 0; index < value.length; index++) {
|
|
85
|
-
const item = value[index]
|
|
86
|
-
value[index] = evaluate(item, awaits)
|
|
77
|
+
for (let i = 0; i < value.length; i++) {
|
|
78
|
+
value[i] = evaluate(value[i], awaits)
|
|
87
79
|
}
|
|
80
|
+
|
|
88
81
|
return value
|
|
89
82
|
}
|
|
90
83
|
|
|
91
84
|
if (isPlainObject(value)) {
|
|
92
|
-
// We muatate the object itself. No need to make a new one.
|
|
93
85
|
for (const key of Object.keys(value)) {
|
|
94
86
|
value[key] = evaluate(value[key], awaits)
|
|
95
87
|
}
|
|
@@ -99,23 +91,56 @@ const evaluate = spy('eval', (value, awaits = []) => {
|
|
|
99
91
|
return value
|
|
100
92
|
})
|
|
101
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Default deserializer for leaf nodes.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} text
|
|
98
|
+
* @returns {any}
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
102
101
|
const defaultLeafDeserializer = (text) => JSON.parse(text)
|
|
103
102
|
|
|
104
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Creates a program parser for a given schema and environment.
|
|
105
|
+
*
|
|
106
|
+
* The parser:
|
|
107
|
+
* - resolves references into runtime functions
|
|
108
|
+
* - deserializes leaf nodes
|
|
109
|
+
* - collects and executes async awaits
|
|
110
|
+
* - evaluates AST-like JSON programs
|
|
111
|
+
*
|
|
112
|
+
* @param {Array<string | [string, Array]>} schemaArg
|
|
113
|
+
* @param {Object} env - runtime environment for refs
|
|
114
|
+
* @param {(text: string) => any} [deserialize=defaultLeafDeserializer]
|
|
115
|
+
* @returns {(program: string) => any|Promise<any>}
|
|
116
|
+
* @public
|
|
117
|
+
*/
|
|
118
|
+
export default function Parse (schemaArg, env, deserialize = defaultLeafDeserializer) {
|
|
105
119
|
const schema = flattenSchema(schemaArg)
|
|
106
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Parses and executes a serialized program.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} program - JSON encoded program
|
|
125
|
+
* @returns {any|Promise<any>}
|
|
126
|
+
* @public
|
|
127
|
+
*/
|
|
107
128
|
return function parse (program) {
|
|
108
129
|
debug('program', program)
|
|
109
130
|
const awaits = []
|
|
110
131
|
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
/**
|
|
133
|
+
* JSON reviver used during parsing.
|
|
134
|
+
* Converts encoded operators into runtime structures.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} key
|
|
137
|
+
* @param {any} value
|
|
138
|
+
* @returns {any}
|
|
139
|
+
*/
|
|
113
140
|
const reviver = spy('revive', (key, value) => {
|
|
114
141
|
if (value === null) return value
|
|
115
142
|
|
|
116
|
-
if (!Array.isArray(value))
|
|
117
|
-
return value
|
|
118
|
-
}
|
|
143
|
+
if (!Array.isArray(value)) return value
|
|
119
144
|
|
|
120
145
|
const [operator, ...rest] = value
|
|
121
146
|
|
|
@@ -126,7 +151,6 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
|
|
|
126
151
|
|
|
127
152
|
if (operator === 'await') {
|
|
128
153
|
const [program] = rest
|
|
129
|
-
|
|
130
154
|
return ['await', awaits.push(program) - 1]
|
|
131
155
|
}
|
|
132
156
|
|
|
@@ -151,19 +175,17 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
|
|
|
151
175
|
})
|
|
152
176
|
|
|
153
177
|
const parsed = JSON.parse(program, reviver)
|
|
154
|
-
|
|
155
178
|
debug('parsed', parsed)
|
|
156
179
|
|
|
157
180
|
if (awaits.length) {
|
|
158
181
|
debug('awaits', awaits)
|
|
159
182
|
|
|
160
183
|
return (async function () {
|
|
161
|
-
for (let
|
|
162
|
-
awaits[
|
|
184
|
+
for (let i = 0; i < awaits.length; i++) {
|
|
185
|
+
awaits[i] = await evaluate(awaits[i], awaits)
|
|
163
186
|
}
|
|
164
187
|
|
|
165
188
|
debug('awaits(awaited)', awaits)
|
|
166
|
-
|
|
167
189
|
return evaluate(parsed, awaits)
|
|
168
190
|
})()
|
|
169
191
|
}
|
package/references.mjs
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { awaitSymbol, call, ref, then, referenceSymbol } from './symbol.mjs'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Creates a callable reference node bound to a path.
|
|
5
|
+
* Used to build a lazy/instrumented execution structure.
|
|
6
|
+
*
|
|
7
|
+
* @param {Array<string>} path - Path representing the function location in schema.
|
|
8
|
+
* @returns {Function} Reference function with attached control methods (.then, .catch, toJSON).
|
|
9
|
+
*/
|
|
3
10
|
function instrument (path) {
|
|
11
|
+
/**
|
|
12
|
+
* Creates a callable reference invocation.
|
|
13
|
+
*
|
|
14
|
+
* @param {...any} args - Arguments passed to the call.
|
|
15
|
+
* @returns {Function} New instrumented reference node.
|
|
16
|
+
*/
|
|
4
17
|
function reference (...args) {
|
|
5
18
|
const called = instrument(path)
|
|
6
19
|
|
|
@@ -13,6 +26,13 @@ function instrument (path) {
|
|
|
13
26
|
return called
|
|
14
27
|
}
|
|
15
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Internal helper for building promise-like continuation nodes.
|
|
31
|
+
*
|
|
32
|
+
* @param {Function|null} resolve
|
|
33
|
+
* @param {Function|null} reject
|
|
34
|
+
* @returns {Function} Instrumented continuation node.
|
|
35
|
+
*/
|
|
16
36
|
function dotThen (resolve, reject) {
|
|
17
37
|
const node = instrument(path)
|
|
18
38
|
|
|
@@ -26,18 +46,32 @@ function instrument (path) {
|
|
|
26
46
|
return node
|
|
27
47
|
}
|
|
28
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Registers rejection handler (promise-style).
|
|
51
|
+
*
|
|
52
|
+
* @param {Function} reject
|
|
53
|
+
* @returns {Function}
|
|
54
|
+
*/
|
|
29
55
|
reference.catch = (reject) => {
|
|
30
56
|
return dotThen(null, reject)
|
|
31
57
|
}
|
|
32
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Handles async chaining or awaiting logic.
|
|
61
|
+
*
|
|
62
|
+
* If resolve/reject contain a reference marker, it behaves like a .then chain.
|
|
63
|
+
* Otherwise it behaves like an await wrapper.
|
|
64
|
+
*
|
|
65
|
+
* @param {Function} resolve
|
|
66
|
+
* @param {Function} reject
|
|
67
|
+
* @returns {Function|any}
|
|
68
|
+
*/
|
|
33
69
|
reference.then = (resolve, reject) => {
|
|
34
|
-
// That is how we know if it is an await or a .then call.
|
|
35
70
|
if (resolve?.[referenceSymbol] || reject?.[referenceSymbol]) {
|
|
36
71
|
return dotThen(resolve, reject)
|
|
37
72
|
}
|
|
38
73
|
|
|
39
74
|
const awaited = instrument(path)
|
|
40
|
-
// Prevent infinite recur
|
|
41
75
|
delete awaited.then
|
|
42
76
|
|
|
43
77
|
awaited.toJSON = () => ({
|
|
@@ -48,6 +82,11 @@ function instrument (path) {
|
|
|
48
82
|
return resolve(awaited)
|
|
49
83
|
}
|
|
50
84
|
|
|
85
|
+
/**
|
|
86
|
+
* JSON representation of the reference path node.
|
|
87
|
+
*
|
|
88
|
+
* @returns {{ref: symbol, path: Array<string>}}
|
|
89
|
+
*/
|
|
51
90
|
reference.toJSON = () => ({
|
|
52
91
|
[ref]: ref,
|
|
53
92
|
path
|
|
@@ -58,17 +97,29 @@ function instrument (path) {
|
|
|
58
97
|
return reference
|
|
59
98
|
}
|
|
60
99
|
|
|
61
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Builds a nested API structure from a schema definition.
|
|
102
|
+
*
|
|
103
|
+
* Schema supports:
|
|
104
|
+
* - string => leaf function node
|
|
105
|
+
* - [name, children] => namespace with nested schema
|
|
106
|
+
*
|
|
107
|
+
* @param {Array<string | [string, Array]>} schema
|
|
108
|
+
* @param {Array<string>} [parentPath=[]]
|
|
109
|
+
* @returns {Object} Nested instrumented API object
|
|
110
|
+
*
|
|
111
|
+
* @throws {Error} If schema format is invalid
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
114
|
+
export default function References (schema, parentPath = []) {
|
|
62
115
|
return schema.reduce((acc, item) => {
|
|
63
116
|
if (typeof item === 'string') {
|
|
64
|
-
// leaf function
|
|
65
117
|
acc[item] = instrument([...parentPath, item])
|
|
66
118
|
} else if (Array.isArray(item)) {
|
|
67
119
|
const [name, children] = item
|
|
68
120
|
|
|
69
121
|
if (Array.isArray(children)) {
|
|
70
|
-
|
|
71
|
-
acc[name] = module(children, [...parentPath, name])
|
|
122
|
+
acc[name] = References(children, [...parentPath, name])
|
|
72
123
|
} else {
|
|
73
124
|
throw new Error(`Expected children array for namespace "${name}"`)
|
|
74
125
|
}
|