sendscript 2.1.0 → 2.3.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,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
+ #### [v2.3.0](https://github.com/bas080/sendscript/compare/v2.2.0...v2.3.0)
8
+
9
+ - Add support for then and catch [`c0bbea8`](https://github.com/bas080/sendscript/commit/c0bbea8efcc6b262c091d9043e96ac8c3f7606e1)
10
+ - Document .catch and .then support [`b35009a`](https://github.com/bas080/sendscript/commit/b35009ace9761d32c0c6ef1d1d045bed2ecfe74e)
11
+
12
+ #### [v2.2.0](https://github.com/bas080/sendscript/compare/v2.1.0...v2.2.0)
13
+
14
+ > 10 April 2026
15
+
16
+ - Have default leaf serializer throw if value has no JSON equivalant [`6752f2f`](https://github.com/bas080/sendscript/commit/6752f2f3ed51e252ab93a31b632fe8fbae51d7cb)
17
+ - Test that prototype chain works when parsing [`2cf0e7e`](https://github.com/bas080/sendscript/commit/2cf0e7e7ced32258102382e7ff1e3f8e91a2cc77)
18
+
7
19
  #### [v2.1.0](https://github.com/bas080/sendscript/compare/v2.0.1...v2.1.0)
8
20
 
21
+ > 9 April 2026
22
+
9
23
  - Make eval order when using await inline with JS [`00db8aa`](https://github.com/bas080/sendscript/commit/00db8aaa563378db814035e3e550269593ed82e2)
10
24
  - Merge release workflow into publish [`6a5db12`](https://github.com/bas080/sendscript/commit/6a5db128b659dd295390505b2e470c5913617abf)
11
25
  - Fix the cli repl [`3cf8b0d`](https://github.com/bas080/sendscript/commit/3cf8b0d9b4531989fb01a53fa9ab4cb6a011c183)
package/README.md CHANGED
@@ -15,7 +15,9 @@ Write JS code that you can run on servers, browsers or other clients.
15
15
  * [Server](#server)
16
16
  * [Client](#client)
17
17
  - [Repl](#repl)
18
- - [Async/Await](#asyncawait)
18
+ - [Promises](#promises)
19
+ * [.then / .catch](#then--catch)
20
+ * [await](#await)
19
21
  - [TypeScript](#typescript)
20
22
  - [Schema and Nested Modules](#schema-and-nested-modules)
21
23
  * [Defining a Nested Module](#defining-a-nested-module)
@@ -27,8 +29,6 @@ Write JS code that you can run on servers, browsers or other clients.
27
29
  - [Formatting](#formatting)
28
30
  - [Changelog](#changelog)
29
31
  - [Dependencies](#dependencies)
30
- - [License](#license)
31
- - [Roadmap](#roadmap)
32
32
 
33
33
  <!-- tocstop -->
34
34
 
@@ -215,7 +215,20 @@ Sendscript ships with a barebones (no-dependencies) node-repl script. One can ru
215
215
 
216
216
  > Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for printingonly sendscript logs.
217
217
 
218
- ## Async/Await
218
+ ## Promises
219
+
220
+ ### .then / .catch
221
+
222
+ Supported since vs `v2.3`.
223
+
224
+ ```js
225
+ const getOrCreatePost = send(createPost(title).catch(createPost(title)))
226
+ ```
227
+
228
+ You will likely need to define better helpers that makes it safer to handle rejections and work with promises. It is however sensible to have
229
+ this basic behavior for the sendscript DSL and parser.
230
+
231
+ ### await
219
232
 
220
233
  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.
221
234
 
@@ -429,19 +442,19 @@ npm t -- report text-summary
429
442
  ```
430
443
  ```
431
444
 
432
- > sendscript@2.1.0 test
445
+ > sendscript@2.3.0 test
433
446
  > tap -R silent
434
447
 
435
448
 
436
- > sendscript@2.1.0 test
449
+ > sendscript@2.3.0 test
437
450
  > tap report text-summary
438
451
 
439
452
 
440
453
  =============================== Coverage summary ===============================
441
- Statements : 100% ( 326/326 )
442
- Branches : 100% ( 118/118 )
443
- Functions : 100% ( 21/21 )
444
- Lines : 100% ( 326/326 )
454
+ Statements : 100% ( 372/372 )
455
+ Branches : 100% ( 137/137 )
456
+ Functions : 100% ( 24/24 )
457
+ Lines : 100% ( 372/372 )
445
458
  ================================================================================
446
459
  ```
447
460
 
@@ -470,19 +483,6 @@ Check if packages are up to date on release.
470
483
  npm outdated && echo 'No outdated packages found'
471
484
  ```
472
485
  ```
473
- No outdated packages found
486
+ Package Current Wanted Latest Location Depended by
487
+ typedoc 0.28.18 0.28.19 0.28.19 node_modules/typedoc master
474
488
  ```
475
-
476
- ## License
477
-
478
- See the [LICENSE.txt][license] file for details.
479
-
480
- ## Roadmap
481
-
482
- - [ ] Support for simple lambdas to compose functions more easily.
483
-
484
- [license]:./LICENSE.txt
485
- [socket.io]:https://socket.io/
486
- [changelog]:./CHANGELOG.md
487
- [auto-changelog]:https://www.npmjs.com/package/auto-changelog
488
- [typedoc]:https://github.com/TypeStrong/typedoc
package/README.mz CHANGED
@@ -183,7 +183,20 @@ Sendscript ships with a barebones (no-dependencies) node-repl script. One can ru
183
183
 
184
184
  > Use the `DEBUG='*'` to enable all logs or `DEBUG='sendscript:*'` for printingonly sendscript logs.
185
185
 
186
- ## Async/Await
186
+ ## Promises
187
+
188
+ ### .then / .catch
189
+
190
+ Supported since vs `v2.3`.
191
+
192
+ ```js
193
+ const getOrCreatePost = send(createPost(title).catch(createPost(title)))
194
+ ```
195
+
196
+ You will likely need to define better helpers that makes it safer to handle rejections and work with promises. It is however sensible to have
197
+ this basic behavior for the sendscript DSL and parser.
198
+
199
+ ### await
187
200
 
188
201
  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.
189
202
 
package/error.mjs CHANGED
@@ -1,2 +1,3 @@
1
1
  export class SendScriptError extends Error {}
2
2
  export class SendScriptReferenceError extends SendScriptError {}
3
+ export class SendScriptSerializationError extends SendScriptError {}
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **add**(`a`, `b`): `number`
10
10
 
11
- Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/3d843187a56e23f0e70f11e904832efb1abd9cf1/example/typescript/math.ts#L1)
11
+ Defined in: [math.ts:1](https://github.com/bas080/sendscript/blob/b35009ace9761d32c0c6ef1d1d045bed2ecfe74e/example/typescript/math.ts#L1)
12
12
 
13
13
  ## Parameters
14
14
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **square**(`a`): `number`
10
10
 
11
- Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/3d843187a56e23f0e70f11e904832efb1abd9cf1/example/typescript/math.ts#L2)
11
+ Defined in: [math.ts:2](https://github.com/bas080/sendscript/blob/b35009ace9761d32c0c6ef1d1d045bed2ecfe74e/example/typescript/math.ts#L2)
12
12
 
13
13
  ## Parameters
14
14
 
package/index.test.mjs CHANGED
@@ -2,10 +2,11 @@ import { test } from 'tap'
2
2
  import Stringify from './stringify.mjs'
3
3
  import references from './references.mjs'
4
4
  import Parse from './parse.mjs'
5
+ import { SendScriptSerializationError } from './error.mjs'
5
6
 
6
7
  const order = []
7
8
 
8
- const myModule = {
9
+ const myModuleOrig = {
9
10
  nested: {
10
11
  again: {
11
12
  T: () => true
@@ -27,11 +28,13 @@ const myModule = {
27
28
  toArray: (...array) => array,
28
29
  always: (x) => () => x,
29
30
  multiply3: (a) => (b) => (c) => a * b * c,
31
+ sayHello: x => `hello ${x}`,
30
32
  map: (fn) => (array) => array.map(fn),
31
33
  filter: (pred) => (array) => array.filter(pred),
32
34
  hello: 'world',
33
35
  noop: () => {},
34
36
  resolve: (x) => Promise.resolve(x),
37
+ reject: x => Promise.reject(x),
35
38
  asyncFn: async () => 'my-async-function',
36
39
  instanceOf: (x, t) => x instanceof t,
37
40
  asyncAdd: async (a, b) => a + b,
@@ -45,7 +48,10 @@ const myModule = {
45
48
  Promise
46
49
  }
47
50
 
48
- const schema = Object.keys(myModule)
51
+ // We want to support prototype chain when parsing.
52
+ const myModule = Object.create(myModuleOrig)
53
+ const schema = Object.keys(myModuleOrig)
54
+
49
55
  schema.push(['nested', [
50
56
  ['again', ['T']]
51
57
  ]])
@@ -63,6 +69,7 @@ test('should evaluate basic expressions correctly', async (t) => {
63
69
  aPromise,
64
70
  asyncAdd,
65
71
  resolve,
72
+ reject,
66
73
  delay,
67
74
  delayedIdentity,
68
75
  noop,
@@ -77,9 +84,27 @@ test('should evaluate basic expressions correctly', async (t) => {
77
84
  identity,
78
85
  always,
79
86
  multiply3,
87
+ sayHello,
80
88
  nested
81
89
  } = api
82
90
 
91
+ t.test('supports .then', async t => {
92
+ t.rejects(run(reject('error')))
93
+
94
+ t.equal(await run(reject('error').catch(sayHello)), 'hello error')
95
+ t.equal(await run(reject('error').then(null, sayHello)), 'hello error')
96
+
97
+ t.equal(await run(resolve('value').then(sayHello)), 'hello value')
98
+ t.equal(await run(resolve('value').then(sayHello, noop)), 'hello value')
99
+
100
+ t.end()
101
+ })
102
+
103
+ t.test('resolve and reject callback works for .then', async t => {
104
+ t.equal(await run(resolve(42).then(multiply3(1)(2))), 84)
105
+ t.end()
106
+ })
107
+
83
108
  t.test('await evaluation order matches JS semantics', async (t) => {
84
109
  const lorder = []
85
110
 
@@ -225,12 +250,30 @@ test('should evaluate basic expressions correctly', async (t) => {
225
250
  t.end()
226
251
  })
227
252
 
253
+ t.test('throws if stringify returns undefined', t => {
254
+ // This is to prevent accidentally sending payloads that make little sense.
255
+
256
+ // Check if it throw the expected error type.
257
+ try {
258
+ run(undefined)
259
+ } catch (error) {
260
+ t.ok(error instanceof SendScriptSerializationError)
261
+ }
262
+
263
+ t.throws(() => run(undefined))
264
+ t.throws(() => run(new Set()))
265
+ t.throws(() => run(identity(undefined)))
266
+ t.throws(() => run(() => {}))
267
+
268
+ t.end()
269
+ })
270
+
228
271
  t.test('basic types and identity', (t) => {
229
272
  t.equal(run(identity(null)), null)
230
- t.equal(run(identity(undefined)), null)
231
273
  t.equal(run(noop()), undefined)
232
274
  t.equal(run(identity(1)), 1)
233
275
  t.strictSame(run(identity([])), [])
276
+ t.strictSame(run(identity({})), {})
234
277
  t.strictSame(run(identity([identity(1), 2, 3])), [1, 2, 3])
235
278
  t.strictSame(run(always('hello')()), 'hello')
236
279
  t.end()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sendscript",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Blur the line between server and client code.",
5
5
  "module": true,
6
6
  "main": "index.mjs",
package/parse.mjs CHANGED
@@ -32,8 +32,14 @@ const isPlainObject = (value) => {
32
32
  return proto === Object.prototype || proto === null
33
33
  }
34
34
 
35
+ const spy = (type, fn) => (...args) => {
36
+ const value = fn(...args)
37
+ debug(type, args, ' => ', value)
38
+ return value
39
+ }
40
+
35
41
  // Recursively resolve awaited values in a parsed tree
36
- const evaluate = (value, awaits = []) => {
42
+ const evaluate = spy('eval', (value, awaits = []) => {
37
43
  if (value === undefinedSentinel) return undefined
38
44
 
39
45
  if (Array.isArray(value)) {
@@ -48,6 +54,11 @@ const evaluate = (value, awaits = []) => {
48
54
  return awaits[index]
49
55
  }
50
56
 
57
+ if (operator === 'then') {
58
+ const [v, onResolve, onReject] = rest
59
+ return evaluate(v, awaits).then(evaluate(onResolve, awaits), evaluate(onReject, awaits))
60
+ }
61
+
51
62
  if (operator === 'call') {
52
63
  // Step 1: evaluate the function itself
53
64
  let [fn, args] = rest
@@ -86,13 +97,7 @@ const evaluate = (value, awaits = []) => {
86
97
  }
87
98
 
88
99
  return value
89
- }
90
-
91
- const spy = (fn) => (...args) => {
92
- const value = fn(...args)
93
- debug(args, ' => ', value)
94
- return value
95
- }
100
+ })
96
101
 
97
102
  const defaultLeafDeserializer = (text) => JSON.parse(text)
98
103
 
@@ -105,7 +110,7 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => {
105
110
 
106
111
  // Creates the list of awaits that will resolve in order
107
112
  // and also deserializes the leaves.
108
- const reviver = spy((key, value) => {
113
+ const reviver = spy('revive', (key, value) => {
109
114
  if (value === null) return value
110
115
 
111
116
  if (!Array.isArray(value)) {
package/references.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { awaitSymbol, call, ref } from './symbol.mjs'
1
+ import { awaitSymbol, call, ref, then, referenceSymbol } from './symbol.mjs'
2
2
 
3
3
  function instrument (path) {
4
4
  function reference (...args) {
@@ -6,7 +6,6 @@ function instrument (path) {
6
6
 
7
7
  called.toJSON = () => ({
8
8
  [call]: call,
9
- call: true,
10
9
  ref: reference,
11
10
  args
12
11
  })
@@ -14,13 +13,35 @@ function instrument (path) {
14
13
  return called
15
14
  }
16
15
 
17
- reference.then = (resolve) => {
16
+ function dotThen (resolve, reject) {
17
+ const node = instrument(path)
18
+
19
+ node.toJSON = () => ({
20
+ [then]: then,
21
+ ref: reference,
22
+ resolve,
23
+ reject
24
+ })
25
+
26
+ return node
27
+ }
28
+
29
+ reference.catch = (reject) => {
30
+ return dotThen(null, reject)
31
+ }
32
+
33
+ reference.then = (resolve, reject) => {
34
+ // That is how we know if it is an await or a .then call.
35
+ if (resolve?.[referenceSymbol] || reject?.[referenceSymbol]) {
36
+ return dotThen(resolve, reject)
37
+ }
38
+
18
39
  const awaited = instrument(path)
40
+ // Prevent infinite recur
19
41
  delete awaited.then
20
42
 
21
43
  awaited.toJSON = () => ({
22
44
  [awaitSymbol]: awaitSymbol,
23
- await: true,
24
45
  ref: reference
25
46
  })
26
47
 
@@ -29,10 +50,11 @@ function instrument (path) {
29
50
 
30
51
  reference.toJSON = () => ({
31
52
  [ref]: ref,
32
- reference: true,
33
53
  path
34
54
  })
35
55
 
56
+ reference[referenceSymbol] = referenceSymbol
57
+
36
58
  return reference
37
59
  }
38
60
 
package/stringify.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import Debug from './debug.mjs'
2
+ import { SendScriptSerializationError } from './error.mjs'
2
3
  import {
3
4
  awaitSymbol,
5
+ then,
4
6
  call,
5
7
  ref
6
8
  } from './symbol.mjs'
@@ -42,6 +44,10 @@ function transformValue (value, leafSerializer) {
42
44
  return ['await', transformValue(value.ref, leafSerializer)]
43
45
  }
44
46
 
47
+ if (value && value[then]) {
48
+ return ['then', transformValue(value.ref, leafSerializer), transformValue(value.resolve || null, leafSerializer), transformValue(value.reject || null, leafSerializer)]
49
+ }
50
+
45
51
  // Handle arrays: quote keyword operators, transform other arrays recursively
46
52
  if (Array.isArray(value)) {
47
53
  const [operator, ...rest] = value
@@ -69,7 +75,17 @@ function transformValue (value, leafSerializer) {
69
75
  return ['leaf', leafSerializer(value)]
70
76
  }
71
77
 
72
- export default function stringify (leafSerializer = JSON.stringify) {
78
+ function strictStringify (x) {
79
+ const typeOf = typeof x
80
+
81
+ if (typeOf === 'object' || typeOf === 'function' || x === undefined) {
82
+ throw new SendScriptSerializationError(`Cannot and should not attempt to serialize ${x}`)
83
+ }
84
+
85
+ return JSON.stringify(x)
86
+ }
87
+
88
+ export default function stringify (leafSerializer = strictStringify) {
73
89
  function stringify (program) {
74
90
  return JSON.stringify(transformValue(program, leafSerializer))
75
91
  }
package/symbol.mjs CHANGED
@@ -1,3 +1,5 @@
1
1
  export const ref = Symbol('ref')
2
2
  export const call = Symbol('call')
3
3
  export const awaitSymbol = Symbol('await')
4
+ export const then = Symbol('then')
5
+ export const referenceSymbol = Symbol('referenceSymbol')