sendscript 2.4.2 → 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 +13 -0
- package/README.md +133 -63
- package/README.mz +82 -56
- package/example/typescript/client.ts +16 -3
- package/example/typescript/math.client.ts +10 -1
- package/example/typescript/math.ts +9 -2
- package/package.json +1 -1
- package/references.mjs +10 -6
- package/references.test.mjs +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,21 @@ 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
|
+
|
|
12
|
+
#### [v2.4.3](https://github.com/bas080/sendscript/compare/v2.4.2...v2.4.3)
|
|
13
|
+
|
|
14
|
+
> 17 August 2026
|
|
15
|
+
|
|
16
|
+
- Improve TypeScript section with comprehensive examples and explanations [`a3c5867`](https://github.com/bas080/sendscript/commit/a3c5867f835bbcde1e802e75c80ca6f02dd4f0e8)
|
|
17
|
+
|
|
7
18
|
#### [v2.4.2](https://github.com/bas080/sendscript/compare/v2.4.1...v2.4.2)
|
|
8
19
|
|
|
20
|
+
> 17 August 2026
|
|
21
|
+
|
|
9
22
|
- Update tap and typedoc [`46421de`](https://github.com/bas080/sendscript/commit/46421de16533066ee13c91ea75505b1ecdd548f0)
|
|
10
23
|
- Remove the unused curry helper [`9dcbb48`](https://github.com/bas080/sendscript/commit/9dcbb485b455937070c65359ecde8236c128bee8)
|
|
11
24
|
- Remove duplicate Promises section with outdated async/await information [`5f43ddb`](https://github.com/bas080/sendscript/commit/5f43ddb91ddb2d77227ddd6a570807f874230ea1)
|
package/README.md
CHANGED
|
@@ -36,6 +36,10 @@ Serialize and execute composable JavaScript function calls with JSON.
|
|
|
36
36
|
* [.then / .catch](#then--catch)
|
|
37
37
|
* [await](#await)
|
|
38
38
|
- [TypeScript](#typescript)
|
|
39
|
+
* [Server-Side Module](#server-side-module)
|
|
40
|
+
* [Client-Side Type Stub](#client-side-type-stub)
|
|
41
|
+
* [Using Typed References](#using-typed-references)
|
|
42
|
+
* [Generating API Documentation](#generating-api-documentation)
|
|
39
43
|
- [Schema and Nested Modules](#schema-and-nested-modules)
|
|
40
44
|
* [Defining a Nested Module](#defining-a-nested-module)
|
|
41
45
|
- [Validation (using Zod)](#validation-using-zod)
|
|
@@ -302,61 +306,121 @@ behavior for the sendscript DSL and parser.
|
|
|
302
306
|
|
|
303
307
|
### await
|
|
304
308
|
|
|
305
|
-
|
|
306
|
-
the
|
|
307
|
-
|
|
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.
|
|
308
312
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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:
|
|
312
323
|
|
|
313
324
|
```js
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
unread: await fetchUnreadMessages(userId),
|
|
317
|
-
emptyTrash: await emptyTrash(userId),
|
|
318
|
-
archived: await archiveMessages(selectMessages({ old: true })),
|
|
319
|
-
}
|
|
325
|
+
import Stringify from 'sendscript/stringify.mjs'
|
|
326
|
+
import references from 'sendscript/references.mjs'
|
|
320
327
|
|
|
321
|
-
const
|
|
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)
|
|
322
339
|
```
|
|
323
340
|
|
|
324
|
-
|
|
325
|
-
|
|
341
|
+
The callback receives the generated SendScript program, and you decide how it is
|
|
342
|
+
transported or executed.
|
|
326
343
|
|
|
327
344
|
## TypeScript
|
|
328
345
|
|
|
329
|
-
|
|
346
|
+
Using SendScript with TypeScript enables **type-safe client-side code**. Your
|
|
347
|
+
client can have full IDE autocomplete and compile-time type checking when
|
|
348
|
+
calling server functions.
|
|
330
349
|
|
|
331
|
-
|
|
332
|
-
coding.
|
|
333
|
-
2. You can use tools like [typedoc][typedoc] to generate docs from your types to
|
|
334
|
-
share with consumers of your API.
|
|
335
|
-
3. You can use the types of the module to coerce your client to adopt the
|
|
336
|
-
module's type.
|
|
350
|
+
### Server-Side Module
|
|
337
351
|
|
|
338
|
-
|
|
352
|
+
Define your API as a TypeScript module on the server:
|
|
339
353
|
|
|
340
354
|
```bash
|
|
341
355
|
cat ./example/typescript/math.ts
|
|
342
356
|
```
|
|
343
357
|
```ts
|
|
344
|
-
|
|
345
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Server-side math module with typed functions
|
|
360
|
+
* These functions will be called from the client through SendScript
|
|
361
|
+
*/
|
|
362
|
+
|
|
363
|
+
export const add = (a: number, b: number): number => a + b
|
|
364
|
+
|
|
365
|
+
export const square = (a: number): number => a * a
|
|
366
|
+
|
|
346
367
|
```
|
|
347
368
|
|
|
348
|
-
|
|
369
|
+
### Client-Side Type Stub
|
|
370
|
+
|
|
371
|
+
Create a client-side file that mirrors your server types using the `as typeof`
|
|
372
|
+
casting pattern:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
cat ./example/typescript/math.client.ts
|
|
376
|
+
```
|
|
377
|
+
```ts
|
|
378
|
+
/**
|
|
379
|
+
* Client-side type-safe stubs for the math API
|
|
380
|
+
*
|
|
381
|
+
* This file creates typed references that mirror the server's functions.
|
|
382
|
+
* The 'as typeof mathTypes' cast gives us full TypeScript support and IDE autocomplete.
|
|
383
|
+
*/
|
|
384
|
+
|
|
385
|
+
import type * as mathTypes from './math.ts'
|
|
386
|
+
import references from 'sendscript/references.mjs'
|
|
387
|
+
|
|
388
|
+
// Create type-safe stubs - this tells TypeScript that 'add' and 'square'
|
|
389
|
+
// have the same signatures as the server functions
|
|
390
|
+
export default references(['add', 'square']) as typeof mathTypes
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
The `as typeof mathTypes` type assertion gives your client-side references the
|
|
395
|
+
exact same types as your server module. This means:
|
|
396
|
+
|
|
397
|
+
- Full IDE autocomplete for function names and parameters
|
|
398
|
+
- Compile-time type checking - catch errors before runtime
|
|
399
|
+
- Your client code looks identical to regular JavaScript calls
|
|
400
|
+
|
|
401
|
+
### Using Typed References
|
|
402
|
+
|
|
403
|
+
Now on your client, you have complete type safety:
|
|
349
404
|
|
|
350
405
|
```bash
|
|
351
406
|
cat ./example/typescript/client.ts
|
|
352
407
|
```
|
|
353
408
|
```ts
|
|
354
|
-
|
|
409
|
+
/**
|
|
410
|
+
* Client-side usage with type-safe SendScript
|
|
411
|
+
*/
|
|
412
|
+
|
|
413
|
+
import math from './math.client.ts'
|
|
355
414
|
import Stringify from 'sendscript/stringify.mjs'
|
|
356
415
|
|
|
357
416
|
const stringify = Stringify()
|
|
358
417
|
|
|
359
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Send a SendScript program to the server
|
|
420
|
+
*
|
|
421
|
+
* TypeScript knows that the return type matches the program's return type.
|
|
422
|
+
* In this case, square(add(1, 2)) returns a number, so T is number.
|
|
423
|
+
*/
|
|
360
424
|
async function send<T>(program: T): Promise<T> {
|
|
361
425
|
return (await fetch('/api', {
|
|
362
426
|
method: 'POST',
|
|
@@ -364,20 +428,41 @@ async function send<T>(program: T): Promise<T> {
|
|
|
364
428
|
})).json()
|
|
365
429
|
}
|
|
366
430
|
|
|
367
|
-
|
|
431
|
+
// TypeScript provides full autocomplete for math.add and math.square
|
|
432
|
+
// It knows they take numbers and return numbers
|
|
433
|
+
const result = await send(math.square(math.add(1, 2)))
|
|
434
|
+
console.log(result) // 9
|
|
435
|
+
|
|
368
436
|
```
|
|
369
437
|
|
|
370
|
-
|
|
438
|
+
TypeScript knows the exact parameter types and return types for every function
|
|
439
|
+
call.
|
|
440
|
+
|
|
441
|
+
### Generating API Documentation
|
|
442
|
+
|
|
443
|
+
You can use [typedoc][typedoc] to automatically generate documentation from your
|
|
444
|
+
TypeScript types:
|
|
371
445
|
|
|
372
446
|
```bash
|
|
373
|
-
npx typedoc --plugin typedoc-plugin-markdown --out ./
|
|
447
|
+
npx typedoc --plugin typedoc-plugin-markdown --out ./docs ./example/typescript/math.ts
|
|
448
|
+
```
|
|
449
|
+
```
|
|
450
|
+
[96m[info][0m Loaded plugin typedoc-plugin-markdown
|
|
451
|
+
[96m[info][0m markdown generated at ./docs
|
|
374
452
|
```
|
|
375
453
|
|
|
376
|
-
|
|
454
|
+
This generates markdown docs that can be shared with API consumers. See the
|
|
455
|
+
[generated docs](./example/typescript/docs/globals.md).
|
|
377
456
|
|
|
378
|
-
> [!
|
|
379
|
-
>
|
|
380
|
-
>
|
|
457
|
+
> [!IMPORTANT] **Type vs. Runtime Values**
|
|
458
|
+
>
|
|
459
|
+
> Type casting on the client side improves the development experience, but
|
|
460
|
+
> remember:
|
|
461
|
+
>
|
|
462
|
+
> - The actual serialized JSON may differ from the static types
|
|
463
|
+
> - Runtime values depend on serialization/deserialization
|
|
464
|
+
> - Always validate user input on the server using schema validation (e.g., Zod)
|
|
465
|
+
> - Use the types as a contract, not a guarantee
|
|
381
466
|
|
|
382
467
|
## Schema and Nested Modules
|
|
383
468
|
|
|
@@ -529,36 +614,21 @@ code to create the AST.
|
|
|
529
614
|
|
|
530
615
|
### Callbacks
|
|
531
616
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
as a templating tool to make sendscript programs; just like one would use
|
|
535
|
-
JavaScript with react templates. `items.map(deleteItem)` would return an array
|
|
536
|
-
of sendscript function calls which can be given to sendscript's parse.
|
|
537
|
-
|
|
538
|
-
Client functions cannot be called by sendscript functions (as of yet) since we
|
|
539
|
-
cannot serialize client functions. No work has been done to have the server send
|
|
540
|
-
back intermediate values to perform client function calls or by performing
|
|
541
|
-
smaller sendscript program payloads that are passed to the client. Very
|
|
542
|
-
interesting stuff to look into. You can achieve this now but it looks less clean
|
|
543
|
-
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.
|
|
544
619
|
|
|
545
620
|
```js
|
|
546
|
-
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
It might be interesting to allow configuration to create references that will
|
|
550
|
-
trigger a send whenever await is called. That would remove the ability to create
|
|
551
|
-
a single payload whenever using await. You can then write the above in the
|
|
552
|
-
following manner.
|
|
621
|
+
const api = references(['map', 'add'])
|
|
553
622
|
|
|
554
|
-
|
|
555
|
-
await updateUser(id, merge(await getUser(id), { ...newValues }))
|
|
623
|
+
const result = api.map((value) => api.add(value, 1))([1, 2, 3])
|
|
556
624
|
```
|
|
557
625
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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.
|
|
562
632
|
|
|
563
633
|
### Error handling
|
|
564
634
|
|
|
@@ -579,10 +649,10 @@ npm t -- report text-summary
|
|
|
579
649
|
```
|
|
580
650
|
|
|
581
651
|
=============================== Coverage summary ===============================
|
|
582
|
-
Statements : 100% (
|
|
583
|
-
Branches : 100% (
|
|
652
|
+
Statements : 100% ( 516/516 )
|
|
653
|
+
Branches : 100% ( 157/157 )
|
|
584
654
|
Functions : 100% ( 23/23 )
|
|
585
|
-
Lines : 100% (
|
|
655
|
+
Lines : 100% ( 516/516 )
|
|
586
656
|
================================================================================
|
|
587
657
|
```
|
|
588
658
|
|
package/README.mz
CHANGED
|
@@ -261,62 +261,103 @@ behavior for the sendscript DSL and parser.
|
|
|
261
261
|
|
|
262
262
|
### await
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
the
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
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
|
|
|
288
|
-
|
|
301
|
+
Using SendScript with TypeScript enables **type-safe client-side code**. Your
|
|
302
|
+
client can have full IDE autocomplete and compile-time type checking when
|
|
303
|
+
calling server functions.
|
|
289
304
|
|
|
290
|
-
|
|
291
|
-
coding.
|
|
292
|
-
2. You can use tools like [typedoc][typedoc] to generate docs from your types to
|
|
293
|
-
share with consumers of your API.
|
|
294
|
-
3. You can use the types of the module to coerce your client to adopt the
|
|
295
|
-
module's type.
|
|
305
|
+
### Server-Side Module
|
|
296
306
|
|
|
297
|
-
|
|
307
|
+
Define your API as a TypeScript module on the server:
|
|
298
308
|
|
|
299
309
|
```bash|ts bash
|
|
300
310
|
cat ./example/typescript/math.ts
|
|
301
311
|
```
|
|
302
312
|
|
|
303
|
-
|
|
313
|
+
### Client-Side Type Stub
|
|
314
|
+
|
|
315
|
+
Create a client-side file that mirrors your server types using the `as typeof`
|
|
316
|
+
casting pattern:
|
|
317
|
+
|
|
318
|
+
```bash|ts bash
|
|
319
|
+
cat ./example/typescript/math.client.ts
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The `as typeof mathTypes` type assertion gives your client-side references the
|
|
323
|
+
exact same types as your server module. This means:
|
|
324
|
+
|
|
325
|
+
- Full IDE autocomplete for function names and parameters
|
|
326
|
+
- Compile-time type checking - catch errors before runtime
|
|
327
|
+
- Your client code looks identical to regular JavaScript calls
|
|
328
|
+
|
|
329
|
+
### Using Typed References
|
|
330
|
+
|
|
331
|
+
Now on your client, you have complete type safety:
|
|
304
332
|
|
|
305
333
|
```bash|ts bash
|
|
306
334
|
cat ./example/typescript/client.ts
|
|
307
335
|
```
|
|
308
336
|
|
|
309
|
-
|
|
337
|
+
TypeScript knows the exact parameter types and return types for every function
|
|
338
|
+
call.
|
|
339
|
+
|
|
340
|
+
### Generating API Documentation
|
|
310
341
|
|
|
311
|
-
|
|
312
|
-
|
|
342
|
+
You can use [typedoc][typedoc] to automatically generate documentation from your
|
|
343
|
+
TypeScript types:
|
|
344
|
+
|
|
345
|
+
```bash bash
|
|
346
|
+
npx typedoc --plugin typedoc-plugin-markdown --out ./docs ./example/typescript/math.ts
|
|
313
347
|
```
|
|
314
348
|
|
|
315
|
-
|
|
349
|
+
This generates markdown docs that can be shared with API consumers. See the
|
|
350
|
+
[generated docs](./example/typescript/docs/globals.md).
|
|
316
351
|
|
|
317
|
-
> [!
|
|
318
|
-
>
|
|
319
|
-
>
|
|
352
|
+
> [!IMPORTANT] **Type vs. Runtime Values**
|
|
353
|
+
>
|
|
354
|
+
> Type casting on the client side improves the development experience, but
|
|
355
|
+
> remember:
|
|
356
|
+
>
|
|
357
|
+
> - The actual serialized JSON may differ from the static types
|
|
358
|
+
> - Runtime values depend on serialization/deserialization
|
|
359
|
+
> - Always validate user input on the server using schema validation (e.g., Zod)
|
|
360
|
+
> - Use the types as a contract, not a guarantee
|
|
320
361
|
|
|
321
362
|
## Schema and Nested Modules
|
|
322
363
|
|
|
@@ -468,36 +509,21 @@ code to create the AST.
|
|
|
468
509
|
|
|
469
510
|
### Callbacks
|
|
470
511
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
as a templating tool to make sendscript programs; just like one would use
|
|
474
|
-
JavaScript with react templates. `items.map(deleteItem)` would return an array
|
|
475
|
-
of sendscript function calls which can be given to sendscript's parse.
|
|
476
|
-
|
|
477
|
-
Client functions cannot be called by sendscript functions (as of yet) since we
|
|
478
|
-
cannot serialize client functions. No work has been done to have the server send
|
|
479
|
-
back intermediate values to perform client function calls or by performing
|
|
480
|
-
smaller sendscript program payloads that are passed to the client. Very
|
|
481
|
-
interesting stuff to look into. You can achieve this now but it looks less clean
|
|
482
|
-
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.
|
|
483
514
|
|
|
484
515
|
```js
|
|
485
|
-
|
|
486
|
-
```
|
|
516
|
+
const api = references(['map', 'add'])
|
|
487
517
|
|
|
488
|
-
|
|
489
|
-
trigger a send whenever await is called. That would remove the ability to create
|
|
490
|
-
a single payload whenever using await. You can then write the above in the
|
|
491
|
-
following manner.
|
|
492
|
-
|
|
493
|
-
```js
|
|
494
|
-
await updateUser(id, merge(await getUser(id), { ...newValues }))
|
|
518
|
+
const result = api.map((value) => api.add(value, 1))([1, 2, 3])
|
|
495
519
|
```
|
|
496
520
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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.
|
|
501
527
|
|
|
502
528
|
### Error handling
|
|
503
529
|
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Client-side usage with type-safe SendScript
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import math from './math.client.ts'
|
|
2
6
|
import Stringify from 'sendscript/stringify.mjs'
|
|
3
7
|
|
|
4
8
|
const stringify = Stringify()
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Send a SendScript program to the server
|
|
12
|
+
*
|
|
13
|
+
* TypeScript knows that the return type matches the program's return type.
|
|
14
|
+
* In this case, square(add(1, 2)) returns a number, so T is number.
|
|
15
|
+
*/
|
|
7
16
|
async function send<T>(program: T): Promise<T> {
|
|
8
17
|
return (await fetch('/api', {
|
|
9
18
|
method: 'POST',
|
|
@@ -11,4 +20,8 @@ async function send<T>(program: T): Promise<T> {
|
|
|
11
20
|
})).json()
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
|
|
23
|
+
// TypeScript provides full autocomplete for math.add and math.square
|
|
24
|
+
// It knows they take numbers and return numbers
|
|
25
|
+
const result = await send(math.square(math.add(1, 2)))
|
|
26
|
+
console.log(result) // 9
|
|
27
|
+
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side type-safe stubs for the math API
|
|
3
|
+
*
|
|
4
|
+
* This file creates typed references that mirror the server's functions.
|
|
5
|
+
* The 'as typeof mathTypes' cast gives us full TypeScript support and IDE autocomplete.
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import type * as mathTypes from './math.ts'
|
|
2
|
-
import Stringify from 'sendscript/stringify.mjs'
|
|
3
9
|
import references from 'sendscript/references.mjs'
|
|
4
10
|
|
|
11
|
+
// Create type-safe stubs - this tells TypeScript that 'add' and 'square'
|
|
12
|
+
// have the same signatures as the server functions
|
|
5
13
|
export default references(['add', 'square']) as typeof mathTypes
|
|
14
|
+
|
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Server-side math module with typed functions
|
|
3
|
+
* These functions will be called from the client through SendScript
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const add = (a: number, b: number): number => a + b
|
|
7
|
+
|
|
8
|
+
export const square = (a: number): number => a * a
|
|
9
|
+
|
package/package.json
CHANGED
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
|
}
|
package/references.test.mjs
CHANGED
|
@@ -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
|
+
})
|