sendscript 2.4.3 → 2.5.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
+ #### [v2.5.1](https://github.com/bas080/sendscript/compare/v2.5.0...v2.5.1)
8
+
9
+ - Update tap and typedoc-plugin-markdown to latest [`f126401`](https://github.com/bas080/sendscript/commit/f126401ac015ba7c6a21b2c89f93b416d49765fb)
10
+ - Express support for Palestine [`c6996c7`](https://github.com/bas080/sendscript/commit/c6996c7f440d1cd6a4d5ca9c5c9d5d856884a472)
11
+
12
+ #### [v2.5.0](https://github.com/bas080/sendscript/compare/v2.4.3...v2.5.0)
13
+
14
+ > 17 August 2026
15
+
16
+ - Clarify native await behavior in README [`a26f1b3`](https://github.com/bas080/sendscript/commit/a26f1b3c204390690eda7950c967d31ee55aa353)
17
+ - Allow passing onAwait callback to references make function [`8fdb1c4`](https://github.com/bas080/sendscript/commit/8fdb1c4bfa0682fabdd9d27ca075cd1e0cfaedf7)
18
+
7
19
  #### [v2.4.3](https://github.com/bas080/sendscript/compare/v2.4.2...v2.4.3)
8
20
 
21
+ > 17 August 2026
22
+
9
23
  - Improve TypeScript section with comprehensive examples and explanations [`a3c5867`](https://github.com/bas080/sendscript/commit/a3c5867f835bbcde1e802e75c80ca6f02dd4f0e8)
10
24
 
11
25
  #### [v2.4.2](https://github.com/bas080/sendscript/compare/v2.4.1...v2.4.2)
package/README.md CHANGED
@@ -6,6 +6,7 @@ Serialize and execute composable JavaScript function calls with JSON.
6
6
  [![100% Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square)](#tests)
7
7
  [![Standard Code Style](https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square)](https://standardjs.com)
8
8
  [![License](https://img.shields.io/npm/l/sendscript?color=brightgreen&style=flat-square)](./LICENSE.txt)
9
+ [![Stand with Palestine](https://img.shields.io/badge/🇵🇸%20%20Stand%20With%20Palestine-007A3D?style=flat-square&color=brightgreen)](https://www.islamic-relief.org.uk/giving/appeals/palestine/)
9
10
 
10
11
  ## Features
11
12
 
@@ -306,27 +307,40 @@ behavior for the sendscript DSL and parser.
306
307
 
307
308
  ### await
308
309
 
309
- SendScript supports async/await seamlessly within a single request. This avoids
310
- the performance pitfalls of waterfall-style messaging, which can be especially
311
- slow on high-latency networks.
310
+ By default, `await api.someMethod(...)` still creates a SendScript await stub
311
+ and keeps the result serializable with `stringify()`. It does not automatically
312
+ send anything over the network.
312
313
 
313
- While it's possible to chain promises manually or use utility functions, native
314
- async/await support makes your code more readable, modern, and easier to reason
315
- about aligning SendScript with today’s JavaScript best practices.
314
+ ```js
315
+ const api = references(['add'])
316
+ const program = api.add(1, 2)
317
+
318
+ Stringify()(program)
319
+ // => "[\"call\",[\"ref\",\"add\"],[1,2]]"
320
+ ```
321
+
322
+ If you want native `await` to cross the transport boundary, pass an `onAwait`
323
+ handler when creating the references:
316
324
 
317
325
  ```js
318
- const userId = 'user-123'
319
- const program = {
320
- unread: await fetchUnreadMessages(userId),
321
- emptyTrash: await emptyTrash(userId),
322
- archived: await archiveMessages(selectMessages({ old: true })),
323
- }
326
+ import Stringify from 'sendscript/stringify.mjs'
327
+ import references from 'sendscript/references.mjs'
324
328
 
325
- const result = await send(program)
329
+ const stringify = Stringify()
330
+
331
+ const api = references(['add'], (program) => {
332
+ return fetch('/api', {
333
+ method: 'POST',
334
+ headers: { 'content-type': 'application/json' },
335
+ body: stringify(program),
336
+ }).then((response) => response.json())
337
+ })
338
+
339
+ const result = await api.add(1, 2)
326
340
  ```
327
341
 
328
- This operation is done in a single round-trip. The result is an object with the
329
- defined properties and returned values.
342
+ The callback receives the generated SendScript program, and you decide how it is
343
+ transported or executed.
330
344
 
331
345
  ## TypeScript
332
346
 
@@ -601,36 +615,21 @@ code to create the AST.
601
615
 
602
616
  ### Callbacks
603
617
 
604
- Although it is possible to mix client and server functions, it works very
605
- different to ordinary functions. Client functions can be used but should be seen
606
- as a templating tool to make sendscript programs; just like one would use
607
- JavaScript with react templates. `items.map(deleteItem)` would return an array
608
- of sendscript function calls which can be given to sendscript's parse.
609
-
610
- Client functions cannot be called by sendscript functions (as of yet) since we
611
- cannot serialize client functions. No work has been done to have the server send
612
- back intermediate values to perform client function calls or by performing
613
- smaller sendscript program payloads that are passed to the client. Very
614
- interesting stuff to look into. You can achieve this now but it looks less clean
615
- because you have to do `send` calls which is a bit manual.
618
+ SendScript supports basic callback-style usage, such as passing a function as an
619
+ argument or using a callback to flip arguments into the generated program.
616
620
 
617
621
  ```js
618
- await send(updateUser(id, merge(await send(getUser(id)), { ...newValues })))
619
- ```
620
-
621
- It might be interesting to allow configuration to create references that will
622
- trigger a send whenever await is called. That would remove the ability to create
623
- a single payload whenever using await. You can then write the above in the
624
- following manner.
622
+ const api = references(['map', 'add'])
625
623
 
626
- ```js
627
- await updateUser(id, merge(await getUser(id), { ...newValues }))
624
+ const result = api.map((value) => api.add(value, 1))([1, 2, 3])
628
625
  ```
629
626
 
630
- Possible footgun is that updateUser is only performed when awaited (.then is
631
- called). This lazy behavior can trip users. This footgun could be resolved by
632
- checking if any outstanding work exists at the end of a step or the beginning of
633
- a new step.
627
+ This is meant for composing SendScript programs, not for executing arbitrary
628
+ client-side logic at runtime.
629
+
630
+ > [!WARNING] Mixing client and server functions can be confusing, because the
631
+ > callback may be used to build a program rather than execute immediately. Keep
632
+ > callback logic simple and deterministic.
634
633
 
635
634
  ### Error handling
636
635
 
@@ -650,11 +649,19 @@ npm t -- report text-summary
650
649
  ```
651
650
  ```
652
651
 
652
+ > sendscript@2.5.1 test
653
+ > tap -R silent
654
+
655
+
656
+ > sendscript@2.5.1 test
657
+ > tap report text-summary
658
+
659
+
653
660
  =============================== Coverage summary ===============================
654
- Statements : 100% ( 512/512 )
655
- Branches : 100% ( 147/147 )
661
+ Statements : 100% ( 516/516 )
662
+ Branches : 100% ( 157/157 )
656
663
  Functions : 100% ( 23/23 )
657
- Lines : 100% ( 512/512 )
664
+ Lines : 100% ( 516/516 )
658
665
  ================================================================================
659
666
  ```
660
667
 
package/README.mz CHANGED
@@ -6,6 +6,7 @@ Serialize and execute composable JavaScript function calls with JSON.
6
6
  [![100% Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square)](#tests)
7
7
  [![Standard Code Style](https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square)](https://standardjs.com)
8
8
  [![License](https://img.shields.io/npm/l/sendscript?color=brightgreen&style=flat-square)](./LICENSE.txt)
9
+ [![Stand with Palestine](https://img.shields.io/badge/🇵🇸%20%20Stand%20With%20Palestine-007A3D?style=flat-square&color=brightgreen)](https://www.islamic-relief.org.uk/giving/appeals/palestine/)
9
10
 
10
11
  ## Features
11
12
 
@@ -261,27 +262,40 @@ behavior for the sendscript DSL and parser.
261
262
 
262
263
  ### await
263
264
 
264
- SendScript supports async/await seamlessly within a single request. This avoids
265
- the performance pitfalls of waterfall-style messaging, which can be especially
266
- slow on high-latency networks.
265
+ By default, `await api.someMethod(...)` still creates a SendScript await stub
266
+ and keeps the result serializable with `stringify()`. It does not automatically
267
+ send anything over the network.
267
268
 
268
- While it's possible to chain promises manually or use utility functions, native
269
- async/await support makes your code more readable, modern, and easier to reason
270
- about aligning SendScript with today’s JavaScript best practices.
269
+ ```js
270
+ const api = references(['add'])
271
+ const program = api.add(1, 2)
272
+
273
+ Stringify()(program)
274
+ // => "[\"call\",[\"ref\",\"add\"],[1,2]]"
275
+ ```
276
+
277
+ If you want native `await` to cross the transport boundary, pass an `onAwait`
278
+ handler when creating the references:
271
279
 
272
280
  ```js
273
- const userId = 'user-123'
274
- const program = {
275
- unread: await fetchUnreadMessages(userId),
276
- emptyTrash: await emptyTrash(userId),
277
- archived: await archiveMessages(selectMessages({ old: true })),
278
- }
281
+ import Stringify from 'sendscript/stringify.mjs'
282
+ import references from 'sendscript/references.mjs'
279
283
 
280
- const result = await send(program)
284
+ const stringify = Stringify()
285
+
286
+ const api = references(['add'], (program) => {
287
+ return fetch('/api', {
288
+ method: 'POST',
289
+ headers: { 'content-type': 'application/json' },
290
+ body: stringify(program),
291
+ }).then((response) => response.json())
292
+ })
293
+
294
+ const result = await api.add(1, 2)
281
295
  ```
282
296
 
283
- This operation is done in a single round-trip. The result is an object with the
284
- defined properties and returned values.
297
+ The callback receives the generated SendScript program, and you decide how it is
298
+ transported or executed.
285
299
 
286
300
  ## TypeScript
287
301
 
@@ -496,36 +510,21 @@ code to create the AST.
496
510
 
497
511
  ### Callbacks
498
512
 
499
- Although it is possible to mix client and server functions, it works very
500
- different to ordinary functions. Client functions can be used but should be seen
501
- as a templating tool to make sendscript programs; just like one would use
502
- JavaScript with react templates. `items.map(deleteItem)` would return an array
503
- of sendscript function calls which can be given to sendscript's parse.
504
-
505
- Client functions cannot be called by sendscript functions (as of yet) since we
506
- cannot serialize client functions. No work has been done to have the server send
507
- back intermediate values to perform client function calls or by performing
508
- smaller sendscript program payloads that are passed to the client. Very
509
- interesting stuff to look into. You can achieve this now but it looks less clean
510
- because you have to do `send` calls which is a bit manual.
513
+ SendScript supports basic callback-style usage, such as passing a function as an
514
+ argument or using a callback to flip arguments into the generated program.
511
515
 
512
516
  ```js
513
- await send(updateUser(id, merge(await send(getUser(id)), { ...newValues })))
514
- ```
515
-
516
- It might be interesting to allow configuration to create references that will
517
- trigger a send whenever await is called. That would remove the ability to create
518
- a single payload whenever using await. You can then write the above in the
519
- following manner.
517
+ const api = references(['map', 'add'])
520
518
 
521
- ```js
522
- await updateUser(id, merge(await getUser(id), { ...newValues }))
519
+ const result = api.map((value) => api.add(value, 1))([1, 2, 3])
523
520
  ```
524
521
 
525
- Possible footgun is that updateUser is only performed when awaited (.then is
526
- called). This lazy behavior can trip users. This footgun could be resolved by
527
- checking if any outstanding work exists at the end of a step or the beginning of
528
- a new step.
522
+ This is meant for composing SendScript programs, not for executing arbitrary
523
+ client-side logic at runtime.
524
+
525
+ > [!WARNING] Mixing client and server functions can be confusing, because the
526
+ > callback may be used to build a program rather than execute immediately. Keep
527
+ > callback logic simple and deterministic.
529
528
 
530
529
  ### Error handling
531
530
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sendscript",
3
- "version": "2.4.3",
3
+ "version": "2.5.1",
4
4
  "description": "Blur the line between server and client code.",
5
5
  "module": true,
6
6
  "main": "index.mjs",
package/references.mjs CHANGED
@@ -7,7 +7,7 @@ import { awaitSymbol, call, ref, then, referenceSymbol } from './symbol.mjs'
7
7
  * @param {Array<string>} path - Path representing the function location in schema.
8
8
  * @returns {Function} Reference function with attached control methods (.then, .catch, toJSON).
9
9
  */
10
- function instrument (path) {
10
+ function instrument (path, onAwait = null) {
11
11
  /**
12
12
  * Creates a callable reference invocation.
13
13
  *
@@ -15,7 +15,7 @@ function instrument (path) {
15
15
  * @returns {Function} New instrumented reference node.
16
16
  */
17
17
  function reference (...args) {
18
- const called = instrument(path)
18
+ const called = instrument(path, onAwait)
19
19
 
20
20
  called.toJSON = () => ({
21
21
  [call]: call,
@@ -71,7 +71,7 @@ function instrument (path) {
71
71
  return dotThen(resolve, reject)
72
72
  }
73
73
 
74
- const awaited = instrument(path)
74
+ const awaited = instrument(path, onAwait)
75
75
  delete awaited.then
76
76
 
77
77
  awaited.toJSON = () => ({
@@ -79,6 +79,10 @@ function instrument (path) {
79
79
  ref: reference
80
80
  })
81
81
 
82
+ if (typeof onAwait === 'function') {
83
+ return resolve(onAwait(awaited))
84
+ }
85
+
82
86
  return resolve(awaited)
83
87
  }
84
88
 
@@ -106,15 +110,15 @@ function instrument (path) {
106
110
  * @throws {Error} If schema format is invalid
107
111
  * @public
108
112
  */
109
- export default function References (schema, parentPath = []) {
113
+ export default function References (schema, onAwait = null, parentPath = []) {
110
114
  return schema.reduce((acc, item) => {
111
115
  if (typeof item === 'string') {
112
- acc[item] = instrument([...parentPath, item])
116
+ acc[item] = instrument([...parentPath, item], onAwait)
113
117
  } else if (Array.isArray(item)) {
114
118
  const [name, children] = item
115
119
 
116
120
  if (Array.isArray(children)) {
117
- acc[name] = References(children, [...parentPath, name])
121
+ acc[name] = References(children, onAwait, [...parentPath, name])
118
122
  } else {
119
123
  throw new Error(`Expected children array for namespace "${name}"`)
120
124
  }
@@ -1,8 +1,62 @@
1
1
  import { test } from 'tap'
2
2
  import references from './references.mjs'
3
+ import Stringify from './stringify.mjs'
3
4
 
4
5
  test('invalid uses of references', t => {
5
6
  t.throws(() => references([['a']]))
6
7
  t.throws(() => references([{}]))
7
8
  t.end()
8
9
  })
10
+
11
+ test('native await hook is optional and backwards compatible', async t => {
12
+ const schema = ['add', 'square', 'identity', ['nested', ['value']]]
13
+ const stringify = Stringify()
14
+
15
+ const defaultApi = references(schema)
16
+ const defaultProgram = defaultApi.add(1, 2)
17
+
18
+ t.doesNotThrow(() => stringify(defaultProgram))
19
+ t.same(JSON.parse(stringify(defaultProgram))[0], 'call')
20
+
21
+ const calls = []
22
+ const api = references(schema, (program) => {
23
+ calls.push(program)
24
+ return 42
25
+ })
26
+
27
+ t.equal(await api.add(1, 2), 42)
28
+ t.equal(calls.length, 1)
29
+ t.equal(typeof calls[0], 'function')
30
+
31
+ const asyncApi = references(schema, async () => {
32
+ return await Promise.resolve(17)
33
+ })
34
+
35
+ t.equal(await asyncApi.identity(9), 17)
36
+
37
+ const rejectedApi = references(schema, async () => {
38
+ throw new Error('boom')
39
+ })
40
+
41
+ await t.rejects((async () => {
42
+ await rejectedApi.identity(9)
43
+ })(), { message: 'boom' })
44
+
45
+ const thenApi = references(schema)
46
+ const thenProgram = thenApi.add(1, 2).then(thenApi.square)
47
+ t.same(JSON.parse(stringify(thenProgram))[0], 'then')
48
+
49
+ const nestedApi = references([['outer', ['inner']]], (program) => program)
50
+ t.equal(typeof nestedApi.outer.inner, 'function')
51
+ t.same(JSON.parse(stringify(nestedApi.outer.inner(1)))[0], 'call')
52
+
53
+ const onAwaitApi = references(['add'], (program) => {
54
+ const serialized = stringify(program)
55
+ t.same(JSON.parse(serialized), ['await', ['call', ['ref', 'add'], [['leaf', '1'], ['leaf', '2']]]])
56
+ return 99
57
+ })
58
+
59
+ t.equal(await onAwaitApi.add(1, 2), 99)
60
+
61
+ t.end()
62
+ })