sendscript 2.4.3 → 2.5.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,15 @@ 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.0](https://github.com/bas080/sendscript/compare/v2.4.3...v2.5.0)
8
+
9
+ - Clarify native await behavior in README [`a26f1b3`](https://github.com/bas080/sendscript/commit/a26f1b3c204390690eda7950c967d31ee55aa353)
10
+ - Allow passing onAwait callback to references make function [`8fdb1c4`](https://github.com/bas080/sendscript/commit/8fdb1c4bfa0682fabdd9d27ca075cd1e0cfaedf7)
11
+
7
12
  #### [v2.4.3](https://github.com/bas080/sendscript/compare/v2.4.2...v2.4.3)
8
13
 
14
+ > 17 August 2026
15
+
9
16
  - Improve TypeScript section with comprehensive examples and explanations [`a3c5867`](https://github.com/bas080/sendscript/commit/a3c5867f835bbcde1e802e75c80ca6f02dd4f0e8)
10
17
 
11
18
  #### [v2.4.2](https://github.com/bas080/sendscript/compare/v2.4.1...v2.4.2)
package/README.md CHANGED
@@ -306,27 +306,40 @@ behavior for the sendscript DSL and parser.
306
306
 
307
307
  ### await
308
308
 
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.
309
+ By default, `await api.someMethod(...)` still creates a SendScript await stub
310
+ and keeps the result serializable with `stringify()`. It does not automatically
311
+ send anything over the network.
312
312
 
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.
313
+ ```js
314
+ const api = references(['add'])
315
+ const program = api.add(1, 2)
316
+
317
+ Stringify()(program)
318
+ // => "[\"call\",[\"ref\",\"add\"],[1,2]]"
319
+ ```
320
+
321
+ If you want native `await` to cross the transport boundary, pass an `onAwait`
322
+ handler when creating the references:
316
323
 
317
324
  ```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
- }
325
+ import Stringify from 'sendscript/stringify.mjs'
326
+ import references from 'sendscript/references.mjs'
324
327
 
325
- const result = await send(program)
328
+ const stringify = Stringify()
329
+
330
+ const api = references(['add'], (program) => {
331
+ return fetch('/api', {
332
+ method: 'POST',
333
+ headers: { 'content-type': 'application/json' },
334
+ body: stringify(program),
335
+ }).then((response) => response.json())
336
+ })
337
+
338
+ const result = await api.add(1, 2)
326
339
  ```
327
340
 
328
- This operation is done in a single round-trip. The result is an object with the
329
- defined properties and returned values.
341
+ The callback receives the generated SendScript program, and you decide how it is
342
+ transported or executed.
330
343
 
331
344
  ## TypeScript
332
345
 
@@ -601,36 +614,21 @@ code to create the AST.
601
614
 
602
615
  ### Callbacks
603
616
 
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.
617
+ SendScript supports basic callback-style usage, such as passing a function as an
618
+ argument or using a callback to flip arguments into the generated program.
616
619
 
617
620
  ```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.
621
+ const api = references(['map', 'add'])
625
622
 
626
- ```js
627
- await updateUser(id, merge(await getUser(id), { ...newValues }))
623
+ const result = api.map((value) => api.add(value, 1))([1, 2, 3])
628
624
  ```
629
625
 
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.
626
+ This is meant for composing SendScript programs, not for executing arbitrary
627
+ client-side logic at runtime.
628
+
629
+ > [!WARNING] Mixing client and server functions can be confusing, because the
630
+ > callback may be used to build a program rather than execute immediately. Keep
631
+ > callback logic simple and deterministic.
634
632
 
635
633
  ### Error handling
636
634
 
@@ -651,10 +649,10 @@ npm t -- report text-summary
651
649
  ```
652
650
 
653
651
  =============================== Coverage summary ===============================
654
- Statements : 100% ( 512/512 )
655
- Branches : 100% ( 147/147 )
652
+ Statements : 100% ( 516/516 )
653
+ Branches : 100% ( 157/157 )
656
654
  Functions : 100% ( 23/23 )
657
- Lines : 100% ( 512/512 )
655
+ Lines : 100% ( 516/516 )
658
656
  ================================================================================
659
657
  ```
660
658
 
package/README.mz CHANGED
@@ -261,27 +261,40 @@ behavior for the sendscript DSL and parser.
261
261
 
262
262
  ### await
263
263
 
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.
264
+ By default, `await api.someMethod(...)` still creates a SendScript await stub
265
+ and keeps the result serializable with `stringify()`. It does not automatically
266
+ send anything over the network.
267
267
 
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.
268
+ ```js
269
+ const api = references(['add'])
270
+ const program = api.add(1, 2)
271
+
272
+ Stringify()(program)
273
+ // => "[\"call\",[\"ref\",\"add\"],[1,2]]"
274
+ ```
275
+
276
+ If you want native `await` to cross the transport boundary, pass an `onAwait`
277
+ handler when creating the references:
271
278
 
272
279
  ```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
- }
280
+ import Stringify from 'sendscript/stringify.mjs'
281
+ import references from 'sendscript/references.mjs'
279
282
 
280
- const result = await send(program)
283
+ const stringify = Stringify()
284
+
285
+ const api = references(['add'], (program) => {
286
+ return fetch('/api', {
287
+ method: 'POST',
288
+ headers: { 'content-type': 'application/json' },
289
+ body: stringify(program),
290
+ }).then((response) => response.json())
291
+ })
292
+
293
+ const result = await api.add(1, 2)
281
294
  ```
282
295
 
283
- This operation is done in a single round-trip. The result is an object with the
284
- defined properties and returned values.
296
+ The callback receives the generated SendScript program, and you decide how it is
297
+ transported or executed.
285
298
 
286
299
  ## TypeScript
287
300
 
@@ -496,36 +509,21 @@ code to create the AST.
496
509
 
497
510
  ### Callbacks
498
511
 
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.
512
+ SendScript supports basic callback-style usage, such as passing a function as an
513
+ argument or using a callback to flip arguments into the generated program.
511
514
 
512
515
  ```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.
516
+ const api = references(['map', 'add'])
520
517
 
521
- ```js
522
- await updateUser(id, merge(await getUser(id), { ...newValues }))
518
+ const result = api.map((value) => api.add(value, 1))([1, 2, 3])
523
519
  ```
524
520
 
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.
521
+ This is meant for composing SendScript programs, not for executing arbitrary
522
+ client-side logic at runtime.
523
+
524
+ > [!WARNING] Mixing client and server functions can be confusing, because the
525
+ > callback may be used to build a program rather than execute immediately. Keep
526
+ > callback logic simple and deterministic.
529
527
 
530
528
  ### Error handling
531
529
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sendscript",
3
- "version": "2.4.3",
3
+ "version": "2.5.0",
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
+ })