sendscript 2.3.2 → 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/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +136 -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 +51 -29
- package/references.mjs +57 -6
- package/stringify.mjs +45 -15
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,16 @@ 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.3](https://github.com/bas080/sendscript/compare/v2.3.2...v2.3.3)
|
|
8
|
+
|
|
9
|
+
- Add JSDoc to relevant function for docs [`d1d3709`](https://github.com/bas080/sendscript/commit/d1d3709088a51b71651170ef642b013eedc9ca3a)
|
|
10
|
+
- Remove tests section from README [`5da7bb3`](https://github.com/bas080/sendscript/commit/5da7bb3f3b1dc8078683e076fe2b520bc6d28896)
|
|
11
|
+
- Add generated from JSDoc reference section [`08eb266`](https://github.com/bas080/sendscript/commit/08eb266b06d8998e6849dbc5c9738fbe782ada18)
|
|
12
|
+
|
|
7
13
|
#### [v2.3.2](https://github.com/bas080/sendscript/compare/v2.3.1...v2.3.2)
|
|
8
14
|
|
|
15
|
+
> 14 April 2026
|
|
16
|
+
|
|
9
17
|
- Format the README.mz file [`df43dbb`](https://github.com/bas080/sendscript/commit/df43dbb8b790cfc612536f49571f9fe4aad03837)
|
|
10
18
|
- Move some commands from README to CONTRIBUTING [`45659c5`](https://github.com/bas080/sendscript/commit/45659c51354ac3bbe3bed5ef3826785d8a1a4a32)
|
|
11
19
|
- 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 parse.mjs stringify.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
|
+
* [defaultLeafDeserializer](#defaultleafdeserializer)
|
|
15
|
+
* [Parse](#parse)
|
|
16
|
+
* [parse](#parse)
|
|
17
|
+
* [References](#references)
|
|
18
|
+
* [strictStringify](#strictstringify)
|
|
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,112 @@ 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
|
+
### defaultLeafDeserializer
|
|
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
|
+
Creates a program parser for a given schema and environment.
|
|
137
|
+
|
|
138
|
+
The parser:
|
|
139
|
+
|
|
140
|
+
* resolves references into runtime functions
|
|
141
|
+
* deserializes leaf nodes
|
|
142
|
+
* collects and executes async awaits
|
|
143
|
+
* evaluates AST-like JSON programs
|
|
144
|
+
|
|
145
|
+
#### Parameters
|
|
146
|
+
|
|
147
|
+
* `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)])>** 
|
|
148
|
+
* `env` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** runtime environment for refs
|
|
149
|
+
* `deserialize` (optional, default `defaultLeafDeserializer`)
|
|
150
|
+
|
|
151
|
+
### parse
|
|
152
|
+
|
|
153
|
+
Parses and executes a serialized program.
|
|
154
|
+
|
|
155
|
+
#### Parameters
|
|
156
|
+
|
|
157
|
+
* `program` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** JSON encoded program
|
|
158
|
+
|
|
159
|
+
Returns **(any | [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<any>)** 
|
|
160
|
+
|
|
161
|
+
### References
|
|
162
|
+
|
|
163
|
+
Builds a nested API structure from a schema definition.
|
|
164
|
+
|
|
165
|
+
Schema supports:
|
|
166
|
+
|
|
167
|
+
* string => leaf function node
|
|
168
|
+
* \[name, children] => namespace with nested schema
|
|
169
|
+
|
|
170
|
+
#### Parameters
|
|
171
|
+
|
|
172
|
+
* `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)])>** 
|
|
173
|
+
* `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 `[]`)
|
|
174
|
+
|
|
175
|
+
<!---->
|
|
176
|
+
|
|
177
|
+
* Throws **[Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)** If schema format is invalid
|
|
178
|
+
|
|
179
|
+
Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Nested instrumented API object
|
|
180
|
+
|
|
181
|
+
### strictStringify
|
|
182
|
+
|
|
183
|
+
Default strict serializer for leaf values.
|
|
184
|
+
|
|
185
|
+
Rejects non-JSON-safe values.
|
|
186
|
+
|
|
187
|
+
#### Parameters
|
|
188
|
+
|
|
189
|
+
* `x` **any** 
|
|
190
|
+
|
|
191
|
+
Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 
|
|
192
|
+
|
|
193
|
+
### Stringify
|
|
194
|
+
|
|
195
|
+
Creates a stringify function for SendScript AST structures.
|
|
196
|
+
|
|
197
|
+
#### Parameters
|
|
198
|
+
|
|
199
|
+
* `leafSerializer` (optional, default `strictStringify`)
|
|
200
|
+
|
|
201
|
+
### stringify
|
|
202
|
+
|
|
203
|
+
Serializes a program into a JSON string representation.
|
|
204
|
+
|
|
205
|
+
#### Parameters
|
|
206
|
+
|
|
207
|
+
* `program` **any** 
|
|
208
|
+
|
|
209
|
+
Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 
|
|
210
|
+
|
|
111
211
|
## Socket example
|
|
112
212
|
|
|
113
213
|
For this example we'll use [socket.io][socket.io].
|
|
@@ -205,9 +305,8 @@ node ./example/client.socket.io.mjs
|
|
|
205
305
|
|
|
206
306
|
pkill sendscript
|
|
207
307
|
```
|
|
208
|
-
|
|
209
|
-
Result: 100
|
|
210
|
-
```
|
|
308
|
+
|
|
309
|
+
Result: 100
|
|
211
310
|
|
|
212
311
|
## Repl
|
|
213
312
|
|
|
@@ -259,18 +358,19 @@ defined properties and returned values.
|
|
|
259
358
|
|
|
260
359
|
There is a good use-case to write a module in TypeScript.
|
|
261
360
|
|
|
262
|
-
1.
|
|
263
|
-
|
|
264
|
-
2.
|
|
265
|
-
|
|
266
|
-
3.
|
|
267
|
-
|
|
361
|
+
1. Obviously the module would have the benefits that TypeScript offers when
|
|
362
|
+
coding.
|
|
363
|
+
2. You can use tools like [typedoc][typedoc] to generate docs from your types to
|
|
364
|
+
share with consumers of your API.
|
|
365
|
+
3. You can use the types of the module to coerce your client to adopt the
|
|
366
|
+
module's type.
|
|
268
367
|
|
|
269
368
|
Let's say we have this module which we use on the server.
|
|
270
369
|
|
|
271
370
|
```bash
|
|
272
371
|
cat ./example/typescript/math.ts
|
|
273
372
|
```
|
|
373
|
+
|
|
274
374
|
```ts
|
|
275
375
|
export const add = (a: number, b: number) => a + b
|
|
276
376
|
export const square = (a: number) => a * a
|
|
@@ -281,6 +381,7 @@ We can then coerce the types of the instrumented stubs.
|
|
|
281
381
|
```bash
|
|
282
382
|
cat ./example/typescript/client.ts
|
|
283
383
|
```
|
|
384
|
+
|
|
284
385
|
```ts
|
|
285
386
|
import math from './math.client.ts'
|
|
286
387
|
import Stringify from 'sendscript/stringify.mjs'
|
|
@@ -306,7 +407,7 @@ npx typedoc --plugin typedoc-plugin-markdown --out ./example/typescript/docs ./e
|
|
|
306
407
|
|
|
307
408
|
You can see the docs [here](./example/typescript/docs/globals.md)
|
|
308
409
|
|
|
309
|
-
> [!NOTE] Although type coercion on the client side can improve the development
|
|
410
|
+
> \[!NOTE] Although type coercion on the client side can improve the development
|
|
310
411
|
> experience, it does not represent the actual type. Values are subject to
|
|
311
412
|
> serialization and deserialization.
|
|
312
413
|
|
|
@@ -373,9 +474,9 @@ export function createUser(user) {
|
|
|
373
474
|
|
|
374
475
|
**Benefits**:
|
|
375
476
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
477
|
+
* Ensures arguments match expected types and shapes.
|
|
478
|
+
* Throws structured errors that can be propagated to clients.
|
|
479
|
+
* Works with TypeScript for automatic type inference.
|
|
379
480
|
|
|
380
481
|
## Leaf Serializer
|
|
381
482
|
|
|
@@ -447,32 +548,6 @@ const result = parse(json)
|
|
|
447
548
|
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous
|
|
448
549
|
and safe from colliding with SendScript operators.
|
|
449
550
|
|
|
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
551
|
## Changelog
|
|
477
552
|
|
|
478
553
|
The [changelog][changelog] is generated using the useful
|
|
@@ -486,12 +561,18 @@ npx auto-changelog -p
|
|
|
486
561
|
|
|
487
562
|
See the [LICENSE.txt][license] file for details.
|
|
488
563
|
|
|
489
|
-
##
|
|
564
|
+
## Issues
|
|
490
565
|
|
|
491
|
-
|
|
566
|
+
See [issues][issues] for roadmap and known bugs.
|
|
492
567
|
|
|
493
568
|
[license]: ./LICENSE.txt
|
|
569
|
+
|
|
494
570
|
[socket.io]: https://socket.io/
|
|
571
|
+
|
|
495
572
|
[changelog]: ./CHANGELOG.md
|
|
573
|
+
|
|
496
574
|
[auto-changelog]: https://www.npmjs.com/package/auto-changelog
|
|
575
|
+
|
|
497
576
|
[typedoc]: https://github.com/TypeStrong/typedoc
|
|
577
|
+
|
|
578
|
+
[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/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
|
}
|
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,21 +77,45 @@ 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
|
|
|
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
|
+
*/
|
|
78
92
|
function strictStringify (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
|
-
|
|
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 (leafSerializer = strictStringify) {
|
|
112
|
+
/**
|
|
113
|
+
* Serializes a program into a JSON string representation.
|
|
114
|
+
*
|
|
115
|
+
* @param {any} program
|
|
116
|
+
* @returns {string}
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
89
119
|
function stringify (program) {
|
|
90
120
|
return JSON.stringify(transformValue(program, leafSerializer))
|
|
91
121
|
}
|