sendscript 2.3.9 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/CONTRIBUTING.md +12 -0
- package/README.md +154 -10
- package/README.mz +109 -10
- package/example/typescript/docs/functions/add.md +1 -1
- package/example/typescript/docs/functions/square.md +1 -1
- package/index.test.mjs +45 -0
- package/package.json +2 -2
- package/parse.mjs +73 -55
- package/stringify.mjs +98 -63
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,24 @@ 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.4.1](https://github.com/bas080/sendscript/compare/v2.4.0...v2.4.1)
|
|
8
|
+
|
|
9
|
+
- Update tap and typedoc version [`3f10410`](https://github.com/bas080/sendscript/commit/3f1041020e00331ff30ffb5f972738dbaf7a50f3)
|
|
10
|
+
- Update tap to 21.7.1 [`d1b5bbd`](https://github.com/bas080/sendscript/commit/d1b5bbd4cb7012e6606973d3d23d08df56cadca2)
|
|
11
|
+
- Document limitations of sendscript [`f577499`](https://github.com/bas080/sendscript/commit/f577499279b85e04ea589a3bcb1866d145216f00)
|
|
12
|
+
|
|
13
|
+
#### [v2.4.0](https://github.com/bas080/sendscript/compare/v2.3.9...v2.4.0)
|
|
14
|
+
|
|
15
|
+
> 16 April 2026
|
|
16
|
+
|
|
17
|
+
- Update tap to 4.5.4 [`5249a85`](https://github.com/bas080/sendscript/commit/5249a85a08b10485f0d64058fd2c8bcaae84edda)
|
|
18
|
+
- Add fn and args support [`6110654`](https://github.com/bas080/sendscript/commit/6110654fa039d17cd9ba0e15f749756b615b5d09)
|
|
19
|
+
- Move transport agnostic section to socket example [`3a92257`](https://github.com/bas080/sendscript/commit/3a9225787df16a0fd83293bfa14d91b74a46d070)
|
|
20
|
+
|
|
7
21
|
#### [v2.3.9](https://github.com/bas080/sendscript/compare/v2.3.8...v2.3.9)
|
|
8
22
|
|
|
23
|
+
> 14 April 2026
|
|
24
|
+
|
|
9
25
|
- Remove schema description from reference and schema shape [`7126829`](https://github.com/bas080/sendscript/commit/7126829b5968588d7c9377ee643b88465f3fe16e)
|
|
10
26
|
|
|
11
27
|
#### [v2.3.8](https://github.com/bas080/sendscript/compare/v2.3.7...v2.3.8)
|
package/CONTRIBUTING.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Contributing
|
|
2
2
|
|
|
3
|
+
The pre-push git hook.
|
|
4
|
+
|
|
5
|
+
```bash cat - > .git/hooks/pre-push
|
|
6
|
+
#!/usr/bin/env bash
|
|
7
|
+
|
|
8
|
+
set -xeuo pipefail
|
|
9
|
+
|
|
10
|
+
npm t
|
|
11
|
+
npx standard
|
|
12
|
+
npx prettier -c --parser markdown *.mz
|
|
13
|
+
```
|
|
14
|
+
|
|
3
15
|
For the README.mz examples to work we need sendscript to be linked.
|
|
4
16
|
|
|
5
17
|
```bash bash > /dev/null
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SendScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
RPC and no-build with composable function calls in a single payload.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/sendscript)
|
|
6
6
|
[](#tests)
|
|
@@ -28,6 +28,9 @@ Write JS code that you can run on servers, browsers or other clients.
|
|
|
28
28
|
- [Promises](#promises)
|
|
29
29
|
* [.then / .catch](#then--catch)
|
|
30
30
|
* [await](#await)
|
|
31
|
+
- [Promises](#promises-1)
|
|
32
|
+
* [.then / .catch](#then--catch-1)
|
|
33
|
+
* [await](#await-1)
|
|
31
34
|
- [TypeScript](#typescript)
|
|
32
35
|
- [Schema and Nested Modules](#schema-and-nested-modules)
|
|
33
36
|
* [Defining a Nested Module](#defining-a-nested-module)
|
|
@@ -35,6 +38,12 @@ Write JS code that you can run on servers, browsers or other clients.
|
|
|
35
38
|
* [Validating structured input](#validating-structured-input)
|
|
36
39
|
- [Leaf Serializer](#leaf-serializer)
|
|
37
40
|
* [Example with superjson](#example-with-superjson)
|
|
41
|
+
- [Limitations](#limitations)
|
|
42
|
+
* [Chaining APIs](#chaining-apis)
|
|
43
|
+
* [Callbacks](#callbacks)
|
|
44
|
+
* [Error handling](#error-handling)
|
|
45
|
+
- [Tests](#tests)
|
|
46
|
+
- [Formatting](#formatting)
|
|
38
47
|
- [Changelog](#changelog)
|
|
39
48
|
- [License](#license)
|
|
40
49
|
- [Issues](#issues)
|
|
@@ -114,17 +123,14 @@ core or around it. Things like supporting more complex (de)serializers, errors
|
|
|
114
123
|
and maybe mixing client functions with sendscript programs. Contact me if I have
|
|
115
124
|
piqued your interest.
|
|
116
125
|
|
|
117
|
-
***
|
|
118
|
-
|
|
119
|
-
SendScript leaves it up to you to choose HTTP, web-sockets or any other method
|
|
120
|
-
of communication between servers and clients that best fits your needs.
|
|
121
|
-
|
|
122
126
|
## Reference
|
|
123
127
|
|
|
124
128
|
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
|
|
125
129
|
|
|
126
130
|
### defaultLeafParse
|
|
127
131
|
|
|
132
|
+
[parse.mjs:49-49](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/parse.mjs#L49-L49 "Source code on GitHub")
|
|
133
|
+
|
|
128
134
|
Default deserializer for leaf nodes.
|
|
129
135
|
|
|
130
136
|
#### Parameters
|
|
@@ -135,6 +141,8 @@ Returns **any** 
|
|
|
135
141
|
|
|
136
142
|
### Parse
|
|
137
143
|
|
|
144
|
+
[parse.mjs:68-215](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/parse.mjs#L68-L215 "Source code on GitHub")
|
|
145
|
+
|
|
138
146
|
#### Parameters
|
|
139
147
|
|
|
140
148
|
* `schema` **[Schema](#schema)** 
|
|
@@ -145,6 +153,8 @@ Returns **[parse](#parse)** 
|
|
|
145
153
|
|
|
146
154
|
### parse
|
|
147
155
|
|
|
156
|
+
[parse.mjs:78-214](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/parse.mjs#L78-L214 "Source code on GitHub")
|
|
157
|
+
|
|
148
158
|
Parses and executes a serialized program.
|
|
149
159
|
|
|
150
160
|
#### Parameters
|
|
@@ -155,6 +165,8 @@ Returns **(any | [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Ref
|
|
|
155
165
|
|
|
156
166
|
### References
|
|
157
167
|
|
|
168
|
+
[references.mjs:109-127](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/references.mjs#L109-L127 "Source code on GitHub")
|
|
169
|
+
|
|
158
170
|
Builds a nested API structure from a schema definition.
|
|
159
171
|
|
|
160
172
|
#### Parameters
|
|
@@ -170,12 +182,16 @@ Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
|
|
|
170
182
|
|
|
171
183
|
### SchemaNode
|
|
172
184
|
|
|
185
|
+
[schema.mjs:1-19](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/schema.mjs#L1-L6 "Source code on GitHub")
|
|
186
|
+
|
|
173
187
|
A single schema node.
|
|
174
188
|
|
|
175
189
|
Type: ([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | \[[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), [Schema](#schema)])
|
|
176
190
|
|
|
177
191
|
### Schema
|
|
178
192
|
|
|
193
|
+
[schema.mjs:1-19](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/schema.mjs#L8-L18 "Source code on GitHub")
|
|
194
|
+
|
|
179
195
|
A schema defines the structure of the runtime API tree.
|
|
180
196
|
|
|
181
197
|
* string → leaf node
|
|
@@ -187,6 +203,8 @@ Type: [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global
|
|
|
187
203
|
|
|
188
204
|
### defaultLeafStringify
|
|
189
205
|
|
|
206
|
+
[stringify.mjs:37-47](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/stringify.mjs#L37-L47 "Source code on GitHub")
|
|
207
|
+
|
|
190
208
|
Default strict serializer for leaf values.
|
|
191
209
|
|
|
192
210
|
Rejects non-JSON-safe values.
|
|
@@ -199,6 +217,8 @@ Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
|
|
|
199
217
|
|
|
200
218
|
### Stringify
|
|
201
219
|
|
|
220
|
+
[stringify.mjs:56-157](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/stringify.mjs#L56-L157 "Source code on GitHub")
|
|
221
|
+
|
|
202
222
|
Creates a stringify function for SendScript AST structures.
|
|
203
223
|
|
|
204
224
|
#### Parameters
|
|
@@ -209,6 +229,8 @@ Returns **[stringify](#stringify)** 
|
|
|
209
229
|
|
|
210
230
|
### stringify
|
|
211
231
|
|
|
232
|
+
[stringify.mjs:64-156](https://github.com/bas080/sendscript/blob/3f1041020e00331ff30ffb5f972738dbaf7a50f3/stringify.mjs#L64-L156 "Source code on GitHub")
|
|
233
|
+
|
|
212
234
|
Serializes a program into a JSON string representation.
|
|
213
235
|
|
|
214
236
|
#### Parameters
|
|
@@ -219,6 +241,9 @@ Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
|
|
|
219
241
|
|
|
220
242
|
## Socket example
|
|
221
243
|
|
|
244
|
+
SendScript leaves it up to you to choose HTTP, web-sockets or any other method
|
|
245
|
+
of communication between servers and clients that best fits your needs.
|
|
246
|
+
|
|
222
247
|
For this example we'll use [socket.io][socket.io].
|
|
223
248
|
|
|
224
249
|
### Module
|
|
@@ -319,11 +344,43 @@ pkill sendscript
|
|
|
319
344
|
|
|
320
345
|
## Repl
|
|
321
346
|
|
|
322
|
-
Sendscript ships with a
|
|
347
|
+
Sendscript ships with a bare-bones (no-dependencies) node-repl script. One can
|
|
323
348
|
run it by simply typing `sendscript` in their console.
|
|
324
349
|
|
|
325
|
-
> Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for
|
|
326
|
-
>
|
|
350
|
+
> Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for printing
|
|
351
|
+
> only sendscript logs.
|
|
352
|
+
|
|
353
|
+
## Promises
|
|
354
|
+
|
|
355
|
+
### .then / .catch
|
|
356
|
+
|
|
357
|
+
Supported since vs `v2.3`.
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
const getOrCreatePost = send(createPost(title).catch(createPost(title)))
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
You will likely need to define better helpers that makes it safer to handle
|
|
364
|
+
rejections and work with promises. It is however sensible to have this basic
|
|
365
|
+
behavior for the sendscript DSL and parser.
|
|
366
|
+
|
|
367
|
+
### await
|
|
368
|
+
|
|
369
|
+
SendScript as of `v2.4` supports functions to do basic templating. It does not
|
|
370
|
+
support async functions and will throw an error when you define one.
|
|
371
|
+
|
|
372
|
+
Under the hood the function is called when it is being parsed. Make sure you
|
|
373
|
+
understand what you are doing when mixing client and server functions.
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
map((a) => add(a, 1))([1, 2, 3]))
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
It also supports nested functions.
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
map(call)(map((a) => () => add(a, 1))([1, 2, 3]))
|
|
383
|
+
```
|
|
327
384
|
|
|
328
385
|
## Promises
|
|
329
386
|
|
|
@@ -549,7 +606,7 @@ const env = {
|
|
|
549
606
|
}
|
|
550
607
|
|
|
551
608
|
// Parse with custom leaf deserializer
|
|
552
|
-
const parse = Parse(schema, env,
|
|
609
|
+
const parse = Parse(schema, env, leafDeserializer)
|
|
553
610
|
|
|
554
611
|
const result = parse(json)
|
|
555
612
|
```
|
|
@@ -557,6 +614,93 @@ const result = parse(json)
|
|
|
557
614
|
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous
|
|
558
615
|
and safe from colliding with SendScript operators.
|
|
559
616
|
|
|
617
|
+
## Limitations
|
|
618
|
+
|
|
619
|
+
The stubs the `references` function returns are limited in the way they can be
|
|
620
|
+
used. This has to do with the fact that they are to be serialized.
|
|
621
|
+
|
|
622
|
+
### Chaining APIs
|
|
623
|
+
|
|
624
|
+
Currently you cannot do chaining APIs. `lib().doThis().doThat()`. It likely is
|
|
625
|
+
possible to implement but will require more extensive schemas and obviously more
|
|
626
|
+
code to create the AST.
|
|
627
|
+
|
|
628
|
+
### Callbacks
|
|
629
|
+
|
|
630
|
+
Although it is possible to mix client and server functions, it works very
|
|
631
|
+
different to ordinary functions. Client functions can be used but should be seen
|
|
632
|
+
as a templating tool to make sendscript programs; just like one would use
|
|
633
|
+
JavaScript with react templates. `items.map(deleteItem)` would return an array
|
|
634
|
+
of sendscript function calls which can be given to sendscript's parse.
|
|
635
|
+
|
|
636
|
+
Client functions cannot be called by sendscript functions (as of yet) since we
|
|
637
|
+
cannot serialize client functions. No work has been done to have the server send
|
|
638
|
+
back intermediate values to perform client function calls or by performing
|
|
639
|
+
smaller sendscript program payloads that are passed to the client. Very
|
|
640
|
+
interesting stuff to look into. You can achieve this now but it looks less clean
|
|
641
|
+
because you have to do `send` calls which is a bit manual.
|
|
642
|
+
|
|
643
|
+
```js
|
|
644
|
+
await send(updateUser(id, merge(await send(getUser(id)), { ...newValues })))
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
It might be interesting to allow configuration to create references that will
|
|
648
|
+
trigger a send whenever await is called. That would remove the ability to create
|
|
649
|
+
a single payload whenever using await. You can then write the above in the
|
|
650
|
+
following manner.
|
|
651
|
+
|
|
652
|
+
```js
|
|
653
|
+
await updateUser(id, merge(await getUser(id), { ...newValues }))
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Possible footgun is that updateUser is only performed when awaited (.then is
|
|
657
|
+
called). This lazy behavior can trip users. This footgun could be resolved by
|
|
658
|
+
checking if any outstanding work exists at the end of a step or the beginning of
|
|
659
|
+
a new step.
|
|
660
|
+
|
|
661
|
+
### Error handling
|
|
662
|
+
|
|
663
|
+
Sendscript does not have builtin tools to handle errors. You can write your own
|
|
664
|
+
utilities for that or use things like \[Ramda's tryCatch]\[tryCatch]. This will
|
|
665
|
+
likely never be supported as JS try catch does not have a return value. It might
|
|
666
|
+
make sense to wrap everything that can throw in promises which is easy to do
|
|
667
|
+
with async functions.
|
|
668
|
+
|
|
669
|
+
## Tests
|
|
670
|
+
|
|
671
|
+
Tests with 100% code coverage.
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
npm t -- -R silent
|
|
675
|
+
npm t -- report text-summary
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
> sendscript@2.4.1 test
|
|
681
|
+
> tap -R silent
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
> sendscript@2.4.1 test
|
|
685
|
+
> tap report text-summary
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
=============================== Coverage summary ===============================
|
|
689
|
+
Statements : 100% ( 523/523 )
|
|
690
|
+
Branches : 100% ( 153/153 )
|
|
691
|
+
Functions : 100% ( 25/25 )
|
|
692
|
+
Lines : 100% ( 523/523 )
|
|
693
|
+
================================================================================
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## Formatting
|
|
697
|
+
|
|
698
|
+
Standard because no config.
|
|
699
|
+
|
|
700
|
+
```bash
|
|
701
|
+
npx standard
|
|
702
|
+
```
|
|
703
|
+
|
|
560
704
|
## Changelog
|
|
561
705
|
|
|
562
706
|
The [changelog][changelog] is generated using the useful
|
package/README.mz
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SendScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
RPC and no-build with composable function calls in a single payload.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/sendscript)
|
|
6
6
|
[](#tests)
|
|
@@ -74,17 +74,15 @@ core or around it. Things like supporting more complex (de)serializers, errors
|
|
|
74
74
|
and maybe mixing client functions with sendscript programs. Contact me if I have
|
|
75
75
|
piqued your interest.
|
|
76
76
|
|
|
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
77
|
## Reference
|
|
83
78
|
|
|
84
79
|
<!-- Reference -->
|
|
85
80
|
|
|
86
81
|
## Socket example
|
|
87
82
|
|
|
83
|
+
SendScript leaves it up to you to choose HTTP, web-sockets or any other method
|
|
84
|
+
of communication between servers and clients that best fits your needs.
|
|
85
|
+
|
|
88
86
|
For this example we'll use [socket.io][socket.io].
|
|
89
87
|
|
|
90
88
|
### Module
|
|
@@ -183,11 +181,43 @@ pkill sendscript
|
|
|
183
181
|
|
|
184
182
|
## Repl
|
|
185
183
|
|
|
186
|
-
Sendscript ships with a
|
|
184
|
+
Sendscript ships with a bare-bones (no-dependencies) node-repl script. One can
|
|
187
185
|
run it by simply typing `sendscript` in their console.
|
|
188
186
|
|
|
189
|
-
> Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for
|
|
190
|
-
>
|
|
187
|
+
> Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for printing
|
|
188
|
+
> only sendscript logs.
|
|
189
|
+
|
|
190
|
+
## Promises
|
|
191
|
+
|
|
192
|
+
### .then / .catch
|
|
193
|
+
|
|
194
|
+
Supported since vs `v2.3`.
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
const getOrCreatePost = send(createPost(title).catch(createPost(title)))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
You will likely need to define better helpers that makes it safer to handle
|
|
201
|
+
rejections and work with promises. It is however sensible to have this basic
|
|
202
|
+
behavior for the sendscript DSL and parser.
|
|
203
|
+
|
|
204
|
+
### await
|
|
205
|
+
|
|
206
|
+
SendScript as of `v2.4` supports functions to do basic templating. It does not
|
|
207
|
+
support async functions and will throw an error when you define one.
|
|
208
|
+
|
|
209
|
+
Under the hood the function is called when it is being parsed. Make sure you
|
|
210
|
+
understand what you are doing when mixing client and server functions.
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
map((a) => add(a, 1))([1, 2, 3]))
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
It also supports nested functions.
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
map(call)(map((a) => () => add(a, 1))([1, 2, 3]))
|
|
220
|
+
```
|
|
191
221
|
|
|
192
222
|
## Promises
|
|
193
223
|
|
|
@@ -391,7 +421,7 @@ const env = {
|
|
|
391
421
|
}
|
|
392
422
|
|
|
393
423
|
// Parse with custom leaf deserializer
|
|
394
|
-
const parse = Parse(schema, env,
|
|
424
|
+
const parse = Parse(schema, env, leafDeserializer)
|
|
395
425
|
|
|
396
426
|
const result = parse(json)
|
|
397
427
|
```
|
|
@@ -399,6 +429,75 @@ const result = parse(json)
|
|
|
399
429
|
The leaf wrapper format is `['leaf', serializedPayload]`, making it unambiguous
|
|
400
430
|
and safe from colliding with SendScript operators.
|
|
401
431
|
|
|
432
|
+
## Limitations
|
|
433
|
+
|
|
434
|
+
The stubs the `references` function returns are limited in the way they can be
|
|
435
|
+
used. This has to do with the fact that they are to be serialized.
|
|
436
|
+
|
|
437
|
+
### Chaining APIs
|
|
438
|
+
|
|
439
|
+
Currently you cannot do chaining APIs. `lib().doThis().doThat()`. It likely is
|
|
440
|
+
possible to implement but will require more extensive schemas and obviously more
|
|
441
|
+
code to create the AST.
|
|
442
|
+
|
|
443
|
+
### Callbacks
|
|
444
|
+
|
|
445
|
+
Although it is possible to mix client and server functions, it works very
|
|
446
|
+
different to ordinary functions. Client functions can be used but should be seen
|
|
447
|
+
as a templating tool to make sendscript programs; just like one would use
|
|
448
|
+
JavaScript with react templates. `items.map(deleteItem)` would return an array
|
|
449
|
+
of sendscript function calls which can be given to sendscript's parse.
|
|
450
|
+
|
|
451
|
+
Client functions cannot be called by sendscript functions (as of yet) since we
|
|
452
|
+
cannot serialize client functions. No work has been done to have the server send
|
|
453
|
+
back intermediate values to perform client function calls or by performing
|
|
454
|
+
smaller sendscript program payloads that are passed to the client. Very
|
|
455
|
+
interesting stuff to look into. You can achieve this now but it looks less clean
|
|
456
|
+
because you have to do `send` calls which is a bit manual.
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
await send(updateUser(id, merge(await send(getUser(id)), { ...newValues })))
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
It might be interesting to allow configuration to create references that will
|
|
463
|
+
trigger a send whenever await is called. That would remove the ability to create
|
|
464
|
+
a single payload whenever using await. You can then write the above in the
|
|
465
|
+
following manner.
|
|
466
|
+
|
|
467
|
+
```js
|
|
468
|
+
await updateUser(id, merge(await getUser(id), { ...newValues }))
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Possible footgun is that updateUser is only performed when awaited (.then is
|
|
472
|
+
called). This lazy behavior can trip users. This footgun could be resolved by
|
|
473
|
+
checking if any outstanding work exists at the end of a step or the beginning of
|
|
474
|
+
a new step.
|
|
475
|
+
|
|
476
|
+
### Error handling
|
|
477
|
+
|
|
478
|
+
Sendscript does not have builtin tools to handle errors. You can write your own
|
|
479
|
+
utilities for that or use things like [Ramda's tryCatch][tryCatch]. This will
|
|
480
|
+
likely never be supported as JS try catch does not have a return value. It might
|
|
481
|
+
make sense to wrap everything that can throw in promises which is easy to do
|
|
482
|
+
with async functions.
|
|
483
|
+
|
|
484
|
+
## Tests
|
|
485
|
+
|
|
486
|
+
Tests with 100% code coverage.
|
|
487
|
+
|
|
488
|
+
```bash bash
|
|
489
|
+
npm t -- -R silent
|
|
490
|
+
npm t -- report text-summary
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Formatting
|
|
494
|
+
|
|
495
|
+
Standard because no config.
|
|
496
|
+
|
|
497
|
+
```bash bash
|
|
498
|
+
npx standard
|
|
499
|
+
```
|
|
500
|
+
|
|
402
501
|
## Changelog
|
|
403
502
|
|
|
404
503
|
The [changelog][changelog] is generated using the useful
|
|
@@ -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/3f1041020e00331ff30ffb5f972738dbaf7a50f3/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/3f1041020e00331ff30ffb5f972738dbaf7a50f3/example/typescript/math.ts#L2)
|
|
12
12
|
|
|
13
13
|
## Parameters
|
|
14
14
|
|
package/index.test.mjs
CHANGED
|
@@ -34,6 +34,7 @@ const myModuleOrig = {
|
|
|
34
34
|
hello: 'world',
|
|
35
35
|
noop: () => {},
|
|
36
36
|
resolve: (x) => Promise.resolve(x),
|
|
37
|
+
call: (fn, ...args) => fn(...args),
|
|
37
38
|
reject: x => Promise.reject(x),
|
|
38
39
|
asyncFn: async () => 'my-async-function',
|
|
39
40
|
instanceOf: (x, t) => x instanceof t,
|
|
@@ -85,15 +86,59 @@ test('should evaluate basic expressions correctly', async (t) => {
|
|
|
85
86
|
always,
|
|
86
87
|
multiply3,
|
|
87
88
|
sayHello,
|
|
89
|
+
call,
|
|
88
90
|
nested
|
|
89
91
|
} = api
|
|
90
92
|
|
|
93
|
+
t.test('throws when async fn is provided', async t => {
|
|
94
|
+
t.throws(() => run(identity(async () => {})))
|
|
95
|
+
t.end()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
t.test('supports fns with arguments', async t => {
|
|
99
|
+
t.same(run(map((a) => add(a, 1))([1, 2, 3])), [2, 3, 4])
|
|
100
|
+
t.end()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
t.test('nested fns with arguments', async t => {
|
|
104
|
+
t.same(run(
|
|
105
|
+
map(call)(
|
|
106
|
+
map((a) => () => add(a, 1))([1, 2, 3])
|
|
107
|
+
)), [2, 3, 4])
|
|
108
|
+
t.end()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
t.test('supports fns without arguments', async t => {
|
|
112
|
+
t.equal(run(() => add(1, 2))(), 3)
|
|
113
|
+
t.equal(run(always(() => 42)()()), 42)
|
|
114
|
+
t.equal(run(always(x => add(x, 42))()(2)), 44)
|
|
115
|
+
t.end()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
t.test('supports binding reference functions on client', async t => {
|
|
119
|
+
const plusOne = add.bind(null, 1)
|
|
120
|
+
|
|
121
|
+
t.equal(run(plusOne(1)), 2)
|
|
122
|
+
t.equal(await run(resolve(1).then(plusOne)), 2)
|
|
123
|
+
|
|
124
|
+
// Even this works.
|
|
125
|
+
t.equal(await run(resolve(1).then(v => plusOne(v))), 2)
|
|
126
|
+
|
|
127
|
+
t.end()
|
|
128
|
+
})
|
|
129
|
+
|
|
91
130
|
t.test('supports .then', async t => {
|
|
131
|
+
const something = 30
|
|
132
|
+
|
|
92
133
|
t.rejects(run(reject('error')))
|
|
93
134
|
|
|
94
135
|
t.equal(await run(reject('error').catch(sayHello)), 'hello error')
|
|
95
136
|
t.equal(await run(reject('error').then(null, sayHello)), 'hello error')
|
|
96
137
|
|
|
138
|
+
// .then with fns just works and I do not know why.
|
|
139
|
+
t.equal(await run(resolve(32).then(() => add(something, 1))), 31)
|
|
140
|
+
t.equal(await run(resolve(32).then(value => add(something, value))), 62)
|
|
141
|
+
|
|
97
142
|
t.equal(await run(resolve('value').then(sayHello)), 'hello value')
|
|
98
143
|
t.equal(await run(resolve('value').then(sayHello, noop)), 'hello value')
|
|
99
144
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sendscript",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Blur the line between server and client code.",
|
|
5
5
|
"module": true,
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"superjson": "^2.2.6",
|
|
28
|
-
"tap": "^21.
|
|
28
|
+
"tap": "^21.7.1",
|
|
29
29
|
"tape-check": "^1.0.0-rc.0",
|
|
30
30
|
"typedoc": "^0.28.18",
|
|
31
31
|
"typedoc-plugin-markdown": "^4.11.0"
|
package/parse.mjs
CHANGED
|
@@ -39,58 +39,6 @@ const spy = (type, fn) => (...args) => {
|
|
|
39
39
|
return value
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const evaluate = spy('eval', (value, awaits = []) => {
|
|
43
|
-
if (value === undefinedSentinel) return undefined
|
|
44
|
-
|
|
45
|
-
if (Array.isArray(value)) {
|
|
46
|
-
const [operator, ...rest] = value
|
|
47
|
-
|
|
48
|
-
if (operator === 'await') {
|
|
49
|
-
const [index] = rest
|
|
50
|
-
return awaits[index]
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (operator === 'then') {
|
|
54
|
-
const [v, onResolve, onReject] = rest
|
|
55
|
-
return evaluate(v, awaits).then(
|
|
56
|
-
evaluate(onResolve, awaits),
|
|
57
|
-
evaluate(onReject, awaits)
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (operator === 'call') {
|
|
62
|
-
let [fn, args] = rest
|
|
63
|
-
fn = evaluate(fn, awaits)
|
|
64
|
-
|
|
65
|
-
for (let i = 0; i < args.length; i++) {
|
|
66
|
-
args[i] = evaluate(args[i], awaits)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return fn(...args)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (operator === 'quote') {
|
|
73
|
-
const [quoted] = rest
|
|
74
|
-
return quoted
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
for (let i = 0; i < value.length; i++) {
|
|
78
|
-
value[i] = evaluate(value[i], awaits)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return value
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (isPlainObject(value)) {
|
|
85
|
-
for (const key of Object.keys(value)) {
|
|
86
|
-
value[key] = evaluate(value[key], awaits)
|
|
87
|
-
}
|
|
88
|
-
return value
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return value
|
|
92
|
-
})
|
|
93
|
-
|
|
94
42
|
/**
|
|
95
43
|
* Default deserializer for leaf nodes.
|
|
96
44
|
*
|
|
@@ -131,6 +79,76 @@ export default function Parse (schema, env, leafParse = defaultLeafParse) {
|
|
|
131
79
|
debug('program', program)
|
|
132
80
|
const awaits = []
|
|
133
81
|
|
|
82
|
+
const evaluateOuter = spy('eval', (env, value) => {
|
|
83
|
+
const evaluate = evaluateOuter.bind(null, env)
|
|
84
|
+
|
|
85
|
+
if (value === undefinedSentinel) return undefined
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
const [operator, ...rest] = value
|
|
89
|
+
|
|
90
|
+
if (operator === 'await') {
|
|
91
|
+
const [index] = rest
|
|
92
|
+
return awaits[index]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (operator === 'then') {
|
|
96
|
+
const [v, onResolve, onReject] = rest
|
|
97
|
+
return evaluate(v).then(
|
|
98
|
+
evaluate(onResolve),
|
|
99
|
+
evaluate(onReject)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (operator === 'fn') {
|
|
104
|
+
const [argIds, body] = [...rest]
|
|
105
|
+
|
|
106
|
+
return (...args) => {
|
|
107
|
+
const newEnv = Object.create(env)
|
|
108
|
+
|
|
109
|
+
argIds.forEach((id, index) => {
|
|
110
|
+
newEnv[id] = args[index]
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return evaluateOuter(newEnv, body)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (operator === 'arg') {
|
|
118
|
+
const [id] = rest
|
|
119
|
+
|
|
120
|
+
return env[id]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (operator === 'call') {
|
|
124
|
+
let [fn, args] = rest
|
|
125
|
+
fn = evaluate(fn)
|
|
126
|
+
|
|
127
|
+
return fn(...args.map(evaluate))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (operator === 'quote') {
|
|
131
|
+
const [quoted] = rest
|
|
132
|
+
return quoted
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < value.length; i++) {
|
|
136
|
+
value[i] = evaluate(value[i])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return value
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (isPlainObject(value)) {
|
|
143
|
+
for (const key of Object.keys(value)) {
|
|
144
|
+
value[key] = evaluate(value[key])
|
|
145
|
+
}
|
|
146
|
+
return value
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return value
|
|
150
|
+
})
|
|
151
|
+
|
|
134
152
|
/**
|
|
135
153
|
* JSON reviver used during parsing.
|
|
136
154
|
* Converts encoded operators into runtime structures.
|
|
@@ -184,14 +202,14 @@ export default function Parse (schema, env, leafParse = defaultLeafParse) {
|
|
|
184
202
|
|
|
185
203
|
return (async function () {
|
|
186
204
|
for (let i = 0; i < awaits.length; i++) {
|
|
187
|
-
awaits[i] = await
|
|
205
|
+
awaits[i] = await evaluateOuter({}, awaits[i])
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
debug('awaits(awaited)', awaits)
|
|
191
|
-
return
|
|
209
|
+
return evaluateOuter({}, parsed)
|
|
192
210
|
})()
|
|
193
211
|
}
|
|
194
212
|
|
|
195
|
-
return
|
|
213
|
+
return evaluateOuter({}, parsed)
|
|
196
214
|
}
|
|
197
215
|
}
|
package/stringify.mjs
CHANGED
|
@@ -2,14 +2,20 @@ import Debug from './debug.mjs'
|
|
|
2
2
|
import { SendScriptSerializationError } from './error.mjs'
|
|
3
3
|
import {
|
|
4
4
|
awaitSymbol,
|
|
5
|
+
referenceSymbol,
|
|
5
6
|
then,
|
|
6
7
|
call,
|
|
7
8
|
ref
|
|
8
9
|
} from './symbol.mjs'
|
|
9
10
|
|
|
11
|
+
const asyncFunction = async function () {}
|
|
12
|
+
asyncFunction()
|
|
13
|
+
const AsyncFunction = (asyncFunction).constructor
|
|
14
|
+
|
|
10
15
|
const debug = Debug.extend('stringify')
|
|
11
16
|
|
|
12
|
-
const keywords = ['ref', 'call', 'quote', 'await', 'leaf']
|
|
17
|
+
const keywords = ['ref', 'call', 'quote', 'await', 'leaf', 'arg', 'fn']
|
|
18
|
+
const argSymbol = Symbol('arg')
|
|
13
19
|
|
|
14
20
|
const isKeyword = (v) => keywords.includes(v)
|
|
15
21
|
|
|
@@ -19,67 +25,6 @@ const isPlainObject = (value) => {
|
|
|
19
25
|
return proto === Object.prototype || proto === null
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
function transformValue (value, leafSerializer) {
|
|
23
|
-
debug(value)
|
|
24
|
-
|
|
25
|
-
if (value === null) return null
|
|
26
|
-
|
|
27
|
-
// unwrap function wrappers (instrumented nodes)
|
|
28
|
-
if (typeof value === 'function' && typeof value.toJSON === 'function') {
|
|
29
|
-
return transformValue(value.toJSON(), leafSerializer)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (value && value[ref]) {
|
|
33
|
-
return ['ref', ...value.path]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (value && value[call]) {
|
|
37
|
-
return [
|
|
38
|
-
'call',
|
|
39
|
-
transformValue(value.ref, leafSerializer),
|
|
40
|
-
transformValue(value.args, leafSerializer)
|
|
41
|
-
]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (value && value[awaitSymbol]) {
|
|
45
|
-
return ['await', transformValue(value.ref, leafSerializer)]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (value && value[then]) {
|
|
49
|
-
return [
|
|
50
|
-
'then',
|
|
51
|
-
transformValue(value.ref, leafSerializer),
|
|
52
|
-
transformValue(value.resolve || null, leafSerializer),
|
|
53
|
-
transformValue(value.reject || null, leafSerializer)
|
|
54
|
-
]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (Array.isArray(value)) {
|
|
58
|
-
const [operator, ...rest] = value
|
|
59
|
-
|
|
60
|
-
if (isKeyword(operator)) {
|
|
61
|
-
return [
|
|
62
|
-
['quote', operator],
|
|
63
|
-
...rest.map((item) => transformValue(item, leafSerializer))
|
|
64
|
-
]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return value.map((item) => transformValue(item, leafSerializer))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (isPlainObject(value)) {
|
|
71
|
-
const result = {}
|
|
72
|
-
|
|
73
|
-
for (const key of Object.keys(value)) {
|
|
74
|
-
result[key] = transformValue(value[key], leafSerializer)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return result
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return ['leaf', leafSerializer(value)]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
28
|
/**
|
|
84
29
|
* Default strict serializer for leaf values.
|
|
85
30
|
*
|
|
@@ -117,6 +62,96 @@ export default function Stringify (leafStringify = defaultLeafStringify) {
|
|
|
117
62
|
* @public
|
|
118
63
|
*/
|
|
119
64
|
return function stringify (program) {
|
|
120
|
-
|
|
65
|
+
// Reset the argId
|
|
66
|
+
let argId = 0
|
|
67
|
+
|
|
68
|
+
function transformValue (value) {
|
|
69
|
+
debug(value)
|
|
70
|
+
|
|
71
|
+
if (value === null) return null
|
|
72
|
+
|
|
73
|
+
// unwrap function wrappers (instrumented nodes)
|
|
74
|
+
if (value && value[referenceSymbol]) {
|
|
75
|
+
return transformValue(value.toJSON())
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Is a simple function. We call it to get the template.
|
|
79
|
+
if (typeof value === 'function') {
|
|
80
|
+
if (value instanceof AsyncFunction) {
|
|
81
|
+
throw new SendScriptSerializationError('Sendscript does not support async functions as of yet.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const args = []
|
|
85
|
+
const argIds = []
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < value.length; i++) {
|
|
88
|
+
const arg = argId++
|
|
89
|
+
const placeholder = ['arg', arg]
|
|
90
|
+
|
|
91
|
+
placeholder[argSymbol] = argSymbol
|
|
92
|
+
|
|
93
|
+
args.push(placeholder)
|
|
94
|
+
argIds.push(arg)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return ['fn', argIds, transformValue(value(...args))]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (value && value[argSymbol]) {
|
|
101
|
+
return value
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (value && value[ref]) {
|
|
105
|
+
return ['ref', ...value.path]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (value && value[call]) {
|
|
109
|
+
return [
|
|
110
|
+
'call',
|
|
111
|
+
transformValue(value.ref),
|
|
112
|
+
transformValue(value.args)
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (value && value[awaitSymbol]) {
|
|
117
|
+
return ['await', transformValue(value.ref)]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (value && value[then]) {
|
|
121
|
+
return [
|
|
122
|
+
'then',
|
|
123
|
+
transformValue(value.ref),
|
|
124
|
+
transformValue(value.resolve || null),
|
|
125
|
+
transformValue(value.reject || null)
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
const [operator, ...rest] = value
|
|
131
|
+
|
|
132
|
+
if (isKeyword(operator)) {
|
|
133
|
+
return [
|
|
134
|
+
['quote', operator],
|
|
135
|
+
...rest.map((item) => transformValue(item))
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return value.map((item) => transformValue(item))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (isPlainObject(value)) {
|
|
143
|
+
const result = {}
|
|
144
|
+
|
|
145
|
+
for (const key of Object.keys(value)) {
|
|
146
|
+
result[key] = transformValue(value[key])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return ['leaf', leafStringify(value)]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return JSON.stringify(transformValue(program))
|
|
121
156
|
}
|
|
122
157
|
}
|