sendscript 0.1.3 → 1.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/CHANGELOG.md +15 -0
- package/README.md +35 -82
- package/README.mz +27 -72
- package/example/client.socket.io.mjs +15 -7
- package/example/server.socket.io.mjs +3 -2
- package/index.mjs +11 -0
- 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 -31
- package/exec.test.mjs +0 -42
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,23 @@ 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.0](https://github.com/bas080/sendscript/compare/v0.1.4...v1.0.0)
|
|
8
|
+
|
|
9
|
+
- Add support for async await [`e949184`](https://github.com/bas080/sendscript/commit/e949184ab31a3bfdf4c6181463dcdd7250aca3a8)
|
|
10
|
+
- Update tap to latest version on 2024-03-17 [`1164994`](https://github.com/bas080/sendscript/commit/11649947737ffd152ac46d1db01946ad0c1261e7)
|
|
11
|
+
- Support nested objects and keywords as first item [`06da388`](https://github.com/bas080/sendscript/commit/06da3887c05ab0c545a53b6e796d2f7d4c53e6e6)
|
|
12
|
+
|
|
13
|
+
#### [v0.1.4](https://github.com/bas080/sendscript/compare/v0.1.3...v0.1.4)
|
|
14
|
+
|
|
15
|
+
> 8 March 2024
|
|
16
|
+
|
|
17
|
+
- Add missing index file that exports both modules [`38054ea`](https://github.com/bas080/sendscript/commit/38054ea8284a1626146bec42309b3c014527ff7d)
|
|
18
|
+
- Prevent ref of non existant property on env object [`49520ad`](https://github.com/bas080/sendscript/commit/49520ad32604683c668d95e479de772a91f6ae2b)
|
|
19
|
+
|
|
7
20
|
#### [v0.1.3](https://github.com/bas080/sendscript/compare/v0.1.2...v0.1.3)
|
|
8
21
|
|
|
22
|
+
> 21 January 2024
|
|
23
|
+
|
|
9
24
|
- Remove the d.ts file gen step [`801b2cf`](https://github.com/bas080/sendscript/commit/801b2cf3b38259455186d4df85d98a42c046287c)
|
|
10
25
|
|
|
11
26
|
#### [v0.1.2](https://github.com/bas080/sendscript/compare/v0.1.1...v0.1.2)
|
package/README.md
CHANGED
|
@@ -13,15 +13,13 @@ 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
|
-
- [Reference](#reference)
|
|
17
|
-
* [`sendscript/api.mjs`](#sendscriptapimjs)
|
|
18
|
-
* [`sendscript/exec.mjs`](#sendscriptexecmjs)
|
|
19
16
|
- [TypeScript](#typescript)
|
|
20
17
|
- [Tests](#tests)
|
|
21
18
|
- [Formatting](#formatting)
|
|
22
19
|
- [Changelog](#changelog)
|
|
23
20
|
- [Dependencies](#dependencies)
|
|
24
21
|
- [License](#license)
|
|
22
|
+
- [Roadmap](#roadmap)
|
|
25
23
|
|
|
26
24
|
<!-- tocstop -->
|
|
27
25
|
|
|
@@ -58,16 +56,17 @@ Here a socket.io server that runs SendScript programs.
|
|
|
58
56
|
// ./example/server.socket.io.mjs
|
|
59
57
|
|
|
60
58
|
import { Server } from 'socket.io'
|
|
61
|
-
import
|
|
59
|
+
import Parse from '../parse.mjs'
|
|
62
60
|
import * as math from './math.mjs'
|
|
63
61
|
|
|
62
|
+
const parse = Parse(math)
|
|
64
63
|
const server = new Server()
|
|
65
64
|
const port = process.env.PORT || 3000
|
|
66
65
|
|
|
67
66
|
server.on('connection', (socket) => {
|
|
68
67
|
socket.on('message', async (program, callback) => {
|
|
69
68
|
try {
|
|
70
|
-
const result =
|
|
69
|
+
const result = parse(program)
|
|
71
70
|
callback(null, result) // Pass null as the first argument to indicate success
|
|
72
71
|
} catch (error) {
|
|
73
72
|
callback(error) // Pass the error to the callback
|
|
@@ -87,14 +86,17 @@ Now for a client that sends a program to the server.
|
|
|
87
86
|
// ./example/client.socket.io.mjs
|
|
88
87
|
|
|
89
88
|
import socketClient from 'socket.io-client'
|
|
90
|
-
import
|
|
89
|
+
import stringify from '../stringify.mjs'
|
|
90
|
+
import module from '../module.mjs'
|
|
91
|
+
import * as math from './math.mjs'
|
|
92
|
+
import assert from 'node:assert'
|
|
91
93
|
|
|
92
94
|
const port = process.env.PORT || 3000
|
|
93
95
|
const client = socketClient(`http://localhost:${port}`)
|
|
94
96
|
|
|
95
|
-
const
|
|
97
|
+
const send = program => {
|
|
96
98
|
return new Promise((resolve, reject) => {
|
|
97
|
-
client.emit('message', program, (error, result) => {
|
|
99
|
+
client.emit('message', stringify(program), (error, result) => {
|
|
98
100
|
error
|
|
99
101
|
? reject(error)
|
|
100
102
|
: resolve(result)
|
|
@@ -102,11 +104,16 @@ const exec = program => {
|
|
|
102
104
|
})
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
const { add, square } =
|
|
107
|
+
const { add, square } = module(math)
|
|
108
|
+
|
|
109
|
+
// The program to be sent over the wire
|
|
110
|
+
const program = square(add(1, add(add(2, 3), 4)))
|
|
111
|
+
|
|
112
|
+
const result = await send(program)
|
|
113
|
+
|
|
114
|
+
console.log('Result: ', result)
|
|
106
115
|
|
|
107
|
-
|
|
108
|
-
await square(add(1, add(add(2, 3), 4)))
|
|
109
|
-
)
|
|
116
|
+
assert.equal(result, 100)
|
|
110
117
|
|
|
111
118
|
process.exit(0)
|
|
112
119
|
```
|
|
@@ -114,6 +121,8 @@ process.exit(0)
|
|
|
114
121
|
Now we run this server and a client script.
|
|
115
122
|
|
|
116
123
|
```bash
|
|
124
|
+
set -e
|
|
125
|
+
|
|
117
126
|
# Run the server
|
|
118
127
|
node ./example/server.socket.io.mjs&
|
|
119
128
|
|
|
@@ -123,78 +132,19 @@ node ./example/client.socket.io.mjs
|
|
|
123
132
|
pkill sendscript
|
|
124
133
|
```
|
|
125
134
|
```
|
|
126
|
-
100
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Reference
|
|
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.
|
|
136
|
-
|
|
137
|
-
### `sendscript/api.mjs`
|
|
138
|
-
|
|
139
|
-
The api module exports a function that takes two arguments.
|
|
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.
|
|
148
|
-
|
|
149
|
-
```js
|
|
150
|
-
import api from './api.mjs'
|
|
151
|
-
|
|
152
|
-
const { add, subtract } = api(
|
|
153
|
-
['add', 'subtract'],
|
|
154
|
-
serializableProgram => sendSomewhereToBeExecuted(serializableProgram)
|
|
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'
|
|
176
|
-
|
|
177
|
-
exec({
|
|
178
|
-
add: (a, b) => a + b,
|
|
179
|
-
subtract: (a, b) => a - b
|
|
180
|
-
}, ['add', 1, [subtract, 1, 2]])
|
|
135
|
+
Result: 100
|
|
181
136
|
```
|
|
182
137
|
|
|
183
|
-
The array you see here is the LISP that SendScript uses to represent programs.
|
|
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.
|
|
187
|
-
|
|
188
138
|
## TypeScript
|
|
189
139
|
|
|
190
|
-
There is a good use-case to write
|
|
140
|
+
There is a good use-case to write a module in TypeScript.
|
|
191
141
|
|
|
192
142
|
1. Obviously the module would have the benefits that TypeScript offers when
|
|
193
143
|
coding.
|
|
194
144
|
2. You can use tools like [typedoc][typedoc] to generate docs from your types to
|
|
195
145
|
share with consumers of your API.
|
|
196
146
|
3. You can use the types of the module to coerce your client to adopt the
|
|
197
|
-
|
|
147
|
+
module's type.
|
|
198
148
|
|
|
199
149
|
```bash
|
|
200
150
|
# Create pretty docs for your module.
|
|
@@ -217,8 +167,7 @@ export default sendScriptApi([
|
|
|
217
167
|
> [!NOTE]
|
|
218
168
|
> Although type coercion on the client side can improve the development
|
|
219
169
|
> experience, it does not represent the actual type.
|
|
220
|
-
> Values are
|
|
221
|
-
> particularly when interfacing with JSON formats.
|
|
170
|
+
> Values are subject to serialization and deserialization.
|
|
222
171
|
|
|
223
172
|
## Tests
|
|
224
173
|
|
|
@@ -230,19 +179,19 @@ npm t -- report text-summary
|
|
|
230
179
|
```
|
|
231
180
|
```
|
|
232
181
|
|
|
233
|
-
> sendscript@0.
|
|
182
|
+
> sendscript@1.0.0 test
|
|
234
183
|
> tap -R silent
|
|
235
184
|
|
|
236
185
|
|
|
237
|
-
> sendscript@0.
|
|
186
|
+
> sendscript@1.0.0 test
|
|
238
187
|
> tap report text-summary
|
|
239
188
|
|
|
240
189
|
|
|
241
190
|
=============================== Coverage summary ===============================
|
|
242
|
-
Statements : 100% (
|
|
243
|
-
Branches : 100% (
|
|
244
|
-
Functions : 100% (
|
|
245
|
-
Lines : 100% (
|
|
191
|
+
Statements : 100% ( 239/239 )
|
|
192
|
+
Branches : 100% ( 71/71 )
|
|
193
|
+
Functions : 100% ( 18/18 )
|
|
194
|
+
Lines : 100% ( 239/239 )
|
|
246
195
|
================================================================================
|
|
247
196
|
```
|
|
248
197
|
|
|
@@ -278,6 +227,10 @@ No outdated packages found
|
|
|
278
227
|
|
|
279
228
|
See the [LICENSE.txt][license] file for details.
|
|
280
229
|
|
|
230
|
+
## Roadmap
|
|
231
|
+
|
|
232
|
+
- [ ] Support for simple lambdas to compose functions more easily.
|
|
233
|
+
|
|
281
234
|
[license]:./LICENSE.txt
|
|
282
235
|
[socket.io]:https://socket.io/
|
|
283
236
|
[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,75 +118,16 @@ node ./example/client.socket.io.mjs
|
|
|
107
118
|
pkill sendscript
|
|
108
119
|
```
|
|
109
120
|
|
|
110
|
-
## Reference
|
|
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.
|
|
145
|
-
|
|
146
|
-
> Notice that you do not have to await the subtract call. You only need to
|
|
147
|
-
> await when you want to execute the program.
|
|
148
|
-
|
|
149
|
-
This API is composable and wrappable.
|
|
150
|
-
|
|
151
|
-
### `sendscript/exec.mjs`
|
|
152
|
-
|
|
153
|
-
The exec function takes an environment object and any valid SendScript program.
|
|
154
|
-
|
|
155
|
-
```js
|
|
156
|
-
import exec from './exec.mjs'
|
|
157
|
-
|
|
158
|
-
exec({
|
|
159
|
-
add: (a, b) => a + b,
|
|
160
|
-
subtract: (a, b) => a - b
|
|
161
|
-
}, ['add', 1, [subtract, 1, 2]])
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
The array you see here is the LISP that SendScript uses to represent programs.
|
|
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.
|
|
168
|
-
|
|
169
121
|
## TypeScript
|
|
170
122
|
|
|
171
|
-
There is a good use-case to write
|
|
123
|
+
There is a good use-case to write a module in TypeScript.
|
|
172
124
|
|
|
173
125
|
1. Obviously the module would have the benefits that TypeScript offers when
|
|
174
126
|
coding.
|
|
175
127
|
2. You can use tools like [typedoc][typedoc] to generate docs from your types to
|
|
176
128
|
share with consumers of your API.
|
|
177
129
|
3. You can use the types of the module to coerce your client to adopt the
|
|
178
|
-
|
|
130
|
+
module's type.
|
|
179
131
|
|
|
180
132
|
```bash
|
|
181
133
|
# Create pretty docs for your module.
|
|
@@ -198,8 +150,7 @@ export default sendScriptApi([
|
|
|
198
150
|
> [!NOTE]
|
|
199
151
|
> Although type coercion on the client side can improve the development
|
|
200
152
|
> experience, it does not represent the actual type.
|
|
201
|
-
> Values are
|
|
202
|
-
> particularly when interfacing with JSON formats.
|
|
153
|
+
> Values are subject to serialization and deserialization.
|
|
203
154
|
|
|
204
155
|
## Tests
|
|
205
156
|
|
|
@@ -239,6 +190,10 @@ npm outdated && echo 'No outdated packages found'
|
|
|
239
190
|
|
|
240
191
|
See the [LICENSE.txt][license] file for details.
|
|
241
192
|
|
|
193
|
+
## Roadmap
|
|
194
|
+
|
|
195
|
+
- [ ] Support for simple lambdas to compose functions more easily.
|
|
196
|
+
|
|
242
197
|
[license]:./LICENSE.txt
|
|
243
198
|
[socket.io]:https://socket.io/
|
|
244
199
|
[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
ADDED
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.
|
|
3
|
+
"version": "1.0.0",
|
|
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,31 +0,0 @@
|
|
|
1
|
-
import _debug from './debug.mjs'
|
|
2
|
-
import curry from './curry.mjs'
|
|
3
|
-
|
|
4
|
-
const debug = _debug.extend('lisp')
|
|
5
|
-
|
|
6
|
-
const exec = curry(async (env, expression) => {
|
|
7
|
-
debug('exec', expression)
|
|
8
|
-
|
|
9
|
-
if (!Array.isArray(expression)) {
|
|
10
|
-
return expression
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const [operator, ...args] = expression
|
|
14
|
-
|
|
15
|
-
if (operator === 'call') {
|
|
16
|
-
const [fnRef, fnArgs] = args
|
|
17
|
-
|
|
18
|
-
const fn = await exec(env, fnRef)
|
|
19
|
-
return fn.apply(env, await exec(env, fnArgs))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (operator === 'ref') {
|
|
23
|
-
const [name] = args
|
|
24
|
-
|
|
25
|
-
return env[name]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return await Promise.all(expression.map(exec(env)))
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
export default exec
|
package/exec.test.mjs
DELETED
|
@@ -1,42 +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.end()
|
|
42
|
-
})
|