sendscript 0.1.4 → 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 CHANGED
@@ -4,8 +4,16 @@ 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
+
7
13
  #### [v0.1.4](https://github.com/bas080/sendscript/compare/v0.1.3...v0.1.4)
8
14
 
15
+ > 8 March 2024
16
+
9
17
  - Add missing index file that exports both modules [`38054ea`](https://github.com/bas080/sendscript/commit/38054ea8284a1626146bec42309b3c014527ff7d)
10
18
  - Prevent ref of non existant property on env object [`49520ad`](https://github.com/bas080/sendscript/commit/49520ad32604683c668d95e479de772a91f6ae2b)
11
19
 
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 exec from '../exec.mjs'
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 = await exec(math, program)
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 api from '../api.mjs'
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 exec = program => {
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 } = api(['add', 'square'], exec)
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
- console.log(
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 an environment module in TypeScript.
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
- modules type.
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 likely subject to serialization and deserialization,
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.1.4 test
182
+ > sendscript@1.0.0 test
234
183
  > tap -R silent
235
184
 
236
185
 
237
- > sendscript@0.1.4 test
186
+ > sendscript@1.0.0 test
238
187
  > tap report text-summary
239
188
 
240
189
 
241
190
  =============================== Coverage summary ===============================
242
- Statements : 100% ( 110/110 )
243
- Branches : 100% ( 32/32 )
244
- Functions : 100% ( 10/10 )
245
- Lines : 100% ( 110/110 )
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 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,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 an environment module in TypeScript.
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
- modules type.
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 likely subject to serialization and deserialization,
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 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.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": "^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
- })