sendscript 2.3.2 → 2.3.4
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/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +129 -55
- package/README.mz +7 -11
- 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 +57 -32
- package/references.mjs +57 -6
- package/stringify.mjs +48 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v2.3.4](https://github.com/bas080/sendscript/compare/v2.3.3...v2.3.4)
|
|
8
|
+
|
|
9
|
+
- Improve naming in JSDocs [`669a40e`](https://github.com/bas080/sendscript/commit/669a40e28a5c77963d855dd4e2122d0ba2c074cd)
|
|
10
|
+
|
|
11
|
+
#### [v2.3.3](https://github.com/bas080/sendscript/compare/v2.3.2...v2.3.3)
|
|
12
|
+
|
|
13
|
+
> 14 April 2026
|
|
14
|
+
|
|
15
|
+
- Add JSDoc to relevant function for docs [`d1d3709`](https://github.com/bas080/sendscript/commit/d1d3709088a51b71651170ef642b013eedc9ca3a)
|
|
16
|
+
- Remove tests section from README [`5da7bb3`](https://github.com/bas080/sendscript/commit/5da7bb3f3b1dc8078683e076fe2b520bc6d28896)
|
|
17
|
+
- Add generated from JSDoc reference section [`08eb266`](https://github.com/bas080/sendscript/commit/08eb266b06d8998e6849dbc5c9738fbe782ada18)
|
|
18
|
+
|
|
7
19
|
#### [v2.3.2](https://github.com/bas080/sendscript/compare/v2.3.1...v2.3.2)
|
|
8
20
|
|
|
21
|
+
> 14 April 2026
|
|
22
|
+
|
|
9
23
|
- Format the README.mz file [`df43dbb`](https://github.com/bas080/sendscript/commit/df43dbb8b790cfc612536f49571f9fe4aad03837)
|
|
10
24
|
- Move some commands from README to CONTRIBUTING [`45659c5`](https://github.com/bas080/sendscript/commit/45659c51354ac3bbe3bed5ef3826785d8a1a4a32)
|
|
11
25
|
- Add format markdown check in contributing md [`e68d4d6`](https://github.com/bas080/sendscript/commit/e68d4d6aea26a7a54c2f933ad98a8e363d7f3357)
|
package/CONTRIBUTING.md
CHANGED
|
@@ -40,7 +40,9 @@ Generate the README from the mz file.
|
|
|
40
40
|
```bash bash
|
|
41
41
|
markatzea ./README.mz | tee ./README.md
|
|
42
42
|
|
|
43
|
-
npx markdown-toc
|
|
43
|
+
npx documentation readme references.mjs stringify.mjs parse.mjs -s Reference --github --a public --markdown-toc false
|
|
44
|
+
|
|
45
|
+
npx markdown-toc --maxdepth 3 -i README.md
|
|
44
46
|
|
|
45
47
|
git add *.md ./example
|
|
46
48
|
```
|
package/README.md
CHANGED
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Write JS code that you can run on servers, browsers or other clients.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/sendscript)
|
|
6
6
|
[](#tests)
|
|
7
7
|
[](https://standardjs.com)
|
|
8
|
-
[](./LICENSE.txt)
|
|
9
9
|
|
|
10
10
|
<!-- toc -->
|
|
11
11
|
|
|
12
12
|
- [Introduction](#introduction)
|
|
13
|
+
- [Reference](#reference)
|
|
14
|
+
* [defaultLeafParse](#defaultleafparse)
|
|
15
|
+
* [Parse](#parse)
|
|
16
|
+
* [parse](#parse)
|
|
17
|
+
* [References](#references)
|
|
18
|
+
* [defaultLeafStringify](#defaultleafstringify)
|
|
19
|
+
* [Stringify](#stringify)
|
|
20
|
+
* [stringify](#stringify)
|
|
13
21
|
- [Socket example](#socket-example)
|
|
14
22
|
* [Module](#module)
|
|
15
23
|
* [Server](#server)
|
|
@@ -25,10 +33,9 @@ Write JS code that you can run on servers, browsers or other clients.
|
|
|
25
33
|
* [Validating structured input](#validating-structured-input)
|
|
26
34
|
- [Leaf Serializer](#leaf-serializer)
|
|
27
35
|
* [Example with superjson](#example-with-superjson)
|
|
28
|
-
- [Tests](#tests)
|
|
29
36
|
- [Changelog](#changelog)
|
|
30
37
|
- [License](#license)
|
|
31
|
-
- [
|
|
38
|
+
- [Issues](#issues)
|
|
32
39
|
|
|
33
40
|
<!-- tocstop -->
|
|
34
41
|
|
|
@@ -37,12 +44,12 @@ Write JS code that you can run on servers, browsers or other clients.
|
|
|
37
44
|
There has been interest in improving APIs by allowing aggregations in a single
|
|
38
45
|
request. Examples include
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
* [JSON-RPC](https://json-rpc.dev/) which allows you to do multiple requests but
|
|
48
|
+
it does not allow you to compose the return value of one endpoint to be the
|
|
49
|
+
input/arguments of another.
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
* [GraphQL](https://graphql.org/) is very cool but also introduces a new
|
|
52
|
+
languages and the tooling that is required to wield it.
|
|
46
53
|
|
|
47
54
|
What SendScript attempts is to allow for very expressive queries and mutations
|
|
48
55
|
to be performed that read and write like ordinary JS. That means that the
|
|
@@ -63,6 +70,7 @@ const stringify = Stringify()
|
|
|
63
70
|
|
|
64
71
|
console.log(stringify(add(1,2)))
|
|
65
72
|
```
|
|
73
|
+
|
|
66
74
|
```json
|
|
67
75
|
["call",["ref","add"],[["leaf","1"],["leaf","2"]]]
|
|
68
76
|
```
|
|
@@ -84,6 +92,7 @@ const program = '["call",["ref","add"],[1,2]]'
|
|
|
84
92
|
|
|
85
93
|
console.log(parse(program))
|
|
86
94
|
```
|
|
95
|
+
|
|
87
96
|
```json
|
|
88
97
|
3
|
|
89
98
|
```
|
|
@@ -93,21 +102,105 @@ composition and even await.
|
|
|
93
102
|
|
|
94
103
|
This package is nothing more than the absolute core of sendscript. It includes:
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
* The `references` function to create stubs to write the programs.
|
|
106
|
+
* `stringify` which takes the program and returns a JSON string.
|
|
107
|
+
* `parse` which takes the `stringify` JSON string and a real module and returns
|
|
108
|
+
the result.
|
|
100
109
|
|
|
101
110
|
The naming could use more love and there are many things to solve either in the
|
|
102
111
|
core or around it. Things like supporting more complex (de)serializers, errors
|
|
103
112
|
and maybe mixing client functions with sendscript programs. Contact me if I have
|
|
104
113
|
piqued your interest.
|
|
105
114
|
|
|
106
|
-
|
|
115
|
+
***
|
|
107
116
|
|
|
108
117
|
SendScript leaves it up to you to choose HTTP, web-sockets or any other method
|
|
109
118
|
of communication between servers and clients that best fits your needs.
|
|
110
119
|
|
|
120
|
+
## Reference
|
|
121
|
+
|
|
122
|
+
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
|
|
123
|
+
|
|
124
|
+
### defaultLeafParse
|
|
125
|
+
|
|
126
|
+
Default deserializer for leaf nodes.
|
|
127
|
+
|
|
128
|
+
#### Parameters
|
|
129
|
+
|
|
130
|
+
* `text` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 
|
|
131
|
+
|
|
132
|
+
Returns **any** 
|
|
133
|
+
|
|
134
|
+
### Parse
|
|
135
|
+
|
|
136
|
+
#### Parameters
|
|
137
|
+
|
|
138
|
+
* `schemaArg` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | \[[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)])>** 
|
|
139
|
+
* `env` **Env** runtime environment for refs
|
|
140
|
+
* `leafParse` (optional, default `defaultLeafParse`)
|
|
141
|
+
|
|
142
|
+
Returns **[parse](#parse)** 
|
|
143
|
+
|
|
144
|
+
### parse
|
|
145
|
+
|
|
146
|
+
Parses and executes a serialized program.
|
|
147
|
+
|
|
148
|
+
#### Parameters
|
|
149
|
+
|
|
150
|
+
* `program` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** JSON encoded program
|
|
151
|
+
|
|
152
|
+
Returns **(any | [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<any>)** 
|
|
153
|
+
|
|
154
|
+
### References
|
|
155
|
+
|
|
156
|
+
Builds a nested API structure from a schema definition.
|
|
157
|
+
|
|
158
|
+
Schema supports:
|
|
159
|
+
|
|
160
|
+
* string => leaf function node
|
|
161
|
+
* \[name, children] => namespace with nested schema
|
|
162
|
+
|
|
163
|
+
#### Parameters
|
|
164
|
+
|
|
165
|
+
* `schema` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | \[[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)])>** 
|
|
166
|
+
* `parentPath` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>** (optional, default `[]`)
|
|
167
|
+
|
|
168
|
+
<!---->
|
|
169
|
+
|
|
170
|
+
* Throws **[Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)** If schema format is invalid
|
|
171
|
+
|
|
172
|
+
Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Nested instrumented API object
|
|
173
|
+
|
|
174
|
+
### defaultLeafStringify
|
|
175
|
+
|
|
176
|
+
Default strict serializer for leaf values.
|
|
177
|
+
|
|
178
|
+
Rejects non-JSON-safe values.
|
|
179
|
+
|
|
180
|
+
#### Parameters
|
|
181
|
+
|
|
182
|
+
* `x` **any** 
|
|
183
|
+
|
|
184
|
+
Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 
|
|
185
|
+
|
|
186
|
+
### Stringify
|
|
187
|
+
|
|
188
|
+
Creates a stringify function for SendScript AST structures.
|
|
189
|
+
|
|
190
|
+
#### Parameters
|
|
191
|
+
|
|
192
|
+
* `leafStringify` (optional, default `defaultLeafStringify`)
|
|
193
|
+
|
|
194
|
+
### stringify
|
|
195
|
+
|
|
196
|
+
Serializes a program into a JSON string representation.
|
|
197
|
+
|
|
198
|
+
#### Parameters
|
|
199
|
+
|
|
200
|
+
* `program` **any** 
|
|
201
|
+
|
|
202
|
+
Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 
|
|
203
|
+
|
|
111
204
|
## Socket example
|
|
112
205
|
|
|
113
206
|
For this example we'll use [socket.io][socket.io].
|
|
@@ -205,9 +298,8 @@ node ./example/client.socket.io.mjs
|
|
|
205
298
|
|
|
206
299
|
pkill sendscript
|
|
207
300
|
```
|
|
208
|
-
|
|
209
|
-
Result: 100
|
|
210
|
-
```
|
|
301
|
+
|
|
302
|
+
Result: 100
|
|
211
303
|
|
|
212
304
|
## Repl
|
|
213
305
|
|
|
@@ -259,18 +351,19 @@ defined properties and returned values.
|
|
|
259
351
|
|
|
260
352
|
There is a good use-case to write a module in TypeScript.
|
|
261
353
|
|
|
262
|
-
1.
|
|
263
|
-
|
|
264
|
-
2.
|
|
265
|
-
|
|
266
|
-
3.
|
|
267
|
-
|
|
354
|
+
1. Obviously the module would have the benefits that TypeScript offers when
|
|
355
|
+
coding.
|
|
356
|
+
2. You can use tools like [typedoc][typedoc] to generate docs from your types to
|
|
357
|
+
share with consumers of your API.
|
|
358
|
+
3. You can use the types of the module to coerce your client to adopt the
|
|
359
|
+
module's type.
|
|
268
360
|
|
|
269
361
|
Let's say we have this module which we use on the server.
|
|
270
362
|
|
|
271
363
|
```bash
|
|
272
364
|
cat ./example/typescript/math.ts
|
|
273
365
|
```
|
|
366
|
+
|
|
274
367
|
```ts
|
|
275
368
|
export const add = (a: number, b: number) => a + b
|
|
276
369
|
export const square = (a: number) => a * a
|
|
@@ -281,6 +374,7 @@ We can then coerce the types of the instrumented stubs.
|
|
|
281
374
|
```bash
|
|
282
375
|
cat ./example/typescript/client.ts
|
|
283
376
|
```
|
|
377
|
+
|
|
284
378
|
```ts
|
|
285
379
|
import math from './math.client.ts'
|
|
286
380
|
import Stringify from 'sendscript/stringify.mjs'
|
|
@@ -306,7 +400,7 @@ npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./e
|
|
|
306
400
|
|
|
307
401
|
You can see the docs [here](./example/typescript/docs/globals.md)
|
|
308
402
|
|
|
309
|
-
> [!NOTE] Although type coercion on the client side can improve the development
|
|
403
|
+
> \[!NOTE] Although type coercion on the client side can improve the development
|
|
310
404
|
> experience, it does not represent the actual type. Values are subject to
|
|
311
405
|
> serialization and deserialization.
|
|
312
406
|
|
|
@@ -373,9 +467,9 @@ export function createUser(user) {
|
|
|
373
467
|
|
|
374
468
|
**Benefits**:
|
|
375
469
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
470
|
+
* Ensures arguments match expected types and shapes.
|
|
471
|
+
* Throws structured errors that can be propagated to clients.
|
|
472
|
+
* Works with TypeScript for automatic type inference.
|
|
379
473
|
|
|
380
474
|
## Leaf Serializer
|
|
381
475
|
|
|
@@ -447,32 +541,6 @@ const result = parse(json)
|
|
|
447
541
|
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous
|
|
448
542
|
and safe from colliding with SendScript operators.
|
|
449
543
|
|
|
450
|
-
## Tests
|
|
451
|
-
|
|
452
|
-
Tests with 100% code coverage.
|
|
453
|
-
|
|
454
|
-
```bash
|
|
455
|
-
npm t -- -R silent
|
|
456
|
-
npm t -- report text-summary
|
|
457
|
-
```
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
> sendscript@2.3.2 test
|
|
461
|
-
> tap -R silent
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
> sendscript@2.3.2 test
|
|
465
|
-
> tap report text-summary
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
=============================== Coverage summary ===============================
|
|
469
|
-
Statements : 100% ( 372/372 )
|
|
470
|
-
Branches : 100% ( 137/137 )
|
|
471
|
-
Functions : 100% ( 24/24 )
|
|
472
|
-
Lines : 100% ( 372/372 )
|
|
473
|
-
================================================================================
|
|
474
|
-
```
|
|
475
|
-
|
|
476
544
|
## Changelog
|
|
477
545
|
|
|
478
546
|
The [changelog][changelog] is generated using the useful
|
|
@@ -486,12 +554,18 @@ npx auto-changelog -p
|
|
|
486
554
|
|
|
487
555
|
See the [LICENSE.txt][license] file for details.
|
|
488
556
|
|
|
489
|
-
##
|
|
557
|
+
## Issues
|
|
490
558
|
|
|
491
|
-
|
|
559
|
+
See [issues][issues] for roadmap and known bugs.
|
|
492
560
|
|
|
493
561
|
[license]: ./LICENSE.txt
|
|
562
|
+
|
|
494
563
|
[socket.io]: https://socket.io/
|
|
564
|
+
|
|
495
565
|
[changelog]: ./CHANGELOG.md
|
|
566
|
+
|
|
496
567
|
[auto-changelog]: https://www.npmjs.com/package/auto-changelog
|
|
568
|
+
|
|
497
569
|
[typedoc]: https://github.com/TypeStrong/typedoc
|
|
570
|
+
|
|
571
|
+
[issues]: https://github.com/bas080/sendscript/issues
|
package/README.mz
CHANGED
|
@@ -79,6 +79,10 @@ piqued your interest.
|
|
|
79
79
|
SendScript leaves it up to you to choose HTTP, web-sockets or any other method
|
|
80
80
|
of communication between servers and clients that best fits your needs.
|
|
81
81
|
|
|
82
|
+
## Reference
|
|
83
|
+
|
|
84
|
+
<!-- Reference -->
|
|
85
|
+
|
|
82
86
|
## Socket example
|
|
83
87
|
|
|
84
88
|
For this example we'll use [socket.io][socket.io].
|
|
@@ -395,15 +399,6 @@ const result = parse(json)
|
|
|
395
399
|
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous
|
|
396
400
|
and safe from colliding with SendScript operators.
|
|
397
401
|
|
|
398
|
-
## Tests
|
|
399
|
-
|
|
400
|
-
Tests with 100% code coverage.
|
|
401
|
-
|
|
402
|
-
```bash bash
|
|
403
|
-
npm t -- -R silent
|
|
404
|
-
npm t -- report text-summary
|
|
405
|
-
```
|
|
406
|
-
|
|
407
402
|
## Changelog
|
|
408
403
|
|
|
409
404
|
The [changelog][changelog] is generated using the useful
|
|
@@ -417,12 +412,13 @@ npx auto-changelog -p
|
|
|
417
412
|
|
|
418
413
|
See the [LICENSE.txt][license] file for details.
|
|
419
414
|
|
|
420
|
-
##
|
|
415
|
+
## Issues
|
|
421
416
|
|
|
422
|
-
|
|
417
|
+
See [issues][issues] for roadmap and known bugs.
|
|
423
418
|
|
|
424
419
|
[license]: ./LICENSE.txt
|
|
425
420
|
[socket.io]: https://socket.io/
|
|
426
421
|
[changelog]: ./CHANGELOG.md
|
|
427
422
|
[auto-changelog]: https://www.npmjs.com/package/auto-changelog
|
|
428
423
|
[typedoc]: https://github.com/TypeStrong/typedoc
|
|
424
|
+
[issues]: https://github.com/bas080/sendscript/issues
|
|
@@ -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/669a40e28a5c77963d855dd4e2122d0ba2c074cd/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/669a40e28a5c77963d855dd4e2122d0ba2c074cd/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,34 +91,69 @@ const evaluate = spy('eval', (value, awaits = []) => {
|
|
|
99
91
|
return value
|
|
100
92
|
})
|
|
101
93
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Default deserializer for leaf nodes.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} text
|
|
98
|
+
* @returns {any}
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
const defaultLeafParse = (text) => JSON.parse(text)
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @template Env
|
|
105
|
+
* @typedef {(program: string) => any | Promise<any>} parse
|
|
106
|
+
*
|
|
107
|
+
* Creates a program parser for a given schema and environment.
|
|
108
|
+
*
|
|
109
|
+
* The parser:
|
|
110
|
+
* - resolves references into runtime functions
|
|
111
|
+
* - deserializes leaf nodes
|
|
112
|
+
* - collects and executes async awaits
|
|
113
|
+
* - evaluates AST-like JSON programs
|
|
114
|
+
*
|
|
115
|
+
* @param {Array<string | [string, Array]>} schemaArg
|
|
116
|
+
* @param {Env} env - runtime environment for refs
|
|
117
|
+
* @param {(text: string) => any} [leafParse=defaultLeafParse]
|
|
118
|
+
* @returns {parse}
|
|
119
|
+
* @public
|
|
120
|
+
*/
|
|
121
|
+
export default function Parse (schemaArg, env, leafParse = defaultLeafParse) {
|
|
105
122
|
const schema = flattenSchema(schemaArg)
|
|
106
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Parses and executes a serialized program.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} program - JSON encoded program
|
|
128
|
+
* @returns {any|Promise<any>}
|
|
129
|
+
* @public
|
|
130
|
+
*/
|
|
107
131
|
return function parse (program) {
|
|
108
132
|
debug('program', program)
|
|
109
133
|
const awaits = []
|
|
110
134
|
|
|
111
|
-
|
|
112
|
-
|
|
135
|
+
/**
|
|
136
|
+
* JSON reviver used during parsing.
|
|
137
|
+
* Converts encoded operators into runtime structures.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} key
|
|
140
|
+
* @param {any} value
|
|
141
|
+
* @returns {any}
|
|
142
|
+
*/
|
|
113
143
|
const reviver = spy('revive', (key, value) => {
|
|
114
144
|
if (value === null) return value
|
|
115
145
|
|
|
116
|
-
if (!Array.isArray(value))
|
|
117
|
-
return value
|
|
118
|
-
}
|
|
146
|
+
if (!Array.isArray(value)) return value
|
|
119
147
|
|
|
120
148
|
const [operator, ...rest] = value
|
|
121
149
|
|
|
122
150
|
if (operator === 'leaf') {
|
|
123
|
-
const leafValue =
|
|
151
|
+
const leafValue = leafParse(rest[0])
|
|
124
152
|
return leafValue === undefined ? undefinedSentinel : leafValue
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
if (operator === 'await') {
|
|
128
156
|
const [program] = rest
|
|
129
|
-
|
|
130
157
|
return ['await', awaits.push(program) - 1]
|
|
131
158
|
}
|
|
132
159
|
|
|
@@ -151,19 +178,17 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
|
|
|
151
178
|
})
|
|
152
179
|
|
|
153
180
|
const parsed = JSON.parse(program, reviver)
|
|
154
|
-
|
|
155
181
|
debug('parsed', parsed)
|
|
156
182
|
|
|
157
183
|
if (awaits.length) {
|
|
158
184
|
debug('awaits', awaits)
|
|
159
185
|
|
|
160
186
|
return (async function () {
|
|
161
|
-
for (let
|
|
162
|
-
awaits[
|
|
187
|
+
for (let i = 0; i < awaits.length; i++) {
|
|
188
|
+
awaits[i] = await evaluate(awaits[i], awaits)
|
|
163
189
|
}
|
|
164
190
|
|
|
165
191
|
debug('awaits(awaited)', awaits)
|
|
166
|
-
|
|
167
192
|
return evaluate(parsed, awaits)
|
|
168
193
|
})()
|
|
169
194
|
}
|
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
|
}
|
package/stringify.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
const debug = Debug.extend('stringify')
|
|
11
11
|
|
|
12
12
|
const keywords = ['ref', 'call', 'quote', 'await', 'leaf']
|
|
13
|
+
|
|
13
14
|
const isKeyword = (v) => keywords.includes(v)
|
|
14
15
|
|
|
15
16
|
const isPlainObject = (value) => {
|
|
@@ -18,26 +19,26 @@ const isPlainObject = (value) => {
|
|
|
18
19
|
return proto === Object.prototype || proto === null
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
// Recursively transform a program tree, encoding SendScript operators and leaf values
|
|
22
22
|
function transformValue (value, leafSerializer) {
|
|
23
23
|
debug(value)
|
|
24
24
|
|
|
25
|
-
if (value === null)
|
|
26
|
-
return null
|
|
27
|
-
}
|
|
25
|
+
if (value === null) return null
|
|
28
26
|
|
|
29
|
-
//
|
|
27
|
+
// unwrap function wrappers (instrumented nodes)
|
|
30
28
|
if (typeof value === 'function' && typeof value.toJSON === 'function') {
|
|
31
29
|
return transformValue(value.toJSON(), leafSerializer)
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
// Encode SendScript operators
|
|
35
32
|
if (value && value[ref]) {
|
|
36
33
|
return ['ref', ...value.path]
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
if (value && value[call]) {
|
|
40
|
-
return [
|
|
37
|
+
return [
|
|
38
|
+
'call',
|
|
39
|
+
transformValue(value.ref, leafSerializer),
|
|
40
|
+
transformValue(value.args, leafSerializer)
|
|
41
|
+
]
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
if (value && value[awaitSymbol]) {
|
|
@@ -45,22 +46,27 @@ function transformValue (value, leafSerializer) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
if (value && value[then]) {
|
|
48
|
-
return [
|
|
49
|
+
return [
|
|
50
|
+
'then',
|
|
51
|
+
transformValue(value.ref, leafSerializer),
|
|
52
|
+
transformValue(value.resolve || null, leafSerializer),
|
|
53
|
+
transformValue(value.reject || null, leafSerializer)
|
|
54
|
+
]
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
// Handle arrays: quote keyword operators, transform other arrays recursively
|
|
52
57
|
if (Array.isArray(value)) {
|
|
53
58
|
const [operator, ...rest] = value
|
|
54
59
|
|
|
55
60
|
if (isKeyword(operator)) {
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
return [
|
|
62
|
+
['quote', operator],
|
|
63
|
+
...rest.map((item) => transformValue(item, leafSerializer))
|
|
64
|
+
]
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
return value.map((item) => transformValue(item, leafSerializer))
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
// Recurse into plain objects
|
|
64
70
|
if (isPlainObject(value)) {
|
|
65
71
|
const result = {}
|
|
66
72
|
|
|
@@ -71,24 +77,46 @@ function transformValue (value, leafSerializer) {
|
|
|
71
77
|
return result
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
// Encode non-JSON leaf values (Date, RegExp, BigInt, etc.)
|
|
75
80
|
return ['leaf', leafSerializer(value)]
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Default strict serializer for leaf values.
|
|
85
|
+
*
|
|
86
|
+
* Rejects non-JSON-safe values.
|
|
87
|
+
*
|
|
88
|
+
* @param {any} x
|
|
89
|
+
* @returns {string}
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
92
|
+
function defaultLeafStringify (x) {
|
|
79
93
|
const typeOf = typeof x
|
|
80
94
|
|
|
81
95
|
if (typeOf === 'object' || typeOf === 'function' || x === undefined) {
|
|
82
|
-
throw new SendScriptSerializationError(
|
|
96
|
+
throw new SendScriptSerializationError(
|
|
97
|
+
`Cannot and should not attempt to serialize ${x}`
|
|
98
|
+
)
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
return JSON.stringify(x)
|
|
86
102
|
}
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Creates a stringify function for SendScript AST structures.
|
|
106
|
+
*
|
|
107
|
+
* @param {(value: any) => string} [leafSerializer=strictStringify]
|
|
108
|
+
* @returns {(program: any) => string}
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export default function Stringify (leafStringify = defaultLeafStringify) {
|
|
112
|
+
/**
|
|
113
|
+
* Serializes a program into a JSON string representation.
|
|
114
|
+
*
|
|
115
|
+
* @param {any} program
|
|
116
|
+
* @returns {string}
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
119
|
+
return function stringify (program) {
|
|
120
|
+
return JSON.stringify(transformValue(program, leafStringify))
|
|
91
121
|
}
|
|
92
|
-
|
|
93
|
-
return stringify
|
|
94
122
|
}
|