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 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
- - [Reference](#reference)
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 exec from '../exec.mjs'
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 = await exec(math, program)
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 api from '../api.mjs'
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 exec = program => {
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 } = api(['add', 'square'], exec)
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
- console.log(
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
- ## 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.
139
+ ## Async/Await
136
140
 
137
- ### `sendscript/api.mjs`
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
- 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.
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
- 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'
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
- exec({
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
- 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.
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 an environment module in TypeScript.
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
- modules type.
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 MyModule from './my-module'
170
+ import type * as math from './example/math.ts'
208
171
 
209
- import sendScriptApi from 'sendscript/api.mjs'
172
+ import module from 'sendscript/module.mjs'
210
173
 
211
- export default sendScriptApi([
212
- fnOne,
213
- fnTwo,
214
- ], /* perform websocket request */) as typeof MyModule
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 likely subject to serialization and deserialization,
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.4 test
195
+ > sendscript@1.0.1 test
234
196
  > tap -R silent
235
197
 
236
198
 
237
- > sendscript@0.1.4 test
199
+ > sendscript@1.0.1 test
238
200
  > tap report text-summary
239
201
 
240
202
 
241
203
  =============================== Coverage summary ===============================
242
- Statements : 100% ( 110/110 )
243
- Branches : 100% ( 32/32 )
244
- Functions : 100% ( 10/10 )
245
- Lines : 100% ( 110/110 )
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 exec from '../exec.mjs'
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 = await exec(math, program)
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 api from '../api.mjs'
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 exec = program => {
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 } = api(['add', 'square'], exec)
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
- console.log(
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
- ## 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.
121
+ ## Async/Await
145
122
 
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.
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
- 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.
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
- import exec from './exec.mjs'
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
- exec({
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
- 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.
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 an environment module in TypeScript.
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
- modules type.
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 MyModule from './my-module'
152
+ import type * as math from './example/math.ts'
189
153
 
190
- import sendScriptApi from 'sendscript/api.mjs'
154
+ import module from 'sendscript/module.mjs'
191
155
 
192
- export default sendScriptApi([
193
- fnOne,
194
- fnTwo,
195
- ], /* perform websocket request */) as typeof MyModule
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 likely subject to serialization and deserialization,
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 api from '../api.mjs'
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 exec = program => {
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 } = api(['add', 'square'], exec)
22
+ const { add, square } = module(math)
20
23
 
21
- console.log(
22
- await square(add(1, add(add(2, 3), 4)))
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 exec from '../exec.mjs'
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 = await exec(math, program)
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 api from './api.mjs'
2
- import exec from './exec.mjs'
1
+ import stringify from './stringify.mjs'
2
+ import makeModule from './module.mjs'
3
+ import parse from './parse.mjs'
3
4
 
4
- export default { api, exec }
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
@@ -0,0 +1,3 @@
1
+ const isNil = x => x == null
2
+
3
+ export default isNil
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.4",
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": "^18.5.2"
26
+ "tap": "^21.0.1"
27
27
  },
28
28
  "dependencies": {
29
- "debug": "^4.3.4"
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
@@ -0,0 +1,3 @@
1
+ export const ref = Symbol('ref')
2
+ export const call = Symbol('call')
3
+ export const awaitSymbol = Symbol('await')
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
@@ -1,11 +0,0 @@
1
- import curry from './curry.mjs'
2
-
3
- export default curry(function awaitWhen (when, array) {
4
- return array.reduce(async (asyncAcc, item) => {
5
- const acc = await asyncAcc
6
-
7
- acc.push(when(item) ? await item : item)
8
-
9
- return acc
10
- }, [])
11
- })
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
- })