sendscript 0.1.4 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +53 -87
- package/README.mz +44 -77
- package/example/client.socket.io.mjs +15 -7
- package/example/server.socket.io.mjs +3 -2
- package/index.mjs +10 -3
- package/index.test.mjs +197 -0
- package/is-nil.mjs +3 -0
- package/module.mjs +52 -0
- package/package.json +4 -4
- package/parse.mjs +88 -0
- package/stringify.mjs +66 -0
- package/symbol.mjs +3 -0
- package/api.mjs +0 -40
- package/await-when.mjs +0 -11
- package/exec.mjs +0 -43
- package/exec.test.mjs +0 -46
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
|
+
#### [v1.0.1](https://github.com/bas080/sendscript/compare/v1.0.0...v1.0.1)
|
|
8
|
+
|
|
9
|
+
- Add async await section to readme [`7e56024`](https://github.com/bas080/sendscript/commit/7e560246e2a7b5b34f1de0ce5eb0df62897cc454)
|
|
10
|
+
|
|
11
|
+
### [v1.0.0](https://github.com/bas080/sendscript/compare/v0.1.4...v1.0.0)
|
|
12
|
+
|
|
13
|
+
> 3 May 2025
|
|
14
|
+
|
|
15
|
+
- Add support for async await [`e949184`](https://github.com/bas080/sendscript/commit/e949184ab31a3bfdf4c6181463dcdd7250aca3a8)
|
|
16
|
+
- Update tap to latest version on 2024-03-17 [`1164994`](https://github.com/bas080/sendscript/commit/11649947737ffd152ac46d1db01946ad0c1261e7)
|
|
17
|
+
- Support nested objects and keywords as first item [`06da388`](https://github.com/bas080/sendscript/commit/06da3887c05ab0c545a53b6e796d2f7d4c53e6e6)
|
|
18
|
+
|
|
7
19
|
#### [v0.1.4](https://github.com/bas080/sendscript/compare/v0.1.3...v0.1.4)
|
|
8
20
|
|
|
21
|
+
> 8 March 2024
|
|
22
|
+
|
|
9
23
|
- Add missing index file that exports both modules [`38054ea`](https://github.com/bas080/sendscript/commit/38054ea8284a1626146bec42309b3c014527ff7d)
|
|
10
24
|
- Prevent ref of non existant property on env object [`49520ad`](https://github.com/bas080/sendscript/commit/49520ad32604683c668d95e479de772a91f6ae2b)
|
|
11
25
|
|
package/README.md
CHANGED
|
@@ -13,15 +13,14 @@ Write JS code that you can run on servers, browsers or other clients.
|
|
|
13
13
|
* [Module](#module)
|
|
14
14
|
* [Server](#server)
|
|
15
15
|
* [Client](#client)
|
|
16
|
-
- [
|
|
17
|
-
* [`sendscript/api.mjs`](#sendscriptapimjs)
|
|
18
|
-
* [`sendscript/exec.mjs`](#sendscriptexecmjs)
|
|
16
|
+
- [Async/Await](#asyncawait)
|
|
19
17
|
- [TypeScript](#typescript)
|
|
20
18
|
- [Tests](#tests)
|
|
21
19
|
- [Formatting](#formatting)
|
|
22
20
|
- [Changelog](#changelog)
|
|
23
21
|
- [Dependencies](#dependencies)
|
|
24
22
|
- [License](#license)
|
|
23
|
+
- [Roadmap](#roadmap)
|
|
25
24
|
|
|
26
25
|
<!-- tocstop -->
|
|
27
26
|
|
|
@@ -58,16 +57,17 @@ Here a socket.io server that runs SendScript programs.
|
|
|
58
57
|
// ./example/server.socket.io.mjs
|
|
59
58
|
|
|
60
59
|
import { Server } from 'socket.io'
|
|
61
|
-
import
|
|
60
|
+
import Parse from '../parse.mjs'
|
|
62
61
|
import * as math from './math.mjs'
|
|
63
62
|
|
|
63
|
+
const parse = Parse(math)
|
|
64
64
|
const server = new Server()
|
|
65
65
|
const port = process.env.PORT || 3000
|
|
66
66
|
|
|
67
67
|
server.on('connection', (socket) => {
|
|
68
68
|
socket.on('message', async (program, callback) => {
|
|
69
69
|
try {
|
|
70
|
-
const result =
|
|
70
|
+
const result = parse(program)
|
|
71
71
|
callback(null, result) // Pass null as the first argument to indicate success
|
|
72
72
|
} catch (error) {
|
|
73
73
|
callback(error) // Pass the error to the callback
|
|
@@ -87,14 +87,17 @@ Now for a client that sends a program to the server.
|
|
|
87
87
|
// ./example/client.socket.io.mjs
|
|
88
88
|
|
|
89
89
|
import socketClient from 'socket.io-client'
|
|
90
|
-
import
|
|
90
|
+
import stringify from '../stringify.mjs'
|
|
91
|
+
import module from '../module.mjs'
|
|
92
|
+
import * as math from './math.mjs'
|
|
93
|
+
import assert from 'node:assert'
|
|
91
94
|
|
|
92
95
|
const port = process.env.PORT || 3000
|
|
93
96
|
const client = socketClient(`http://localhost:${port}`)
|
|
94
97
|
|
|
95
|
-
const
|
|
98
|
+
const send = program => {
|
|
96
99
|
return new Promise((resolve, reject) => {
|
|
97
|
-
client.emit('message', program, (error, result) => {
|
|
100
|
+
client.emit('message', stringify(program), (error, result) => {
|
|
98
101
|
error
|
|
99
102
|
? reject(error)
|
|
100
103
|
: resolve(result)
|
|
@@ -102,11 +105,16 @@ const exec = program => {
|
|
|
102
105
|
})
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
const { add, square } =
|
|
108
|
+
const { add, square } = module(math)
|
|
109
|
+
|
|
110
|
+
// The program to be sent over the wire
|
|
111
|
+
const program = square(add(1, add(add(2, 3), 4)))
|
|
112
|
+
|
|
113
|
+
const result = await send(program)
|
|
114
|
+
|
|
115
|
+
console.log('Result: ', result)
|
|
106
116
|
|
|
107
|
-
|
|
108
|
-
await square(add(1, add(add(2, 3), 4)))
|
|
109
|
-
)
|
|
117
|
+
assert.equal(result, 100)
|
|
110
118
|
|
|
111
119
|
process.exit(0)
|
|
112
120
|
```
|
|
@@ -114,6 +122,8 @@ process.exit(0)
|
|
|
114
122
|
Now we run this server and a client script.
|
|
115
123
|
|
|
116
124
|
```bash
|
|
125
|
+
set -e
|
|
126
|
+
|
|
117
127
|
# Run the server
|
|
118
128
|
node ./example/server.socket.io.mjs&
|
|
119
129
|
|
|
@@ -123,102 +133,54 @@ node ./example/client.socket.io.mjs
|
|
|
123
133
|
pkill sendscript
|
|
124
134
|
```
|
|
125
135
|
```
|
|
126
|
-
100
|
|
136
|
+
Result: 100
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
##
|
|
130
|
-
|
|
131
|
-
SendScript is essentially a way to serialize a program to then send over the
|
|
132
|
-
wire and execute it somewhere else.
|
|
133
|
-
|
|
134
|
-
We only have two modules. One that helps you write programs that can be sent
|
|
135
|
-
over the wire and another for running that program.
|
|
139
|
+
## Async/Await
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
SendScript supports async/await seamlessly within a single request. This avoids the performance pitfalls of waterfall-style messaging, which can be especially slow on high-latency networks.
|
|
138
142
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
1. The schema, which represents the values that are available.
|
|
142
|
-
2. The function that will be called with the serializable version of the
|
|
143
|
-
program.
|
|
144
|
-
|
|
145
|
-
It returns an object that contains functions which are defined in the schema.
|
|
146
|
-
These functions are a JavaScript API for writing programs that can be sent to
|
|
147
|
-
a server.
|
|
143
|
+
While it's possible to chain promises manually or use utility functions, native async/await support makes your code more readable, modern, and easier to reason about — aligning SendScript with today’s JavaScript best practices.
|
|
148
144
|
|
|
149
145
|
```js
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await add(1, 2) // => 3
|
|
158
|
-
await subtract(1, 2) // => -1
|
|
159
|
-
await add(1, subtract(2, 3)) // => 0
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
The add and subtract functions are thennable. The execute function is called as
|
|
163
|
-
soon as await or `.then` is used.
|
|
164
|
-
|
|
165
|
-
> Notice that you do not have to await the subtract call. You only need to
|
|
166
|
-
> await when you want to execute the program.
|
|
167
|
-
|
|
168
|
-
This API is composable and wrappable.
|
|
169
|
-
|
|
170
|
-
### `sendscript/exec.mjs`
|
|
171
|
-
|
|
172
|
-
The exec function takes an environment object and any valid SendScript program.
|
|
173
|
-
|
|
174
|
-
```js
|
|
175
|
-
import exec from './exec.mjs'
|
|
146
|
+
const userId = 'user-123'
|
|
147
|
+
const program = {
|
|
148
|
+
unread: await fetchUnreadMessages(userId),
|
|
149
|
+
emptyTrash: await emptyTrash(userId),
|
|
150
|
+
archived: await archiveMessages(selectMessages({ old: true }))
|
|
151
|
+
}
|
|
176
152
|
|
|
177
|
-
|
|
178
|
-
add: (a, b) => a + b,
|
|
179
|
-
subtract: (a, b) => a - b
|
|
180
|
-
}, ['add', 1, [subtract, 1, 2]])
|
|
153
|
+
const result = await send(program)
|
|
181
154
|
```
|
|
182
155
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
You could use SendScript without knowing the details of how the LISP works. It
|
|
186
|
-
is an implementation detail and might change over time.
|
|
156
|
+
This operation is done in a single round-trip. The result is an object with the defined properties and returned values.
|
|
187
157
|
|
|
188
158
|
## TypeScript
|
|
189
159
|
|
|
190
|
-
There is a good use-case to write
|
|
160
|
+
There is a good use-case to write a module in TypeScript.
|
|
191
161
|
|
|
192
162
|
1. Obviously the module would have the benefits that TypeScript offers when
|
|
193
163
|
coding.
|
|
194
164
|
2. You can use tools like [typedoc][typedoc] to generate docs from your types to
|
|
195
165
|
share with consumers of your API.
|
|
196
166
|
3. You can use the types of the module to coerce your client to adopt the
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```bash
|
|
200
|
-
# Create pretty docs for your module.
|
|
201
|
-
npx typedoc my-module.ts
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
Now we can use the `my-module.ts` file for the client API.
|
|
167
|
+
module's type.
|
|
205
168
|
|
|
206
169
|
```ts
|
|
207
|
-
import type * as
|
|
170
|
+
import type * as math from './example/math.ts'
|
|
208
171
|
|
|
209
|
-
import
|
|
172
|
+
import module from 'sendscript/module.mjs'
|
|
210
173
|
|
|
211
|
-
export default
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
]
|
|
174
|
+
export default module([
|
|
175
|
+
add,
|
|
176
|
+
squer,
|
|
177
|
+
]) as typeof math
|
|
215
178
|
```
|
|
216
179
|
|
|
217
180
|
> [!NOTE]
|
|
218
181
|
> Although type coercion on the client side can improve the development
|
|
219
182
|
> experience, it does not represent the actual type.
|
|
220
|
-
> Values are
|
|
221
|
-
> particularly when interfacing with JSON formats.
|
|
183
|
+
> Values are subject to serialization and deserialization.
|
|
222
184
|
|
|
223
185
|
## Tests
|
|
224
186
|
|
|
@@ -230,19 +192,19 @@ npm t -- report text-summary
|
|
|
230
192
|
```
|
|
231
193
|
```
|
|
232
194
|
|
|
233
|
-
> sendscript@0.1
|
|
195
|
+
> sendscript@1.0.1 test
|
|
234
196
|
> tap -R silent
|
|
235
197
|
|
|
236
198
|
|
|
237
|
-
> sendscript@0.1
|
|
199
|
+
> sendscript@1.0.1 test
|
|
238
200
|
> tap report text-summary
|
|
239
201
|
|
|
240
202
|
|
|
241
203
|
=============================== Coverage summary ===============================
|
|
242
|
-
Statements : 100% (
|
|
243
|
-
Branches : 100% (
|
|
244
|
-
Functions : 100% (
|
|
245
|
-
Lines : 100% (
|
|
204
|
+
Statements : 100% ( 239/239 )
|
|
205
|
+
Branches : 100% ( 71/71 )
|
|
206
|
+
Functions : 100% ( 18/18 )
|
|
207
|
+
Lines : 100% ( 239/239 )
|
|
246
208
|
================================================================================
|
|
247
209
|
```
|
|
248
210
|
|
|
@@ -278,6 +240,10 @@ No outdated packages found
|
|
|
278
240
|
|
|
279
241
|
See the [LICENSE.txt][license] file for details.
|
|
280
242
|
|
|
243
|
+
## Roadmap
|
|
244
|
+
|
|
245
|
+
- [ ] Support for simple lambdas to compose functions more easily.
|
|
246
|
+
|
|
281
247
|
[license]:./LICENSE.txt
|
|
282
248
|
[socket.io]:https://socket.io/
|
|
283
249
|
[changelog]:./CHANGELOG.md
|
package/README.mz
CHANGED
|
@@ -42,16 +42,17 @@ Here a socket.io server that runs SendScript programs.
|
|
|
42
42
|
// ./example/server.socket.io.mjs
|
|
43
43
|
|
|
44
44
|
import { Server } from 'socket.io'
|
|
45
|
-
import
|
|
45
|
+
import Parse from '../parse.mjs'
|
|
46
46
|
import * as math from './math.mjs'
|
|
47
47
|
|
|
48
|
+
const parse = Parse(math)
|
|
48
49
|
const server = new Server()
|
|
49
50
|
const port = process.env.PORT || 3000
|
|
50
51
|
|
|
51
52
|
server.on('connection', (socket) => {
|
|
52
53
|
socket.on('message', async (program, callback) => {
|
|
53
54
|
try {
|
|
54
|
-
const result =
|
|
55
|
+
const result = parse(program)
|
|
55
56
|
callback(null, result) // Pass null as the first argument to indicate success
|
|
56
57
|
} catch (error) {
|
|
57
58
|
callback(error) // Pass the error to the callback
|
|
@@ -71,14 +72,17 @@ Now for a client that sends a program to the server.
|
|
|
71
72
|
// ./example/client.socket.io.mjs
|
|
72
73
|
|
|
73
74
|
import socketClient from 'socket.io-client'
|
|
74
|
-
import
|
|
75
|
+
import stringify from '../stringify.mjs'
|
|
76
|
+
import module from '../module.mjs'
|
|
77
|
+
import * as math from './math.mjs'
|
|
78
|
+
import assert from 'node:assert'
|
|
75
79
|
|
|
76
80
|
const port = process.env.PORT || 3000
|
|
77
81
|
const client = socketClient(`http://localhost:${port}`)
|
|
78
82
|
|
|
79
|
-
const
|
|
83
|
+
const send = program => {
|
|
80
84
|
return new Promise((resolve, reject) => {
|
|
81
|
-
client.emit('message', program, (error, result) => {
|
|
85
|
+
client.emit('message', stringify(program), (error, result) => {
|
|
82
86
|
error
|
|
83
87
|
? reject(error)
|
|
84
88
|
: resolve(result)
|
|
@@ -86,11 +90,16 @@ const exec = program => {
|
|
|
86
90
|
})
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
const { add, square } =
|
|
93
|
+
const { add, square } = module(math)
|
|
94
|
+
|
|
95
|
+
// The program to be sent over the wire
|
|
96
|
+
const program = square(add(1, add(add(2, 3), 4)))
|
|
97
|
+
|
|
98
|
+
const result = await send(program)
|
|
99
|
+
|
|
100
|
+
console.log('Result: ', result)
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
await square(add(1, add(add(2, 3), 4)))
|
|
93
|
-
)
|
|
102
|
+
assert.equal(result, 100)
|
|
94
103
|
|
|
95
104
|
process.exit(0)
|
|
96
105
|
```
|
|
@@ -98,6 +107,8 @@ process.exit(0)
|
|
|
98
107
|
Now we run this server and a client script.
|
|
99
108
|
|
|
100
109
|
```bash bash
|
|
110
|
+
set -e
|
|
111
|
+
|
|
101
112
|
# Run the server
|
|
102
113
|
node ./example/server.socket.io.mjs&
|
|
103
114
|
|
|
@@ -107,99 +118,51 @@ node ./example/client.socket.io.mjs
|
|
|
107
118
|
pkill sendscript
|
|
108
119
|
```
|
|
109
120
|
|
|
110
|
-
##
|
|
111
|
-
|
|
112
|
-
SendScript is essentially a way to serialize a program to then send over the
|
|
113
|
-
wire and execute it somewhere else.
|
|
114
|
-
|
|
115
|
-
We only have two modules. One that helps you write programs that can be sent
|
|
116
|
-
over the wire and another for running that program.
|
|
117
|
-
|
|
118
|
-
### `sendscript/api.mjs`
|
|
119
|
-
|
|
120
|
-
The api module exports a function that takes two arguments.
|
|
121
|
-
|
|
122
|
-
1. The schema, which represents the values that are available.
|
|
123
|
-
2. The function that will be called with the serializable version of the
|
|
124
|
-
program.
|
|
125
|
-
|
|
126
|
-
It returns an object that contains functions which are defined in the schema.
|
|
127
|
-
These functions are a JavaScript API for writing programs that can be sent to
|
|
128
|
-
a server.
|
|
129
|
-
|
|
130
|
-
```js
|
|
131
|
-
import api from './api.mjs'
|
|
132
|
-
|
|
133
|
-
const { add, subtract } = api(
|
|
134
|
-
['add', 'subtract'],
|
|
135
|
-
serializableProgram => sendSomewhereToBeExecuted(serializableProgram)
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
await add(1, 2) // => 3
|
|
139
|
-
await subtract(1, 2) // => -1
|
|
140
|
-
await add(1, subtract(2, 3)) // => 0
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
The add and subtract functions are thennable. The execute function is called as
|
|
144
|
-
soon as await or `.then` is used.
|
|
121
|
+
## Async/Await
|
|
145
122
|
|
|
146
|
-
|
|
147
|
-
> await when you want to execute the program.
|
|
123
|
+
SendScript supports async/await seamlessly within a single request. This avoids the performance pitfalls of waterfall-style messaging, which can be especially slow on high-latency networks.
|
|
148
124
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
### `sendscript/exec.mjs`
|
|
152
|
-
|
|
153
|
-
The exec function takes an environment object and any valid SendScript program.
|
|
125
|
+
While it's possible to chain promises manually or use utility functions, native async/await support makes your code more readable, modern, and easier to reason about — aligning SendScript with today’s JavaScript best practices.
|
|
154
126
|
|
|
155
127
|
```js
|
|
156
|
-
|
|
128
|
+
const userId = 'user-123'
|
|
129
|
+
const program = {
|
|
130
|
+
unread: await fetchUnreadMessages(userId),
|
|
131
|
+
emptyTrash: await emptyTrash(userId),
|
|
132
|
+
archived: await archiveMessages(selectMessages({ old: true }))
|
|
133
|
+
}
|
|
157
134
|
|
|
158
|
-
|
|
159
|
-
add: (a, b) => a + b,
|
|
160
|
-
subtract: (a, b) => a - b
|
|
161
|
-
}, ['add', 1, [subtract, 1, 2]])
|
|
135
|
+
const result = await send(program)
|
|
162
136
|
```
|
|
163
137
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
You could use SendScript without knowing the details of how the LISP works. It
|
|
167
|
-
is an implementation detail and might change over time.
|
|
138
|
+
This operation is done in a single round-trip. The result is an object with the defined properties and returned values.
|
|
168
139
|
|
|
169
140
|
## TypeScript
|
|
170
141
|
|
|
171
|
-
There is a good use-case to write
|
|
142
|
+
There is a good use-case to write a module in TypeScript.
|
|
172
143
|
|
|
173
144
|
1. Obviously the module would have the benefits that TypeScript offers when
|
|
174
145
|
coding.
|
|
175
146
|
2. You can use tools like [typedoc][typedoc] to generate docs from your types to
|
|
176
147
|
share with consumers of your API.
|
|
177
148
|
3. You can use the types of the module to coerce your client to adopt the
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
# Create pretty docs for your module.
|
|
182
|
-
npx typedoc my-module.ts
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Now we can use the `my-module.ts` file for the client API.
|
|
149
|
+
module's type.
|
|
186
150
|
|
|
187
151
|
```ts
|
|
188
|
-
import type * as
|
|
152
|
+
import type * as math from './example/math.ts'
|
|
189
153
|
|
|
190
|
-
import
|
|
154
|
+
import module from 'sendscript/module.mjs'
|
|
191
155
|
|
|
192
|
-
export default
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
]
|
|
156
|
+
export default module([
|
|
157
|
+
add,
|
|
158
|
+
squer,
|
|
159
|
+
]) as typeof math
|
|
196
160
|
```
|
|
197
161
|
|
|
198
162
|
> [!NOTE]
|
|
199
163
|
> Although type coercion on the client side can improve the development
|
|
200
164
|
> experience, it does not represent the actual type.
|
|
201
|
-
> Values are
|
|
202
|
-
> particularly when interfacing with JSON formats.
|
|
165
|
+
> Values are subject to serialization and deserialization.
|
|
203
166
|
|
|
204
167
|
## Tests
|
|
205
168
|
|
|
@@ -239,6 +202,10 @@ npm outdated && echo 'No outdated packages found'
|
|
|
239
202
|
|
|
240
203
|
See the [LICENSE.txt][license] file for details.
|
|
241
204
|
|
|
205
|
+
## Roadmap
|
|
206
|
+
|
|
207
|
+
- [ ] Support for simple lambdas to compose functions more easily.
|
|
208
|
+
|
|
242
209
|
[license]:./LICENSE.txt
|
|
243
210
|
[socket.io]:https://socket.io/
|
|
244
211
|
[changelog]:./CHANGELOG.md
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// ./example/client.socket.io.mjs
|
|
2
2
|
|
|
3
3
|
import socketClient from 'socket.io-client'
|
|
4
|
-
import
|
|
4
|
+
import stringify from '../stringify.mjs'
|
|
5
|
+
import module from '../module.mjs'
|
|
6
|
+
import * as math from './math.mjs'
|
|
7
|
+
import assert from 'node:assert'
|
|
5
8
|
|
|
6
9
|
const port = process.env.PORT || 3000
|
|
7
10
|
const client = socketClient(`http://localhost:${port}`)
|
|
8
11
|
|
|
9
|
-
const
|
|
12
|
+
const send = program => {
|
|
10
13
|
return new Promise((resolve, reject) => {
|
|
11
|
-
client.emit('message', program, (error, result) => {
|
|
14
|
+
client.emit('message', stringify(program), (error, result) => {
|
|
12
15
|
error
|
|
13
16
|
? reject(error)
|
|
14
17
|
: resolve(result)
|
|
@@ -16,10 +19,15 @@ const exec = program => {
|
|
|
16
19
|
})
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
const { add, square } =
|
|
22
|
+
const { add, square } = module(math)
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
// The program to be sent over the wire
|
|
25
|
+
const program = square(add(1, add(add(2, 3), 4)))
|
|
26
|
+
|
|
27
|
+
const result = await send(program)
|
|
28
|
+
|
|
29
|
+
console.log('Result: ', result)
|
|
30
|
+
|
|
31
|
+
assert.equal(result, 100)
|
|
24
32
|
|
|
25
33
|
process.exit(0)
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// ./example/server.socket.io.mjs
|
|
2
2
|
|
|
3
3
|
import { Server } from 'socket.io'
|
|
4
|
-
import
|
|
4
|
+
import Parse from '../parse.mjs'
|
|
5
5
|
import * as math from './math.mjs'
|
|
6
6
|
|
|
7
|
+
const parse = Parse(math)
|
|
7
8
|
const server = new Server()
|
|
8
9
|
const port = process.env.PORT || 3000
|
|
9
10
|
|
|
10
11
|
server.on('connection', (socket) => {
|
|
11
12
|
socket.on('message', async (program, callback) => {
|
|
12
13
|
try {
|
|
13
|
-
const result =
|
|
14
|
+
const result = parse(program)
|
|
14
15
|
callback(null, result) // Pass null as the first argument to indicate success
|
|
15
16
|
} catch (error) {
|
|
16
17
|
callback(error) // Pass the error to the callback
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import stringify from './stringify.mjs'
|
|
2
|
+
import makeModule from './module.mjs'
|
|
3
|
+
import parse from './parse.mjs'
|
|
3
4
|
|
|
4
|
-
export default
|
|
5
|
+
export default function sendscript (module) {
|
|
6
|
+
return {
|
|
7
|
+
stringify,
|
|
8
|
+
parse: parse(module),
|
|
9
|
+
module: makeModule(module)
|
|
10
|
+
}
|
|
11
|
+
}
|
package/index.test.mjs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { test } from 'tap'
|
|
2
|
+
import Sendscript from './index.mjs'
|
|
3
|
+
|
|
4
|
+
const module = {
|
|
5
|
+
add: (a, b) => a + b,
|
|
6
|
+
identity: (x) => x,
|
|
7
|
+
concat: (a, b) => a.concat(b),
|
|
8
|
+
toArray: (...array) => array,
|
|
9
|
+
always: (x) => () => x,
|
|
10
|
+
multiply3: (a) => (b) => (c) => a * b * c,
|
|
11
|
+
map: (fn) => (array) => array.map(fn),
|
|
12
|
+
filter: (pred) => (array) => array.filter(pred),
|
|
13
|
+
hello: 'world',
|
|
14
|
+
noop: () => {},
|
|
15
|
+
resolve: (x) => Promise.resolve(x),
|
|
16
|
+
asyncFn: async () => 'my-async-function',
|
|
17
|
+
instanceOf: (x, t) => x instanceof t,
|
|
18
|
+
asyncAdd: async (a, b) => a + b,
|
|
19
|
+
aPromise: Promise.resolve(42),
|
|
20
|
+
delayedIdentity: async (x) => x,
|
|
21
|
+
Function,
|
|
22
|
+
Promise
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sendscript = Sendscript(module)
|
|
26
|
+
const { parse, stringify } = sendscript
|
|
27
|
+
const run = (program) => parse(stringify(program))
|
|
28
|
+
|
|
29
|
+
const RealPromise = Promise
|
|
30
|
+
|
|
31
|
+
test('should evaluate basic expressions correctly', async (t) => {
|
|
32
|
+
const {
|
|
33
|
+
aPromise,
|
|
34
|
+
asyncAdd,
|
|
35
|
+
resolve,
|
|
36
|
+
delayedIdentity,
|
|
37
|
+
noop,
|
|
38
|
+
Function,
|
|
39
|
+
Promise,
|
|
40
|
+
instanceOf,
|
|
41
|
+
asyncFn,
|
|
42
|
+
hello,
|
|
43
|
+
map,
|
|
44
|
+
toArray,
|
|
45
|
+
add,
|
|
46
|
+
concat,
|
|
47
|
+
identity,
|
|
48
|
+
always,
|
|
49
|
+
multiply3
|
|
50
|
+
} = sendscript.module
|
|
51
|
+
|
|
52
|
+
t.test('nested await works', async (t) => {
|
|
53
|
+
// Async identity passthrough
|
|
54
|
+
const resolvedId = await delayedIdentity
|
|
55
|
+
|
|
56
|
+
t.equal(await run(resolvedId('X')), 'X')
|
|
57
|
+
|
|
58
|
+
t.end()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
t.test('deep nested awaits', async (t) => {
|
|
62
|
+
const nested = async () => await RealPromise.resolve(await delayedIdentity('deep'))
|
|
63
|
+
t.equal(await run(await nested()), 'deep')
|
|
64
|
+
t.end()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
t.test('awaits in nested array structure', async (t) => {
|
|
68
|
+
const arr = [
|
|
69
|
+
await resolve(1),
|
|
70
|
+
[await resolve(2), [await resolve(3)]],
|
|
71
|
+
await delayedIdentity(4)
|
|
72
|
+
]
|
|
73
|
+
t.same(await run(arr), [1, [2, [3]], 4])
|
|
74
|
+
t.end()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
t.test('awaits in deeply nested object structure', async (t) => {
|
|
78
|
+
const obj = {
|
|
79
|
+
a: await resolve('a'),
|
|
80
|
+
b: {
|
|
81
|
+
c: await delayedIdentity('c'),
|
|
82
|
+
d: {
|
|
83
|
+
e: await resolve('e')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
t.same(await run(obj), {
|
|
88
|
+
a: 'a',
|
|
89
|
+
b: {
|
|
90
|
+
c: 'c',
|
|
91
|
+
d: { e: 'e' }
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
t.end()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
t.test('await as computed value inside nested async function', async (t) => {
|
|
98
|
+
const asyncOuter = async () => {
|
|
99
|
+
const val = await delayedIdentity('nested')
|
|
100
|
+
return val
|
|
101
|
+
}
|
|
102
|
+
t.equal(await run(await asyncOuter()), 'nested')
|
|
103
|
+
t.end()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// return t.end()
|
|
107
|
+
|
|
108
|
+
t.test('promise resolution', async (t) => {
|
|
109
|
+
t.equal(await run(identity(await aPromise)), 42)
|
|
110
|
+
t.strictSame(await run(asyncFn()), 'my-async-function')
|
|
111
|
+
t.strictSame(await run(await resolve('my-promise')), 'my-promise')
|
|
112
|
+
t.strictSame(run(instanceOf(resolve(asyncFn), Promise)), true)
|
|
113
|
+
t.strictSame(
|
|
114
|
+
await run(instanceOf(await resolve(asyncFn), Function)), true
|
|
115
|
+
)
|
|
116
|
+
t.strictSame(
|
|
117
|
+
await run({ a: await resolve('b') }),
|
|
118
|
+
{ a: 'b' }
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
await t.test('async and promise handling', async (t) => {
|
|
123
|
+
// Await inside run input
|
|
124
|
+
const resolvedAdd = await RealPromise.resolve(asyncAdd)
|
|
125
|
+
t.equal(await run(resolvedAdd(2, 3)), 5)
|
|
126
|
+
|
|
127
|
+
// Using asyncFn in a nested structure
|
|
128
|
+
const nestedAsync = async () => await asyncFn()
|
|
129
|
+
t.equal(await run(await nestedAsync()), 'my-async-function')
|
|
130
|
+
|
|
131
|
+
// Awaiting inside object structure
|
|
132
|
+
t.same(await run({
|
|
133
|
+
type: 'response',
|
|
134
|
+
data: await resolve('some-data')
|
|
135
|
+
}), {
|
|
136
|
+
type: 'response',
|
|
137
|
+
data: 'some-data'
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
t.end()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
t.test('basic types and identity', (t) => {
|
|
144
|
+
t.equal(run(identity(null)), null)
|
|
145
|
+
t.equal(run(identity(undefined)), null)
|
|
146
|
+
t.equal(run(noop()), undefined)
|
|
147
|
+
t.equal(run(identity(1)), 1)
|
|
148
|
+
t.strictSame(run(identity([])), [])
|
|
149
|
+
t.strictSame(run(identity([identity(1), 2, 3])), [1, 2, 3])
|
|
150
|
+
t.strictSame(run(always('hello')()), 'hello')
|
|
151
|
+
t.end()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
t.test('objects and arrays', (t) => {
|
|
155
|
+
t.strictSame(
|
|
156
|
+
run(identity({ a: identity(1), b: always(2)(), c: add(1, 2) })),
|
|
157
|
+
{ a: 1, b: 2, c: 3 }
|
|
158
|
+
)
|
|
159
|
+
t.strictSame(run(concat([1, 2], [[add(1, 2)]])), [1, 2, [3]])
|
|
160
|
+
t.strictSame(run(concat([1, 2], [add(1, 2), add(2, 2)])), [1, 2, 3, 4])
|
|
161
|
+
t.strictSame(run(map(identity)([1, 2, 3, 4])), [1, 2, 3, 4])
|
|
162
|
+
t.end()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
t.test('function composition and currying', (t) => {
|
|
166
|
+
t.strictSame(run(multiply3(1)(2)(3)), 6)
|
|
167
|
+
t.end()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
t.test('special cases and errors', (t) => {
|
|
171
|
+
t.throws(() => parse('["ref", "notDefined"]'))
|
|
172
|
+
t.strictSame(
|
|
173
|
+
run(identity(['ref', 'doesNotExist'])),
|
|
174
|
+
['ref', 'doesNotExist']
|
|
175
|
+
)
|
|
176
|
+
t.strictSame(
|
|
177
|
+
run(identity(['ref', 'hello'])),
|
|
178
|
+
run(identity(toArray('ref', 'hello')))
|
|
179
|
+
)
|
|
180
|
+
t.end()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
t.test('primitives and built-ins', (t) => {
|
|
184
|
+
t.equal(JSON.stringify([undefined]), '[null]')
|
|
185
|
+
t.equal(run(hello), 'world')
|
|
186
|
+
t.equal(run(add(1, 2)), 3)
|
|
187
|
+
t.end()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
t.test('identity with arrays', (t) => {
|
|
191
|
+
t.strictSame(
|
|
192
|
+
run(identity([identity(1), identity(2), identity(3), identity(4)])),
|
|
193
|
+
[1, 2, 3, 4]
|
|
194
|
+
)
|
|
195
|
+
t.end()
|
|
196
|
+
})
|
|
197
|
+
})
|
package/is-nil.mjs
ADDED
package/module.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
awaitSymbol,
|
|
3
|
+
call,
|
|
4
|
+
ref
|
|
5
|
+
} from './symbol.mjs'
|
|
6
|
+
|
|
7
|
+
function instrument (name) {
|
|
8
|
+
function reference (...args) {
|
|
9
|
+
const called = instrument(name)
|
|
10
|
+
|
|
11
|
+
called.toJSON = () => ({
|
|
12
|
+
[call]: call,
|
|
13
|
+
call: true,
|
|
14
|
+
ref: reference,
|
|
15
|
+
args
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return called
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
reference.then = (resolve) => {
|
|
22
|
+
const awaited = instrument(name)
|
|
23
|
+
|
|
24
|
+
delete awaited.then
|
|
25
|
+
|
|
26
|
+
awaited.toJSON = () => ({
|
|
27
|
+
[awaitSymbol]: awaitSymbol,
|
|
28
|
+
await: true,
|
|
29
|
+
ref: reference
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return resolve(awaited)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
reference.toJSON = () => ({
|
|
36
|
+
[ref]: ref,
|
|
37
|
+
reference: true,
|
|
38
|
+
name
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return reference
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function module (schema) {
|
|
45
|
+
if (!Array.isArray(schema)) return module(Object.keys(schema))
|
|
46
|
+
|
|
47
|
+
return schema.reduce((api, name) => {
|
|
48
|
+
api[name] = instrument(name)
|
|
49
|
+
|
|
50
|
+
return api
|
|
51
|
+
}, {})
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sendscript",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Blur the line between server and client code.",
|
|
5
5
|
"module": true,
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "tap",
|
|
20
|
-
"version": "npm run docs && git add *.md",
|
|
20
|
+
"version": "npm run docs && git add *.md example",
|
|
21
21
|
"docs": "markatzea README.mz | tee README.md && npx markdown-toc -i README.md"
|
|
22
22
|
},
|
|
23
23
|
"author": "Bas Huis",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"tap": "^
|
|
26
|
+
"tap": "^21.0.1"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"debug": "^4.3.
|
|
29
|
+
"debug": "^4.3.7"
|
|
30
30
|
}
|
|
31
31
|
}
|
package/parse.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import Debug from './debug.mjs'
|
|
2
|
+
import isNil from './is-nil.mjs'
|
|
3
|
+
|
|
4
|
+
class SendScriptError extends Error {}
|
|
5
|
+
class SendScriptReferenceError extends SendScriptError {}
|
|
6
|
+
|
|
7
|
+
const debug = Debug.extend('parse')
|
|
8
|
+
|
|
9
|
+
export default (env) =>
|
|
10
|
+
function parse (program) {
|
|
11
|
+
debug('program', program)
|
|
12
|
+
|
|
13
|
+
const awaits = []
|
|
14
|
+
const resolved = {}
|
|
15
|
+
|
|
16
|
+
JSON.parse(program, (key, value) => {
|
|
17
|
+
if (Array.isArray(value) && value[0] === 'await') {
|
|
18
|
+
awaits.push(((program, awaitId) => async () => {
|
|
19
|
+
const value = await JSON.parse(JSON.stringify(program), reviver)
|
|
20
|
+
resolved[awaitId] = value
|
|
21
|
+
|
|
22
|
+
debug('awaits', awaits)
|
|
23
|
+
})(value[1], value[2]))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return value
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const spy = fn => (...args) => {
|
|
30
|
+
const value = fn(...args)
|
|
31
|
+
debug(args, ' => ', value)
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const reviver = spy((key, value) => {
|
|
36
|
+
if (isNil(value)) return value
|
|
37
|
+
|
|
38
|
+
if (!Array.isArray(value)) {
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [operator, ...rest] = value
|
|
43
|
+
|
|
44
|
+
if (operator === 'await') {
|
|
45
|
+
const [, awaitId] = rest
|
|
46
|
+
debug('read awaits', resolved[awaitId], awaitId)
|
|
47
|
+
|
|
48
|
+
return resolved[awaitId]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(operator) && operator[0] === 'quote') {
|
|
52
|
+
const [, quoted] = operator
|
|
53
|
+
|
|
54
|
+
return [quoted, ...rest]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (operator === 'call') {
|
|
58
|
+
const [fn, args] = rest
|
|
59
|
+
|
|
60
|
+
return fn(...args)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (operator === 'ref') {
|
|
64
|
+
const [name] = rest
|
|
65
|
+
|
|
66
|
+
if (Object.hasOwn(env, name)) return env[name]
|
|
67
|
+
|
|
68
|
+
throw new SendScriptReferenceError({ key, value })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return value
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
if (awaits.length) {
|
|
75
|
+
return sequential(awaits).then(() => {
|
|
76
|
+
return JSON.parse(program, reviver)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return JSON.parse(program, reviver)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sequential (promises) {
|
|
84
|
+
return promises.reduce(
|
|
85
|
+
(acc, curr) => acc.then(results => curr().then(res => [...results, res])),
|
|
86
|
+
Promise.resolve([])
|
|
87
|
+
)
|
|
88
|
+
}
|
package/stringify.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Debug from './debug.mjs'
|
|
2
|
+
import isNil from './is-nil.mjs'
|
|
3
|
+
import {
|
|
4
|
+
awaitSymbol,
|
|
5
|
+
call,
|
|
6
|
+
ref
|
|
7
|
+
} from './symbol.mjs'
|
|
8
|
+
|
|
9
|
+
const debug = Debug.extend('stringify')
|
|
10
|
+
|
|
11
|
+
const replaced = Symbol('replaced')
|
|
12
|
+
const keywords = ['ref', 'call', 'quote', 'await']
|
|
13
|
+
const isKeyword = (v) => keywords.includes(v)
|
|
14
|
+
let awaitId = -1
|
|
15
|
+
|
|
16
|
+
function replacer (key, value) {
|
|
17
|
+
debug(this, key, value)
|
|
18
|
+
|
|
19
|
+
if (isNil(value)) {
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (value[ref]) {
|
|
24
|
+
const result = ['ref', value.name]
|
|
25
|
+
|
|
26
|
+
result[replaced] = replaced
|
|
27
|
+
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (value[call]) {
|
|
32
|
+
const result = ['call', value.ref, value.args]
|
|
33
|
+
|
|
34
|
+
result[replaced] = replaced
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (value[awaitSymbol]) {
|
|
40
|
+
awaitId += 1
|
|
41
|
+
const result = ['await', value.ref, awaitId]
|
|
42
|
+
|
|
43
|
+
result[replaced] = replaced
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Quote only the reserved string and not the complete array. Quoted values
|
|
49
|
+
// will be unquoted on parse. A quoted quote also.
|
|
50
|
+
if (!value[replaced] && Array.isArray(value)) {
|
|
51
|
+
const [operator, ...rest] = value
|
|
52
|
+
|
|
53
|
+
if (isKeyword(operator)) {
|
|
54
|
+
const quoted = ['quote', operator]
|
|
55
|
+
quoted[replaced] = replaced
|
|
56
|
+
|
|
57
|
+
return [quoted, ...rest]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default function stringify (program) {
|
|
65
|
+
return JSON.stringify(program, replacer)
|
|
66
|
+
}
|
package/symbol.mjs
ADDED
package/api.mjs
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import awaitWhen from './await-when.mjs'
|
|
2
|
-
|
|
3
|
-
const symbol = Symbol('api')
|
|
4
|
-
const isNotStub = v => v?.[symbol] !== symbol
|
|
5
|
-
const awaitWhenNotStub = awaitWhen(isNotStub)
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Create stubs to be used to build the program.
|
|
9
|
-
*
|
|
10
|
-
* @param {string[]} schema - Names of stubs.
|
|
11
|
-
* @param {Function} call - Function to call when awaited.
|
|
12
|
-
*
|
|
13
|
-
* @return {Object} An object containing the named stubs.
|
|
14
|
-
*/
|
|
15
|
-
export default function api (schema, call) {
|
|
16
|
-
return schema.reduce((api, name) => {
|
|
17
|
-
const then = (program) => (resolve, reject) =>
|
|
18
|
-
Promise.resolve(call(program)).then(resolve, reject)
|
|
19
|
-
|
|
20
|
-
const fn = (...args) => {
|
|
21
|
-
const toJSON = () => ['call', fn, args]
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
[symbol]: symbol,
|
|
25
|
-
toJSON,
|
|
26
|
-
async then (resolve, reject) {
|
|
27
|
-
return then(await awaitWhenNotStub(toJSON()))(resolve, reject)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
fn.toJSON = () => ['ref', name]
|
|
33
|
-
fn.then = then(fn)
|
|
34
|
-
fn[symbol] = symbol
|
|
35
|
-
|
|
36
|
-
api[name] = fn
|
|
37
|
-
|
|
38
|
-
return api
|
|
39
|
-
}, {})
|
|
40
|
-
}
|
package/await-when.mjs
DELETED
package/exec.mjs
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import _debug from './debug.mjs'
|
|
2
|
-
import curry from './curry.mjs'
|
|
3
|
-
|
|
4
|
-
const debug = _debug.extend('lisp')
|
|
5
|
-
|
|
6
|
-
class SendScriptError extends Error {};
|
|
7
|
-
class RefError extends SendScriptError {};
|
|
8
|
-
|
|
9
|
-
const exec = curry(async (env, expression) => {
|
|
10
|
-
debug('exec', expression)
|
|
11
|
-
|
|
12
|
-
if (!Array.isArray(expression)) {
|
|
13
|
-
return expression
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const [operator, ...args] = expression
|
|
17
|
-
|
|
18
|
-
if (operator === 'call') {
|
|
19
|
-
const [fnRef, fnArgs] = args
|
|
20
|
-
|
|
21
|
-
const fn = await exec(env, fnRef)
|
|
22
|
-
return fn.apply(env, await exec(env, fnArgs))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (operator === 'ref') {
|
|
26
|
-
const [name] = args
|
|
27
|
-
|
|
28
|
-
if (!Object.hasOwn(env, name)) {
|
|
29
|
-
throw new RefError(expression)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return env[name]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return await Promise.all(expression.map(exec(env)))
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
export {
|
|
39
|
-
SendScriptError,
|
|
40
|
-
RefError
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export default exec
|
package/exec.test.mjs
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { test } from 'tap'
|
|
2
|
-
import api from './api.mjs'
|
|
3
|
-
import exec from './exec.mjs'
|
|
4
|
-
|
|
5
|
-
test('should evaluate basic expressions correctly', async (t) => {
|
|
6
|
-
const module = {
|
|
7
|
-
add: (a, b) => a + b,
|
|
8
|
-
identity: x => x,
|
|
9
|
-
concat: (a, b) => a.concat(b)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const evaluate = exec(module)
|
|
13
|
-
const { add, concat, identity } = api(Object.keys(module), program =>
|
|
14
|
-
evaluate(JSON.parse(JSON.stringify(program))))
|
|
15
|
-
|
|
16
|
-
t.equal(
|
|
17
|
-
await evaluate(add(1, 2)),
|
|
18
|
-
3
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
t.strictSame(
|
|
22
|
-
await evaluate([]),
|
|
23
|
-
[]
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
t.strictSame(
|
|
27
|
-
await evaluate(concat([1, 2], [[add(1, 2)]])),
|
|
28
|
-
[1, 2, [3]]
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
t.strictSame(
|
|
32
|
-
await evaluate(concat([1, 2], [add(1, 2), add(2, 2)])),
|
|
33
|
-
[1, 2, 3, 4]
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
t.strictSame(
|
|
37
|
-
await evaluate([identity(1), 2, 3]),
|
|
38
|
-
[1, 2, 3]
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
t.rejects(async () => {
|
|
42
|
-
await evaluate(['ref', 'doesNotExist'])
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
t.end()
|
|
46
|
-
})
|