sendscript 0.0.0
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/LICENSE.txt +22 -0
- package/README.md +223 -0
- package/README.mz +196 -0
- package/curry.mjs +11 -0
- package/curry.test.mjs +35 -0
- package/debug.mjs +5 -0
- package/dsl.mjs +40 -0
- package/dsl.test.mjs +31 -0
- package/error.mjs +9 -0
- package/example/client.socket.io.mjs +25 -0
- package/example/math.mjs +3 -0
- package/example/server.socket.io.mjs +22 -0
- package/exec.mjs +69 -0
- package/exec.test.mjs +110 -0
- package/package.json +24 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2023
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# SendScript
|
|
2
|
+
|
|
3
|
+
Write JS code that you can run on servers, browsers or other clients.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/sendscript)
|
|
6
|
+
[](https://standardjs.com)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
> SendScript leaves it up to you to choose HTTP, web-sockets or any other method of
|
|
10
|
+
> communication between servers and clients that best fits your needs.
|
|
11
|
+
|
|
12
|
+
## Socket example
|
|
13
|
+
|
|
14
|
+
For this example we'll use [socket.io][socket.io].
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --no-save socket.io socket.io-client
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> We use the `--no-save` option because it's only for demonstration purposes.
|
|
21
|
+
|
|
22
|
+
### Module
|
|
23
|
+
|
|
24
|
+
We write a simple module that only has an add function
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// ./example/math.mjs
|
|
28
|
+
|
|
29
|
+
export const add = (a, b) => a + b
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Server
|
|
33
|
+
|
|
34
|
+
Here a simple module for running a socket.io server that runs SendScript programs.
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// ./example/server.socket.io.mjs
|
|
38
|
+
|
|
39
|
+
import { Server } from 'socket.io'
|
|
40
|
+
import exec from '../exec.mjs'
|
|
41
|
+
import * as math from './math.mjs'
|
|
42
|
+
|
|
43
|
+
const server = new Server()
|
|
44
|
+
const port = process.env.PORT || 3000
|
|
45
|
+
|
|
46
|
+
server.on('connection', (socket) => {
|
|
47
|
+
socket.on('message', async (program, callback) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await exec(math, program)
|
|
50
|
+
callback(null, result) // Pass null as the first argument to indicate success
|
|
51
|
+
} catch (error) {
|
|
52
|
+
callback(error) // Pass the error to the callback
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
server.listen(port)
|
|
58
|
+
process.title = 'sendscript'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Client
|
|
62
|
+
|
|
63
|
+
Now for a client that sends a program to the server.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// ./example/client.socket.io.mjs
|
|
67
|
+
|
|
68
|
+
import socketClient from 'socket.io-client'
|
|
69
|
+
import dsl from '../dsl.mjs'
|
|
70
|
+
|
|
71
|
+
const port = process.env.PORT || 3000
|
|
72
|
+
const client = socketClient(`http://localhost:${port}`)
|
|
73
|
+
|
|
74
|
+
const exec = program => {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
client.emit('message', program, (error, result) => {
|
|
77
|
+
error
|
|
78
|
+
? reject(error)
|
|
79
|
+
: resolve(result)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { add } = dsl(['add'], exec)
|
|
85
|
+
|
|
86
|
+
console.log(
|
|
87
|
+
await add(1, add(add(2, 3), 4))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
process.exit(0)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Now we run this server and a client script.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Run the server
|
|
97
|
+
node ./example/server.socket.io.mjs&
|
|
98
|
+
|
|
99
|
+
# Run the client example
|
|
100
|
+
node ./example/client.socket.io.mjs
|
|
101
|
+
|
|
102
|
+
pkill sendscript
|
|
103
|
+
```
|
|
104
|
+
```
|
|
105
|
+
10
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Reference
|
|
109
|
+
|
|
110
|
+
SendScript is essentially a way to serialize a program to then send over the wire
|
|
111
|
+
and execute it somewhere else.
|
|
112
|
+
|
|
113
|
+
We only have two modules. One that helps you write programs that can be sent
|
|
114
|
+
over the wire and another for running that program.
|
|
115
|
+
|
|
116
|
+
### `sendscript/dsl.mjs`
|
|
117
|
+
|
|
118
|
+
The dsl module exports a function that takes two arguments.
|
|
119
|
+
|
|
120
|
+
1. The schema, which represents the values that are available.
|
|
121
|
+
2. The function that will be called with the serializable version of the
|
|
122
|
+
program.
|
|
123
|
+
|
|
124
|
+
It returns an object that contains functions which are defined in the schema.
|
|
125
|
+
These functions are a JavaScript DSL for writing programs that can be sent to
|
|
126
|
+
a server.
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
import dsl from './dsl.mjs'
|
|
130
|
+
|
|
131
|
+
const { add, subtract } = dsl(
|
|
132
|
+
['add', 'subtract'],
|
|
133
|
+
serializableProgram => sendSomewhereToBeExecuted(serializableProgram)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
await add(1, 2) // => 3
|
|
137
|
+
await subtract(1, 2) // => -1
|
|
138
|
+
await add(1, subtract(2, 3)) // => 0
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The add and subtract functions are thennable. The execute function is called as
|
|
142
|
+
soon as await or `.then` is used.
|
|
143
|
+
|
|
144
|
+
> Notice that you do not have to await the subtract call. You only need to
|
|
145
|
+
> await when you want to execute the program.
|
|
146
|
+
|
|
147
|
+
This DSL is composable and wrappable.
|
|
148
|
+
|
|
149
|
+
### `sendscript/exec.mjs`
|
|
150
|
+
|
|
151
|
+
The exec function takes an environment object and any valid SendScript program.
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
import exec from './exec.mjs'
|
|
155
|
+
|
|
156
|
+
exec({
|
|
157
|
+
add: (a, b) => a + b,
|
|
158
|
+
subtract: (a, b) => a - b
|
|
159
|
+
}, ['add', 1, [subtract, 1, 2]])
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The array you see here is the LISP that SendScript uses to represent programs.
|
|
163
|
+
|
|
164
|
+
You could use SendScript without knowing the details of how the LISP works. It is an
|
|
165
|
+
implementation detail and might change over time.
|
|
166
|
+
|
|
167
|
+
## Tests
|
|
168
|
+
|
|
169
|
+
Tests with 100% code coverage.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npx c8 --100 npm t -- -R classic
|
|
173
|
+
```
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> sendscript@0.0.0 test
|
|
177
|
+
> tap *.test.mjs --no-cov -R classic
|
|
178
|
+
|
|
179
|
+
curry.test.mjs ........................................ 4/4
|
|
180
|
+
dsl.test.mjs .......................................... 5/5
|
|
181
|
+
exec.test.mjs ....................................... 15/15
|
|
182
|
+
total ............................................... 24/24
|
|
183
|
+
|
|
184
|
+
24 passing (400.194ms)
|
|
185
|
+
|
|
186
|
+
ok
|
|
187
|
+
-----------|---------|----------|---------|---------|-------------------
|
|
188
|
+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
189
|
+
-----------|---------|----------|---------|---------|-------------------
|
|
190
|
+
All files | 100 | 100 | 100 | 100 |
|
|
191
|
+
curry.mjs | 100 | 100 | 100 | 100 |
|
|
192
|
+
debug.mjs | 100 | 100 | 100 | 100 |
|
|
193
|
+
dsl.mjs | 100 | 100 | 100 | 100 |
|
|
194
|
+
error.mjs | 100 | 100 | 100 | 100 |
|
|
195
|
+
exec.mjs | 100 | 100 | 100 | 100 |
|
|
196
|
+
-----------|---------|----------|---------|---------|-------------------
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Formatting
|
|
200
|
+
|
|
201
|
+
Standard because no config.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npx standard
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Changelog
|
|
208
|
+
|
|
209
|
+
The [changelog][changelog] is generated using the useful
|
|
210
|
+
[auto-changelog][auto-changelog] project.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx auto-changelog -p
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
See the [LICENSE.txt][license] file for details.
|
|
219
|
+
|
|
220
|
+
[license]:./LICENSE.txt
|
|
221
|
+
[socket.io]:https://socket.io/
|
|
222
|
+
[changelog]:./CHANGELOG.md
|
|
223
|
+
[auto-changelog]:https://www.npmjs.com/package/auto-changelog
|
package/README.mz
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# SendScript
|
|
2
|
+
|
|
3
|
+
Write JS code that you can run on servers, browsers or other clients.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/sendscript)
|
|
6
|
+
[](https://standardjs.com)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
> SendScript leaves it up to you to choose HTTP, web-sockets or any other method of
|
|
10
|
+
> communication between servers and clients that best fits your needs.
|
|
11
|
+
|
|
12
|
+
## Socket example
|
|
13
|
+
|
|
14
|
+
For this example we'll use [socket.io][socket.io].
|
|
15
|
+
|
|
16
|
+
```bash bash > /dev/null
|
|
17
|
+
npm install --no-save socket.io socket.io-client
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> We use the `--no-save` option because it's only for demonstration purposes.
|
|
21
|
+
|
|
22
|
+
### Module
|
|
23
|
+
|
|
24
|
+
We write a simple module that only has an add function
|
|
25
|
+
|
|
26
|
+
```js cat - > ./example/math.mjs
|
|
27
|
+
// ./example/math.mjs
|
|
28
|
+
|
|
29
|
+
export const add = (a, b) => a + b
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Server
|
|
33
|
+
|
|
34
|
+
Here a simple module for running a socket.io server that runs SendScript programs.
|
|
35
|
+
|
|
36
|
+
```js cat - > ./example/server.socket.io.mjs
|
|
37
|
+
// ./example/server.socket.io.mjs
|
|
38
|
+
|
|
39
|
+
import { Server } from 'socket.io'
|
|
40
|
+
import exec from '../exec.mjs'
|
|
41
|
+
import * as math from './math.mjs'
|
|
42
|
+
|
|
43
|
+
const server = new Server()
|
|
44
|
+
const port = process.env.PORT || 3000
|
|
45
|
+
|
|
46
|
+
server.on('connection', (socket) => {
|
|
47
|
+
socket.on('message', async (program, callback) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await exec(math, program)
|
|
50
|
+
callback(null, result) // Pass null as the first argument to indicate success
|
|
51
|
+
} catch (error) {
|
|
52
|
+
callback(error) // Pass the error to the callback
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
server.listen(port)
|
|
58
|
+
process.title = 'sendscript'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Client
|
|
62
|
+
|
|
63
|
+
Now for a client that sends a program to the server.
|
|
64
|
+
|
|
65
|
+
```js cat - > ./example/client.socket.io.mjs
|
|
66
|
+
// ./example/client.socket.io.mjs
|
|
67
|
+
|
|
68
|
+
import socketClient from 'socket.io-client'
|
|
69
|
+
import dsl from '../dsl.mjs'
|
|
70
|
+
|
|
71
|
+
const port = process.env.PORT || 3000
|
|
72
|
+
const client = socketClient(`http://localhost:${port}`)
|
|
73
|
+
|
|
74
|
+
const exec = program => {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
client.emit('message', program, (error, result) => {
|
|
77
|
+
error
|
|
78
|
+
? reject(error)
|
|
79
|
+
: resolve(result)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { add } = dsl(['add'], exec)
|
|
85
|
+
|
|
86
|
+
console.log(
|
|
87
|
+
await add(1, add(add(2, 3), 4))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
process.exit(0)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Now we run this server and a client script.
|
|
94
|
+
|
|
95
|
+
```bash bash
|
|
96
|
+
# Run the server
|
|
97
|
+
node ./example/server.socket.io.mjs&
|
|
98
|
+
|
|
99
|
+
# Run the client example
|
|
100
|
+
node ./example/client.socket.io.mjs
|
|
101
|
+
|
|
102
|
+
pkill sendscript
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Reference
|
|
106
|
+
|
|
107
|
+
SendScript is essentially a way to serialize a program to then send over the wire
|
|
108
|
+
and execute it somewhere else.
|
|
109
|
+
|
|
110
|
+
We only have two modules. One that helps you write programs that can be sent
|
|
111
|
+
over the wire and another for running that program.
|
|
112
|
+
|
|
113
|
+
### `sendscript/dsl.mjs`
|
|
114
|
+
|
|
115
|
+
The dsl module exports a function that takes two arguments.
|
|
116
|
+
|
|
117
|
+
1. The schema, which represents the values that are available.
|
|
118
|
+
2. The function that will be called with the serializable version of the
|
|
119
|
+
program.
|
|
120
|
+
|
|
121
|
+
It returns an object that contains functions which are defined in the schema.
|
|
122
|
+
These functions are a JavaScript DSL for writing programs that can be sent to
|
|
123
|
+
a server.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
import dsl from './dsl.mjs'
|
|
127
|
+
|
|
128
|
+
const { add, subtract } = dsl(
|
|
129
|
+
['add', 'subtract'],
|
|
130
|
+
serializableProgram => sendSomewhereToBeExecuted(serializableProgram)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
await add(1, 2) // => 3
|
|
134
|
+
await subtract(1, 2) // => -1
|
|
135
|
+
await add(1, subtract(2, 3)) // => 0
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The add and subtract functions are thennable. The execute function is called as
|
|
139
|
+
soon as await or `.then` is used.
|
|
140
|
+
|
|
141
|
+
> Notice that you do not have to await the subtract call. You only need to
|
|
142
|
+
> await when you want to execute the program.
|
|
143
|
+
|
|
144
|
+
This DSL is composable and wrappable.
|
|
145
|
+
|
|
146
|
+
### `sendscript/exec.mjs`
|
|
147
|
+
|
|
148
|
+
The exec function takes an environment object and any valid SendScript program.
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import exec from './exec.mjs'
|
|
152
|
+
|
|
153
|
+
exec({
|
|
154
|
+
add: (a, b) => a + b,
|
|
155
|
+
subtract: (a, b) => a - b
|
|
156
|
+
}, ['add', 1, [subtract, 1, 2]])
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The array you see here is the LISP that SendScript uses to represent programs.
|
|
160
|
+
|
|
161
|
+
You could use SendScript without knowing the details of how the LISP works. It is an
|
|
162
|
+
implementation detail and might change over time.
|
|
163
|
+
|
|
164
|
+
## Tests
|
|
165
|
+
|
|
166
|
+
Tests with 100% code coverage.
|
|
167
|
+
|
|
168
|
+
```bash bash
|
|
169
|
+
npx c8 --100 npm t -- -R classic
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Formatting
|
|
173
|
+
|
|
174
|
+
Standard because no config.
|
|
175
|
+
|
|
176
|
+
```bash bash
|
|
177
|
+
npx standard
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Changelog
|
|
181
|
+
|
|
182
|
+
The [changelog][changelog] is generated using the useful
|
|
183
|
+
[auto-changelog][auto-changelog] project.
|
|
184
|
+
|
|
185
|
+
```bash bash > /dev/null
|
|
186
|
+
npx auto-changelog -p
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
See the [LICENSE.txt][license] file for details.
|
|
192
|
+
|
|
193
|
+
[license]:./LICENSE.txt
|
|
194
|
+
[socket.io]:https://socket.io/
|
|
195
|
+
[changelog]:./CHANGELOG.md
|
|
196
|
+
[auto-changelog]:https://www.npmjs.com/package/auto-changelog
|
package/curry.mjs
ADDED
package/curry.test.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test } from 'tap'
|
|
2
|
+
import curry from './curry.mjs'
|
|
3
|
+
|
|
4
|
+
test('curry function', (t) => {
|
|
5
|
+
t.test('curry function returns a function', (assert) => {
|
|
6
|
+
const curried = curry((a, b, c) => a + b + c)
|
|
7
|
+
assert.type(curried, 'function', 'returns a function')
|
|
8
|
+
assert.end()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
t.test('curried function returns correct result', (assert) => {
|
|
12
|
+
const curried = curry((a, b, c) => a + b + c)
|
|
13
|
+
const result = curried(1)(2)(3)
|
|
14
|
+
assert.equal(result, 6, 'returns correct result')
|
|
15
|
+
assert.end()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
t.test('curried function handles partial application', (assert) => {
|
|
19
|
+
const curried = curry((a, b, c) => a + b + c)
|
|
20
|
+
const partial = curried(1, 2)
|
|
21
|
+
const result = partial(3)
|
|
22
|
+
assert.equal(result, 6, 'handles partial application')
|
|
23
|
+
assert.end()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
t.test('curried function handles multiple arguments', (assert) => {
|
|
27
|
+
const curried = curry((a, b, c, d) => a + b + c + d)
|
|
28
|
+
const partial = curried(1)
|
|
29
|
+
const result = partial(2)(3, 4)
|
|
30
|
+
assert.equal(result, 10, 'handles multiple arguments')
|
|
31
|
+
assert.end()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
t.end()
|
|
35
|
+
})
|
package/debug.mjs
ADDED
package/dsl.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const symbol = Symbol('dsl')
|
|
2
|
+
|
|
3
|
+
// Only await values that are not part of the dsl.
|
|
4
|
+
const promiseAllNonDSL = async values => values.reduce(async (_acc, value) => {
|
|
5
|
+
const acc = await _acc
|
|
6
|
+
|
|
7
|
+
if (value[symbol] === symbol) {
|
|
8
|
+
acc.push(value)
|
|
9
|
+
} else {
|
|
10
|
+
acc.push(await value)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return acc
|
|
14
|
+
}, [])
|
|
15
|
+
|
|
16
|
+
export default function dsl (schema, call) {
|
|
17
|
+
return schema.reduce((api, name) => {
|
|
18
|
+
const then = (program) => (resolve, reject) =>
|
|
19
|
+
Promise.resolve(call(program)).then(resolve, reject)
|
|
20
|
+
|
|
21
|
+
const fn = (...args) => {
|
|
22
|
+
// Only await non dsl values
|
|
23
|
+
const v = [fn, ...args]
|
|
24
|
+
|
|
25
|
+
v[symbol] = symbol
|
|
26
|
+
v.then = async (...thenArgs) => {
|
|
27
|
+
return then(await promiseAllNonDSL(v))(...thenArgs)
|
|
28
|
+
}
|
|
29
|
+
return v
|
|
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/dsl.test.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { test } from 'tap'
|
|
2
|
+
import dsl from './dsl.mjs'
|
|
3
|
+
|
|
4
|
+
test('return a constant value', async t => {
|
|
5
|
+
t.plan(1)
|
|
6
|
+
|
|
7
|
+
const { constant } = dsl(['constant'], v => {
|
|
8
|
+
t.equal(JSON.stringify(v), '["ref","constant"]')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
await constant
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('use function calls as arguments', async t => {
|
|
15
|
+
t.plan(4)
|
|
16
|
+
|
|
17
|
+
const { add } = dsl(['add'], b => {
|
|
18
|
+
t.equal(JSON.stringify(awaited), JSON.stringify(b))
|
|
19
|
+
return 'done'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const program = add(add(add(1, 2), 3), 4)
|
|
23
|
+
const awaited = program
|
|
24
|
+
const thenned = program
|
|
25
|
+
|
|
26
|
+
t.equal(await awaited, 'done')
|
|
27
|
+
|
|
28
|
+
await thenned.then(result => {
|
|
29
|
+
t.equal(result, 'done')
|
|
30
|
+
})
|
|
31
|
+
})
|
package/error.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ./example/client.socket.io.mjs
|
|
2
|
+
|
|
3
|
+
import socketClient from 'socket.io-client'
|
|
4
|
+
import dsl from '../dsl.mjs'
|
|
5
|
+
|
|
6
|
+
const port = process.env.PORT || 3000
|
|
7
|
+
const client = socketClient(`http://localhost:${port}`)
|
|
8
|
+
|
|
9
|
+
const exec = program => {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
client.emit('message', program, (error, result) => {
|
|
12
|
+
error
|
|
13
|
+
? reject(error)
|
|
14
|
+
: resolve(result)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { add } = dsl(['add'], exec)
|
|
20
|
+
|
|
21
|
+
console.log(
|
|
22
|
+
await add(1, add(add(2, 3), 4))
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
process.exit(0)
|
package/example/math.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// ./example/server.socket.io.mjs
|
|
2
|
+
|
|
3
|
+
import { Server } from 'socket.io'
|
|
4
|
+
import exec from '../exec.mjs'
|
|
5
|
+
import * as math from './math.mjs'
|
|
6
|
+
|
|
7
|
+
const server = new Server()
|
|
8
|
+
const port = process.env.PORT || 3000
|
|
9
|
+
|
|
10
|
+
server.on('connection', (socket) => {
|
|
11
|
+
socket.on('message', async (program, callback) => {
|
|
12
|
+
try {
|
|
13
|
+
const result = await exec(math, program)
|
|
14
|
+
callback(null, result) // Pass null as the first argument to indicate success
|
|
15
|
+
} catch (error) {
|
|
16
|
+
callback(error) // Pass the error to the callback
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
server.listen(port)
|
|
22
|
+
process.title = 'sendscript'
|
package/exec.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import _debug from './debug.mjs'
|
|
2
|
+
import { BlendExpressionError } from './error.mjs'
|
|
3
|
+
import curry from './curry.mjs'
|
|
4
|
+
|
|
5
|
+
const debug = _debug.extend('lisp')
|
|
6
|
+
const debugError = debug.extend('error')
|
|
7
|
+
const isFunction = x => typeof x === 'function'
|
|
8
|
+
const castFunction = x => isFunction(x) ? x : () => x
|
|
9
|
+
|
|
10
|
+
const exec = curry(async (env, expression) => {
|
|
11
|
+
debug('exec', expression)
|
|
12
|
+
|
|
13
|
+
if (!Array.isArray(expression)) {
|
|
14
|
+
return expression
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [operator, ...args] = expression
|
|
18
|
+
|
|
19
|
+
// Yey, resolved and all and ready to call.
|
|
20
|
+
if (isFunction(operator)) {
|
|
21
|
+
try {
|
|
22
|
+
return await operator.call(env, ...(await Promise.all(args.map(exec(env)))))
|
|
23
|
+
} catch (error) {
|
|
24
|
+
debugError(error)
|
|
25
|
+
throw error
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Use JSON value when toJSON is implemented.
|
|
30
|
+
// NECESSARY? if (operator?.toJSON) { operator = operator.toJSON() }
|
|
31
|
+
|
|
32
|
+
if (operator === 'fn') {
|
|
33
|
+
const [body] = args
|
|
34
|
+
|
|
35
|
+
return (fnEnv) => exec(Object.assign(Object.create(env), fnEnv), body)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (operator === 'let') {
|
|
39
|
+
const letEnv = Object.create(env)
|
|
40
|
+
const [letBindings, letBody] = args
|
|
41
|
+
|
|
42
|
+
await Promise.all(letBindings.map(async ([name, letExpr]) => {
|
|
43
|
+
letEnv[name] = await exec(letEnv, letExpr)
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
return exec(letEnv, letBody)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (operator === 'ref') {
|
|
50
|
+
const [name] = args
|
|
51
|
+
return env[name]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (Array.isArray(operator)) {
|
|
55
|
+
return exec(env, [await exec(env, operator), ...args])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Throw error if operator is not in env.
|
|
59
|
+
if (!(operator in env)) {
|
|
60
|
+
const error = new BlendExpressionError(`Unknown expression: ${operator}`)
|
|
61
|
+
debugError(error)
|
|
62
|
+
throw error
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return exec(env, [castFunction(env[operator]), ...args])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export { exec }
|
|
69
|
+
export default exec
|
package/exec.test.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { test } from 'tap'
|
|
2
|
+
import { exec } from './exec.mjs'
|
|
3
|
+
|
|
4
|
+
test('should evaluate basic expressions correctly', (t) => {
|
|
5
|
+
t.plan(4)
|
|
6
|
+
|
|
7
|
+
exec({ '+': (a, b) => a + b }, ['+', 1, 2])
|
|
8
|
+
.then((result) => t.equal(result, 3, '1 + 2 = 3'))
|
|
9
|
+
|
|
10
|
+
exec({ '-': (a, b) => a - b }, ['-', 5, 3])
|
|
11
|
+
.then((result) => t.equal(result, 2, '5 - 3 = 2'))
|
|
12
|
+
|
|
13
|
+
exec({ '*': (a, b) => a * b }, ['*', 4, 3])
|
|
14
|
+
.then((result) => t.equal(result, 12, '4 * 3 = 12'))
|
|
15
|
+
|
|
16
|
+
exec({ '/': (a, b) => a / b }, ['/', 10, 2])
|
|
17
|
+
.then((result) => t.equal(result, 5, '10 / 2 = 5'))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('should be able to call a partially applied function', async t => {
|
|
21
|
+
const env = {
|
|
22
|
+
'+': a => b => a + b
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
t.equal(typeof (await exec(env, ['+', 1])), 'function')
|
|
26
|
+
t.equal(await exec(env, [['+', 1], 2]), 3)
|
|
27
|
+
t.end()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('should handle let expressions', (t) => {
|
|
31
|
+
t.plan(1)
|
|
32
|
+
|
|
33
|
+
exec({ '+': (a, b) => a + b }, ['let', [['x', 2], ['y', 3]], ['+', ['x'], ['y']]])
|
|
34
|
+
.then((result) => t.equal(result, 5, 'x + y = 5'))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('should handle fn expressions', (t) => {
|
|
38
|
+
t.plan(2)
|
|
39
|
+
|
|
40
|
+
exec({}, ['fn', ['+', 1, 'x']])
|
|
41
|
+
.then((result) => t.equal(typeof result, 'function', 'should return a function'))
|
|
42
|
+
|
|
43
|
+
exec({ '+': (a, b) => a + b }, ['fn', ['+', 1, ['x']]])
|
|
44
|
+
.then(async (result) => {
|
|
45
|
+
t.equal(await result({ x: 2 }), 3, '1 + 2 = 3')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('should throw an error for unknown expressions', (t) => {
|
|
50
|
+
t.plan(1)
|
|
51
|
+
|
|
52
|
+
exec({ '+': (a, b) => a + b }, ['-', 1, 2])
|
|
53
|
+
.catch((err) => t.equal(err.message, 'Unknown expression: -', 'should throw an error for unknown expressions'))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('should evaluate nested expressions correctly', (t) => {
|
|
57
|
+
t.plan(1)
|
|
58
|
+
|
|
59
|
+
const env = {
|
|
60
|
+
'+': (a, b) => a + b,
|
|
61
|
+
'*': (a, b) => a * b
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
exec(env, ['*', 2, ['+', 1, 2]])
|
|
65
|
+
.then((result) => t.equal(result, 6, '2 * (1 + 2) = 6'))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should concatenate two arrays', async t => {
|
|
69
|
+
t.plan(1)
|
|
70
|
+
|
|
71
|
+
const env = {
|
|
72
|
+
array: (...args) => args,
|
|
73
|
+
concat: (a, b) => a.concat(b)
|
|
74
|
+
}
|
|
75
|
+
const expression = ['concat', ['array', 1, 2, 3], ['array', 4, 5, 6]]
|
|
76
|
+
const result = await exec(env, expression)
|
|
77
|
+
|
|
78
|
+
t.same(result, [1, 2, 3, 4, 5, 6])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('should fetch the reference to the item on the env', async t => {
|
|
82
|
+
const value = 'yey'
|
|
83
|
+
|
|
84
|
+
const env = {
|
|
85
|
+
thing: () => value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const expression = ['ref', 'thing']
|
|
89
|
+
const result = await exec(env, expression)
|
|
90
|
+
|
|
91
|
+
t.same(result(), value)
|
|
92
|
+
|
|
93
|
+
const result2 = await exec(env, ['thing'])
|
|
94
|
+
t.same(result2, value)
|
|
95
|
+
|
|
96
|
+
t.end()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('should reject with the error of the called function', async t => {
|
|
100
|
+
const error = new Error('BOOM')
|
|
101
|
+
const env = {
|
|
102
|
+
throws () {
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await t.rejects(exec(env, ['throws']))
|
|
108
|
+
|
|
109
|
+
t.end()
|
|
110
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sendscript",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Blur the line between server and client code.",
|
|
5
|
+
"module": true,
|
|
6
|
+
"main": "index.mjs",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git@github.com:bas080/sendscript.git"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "tap *.test.mjs --no-cov",
|
|
13
|
+
"version": "npm run docs && git add *.md",
|
|
14
|
+
"docs": "markatzea README.mz | tee README.md"
|
|
15
|
+
},
|
|
16
|
+
"author": "Bas Huis",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tap": "^16.3.4"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"debug": "^4.3.4"
|
|
23
|
+
}
|
|
24
|
+
}
|