├── .nojekyll ├── pnpm-workspace.yaml ├── docs ├── _config.yml ├── index.md ├── async-call-rpc.request.id.md ├── async-call-rpc.id.md ├── async-call-rpc.request.jsonrpc.md ├── async-call-rpc.request.method.md ├── async-call-rpc.errorresponse.id.md ├── async-call-rpc.successresponse.id.md ├── async-call-rpc.request.params.md ├── async-call-rpc.abortsignallike.reason.md ├── async-call-rpc.errorresponse.jsonrpc.md ├── async-call-rpc.successresponse.result.md ├── async-call-rpc.jsonencoder.default.md ├── async-call-rpc.successresponse.jsonrpc.md ├── async-call-rpc.abortsignallike.aborted.md ├── async-call-rpc.errorresponse.error.md ├── async-call-rpc.errorresponsedetail.code.md ├── async-call-rpc.errorresponsedetail.data.md ├── async-call-rpc.errorresponsedetail.message.md ├── async-call-rpc.asynccalloptions.name.md ├── async-call-rpc.asynccalloptions.logger.md ├── async-call-rpc.requests.md ├── async-call-rpc.asynccallloglevel.becalled.md ├── async-call-rpc.responses.md ├── async-call-rpc.abortsignallike.throwifaborted.md ├── async-call-rpc.asynccallloglevel.remoteerror.md ├── async-call-rpc.request.remotestack.md ├── async-call-rpc.asynccallloglevel.localerror.md ├── async-call-rpc.jsonencoder.md ├── async-call-rpc.response.md ├── async-call-rpc.asynccallloglevel.sendlocalstack.md ├── async-call-rpc.jsonencoderoptions.replacer.md ├── async-call-rpc.asynccalloptions.maperror.md ├── async-call-rpc.asynccalloptions.idgenerator.md ├── async-call-rpc.asynccalloptions.key.md ├── async-call-rpc.errormapfunction.md ├── async-call-rpc.jsonencoderoptions.space.md ├── async-call-rpc.consoleinterface.log.md ├── async-call-rpc.consoleinterface.warn.md ├── async-call-rpc.consoleinterface.debug.md ├── async-call-rpc.consoleinterface.error.md ├── async-call-rpc.consoleinterface.groupend.md ├── async-call-rpc.noserialization.md ├── async-call-rpc.asynccalloptions.signal.md ├── async-call-rpc.asynccalloptions.strict.md ├── async-call-rpc.consoleinterface.groupcollapsed.md ├── async-call-rpc.asynccalloptions.serializer.md ├── async-call-rpc.jsonencoderoptions.reviver.md ├── async-call-rpc.asynccalloptions.parameterstructure.md ├── async-call-rpc.asynccallloglevel.requestreplay.md ├── async-call-rpc.eventbasedchannel.send.md ├── async-call-rpc.abortsignallike.removeeventlistener.md ├── async-call-rpc.asynccalloptions.encoder.md ├── async-call-rpc.asynccalloptions.forcesignal.md ├── async-call-rpc.asynccallstrictoptions.methodnotfound.md ├── async-call-rpc.serialization.serialization.md ├── async-call-rpc.clientencoding.md ├── async-call-rpc.serverencoding.md ├── async-call-rpc.asynccallloglevel.type.md ├── async-call-rpc.serialization.deserialization.md ├── async-call-rpc.asynccalloptions.log.md ├── async-call-rpc.notify.md ├── async-call-rpc.isomorphicencoder.md ├── async-call-rpc.abortsignallike.addeventlistener.md ├── async-call-rpc.serverencoding.encoderesponse.md ├── async-call-rpc.clientencoding.encoderequest.md ├── async-call-rpc.asynccallstrictjsonrpc.md ├── async-call-rpc.asynccallstrictoptions.unknownmessage.md ├── async-call-rpc.serverencoding.decoderequest.md ├── async-call-rpc.asynccalloptions.parameterstructures.md ├── async-call-rpc.asynccalloptions.preferlocalimplementation.md ├── async-call-rpc.asyncversionof.md ├── async-call-rpc.clientencoding.decoderesponse.md ├── async-call-rpc.eventbasedchannel.on.md ├── async-call-rpc.serialization.md ├── async-call-rpc.asyncgeneratorversionof.md ├── async-call-rpc.errorresponsedetail.md ├── async-call-rpc.errorresponse.md ├── async-call-rpc.isomorphicencoder.encode.md ├── async-call-rpc.successresponse.md ├── async-call-rpc.eventbasedchannel.md ├── async-call-rpc.isomorphicencoderfull.md ├── async-call-rpc.isomorphicencoder.decode.md ├── async-call-rpc.asynccalloptions.channel.md ├── async-call-rpc.asynccallstrictoptions.md ├── async-call-rpc.callbackbasedchannel.md ├── async-call-rpc.request.md ├── async-call-rpc.successresponse.undef.md ├── async-call-rpc.consoleinterface.md ├── async-call-rpc.jsonencoderoptions.keepundefined.md ├── async-call-rpc.batch.md ├── async-call-rpc.abortsignallike.md ├── async-call-rpc.callbackbasedchannel.setup.md ├── async-call-rpc.asynccallloglevel.md ├── async-call-rpc.asynccalloptions.thenable.md ├── async-call-rpc.jsonencoderoptions.md ├── async-call-rpc.asynccall.md ├── async-call-rpc.jsonserialization.md ├── async-call-rpc.asyncgeneratorcall.md └── index.html ├── src ├── package.json ├── utils │ ├── generateRandomID.ts │ ├── internalSymbol.ts │ ├── constants.ts │ ├── normalizeOptions.ts │ ├── serialization.ts │ ├── encoder.ts │ └── error.ts ├── index.ts ├── tsconfig.json └── core │ ├── notify.ts │ └── batch.ts ├── __tests__ ├── package.json ├── utils │ ├── global.d.ts │ ├── reproduce.ts │ └── logger.ts ├── __file_snapshots__ │ ├── async-call-key-name.md │ ├── async-call-symbols.md │ ├── async-call-thenable-false.md │ ├── async-call-channel-rejected.md │ ├── async-call-generator-symbols.md │ ├── async-call-preferLocalImplementation.md │ ├── async-call-batch-notify-2.md │ ├── bad-state.md │ ├── async-call-preserved-names.md │ ├── CallbackBasedChannel-2.md │ ├── async-call-generator-brand.md │ ├── async-call-log-requestReplay.md │ ├── serialization-json-no-keep.md │ ├── serialization-json-default.md │ ├── serialization-json-keep-undefined.md │ ├── async-call-impl-promise-like.md │ ├── no-encoder-no-serialization.md │ ├── async-call-brand.md │ ├── CallbackBasedChannel.md │ ├── async-call-impl-resolved.md │ ├── async-call-channel-resolved.md │ ├── async-call-channel-promise-like.md │ ├── generateRandomID.md │ ├── deserialize-failed.md │ ├── async-call-parameterStructures-by-name.md │ ├── async-call-impl-pending.md │ ├── async-call-channel-pending.md │ ├── bad-data.md │ ├── async-call-default-logger.md │ ├── async-call-non-strict.md │ ├── async-call-log-false.md │ ├── async-call-strict-partial.md │ ├── encoder-json.md │ ├── encoder-json-Default.md │ ├── DOMException.md │ ├── async-call-custom-error-mapper.md │ ├── recover-error-with-bad-implementation.md │ ├── bad-state-generator-non-strict.md │ ├── async-call-batch-notify-3.md │ ├── async-call-notify.md │ ├── async-call-impl-rejected.md │ ├── async-call-log-object.md │ ├── encode-isomorphic-hinted.md │ ├── encoder-json-no-keep.md │ ├── encode-isomorphic-not-hinted.md │ ├── encode-isomorphic-full-not-hinted.md │ ├── encode-isomorphic-full-hinted.md │ ├── async-call-batch-notify-1.md │ ├── generateRandomID-2.md │ ├── async-call-log-true.md │ ├── async-call-strict.md │ ├── async-call-batch.md │ ├── async-call-client-abort-signal.md │ ├── async-call-log-all.md │ └── serialization-no-serialization.md ├── tsconfig.json ├── notify.ts ├── special-names.ts ├── DOMException.ts ├── options-normalize.ts ├── generator.ts ├── non-strict.ts ├── batch-and-notify.ts ├── serialization.ts ├── brand.ts └── batch.ts ├── utils-src ├── web │ ├── package.json │ ├── tsconfig.json │ ├── worker.ts │ ├── msgpack.ts │ ├── bson.ts │ ├── broadcast.channel.ts │ └── websocket.client.ts └── node │ ├── tsconfig.json │ ├── bson.ts │ ├── msgpack.ts │ └── websocket.server.ts ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── examples ├── package.json ├── browser.worker-worker.js ├── pnpm-lock.yaml ├── browser.worker-main.js ├── node.websocket.server.js ├── deno.websocket.server.ts └── browser.websocket.client.js ├── .prettierrc ├── .editorconfig ├── vite.config.ts ├── .changeset ├── config.json └── README.md ├── .github ├── publish.mjs └── workflows │ ├── test.yml │ ├── build.yml │ ├── release.yml │ └── codeql-analysis.yml ├── jsr.json ├── api-extractor-base.json ├── LICENSE ├── tsconfig.json ├── .gitignore ├── utils └── deno │ └── websocket.server.ts ├── index.html └── rollup.config.mjs /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ./ 3 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-leap-day 2 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/utils/global.d.ts: -------------------------------------------------------------------------------- 1 | type CustomMatcherResult = any 2 | -------------------------------------------------------------------------------- /utils-src/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-key-name.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-symbols.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-thenable-false.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-channel-rejected.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-generator-symbols.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-preferLocalImplementation.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "async-call-rpc": "file:.." 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/generateRandomID.ts: -------------------------------------------------------------------------------- 1 | export const generateRandomID = () => Math.random().toString(36).slice(2) 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "jsxBracketSameLine": true 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-batch-notify-2.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "No log in this file", 8 | ] 9 | ``` 10 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/bad-state.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: server => client 4 | 5 | ```php 6 | Object { 7 | "id": 123, 8 | "jsonrpc": "2.0", 9 | "result": Object {}, 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /src/utils/internalSymbol.ts: -------------------------------------------------------------------------------- 1 | const i = 'AsyncCall/' 2 | // ! side effect 3 | export const AsyncCallIgnoreResponse = Symbol.for(i + 'ignored') 4 | export const AsyncCallNotify = Symbol.for(i + 'notify') 5 | export const AsyncCallBatch = Symbol.for(i + 'batch') 6 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-preserved-names.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: client/log 4 | 5 | ```php 6 | Array [ 7 | [TypeError: RPC used as Promise: Error 3: https://github.com/Jack-Works/async-call-rpc/wiki/Errors#3], 8 | ] 9 | ``` 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 4 5 | charset = utf-8 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | 11 | [{.*rc,ts*.json,package.json}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['./__tests__/*.ts'], 6 | coverage: { 7 | provider: 'v8', 8 | include: ['src/**'], 9 | }, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/browser.worker-worker.js: -------------------------------------------------------------------------------- 1 | import * as rpc from '../out/base.mjs' 2 | import { WorkerChannel } from '../utils/web/worker.js' 3 | 4 | const impl = { 5 | hello: () => 'hello from worker', 6 | } 7 | const host = rpc.AsyncCall(impl, { channel: new WorkerChannel(), log: false }) 8 | host.hello().then(console.log) 9 | -------------------------------------------------------------------------------- /examples/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | async-call-rpc: file:.. 5 | 6 | dependencies: 7 | async-call-rpc: file:.. 8 | 9 | packages: 10 | file:..: 11 | resolution: { directory: .., type: directory } 12 | name: async-call-rpc 13 | version: 6.1.2 14 | dev: false 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A light implementation of {@link https://www.jsonrpc.org/specification | JSON RPC 2.0} 3 | * @packageDocumentation 4 | * @remarks 5 | * See the introduction at {@link https://github.com/Jack-Works/async-call | Github} 6 | */ 7 | 8 | export * from './Async-Call.ts' 9 | export * from './Async-Call-Generator.ts' 10 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/publish.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises' 2 | 3 | const jsr_path = new URL('../jsr.json', import.meta.url) 4 | const jsr = await readFile(jsr_path, 'utf8') 5 | const { version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')) 6 | 7 | await writeFile(jsr_path, jsr.replace('{{version}}', version), 'utf8') 8 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/CallbackBasedChannel-2.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "invalid": true, 8 | } 9 | ``` 10 | 11 | ## T=1 Log: server/log 12 | 13 | ```php 14 | Array [ 15 | "Invalid JSON RPC received, ignore", 16 | Object { 17 | "invalid": true, 18 | }, 19 | ] 20 | ``` 21 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-generator-brand.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client sent 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "rpc.async-iterator.start", 10 | "params": Array [ 11 | "echo", 12 | Array [ 13 | Array [], 14 | ], 15 | ], 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) 4 | 5 | ## API Reference 6 | 7 | ## Packages 8 | 9 | | Package | Description | 10 | | --- | --- | 11 | | [async-call-rpc](./async-call-rpc.md) | A light implementation of [JSON RPC 2.0](https://www.jsonrpc.org/specification) | 12 | 13 | -------------------------------------------------------------------------------- /docs/async-call-rpc.request.id.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Request](./async-call-rpc.request.md) > [id](./async-call-rpc.request.id.md) 4 | 5 | ## Request.id property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly id?: ID; 11 | ``` 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "start", 9 | "problemMatcher": ["$tsc-watch"], 10 | "isBackground": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.id.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ID](./async-call-rpc.id.md) 4 | 5 | ## ID type 6 | 7 | ID of a JSON-RPC request/response. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export type ID = string | number | null | undefined; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.request.jsonrpc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Request](./async-call-rpc.request.md) > [jsonrpc](./async-call-rpc.request.jsonrpc.md) 4 | 5 | ## Request.jsonrpc property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly jsonrpc: '2.0'; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.request.method.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Request](./async-call-rpc.request.md) > [method](./async-call-rpc.request.method.md) 4 | 5 | ## Request.method property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly method: string; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errorresponse.id.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorResponse](./async-call-rpc.errorresponse.md) > [id](./async-call-rpc.errorresponse.id.md) 4 | 5 | ## ErrorResponse.id property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly id?: ID; 11 | ``` 12 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-log-requestReplay.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "twice", 10 | "params": Array [], 11 | } 12 | ``` 13 | 14 | ## T=1 Message: server => client 15 | 16 | ```php 17 | Object { 18 | "id": 0, 19 | "jsonrpc": "2.0", 20 | "result": undefined, 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/async-call-rpc.successresponse.id.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [SuccessResponse](./async-call-rpc.successresponse.md) > [id](./async-call-rpc.successresponse.id.md) 4 | 5 | ## SuccessResponse.id property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly id?: ID; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.request.params.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Request](./async-call-rpc.request.md) > [params](./async-call-rpc.request.params.md) 4 | 5 | ## Request.params property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly params: readonly unknown[] | object; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.abortsignallike.reason.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AbortSignalLike](./async-call-rpc.abortsignallike.md) > [reason](./async-call-rpc.abortsignallike.reason.md) 4 | 5 | ## AbortSignalLike.reason property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | reason: any; 11 | ``` 12 | -------------------------------------------------------------------------------- /__tests__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": false, 5 | "incremental": false, 6 | "noEmit": true, 7 | "moduleResolution": "NodeNext", 8 | "rootDir": "./", 9 | "lib": ["ES2018", "DOM"], 10 | "types": ["node"] 11 | }, 12 | "include": ["./**/*.ts"], 13 | "references": [{ "path": "../src" }, { "path": "../utils-src/node/tsconfig.json" }] 14 | } 15 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errorresponse.jsonrpc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorResponse](./async-call-rpc.errorresponse.md) > [jsonrpc](./async-call-rpc.errorresponse.jsonrpc.md) 4 | 5 | ## ErrorResponse.jsonrpc property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly jsonrpc: '2.0'; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.successresponse.result.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [SuccessResponse](./async-call-rpc.successresponse.md) > [result](./async-call-rpc.successresponse.result.md) 4 | 5 | ## SuccessResponse.result property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | result: unknown; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.jsonencoder.default.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [JSONEncoder](./async-call-rpc.jsonencoder.md) > [Default](./async-call-rpc.jsonencoder.default.md) 4 | 5 | ## JSONEncoder.Default variable 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | Default: IsomorphicEncoder 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.successresponse.jsonrpc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [SuccessResponse](./async-call-rpc.successresponse.md) > [jsonrpc](./async-call-rpc.successresponse.jsonrpc.md) 4 | 5 | ## SuccessResponse.jsonrpc property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly jsonrpc: '2.0'; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.abortsignallike.aborted.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AbortSignalLike](./async-call-rpc.abortsignallike.md) > [aborted](./async-call-rpc.abortsignallike.aborted.md) 4 | 5 | ## AbortSignalLike.aborted property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly aborted: boolean; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errorresponse.error.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorResponse](./async-call-rpc.errorresponse.md) > [error](./async-call-rpc.errorresponse.error.md) 4 | 5 | ## ErrorResponse.error property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly error: ErrorResponseDetail; 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/browser.worker-main.js: -------------------------------------------------------------------------------- 1 | import * as rpc from '../out/base.mjs' 2 | import { WorkerChannel } from '../utils/web/worker.js' 3 | 4 | const channel = new WorkerChannel( 5 | new Worker(new URL('./browser.worker-worker.js', import.meta.url).pathname, { type: 'module' }), 6 | ) 7 | const impl = { 8 | hello: () => 'hello from main', 9 | } 10 | const worker = rpc.AsyncCall(impl, { channel, log: false }) 11 | worker.hello().then(console.log) 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errorresponsedetail.code.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorResponseDetail](./async-call-rpc.errorresponsedetail.md) > [code](./async-call-rpc.errorresponsedetail.code.md) 4 | 5 | ## ErrorResponseDetail.code property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly code: number; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errorresponsedetail.data.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorResponseDetail](./async-call-rpc.errorresponsedetail.md) > [data](./async-call-rpc.errorresponsedetail.data.md) 4 | 5 | ## ErrorResponseDetail.data property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly data?: Error; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errorresponsedetail.message.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorResponseDetail](./async-call-rpc.errorresponsedetail.md) > [message](./async-call-rpc.errorresponsedetail.message.md) 4 | 5 | ## ErrorResponseDetail.message property 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | readonly message: string; 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.name.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [name](./async-call-rpc.asynccalloptions.name.md) 4 | 5 | ## AsyncCallOptions.name property 6 | 7 | Name used when pretty log is enabled. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | name?: string; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.logger.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [logger](./async-call-rpc.asynccalloptions.logger.md) 4 | 5 | ## AsyncCallOptions.logger property 6 | 7 | Provide the logger 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | logger?: ConsoleInterface; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.requests.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Requests](./async-call-rpc.requests.md) 4 | 5 | ## Requests type 6 | 7 | A request object or an array of request objects. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export type Requests = Request | readonly Request[]; 13 | ``` 14 | **References:** [Request](./async-call-rpc.request.md) 15 | 16 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallloglevel.becalled.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) > [beCalled](./async-call-rpc.asynccallloglevel.becalled.md) 4 | 5 | ## AsyncCallLogLevel.beCalled property 6 | 7 | Log all incoming requests 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | beCalled?: boolean; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.responses.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Responses](./async-call-rpc.responses.md) 4 | 5 | ## Responses type 6 | 7 | A response object or an array of response objects. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export type Responses = Response | readonly Response[]; 13 | ``` 14 | **References:** [Response](./async-call-rpc.response.md) 15 | 16 | -------------------------------------------------------------------------------- /docs/async-call-rpc.abortsignallike.throwifaborted.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AbortSignalLike](./async-call-rpc.abortsignallike.md) > [throwIfAborted](./async-call-rpc.abortsignallike.throwifaborted.md) 4 | 5 | ## AbortSignalLike.throwIfAborted() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | throwIfAborted(): void; 11 | ``` 12 | **Returns:** 13 | 14 | void 15 | 16 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallloglevel.remoteerror.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) > [remoteError](./async-call-rpc.asynccallloglevel.remoteerror.md) 4 | 5 | ## AsyncCallLogLevel.remoteError property 6 | 7 | Log errors from the remote 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | remoteError?: boolean; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.request.remotestack.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Request](./async-call-rpc.request.md) > [remoteStack](./async-call-rpc.request.remotestack.md) 4 | 5 | ## Request.remoteStack property 6 | 7 | Non-standard field. It records the caller's stack of this Request. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | readonly remoteStack?: string | undefined; 13 | ``` 14 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "allowImportingTsExtensions": true, 7 | "emitDeclarationOnly": true, 8 | "rootDir": "./", 9 | "outDir": "../dist/core/", 10 | // This lib does not require any external libs 11 | "typeRoots": [], 12 | "types": [], 13 | "lib": ["ES2018"], 14 | "tsBuildInfoFile": "../dist/core/.tsbuildinfo" 15 | }, 16 | "include": ["./"] 17 | } 18 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallloglevel.localerror.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) > [localError](./async-call-rpc.asynccallloglevel.localerror.md) 4 | 5 | ## AsyncCallLogLevel.localError property 6 | 7 | Log all errors when responding requests 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | localError?: boolean; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.jsonencoder.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [JSONEncoder](./async-call-rpc.jsonencoder.md) 4 | 5 | ## JSONEncoder namespace 6 | 7 | 8 | **Signature:** 9 | 10 | ```typescript 11 | export declare namespace JSONEncoder 12 | ``` 13 | 14 | ## Variables 15 | 16 | | Variable | Description | 17 | | --- | --- | 18 | | [Default](./async-call-rpc.jsonencoder.default.md) | | 19 | 20 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /docs/async-call-rpc.response.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Response](./async-call-rpc.response.md) 4 | 5 | ## Response type 6 | 7 | A JSON-RPC response object 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export type Response = SuccessResponse | ErrorResponse; 13 | ``` 14 | **References:** [SuccessResponse](./async-call-rpc.successresponse.md), [ErrorResponse](./async-call-rpc.errorresponse.md) 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: "20" 13 | - uses: pnpm/action-setup@v2 14 | - run: pnpm install 15 | - run: pnpm test 16 | - uses: codecov/codecov-action@v1 17 | with: 18 | file: ./coverage/clover.xml 19 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/serialization-json-no-keep.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"undefined\",\"params\":[]}" 7 | ``` 8 | 9 | ## T=1 Log: server/log 10 | 11 | ```php 12 | Array [ 13 | "rpc.%cundefined%c(%c) 14 | %o %c@0", 15 | "color:#d2c057", 16 | "", 17 | "", 18 | Promise {}, 19 | "color:gray;font-style:italic;", 20 | ] 21 | ``` 22 | 23 | ## T=2 Message: server => client 24 | 25 | ```php 26 | "{\"jsonrpc\":\"2.0\",\"id\":0}" 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallloglevel.sendlocalstack.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) > [sendLocalStack](./async-call-rpc.asynccallloglevel.sendlocalstack.md) 4 | 5 | ## AsyncCallLogLevel.sendLocalStack property 6 | 7 | Send the stack to the remote when making requests 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | sendLocalStack?: boolean; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.jsonencoderoptions.replacer.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [JSONEncoderOptions](./async-call-rpc.jsonencoderoptions.md) > [replacer](./async-call-rpc.jsonencoderoptions.replacer.md) 4 | 5 | ## JSONEncoderOptions.replacer property 6 | 7 | A function that transforms the results. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | replacer?: ((this: any, key: string, value: any) => any) | undefined; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.maperror.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [mapError](./async-call-rpc.asynccalloptions.maperror.md) 4 | 5 | ## AsyncCallOptions.mapError property 6 | 7 | Change the [ErrorResponseDetail](./async-call-rpc.errorresponsedetail.md). 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | mapError?: ErrorMapFunction; 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/node.websocket.server.js: -------------------------------------------------------------------------------- 1 | const { WebSocketChannel } = require('async-call-rpc/utils/node/websocket.server.js') 2 | const { Msgpack_Serialization } = require('async-call-rpc/utils/node/msgpack.js') 3 | const { AsyncCall } = require('async-call-rpc') 4 | const { Server } = require('ws') 5 | 6 | const ws = new Server({ port: 3456 }) 7 | const channel = new WebSocketChannel(ws) 8 | const server = (module.exports.server = { 9 | now: Date.now, 10 | echo: (x) => x, 11 | }) 12 | AsyncCall(server, { channel: channel, serializer: Msgpack_Serialization() }) 13 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/serialization-json-default.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"undefined\",\"params\":[]}" 7 | ``` 8 | 9 | ## T=1 Log: server/log 10 | 11 | ```php 12 | Array [ 13 | "rpc.%cundefined%c(%c) 14 | %o %c@0", 15 | "color:#d2c057", 16 | "", 17 | "", 18 | Promise {}, 19 | "color:gray;font-style:italic;", 20 | ] 21 | ``` 22 | 23 | ## T=2 Message: server => client 24 | 25 | ```php 26 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":null}" 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.idgenerator.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [idGenerator](./async-call-rpc.asynccalloptions.idgenerator.md) 4 | 5 | ## AsyncCallOptions.idGenerator() method 6 | 7 | The ID generator of each JSON RPC request 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | idGenerator?(): string | number; 13 | ``` 14 | **Returns:** 15 | 16 | string \| number 17 | 18 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.key.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [key](./async-call-rpc.asynccalloptions.key.md) 4 | 5 | ## AsyncCallOptions.key property 6 | 7 | > Warning: This API is now obsolete. 8 | > 9 | > Renamed to "name". 10 | > 11 | 12 | Name used when pretty log is enabled. 13 | 14 | **Signature:** 15 | 16 | ```typescript 17 | key?: string; 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errormapfunction.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorMapFunction](./async-call-rpc.errormapfunction.md) 4 | 5 | ## ErrorMapFunction type 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | export type ErrorMapFunction = (error: unknown, request: Readonly) => { 11 | code: number; 12 | message: string; 13 | data?: T; 14 | }; 15 | ``` 16 | **References:** [Request](./async-call-rpc.request.md) 17 | 18 | -------------------------------------------------------------------------------- /utils-src/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "lib": ["ES2018", "DOM", "DOM.Iterable"], 7 | "outDir": "../../utils/web/", 8 | "rootDir": "./", 9 | "baseUrl": "./", 10 | "tsBuildInfoFile": "../../dist/web.tsbuildinfo", 11 | "paths": { 12 | "async-call-rpc": ["../../src/index.ts"], 13 | "bson": ["../../node_modules/bson"] 14 | } 15 | }, 16 | "include": ["./**/*.ts"], 17 | "references": [{ "path": "../../src" }] 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: "20" 13 | - uses: pnpm/action-setup@v2 14 | - run: pnpm install 15 | - run: pnpm run build 16 | - uses: actions/upload-artifact@v2 17 | with: 18 | name: build 19 | path: ./out/ 20 | -------------------------------------------------------------------------------- /docs/async-call-rpc.jsonencoderoptions.space.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [JSONEncoderOptions](./async-call-rpc.jsonencoderoptions.md) > [space](./async-call-rpc.jsonencoderoptions.space.md) 4 | 5 | ## JSONEncoderOptions.space property 6 | 7 | Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | space?: string | number | undefined; 13 | ``` 14 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/serialization-json-keep-undefined.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"undefined\",\"params\":[]}" 7 | ``` 8 | 9 | ## T=1 Log: server/log 10 | 11 | ```php 12 | Array [ 13 | "rpc.%cundefined%c(%c) 14 | %o %c@0", 15 | "color:#d2c057", 16 | "", 17 | "", 18 | Promise {}, 19 | "color:gray;font-style:italic;", 20 | ] 21 | ``` 22 | 23 | ## T=2 Message: server => client 24 | 25 | ```php 26 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":null,\"undef\":true}" 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/async-call-rpc.consoleinterface.log.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ConsoleInterface](./async-call-rpc.consoleinterface.md) > [log](./async-call-rpc.consoleinterface.log.md) 4 | 5 | ## ConsoleInterface.log() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | log(...args: unknown[]): void; 11 | ``` 12 | 13 | ## Parameters 14 | 15 | | Parameter | Type | Description | 16 | | --- | --- | --- | 17 | | args | unknown\[\] | | 18 | 19 | **Returns:** 20 | 21 | void 22 | 23 | -------------------------------------------------------------------------------- /docs/async-call-rpc.consoleinterface.warn.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ConsoleInterface](./async-call-rpc.consoleinterface.md) > [warn](./async-call-rpc.consoleinterface.warn.md) 4 | 5 | ## ConsoleInterface.warn() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | warn?(...args: unknown[]): void; 11 | ``` 12 | 13 | ## Parameters 14 | 15 | | Parameter | Type | Description | 16 | | --- | --- | --- | 17 | | args | unknown\[\] | | 18 | 19 | **Returns:** 20 | 21 | void 22 | 23 | -------------------------------------------------------------------------------- /docs/async-call-rpc.consoleinterface.debug.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ConsoleInterface](./async-call-rpc.consoleinterface.md) > [debug](./async-call-rpc.consoleinterface.debug.md) 4 | 5 | ## ConsoleInterface.debug() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | debug?(...args: unknown[]): void; 11 | ``` 12 | 13 | ## Parameters 14 | 15 | | Parameter | Type | Description | 16 | | --- | --- | --- | 17 | | args | unknown\[\] | | 18 | 19 | **Returns:** 20 | 21 | void 22 | 23 | -------------------------------------------------------------------------------- /docs/async-call-rpc.consoleinterface.error.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ConsoleInterface](./async-call-rpc.consoleinterface.md) > [error](./async-call-rpc.consoleinterface.error.md) 4 | 5 | ## ConsoleInterface.error() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | error?(...args: unknown[]): void; 11 | ``` 12 | 13 | ## Parameters 14 | 15 | | Parameter | Type | Description | 16 | | --- | --- | --- | 17 | | args | unknown\[\] | | 18 | 19 | **Returns:** 20 | 21 | void 22 | 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "pwa-node", 7 | "request": "launch", 8 | "name": "Debug Current Test File", 9 | "autoAttachChildProcesses": true, 10 | "skipFiles": ["/**", "**/node_modules/**"], 11 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 12 | "args": ["run", "${relativeFile}"], 13 | "smartStep": true, 14 | "console": "integratedTerminal" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /utils-src/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "verbatimModuleSyntax": false, 7 | "lib": ["ES2018"], 8 | "types": ["node"], 9 | "outDir": "../../utils/node/", 10 | "rootDir": "./", 11 | "baseUrl": "./", 12 | "tsBuildInfoFile": "../../dist/node.tsbuildinfo", 13 | "paths": { 14 | "bson": ["../../node_modules/bson"], 15 | "async-call-rpc": ["../../src/Async-Call.ts"] 16 | } 17 | }, 18 | "include": ["./**/*.ts"], 19 | "references": [{ "path": "../../src/" }] 20 | } 21 | -------------------------------------------------------------------------------- /docs/async-call-rpc.consoleinterface.groupend.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ConsoleInterface](./async-call-rpc.consoleinterface.md) > [groupEnd](./async-call-rpc.consoleinterface.groupend.md) 4 | 5 | ## ConsoleInterface.groupEnd() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | groupEnd?(...args: unknown[]): void; 11 | ``` 12 | 13 | ## Parameters 14 | 15 | | Parameter | Type | Description | 16 | | --- | --- | --- | 17 | | args | unknown\[\] | | 18 | 19 | **Returns:** 20 | 21 | void 22 | 23 | -------------------------------------------------------------------------------- /docs/async-call-rpc.noserialization.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [NoSerialization](./async-call-rpc.noserialization.md) 4 | 5 | ## NoSerialization variable 6 | 7 | > Warning: This API is now obsolete. 8 | > 9 | > Will be removed in next major version 10 | > 11 | 12 | Serialization implementation that do nothing 13 | 14 | **Signature:** 15 | 16 | ```typescript 17 | NoSerialization: Serialization 18 | ``` 19 | 20 | ## Remarks 21 | 22 | [Serialization](./async-call-rpc.serialization.md) 23 | 24 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-impl-promise-like.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "otherMethods", 10 | "params": Array [], 11 | } 12 | ``` 13 | 14 | ## T=1 Log: server/log 15 | 16 | ```php 17 | Array [ 18 | "rpc.%cotherMethods%c(%c) 19 | %o %c@0", 20 | "color:#d2c057", 21 | "", 22 | "", 23 | Promise {}, 24 | "color:gray;font-style:italic;", 25 | ] 26 | ``` 27 | 28 | ## T=2 Message: server => client 29 | 30 | ```php 31 | Object { 32 | "id": 0, 33 | "jsonrpc": "2.0", 34 | "result": 1, 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /utils-src/web/worker.ts: -------------------------------------------------------------------------------- 1 | import type { EventBasedChannel } from 'async-call-rpc' 2 | 3 | export class WorkerChannel implements EventBasedChannel { 4 | /** 5 | * @param worker Pass the Worker in the main thread. 6 | */ 7 | constructor(public worker: Worker = self as any) {} 8 | on(listener: (data: unknown) => void): void | (() => void) { 9 | const f = (ev: MessageEvent): void => listener(ev.data) 10 | this.worker.addEventListener('message', f) 11 | return () => this.worker.removeEventListener('message', f) 12 | } 13 | send(data: unknown): void { 14 | this.worker.postMessage(data) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/no-encoder-no-serialization.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "undefined", 10 | "params": Array [], 11 | } 12 | ``` 13 | 14 | ## T=1 Log: server/log 15 | 16 | ```php 17 | Array [ 18 | "rpc.%cundefined%c(%c) 19 | %o %c@0", 20 | "color:#d2c057", 21 | "", 22 | "", 23 | Promise {}, 24 | "color:gray;font-style:italic;", 25 | ] 26 | ``` 27 | 28 | ## T=2 Message: server => client 29 | 30 | ```php 31 | Object { 32 | "id": 0, 33 | "jsonrpc": "2.0", 34 | "result": undefined, 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.signal.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [signal](./async-call-rpc.asynccalloptions.signal.md) 4 | 5 | ## AsyncCallOptions.signal property 6 | 7 | AbortSignal to stop the instance. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | signal?: AbortSignalLike; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | `signal` is used to stop the instance. If the `signal` is aborted, then all new requests will be rejected, except for the pending ones. 18 | 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.strict.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [strict](./async-call-rpc.asynccalloptions.strict.md) 4 | 5 | ## AsyncCallOptions.strict property 6 | 7 | Control the behavior that different from the JSON-RPC spec 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | strict?: AsyncCallStrictJSONRPC | boolean; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | - `true` is to enable all strict options - `false` is to disable all strict options 18 | 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.consoleinterface.groupcollapsed.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ConsoleInterface](./async-call-rpc.consoleinterface.md) > [groupCollapsed](./async-call-rpc.consoleinterface.groupcollapsed.md) 4 | 5 | ## ConsoleInterface.groupCollapsed() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | groupCollapsed?(...args: unknown[]): void; 11 | ``` 12 | 13 | ## Parameters 14 | 15 | | Parameter | Type | Description | 16 | | --- | --- | --- | 17 | | args | unknown\[\] | | 18 | 19 | **Returns:** 20 | 21 | void 22 | 23 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const isString = (x: unknown): x is string => typeof x === 'string' 2 | export const isBoolean = (x: unknown): x is boolean => typeof x === 'boolean' 3 | export const isFunction = (x: unknown): x is Function => typeof x === 'function' 4 | export const isObject = (params: any): params is object => typeof params === 'object' && params !== null 5 | export const ERROR = 'Error' 6 | export const undefined = void 0 7 | export const { setPrototypeOf } = Object 8 | export const Promise_resolve = (x: T) => Promise.resolve(x) 9 | export const { isArray } = Array as { isArray(arg: any): arg is readonly any[] } 10 | export const { apply } = Reflect 11 | -------------------------------------------------------------------------------- /utils-src/web/msgpack.ts: -------------------------------------------------------------------------------- 1 | import type { encode as S, decode as D } from '@msgpack/msgpack' 2 | import type { Serialization } from 'async-call-rpc' 3 | 4 | /** 5 | * @deprecated This will be removed in the next major version. 6 | */ 7 | export const Msgpack_Serialization = ({ encode, decode }: { encode: typeof S; decode: typeof D }): Serialization => ({ 8 | async deserialization(data: any) { 9 | if (data instanceof Blob) data = await data.arrayBuffer() 10 | if (data instanceof ArrayBuffer) data = new Uint8Array(data) 11 | return decode(data) 12 | }, 13 | serialization(data: any) { 14 | return encode(data) 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /utils-src/node/bson.ts: -------------------------------------------------------------------------------- 1 | import type { serialize as S, deserialize as D } from 'bson' 2 | import type { Serialization } from 'async-call-rpc' with { 'resolution-mode': 'import' } 3 | 4 | /** 5 | * @deprecated This will be removed in the next major version. 6 | */ 7 | export const BSON_Serialization = ( 8 | { 9 | deserialize, 10 | serialize, 11 | }: { 12 | serialize: typeof S 13 | deserialize: typeof D 14 | } = require('bson'), 15 | ): Serialization => ({ 16 | deserialization(data: Buffer) { 17 | return deserialize(data) 18 | }, 19 | serialization(data: any) { 20 | return serialize(data) 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /utils-src/node/msgpack.ts: -------------------------------------------------------------------------------- 1 | import type { encode as S, decode as D } from '@msgpack/msgpack' 2 | import type { Serialization } from 'async-call-rpc' with { 'resolution-mode': 'import' } 3 | 4 | /** 5 | * @deprecated This will be removed in the next major version. 6 | */ 7 | export const Msgpack_Serialization = ( 8 | { 9 | encode, 10 | decode, 11 | }: { 12 | encode: typeof S 13 | decode: typeof D 14 | } = require('@msgpack/msgpack'), 15 | ): Serialization => ({ 16 | deserialization(data: Uint8Array) { 17 | return decode(data) 18 | }, 19 | serialization(data: any) { 20 | return encode(data) 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.serializer.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [serializer](./async-call-rpc.asynccalloptions.serializer.md) 4 | 5 | ## AsyncCallOptions.serializer property 6 | 7 | > Warning: This API is now obsolete. 8 | > 9 | > Use "encoding" option instead. This option will be removed in the next major version. 10 | > 11 | 12 | Serializer of the requests and responses. 13 | 14 | **Signature:** 15 | 16 | ```typescript 17 | serializer?: Serialization; 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/deno.websocket.server.ts: -------------------------------------------------------------------------------- 1 | import { serve } from 'https://deno.land/std@0.61.0/http/server.ts' 2 | import * as MessagePack from 'https://jspm.dev/@msgpack/msgpack' 3 | import { Msgpack_Serialization } from '../utils/web/msgpack.js' 4 | import { WebSocketChannel } from '../utils/deno/websocket.server.ts' 5 | import { AsyncCall } from '../out/base.mjs' 6 | 7 | export const server = { 8 | now: Date.now, 9 | rand: Math.random, 10 | echo: (x: T) => x, 11 | } 12 | 13 | AsyncCall(server, { 14 | channel: new WebSocketChannel(serve({ port: 3456 })), 15 | serializer: Msgpack_Serialization(MessagePack), 16 | log: { type: 'basic' }, 17 | }) 18 | console.log('Server is ready') 19 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-brand.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 0, 12 | 1, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cadd%c(%o, %o%c) 22 | %o %c@0", 23 | "color:#d2c057", 24 | "", 25 | 0, 26 | 1, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: server => client 34 | 35 | ```php 36 | Object { 37 | "id": 0, 38 | "jsonrpc": "2.0", 39 | "result": 1, 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/CallbackBasedChannel.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cadd%c(%o, %o%c) 22 | %o %c@0", 23 | "color:#d2c057", 24 | "", 25 | 1, 26 | 2, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: server => client 34 | 35 | ```php 36 | Object { 37 | "id": 0, 38 | "jsonrpc": "2.0", 39 | "result": 3, 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-impl-resolved.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cadd%c(%o, %o%c) 22 | %o %c@0", 23 | "color:#d2c057", 24 | "", 25 | 1, 26 | 2, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: server => client 34 | 35 | ```php 36 | Object { 37 | "id": 0, 38 | "jsonrpc": "2.0", 39 | "result": 3, 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/async-call-rpc.jsonencoderoptions.reviver.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [JSONEncoderOptions](./async-call-rpc.jsonencoderoptions.md) > [reviver](./async-call-rpc.jsonencoderoptions.reviver.md) 4 | 5 | ## JSONEncoderOptions.reviver property 6 | 7 | A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | reviver?: ((this: any, key: string, value: any) => any) | undefined; 13 | ``` 14 | -------------------------------------------------------------------------------- /utils-src/node/websocket.server.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from 'ws' 2 | import type WebSocket from 'ws' 3 | import type { CallbackBasedChannel } from 'async-call-rpc' with { 'resolution-mode': 'import' } 4 | 5 | type JSONRPCHandlerCallback = (data: unknown) => Promise 6 | export class WebSocketChannel implements CallbackBasedChannel { 7 | constructor(public server: Server) {} 8 | setup(callback: JSONRPCHandlerCallback) { 9 | const f = (ws: WebSocket) => { 10 | ws.on('message', (data) => callback(data).then((x) => x && ws.send(x as any))) 11 | } 12 | this.server.on('connection', f) 13 | return () => this.server.off('connection', f) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-channel-resolved.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cadd%c(%o, %o%c) 22 | %o %c@0", 23 | "color:#d2c057", 24 | "", 25 | 1, 26 | 2, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: server => client 34 | 35 | ```php 36 | Object { 37 | "id": 0, 38 | "jsonrpc": "2.0", 39 | "result": 3, 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-channel-promise-like.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cadd%c(%o, %o%c) 22 | %o %c@0", 23 | "color:#d2c057", 24 | "", 25 | 1, 26 | 2, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: server => client 34 | 35 | ```php 36 | Object { 37 | "id": 0, 38 | "jsonrpc": "2.0", 39 | "result": 3, 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.parameterstructure.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [parameterStructure](./async-call-rpc.asynccalloptions.parameterstructure.md) 4 | 5 | ## AsyncCallOptions.parameterStructure property 6 | 7 | Choose flavor of parameter structures defined in the spec 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | parameterStructure?: 'by-position' | 'by-name'; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | When using `by-name`, only first parameter is sent to the remote and it must be an object. 18 | 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallloglevel.requestreplay.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) > [requestReplay](./async-call-rpc.asynccallloglevel.requestreplay.md) 4 | 5 | ## AsyncCallLogLevel.requestReplay property 6 | 7 | If log a function that can replay the request 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | requestReplay?: boolean; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | Do not use this options in the production environment because it will log a closure that captures all arguments of requests. This may cause memory leak. 18 | 19 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/generateRandomID.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": "ndom-id-1", 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cadd%c(%o, %o%c) 22 | %o %c@ndom-id-1", 23 | "color:#d2c057", 24 | "", 25 | 1, 26 | 2, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: server => client 34 | 35 | ```php 36 | Object { 37 | "id": "ndom-id-1", 38 | "jsonrpc": "2.0", 39 | "result": 3, 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/async-call-rpc.eventbasedchannel.send.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [EventBasedChannel](./async-call-rpc.eventbasedchannel.md) > [send](./async-call-rpc.eventbasedchannel.send.md) 4 | 5 | ## EventBasedChannel.send() method 6 | 7 | Send the data to the remote side. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | send(data: Data): void | Promise; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | data | Data | The data should send to the remote side. | 20 | 21 | **Returns:** 22 | 23 | void \| Promise<void> 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.abortsignallike.removeeventlistener.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AbortSignalLike](./async-call-rpc.abortsignallike.md) > [removeEventListener](./async-call-rpc.abortsignallike.removeeventlistener.md) 4 | 5 | ## AbortSignalLike.removeEventListener() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | removeEventListener(type: 'abort', listener: () => void): void; 11 | ``` 12 | 13 | ## Parameters 14 | 15 | | Parameter | Type | Description | 16 | | --- | --- | --- | 17 | | type | 'abort' | | 18 | | listener | () => void | | 19 | 20 | **Returns:** 21 | 22 | void 23 | 24 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.encoder.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [encoder](./async-call-rpc.asynccalloptions.encoder.md) 4 | 5 | ## AsyncCallOptions.encoder property 6 | 7 | Encoder of requests and responses. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | encoder?: IsomorphicEncoder | IsomorphicEncoderFull; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | There are some built-in encoders: 18 | 19 | - JSONEncoder: is using JSON.parser and JSON.stringify under the hood. 20 | 21 | -------------------------------------------------------------------------------- /utils-src/web/bson.ts: -------------------------------------------------------------------------------- 1 | import type { Serialization } from 'async-call-rpc' 2 | import type { serialize as S, deserialize as D } from 'bson' 3 | 4 | /** 5 | * @deprecated This will be removed in the next major version. 6 | */ 7 | export const BSON_Serialization = ({ 8 | deserialize, 9 | serialize, 10 | }: { 11 | serialize: typeof S 12 | deserialize: typeof D 13 | }): Serialization => ({ 14 | async deserialization(data: unknown) { 15 | if (data instanceof Blob) data = await data.arrayBuffer() 16 | if (data instanceof ArrayBuffer) data = new Uint8Array(data) 17 | return deserialize(data as any) 18 | }, 19 | serialization(data: any) { 20 | return serialize(data) 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.forcesignal.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [forceSignal](./async-call-rpc.asynccalloptions.forcesignal.md) 4 | 5 | ## AsyncCallOptions.forceSignal property 6 | 7 | AbortSignal to force stop the instance. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | forceSignal?: AbortSignalLike; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | `signal` is used to stop the instance. If the `signal` is aborted, then all new requests will be rejected, and the pending requests will be forcibly rejected and pending results will be ignored. 18 | 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallstrictoptions.methodnotfound.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallStrictOptions](./async-call-rpc.asynccallstrictoptions.md) > [methodNotFound](./async-call-rpc.asynccallstrictoptions.methodnotfound.md) 4 | 5 | ## AsyncCallStrictOptions.methodNotFound property 6 | 7 | Controls if AsyncCall send an ErrorResponse when the requested method is not defined. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | methodNotFound?: boolean; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | If this option is set to false, AsyncCall will ignore the request and print a log if the method is not defined. 18 | 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.serialization.serialization.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Serialization](./async-call-rpc.serialization.md) > [serialization](./async-call-rpc.serialization.serialization.md) 4 | 5 | ## Serialization.serialization() method 6 | 7 | Serialize data 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | serialization(from: any): unknown | PromiseLike; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | from | any | original data | 20 | 21 | **Returns:** 22 | 23 | unknown \| PromiseLike<unknown> 24 | 25 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@works/json-rpc", 3 | "version": "{{version}}", 4 | "exports": { 5 | ".": "./src/Async-Call.ts", 6 | "./full": "./src/index.ts" 7 | }, 8 | "include": ["src/**", "jsr.json"], 9 | // https://github.com/jsr-io/jsr/issues/194 10 | "exclude": [ 11 | "__tests__", 12 | "docs", 13 | "api-extractor-base.json", 14 | "api-extractor.json", 15 | "package.json", 16 | "tsconfig.json", 17 | "src/*.json", 18 | "api", 19 | "examples", 20 | "utils-src", 21 | "vite.config.ts", 22 | "index.html", 23 | "*.yaml", 24 | "rollup.config.mjs", 25 | "utils", 26 | "next-docs" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/deserialize-failed.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | "invalid JSON" 7 | ``` 8 | 9 | ## T=1 Log: server/log 10 | 11 | ```php 12 | Array [ 13 | [SyntaxError: Unexpected token 'i', "invalid JSON" is not valid JSON], 14 | undefined, 15 | undefined, 16 | ] 17 | ``` 18 | 19 | ## T=2 Message: server => client 20 | 21 | ```php 22 | "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32700,\"message\":\"Parse error\",\"data\":{\"stack\":\"\",\"type\":\"SyntaxError\"}}}" 23 | ``` 24 | 25 | ## T=3 Log: client/log 26 | 27 | ```php 28 | Array [ 29 | "SyntaxError: Parse error(-32700) %c@null 30 | %c", 31 | "color: gray", 32 | "", 33 | ] 34 | ``` 35 | -------------------------------------------------------------------------------- /utils-src/web/broadcast.channel.ts: -------------------------------------------------------------------------------- 1 | import type { EventBasedChannel } from 'async-call-rpc' 2 | 3 | /** 4 | * BroadcastChannel support for AsyncCall. 5 | * Please make sure your serializer can convert JSON RPC payload into one of the following data types: 6 | * - Data that can be [structure cloned](http://mdn.io/structure-clone) 7 | */ 8 | export class BroadcastMessageChannel extends BroadcastChannel implements EventBasedChannel { 9 | on(eventListener: (data: unknown) => void) { 10 | const f = (e: MessageEvent): void => eventListener(e.data) 11 | this.addEventListener('message', f) 12 | return () => this.removeEventListener('message', f) 13 | } 14 | send(data: any) { 15 | super.postMessage(data) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/async-call-rpc.clientencoding.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ClientEncoding](./async-call-rpc.clientencoding.md) 4 | 5 | ## ClientEncoding interface 6 | 7 | Encoder of the client. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface ClientEncoding 13 | ``` 14 | 15 | ## Methods 16 | 17 | | Method | Description | 18 | | --- | --- | 19 | | [decodeResponse(encoded)](./async-call-rpc.clientencoding.decoderesponse.md) | Decode the response object. | 20 | | [encodeRequest(data)](./async-call-rpc.clientencoding.encoderequest.md) | Encode the request object. | 21 | 22 | -------------------------------------------------------------------------------- /docs/async-call-rpc.serverencoding.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ServerEncoding](./async-call-rpc.serverencoding.md) 4 | 5 | ## ServerEncoding interface 6 | 7 | Encoder of the server. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface ServerEncoding 13 | ``` 14 | 15 | ## Methods 16 | 17 | | Method | Description | 18 | | --- | --- | 19 | | [decodeRequest(encoded)](./async-call-rpc.serverencoding.decoderequest.md) | Encode the response object. | 20 | | [encodeResponse(data)](./async-call-rpc.serverencoding.encoderesponse.md) | Decode the request object. | 21 | 22 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallloglevel.type.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) > [type](./async-call-rpc.asynccallloglevel.type.md) 4 | 5 | ## AsyncCallLogLevel.type property 6 | 7 | Style of the log 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | type?: 'basic' | 'pretty'; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | If this option is set to "pretty", it will log with some CSS to make the log easier to read in the browser devtools. Check out this article to know more about it: [Console.log with CSS Style](https://dev.to/annlin/consolelog-with-css-style-1mmp) 18 | 19 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-parameterStructures-by-name.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "byPos", 10 | "params": Object { 11 | "a": 1, 12 | "b": 2, 13 | }, 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cbyPos%c(%o%c) 22 | %o %c@0", 23 | "color:#d2c057", 24 | "", 25 | Object { 26 | "a": 1, 27 | "b": 2, 28 | }, 29 | "", 30 | Promise {}, 31 | "color:gray;font-style:italic;", 32 | ] 33 | ``` 34 | 35 | ## T=2 Message: server => client 36 | 37 | ```php 38 | Object { 39 | "id": 0, 40 | "jsonrpc": "2.0", 41 | "result": 3, 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /api-extractor-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | 4 | "extends": "./api-extractor.json", 5 | "mainEntryPointFilePath": "./dist/core/Async-Call.d.ts", 6 | "bundledPackages": [], 7 | "newlineKind": "lf", 8 | "apiReport": { 9 | "enabled": true, 10 | "reportFileName": "base.api.md", 11 | "reportFolder": "./api/", 12 | "reportTempFolder": "/dist/api-extractor-base/" 13 | }, 14 | "docModel": { 15 | "enabled": false 16 | }, 17 | "dtsRollup": { 18 | "enabled": true, 19 | "untrimmedFilePath": "./out/base.d.ts" 20 | }, 21 | "tsdocMetadata": { 22 | "enabled": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.serialization.deserialization.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Serialization](./async-call-rpc.serialization.md) > [deserialization](./async-call-rpc.serialization.deserialization.md) 4 | 5 | ## Serialization.deserialization() method 6 | 7 | Deserialize data 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | deserialization(serialized: unknown): unknown | PromiseLike; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | serialized | unknown | Serialized data | 20 | 21 | **Returns:** 22 | 23 | unknown \| PromiseLike<unknown> 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.log.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [log](./async-call-rpc.asynccalloptions.log.md) 4 | 5 | ## AsyncCallOptions.log property 6 | 7 | Choose log level. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | log?: AsyncCallLogLevel | boolean | 'all'; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | - `true` is a reasonable default value, which means all options are the default in the [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) 18 | 19 | - `false` is disable all logs 20 | 21 | - `"all"` is enable all logs (stronger than `true`). 22 | 23 | -------------------------------------------------------------------------------- /docs/async-call-rpc.notify.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [notify](./async-call-rpc.notify.md) 4 | 5 | ## notify() function 6 | 7 | Wrap the AsyncCall instance to send notification. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export declare function notify(instanceOrFnOnInstance: T): _IgnoreResponse; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | instanceOrFnOnInstance | T | The AsyncCall instance or function on the AsyncCall instance | 20 | 21 | **Returns:** 22 | 23 | \_IgnoreResponse<T> 24 | 25 | ## Example 26 | 27 | const notifyOnly = notify(AsyncCall(...)) 28 | 29 | -------------------------------------------------------------------------------- /docs/async-call-rpc.isomorphicencoder.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [IsomorphicEncoder](./async-call-rpc.isomorphicencoder.md) 4 | 5 | ## IsomorphicEncoder interface 6 | 7 | Encoder that work for both server and client. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface IsomorphicEncoder 13 | ``` 14 | 15 | ## Methods 16 | 17 | | Method | Description | 18 | | --- | --- | 19 | | [decode(encoded)](./async-call-rpc.isomorphicencoder.decode.md) | Decode the request or response object. | 20 | | [encode(data)](./async-call-rpc.isomorphicencoder.encode.md) | Encode the request or response object. | 21 | 22 | -------------------------------------------------------------------------------- /examples/browser.websocket.client.js: -------------------------------------------------------------------------------- 1 | import * as MessagePack from 'https://jspm.dev/@msgpack/msgpack' 2 | // Need to run the build first to get those files. 3 | import { AsyncCall } from '../out/base.mjs' 4 | import { WebSocketMessageChannel } from '../utils/web/websocket.client.js' 5 | import { Msgpack_Serialization } from '../utils/web/msgpack.js' 6 | 7 | /** @type {typeof import('./node.websocket.server').server} */ 8 | const server = AsyncCall( 9 | {}, 10 | { 11 | channel: new WebSocketMessageChannel('ws://localhost:3456/'), 12 | serializer: Msgpack_Serialization(MessagePack), 13 | }, 14 | ) 15 | 16 | window.remote = window.server = server 17 | window.ac = rpc 18 | server.echo 19 | server.now 20 | console.log('Server at window.remote', server, 'library at window.ac', ac) 21 | -------------------------------------------------------------------------------- /docs/async-call-rpc.abortsignallike.addeventlistener.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AbortSignalLike](./async-call-rpc.abortsignallike.md) > [addEventListener](./async-call-rpc.abortsignallike.addeventlistener.md) 4 | 5 | ## AbortSignalLike.addEventListener() method 6 | 7 | **Signature:** 8 | 9 | ```typescript 10 | addEventListener(type: 'abort', listener: () => void, options: { 11 | once: boolean; 12 | }): void; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | type | 'abort' | | 20 | | listener | () => void | | 21 | | options | { once: boolean; } | | 22 | 23 | **Returns:** 24 | 25 | void 26 | 27 | -------------------------------------------------------------------------------- /docs/async-call-rpc.serverencoding.encoderesponse.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ServerEncoding](./async-call-rpc.serverencoding.md) > [encodeResponse](./async-call-rpc.serverencoding.encoderesponse.md) 4 | 5 | ## ServerEncoding.encodeResponse() method 6 | 7 | Decode the request object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | encodeResponse(data: Responses): EncodedResponse | PromiseLike; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | data | [Responses](./async-call-rpc.responses.md) | | 20 | 21 | **Returns:** 22 | 23 | EncodedResponse \| PromiseLike<EncodedResponse> 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.clientencoding.encoderequest.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ClientEncoding](./async-call-rpc.clientencoding.md) > [encodeRequest](./async-call-rpc.clientencoding.encoderequest.md) 4 | 5 | ## ClientEncoding.encodeRequest() method 6 | 7 | Encode the request object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | encodeRequest(data: Requests): EncodedRequest | PromiseLike; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | data | [Requests](./async-call-rpc.requests.md) | The request object | 20 | 21 | **Returns:** 22 | 23 | EncodedRequest \| PromiseLike<EncodedRequest> 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallstrictjsonrpc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallStrictJSONRPC](./async-call-rpc.asynccallstrictjsonrpc.md) 4 | 5 | ## AsyncCallStrictJSONRPC interface 6 | 7 | > Warning: This API is now obsolete. 8 | > 9 | > renamed to [AsyncCallStrictOptions](./async-call-rpc.asynccallstrictoptions.md) 10 | > 11 | 12 | Strict options 13 | 14 | **Signature:** 15 | 16 | ```typescript 17 | export interface AsyncCallStrictJSONRPC extends AsyncCallStrictOptions 18 | ``` 19 | **Extends:** [AsyncCallStrictOptions](./async-call-rpc.asynccallstrictoptions.md) 20 | 21 | ## Remarks 22 | 23 | Control the behavior that different from the JSON-RPC specification. 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallstrictoptions.unknownmessage.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallStrictOptions](./async-call-rpc.asynccallstrictoptions.md) > [unknownMessage](./async-call-rpc.asynccallstrictoptions.unknownmessage.md) 4 | 5 | ## AsyncCallStrictOptions.unknownMessage property 6 | 7 | Controls if AsyncCall send an ErrorResponse when the message is not valid. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | unknownMessage?: boolean; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | If this option is set to false, AsyncCall will ignore the request that cannot be parsed as a valid JSON RPC payload. This is useful when the message channel is also used to transfer other kinds of messages. 18 | 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.serverencoding.decoderequest.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ServerEncoding](./async-call-rpc.serverencoding.md) > [decodeRequest](./async-call-rpc.serverencoding.decoderequest.md) 4 | 5 | ## ServerEncoding.decodeRequest() method 6 | 7 | Encode the response object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | decodeRequest(encoded: EncodedRequest): Requests | PromiseLike; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | encoded | EncodedRequest | | 20 | 21 | **Returns:** 22 | 23 | [Requests](./async-call-rpc.requests.md) \| PromiseLike<[Requests](./async-call-rpc.requests.md)> 24 | 25 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-impl-pending.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "Request should not resolve before this line", 8 | ] 9 | ``` 10 | 11 | ## T=1 Message: client => server 12 | 13 | ```php 14 | Object { 15 | "id": 0, 16 | "jsonrpc": "2.0", 17 | "method": "add", 18 | "params": Array [ 19 | 1, 20 | 2, 21 | ], 22 | } 23 | ``` 24 | 25 | ## T=2 Log: server/log 26 | 27 | ```php 28 | Array [ 29 | "rpc.%cadd%c(%o, %o%c) 30 | %o %c@0", 31 | "color:#d2c057", 32 | "", 33 | 1, 34 | 2, 35 | "", 36 | Promise {}, 37 | "color:gray;font-style:italic;", 38 | ] 39 | ``` 40 | 41 | ## T=3 Message: server => client 42 | 43 | ```php 44 | Object { 45 | "id": 0, 46 | "jsonrpc": "2.0", 47 | "result": 3, 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.parameterstructures.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [parameterStructures](./async-call-rpc.asynccalloptions.parameterstructures.md) 4 | 5 | ## AsyncCallOptions.parameterStructures property 6 | 7 | > Warning: This API is now obsolete. 8 | > 9 | > renamed to "parameterStructure" 10 | > 11 | 12 | Choose flavor of parameter structures defined in the spec 13 | 14 | **Signature:** 15 | 16 | ```typescript 17 | parameterStructures?: 'by-position' | 'by-name'; 18 | ``` 19 | 20 | ## Remarks 21 | 22 | When using `by-name`, only first parameter is sent to the remote and it must be an object. 23 | 24 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-channel-pending.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "Request should not resolve before this line", 8 | ] 9 | ``` 10 | 11 | ## T=1 Message: client => server 12 | 13 | ```php 14 | Object { 15 | "id": 0, 16 | "jsonrpc": "2.0", 17 | "method": "add", 18 | "params": Array [ 19 | 1, 20 | 2, 21 | ], 22 | } 23 | ``` 24 | 25 | ## T=2 Log: server/log 26 | 27 | ```php 28 | Array [ 29 | "rpc.%cadd%c(%o, %o%c) 30 | %o %c@0", 31 | "color:#d2c057", 32 | "", 33 | 1, 34 | 2, 35 | "", 36 | Promise {}, 37 | "color:gray;font-style:italic;", 38 | ] 39 | ``` 40 | 41 | ## T=3 Message: server => client 42 | 43 | ```php 44 | Object { 45 | "id": 0, 46 | "jsonrpc": "2.0", 47 | "result": 3, 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.preferlocalimplementation.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [preferLocalImplementation](./async-call-rpc.asynccalloptions.preferlocalimplementation.md) 4 | 5 | ## AsyncCallOptions.preferLocalImplementation property 6 | 7 | Prefer local implementation than remote. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | preferLocalImplementation?: boolean; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | If you call a RPC method and it is also defined in the local, open this flag will call the local implementation directly instead of send a RPC request. No logs / serialization will be performed if a local implementation is used. 18 | 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asyncversionof.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncVersionOf](./async-call-rpc.asyncversionof.md) 4 | 5 | ## AsyncVersionOf type 6 | 7 | Make all functions in T becomes an async function and filter non-Functions out. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export type AsyncVersionOf = T extends Record PromiseLike> ? 'then' extends keyof T ? Omit, 'then'> : T : _AsyncVersionOf; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | Only generics signatures on function that returning an Promise will be preserved due to the limitation of TypeScript. 18 | 19 | Method called `then` are intentionally removed because it is very likely to be a foot gun in promise auto-unwrap. 20 | 21 | -------------------------------------------------------------------------------- /docs/async-call-rpc.clientencoding.decoderesponse.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ClientEncoding](./async-call-rpc.clientencoding.md) > [decodeResponse](./async-call-rpc.clientencoding.decoderesponse.md) 4 | 5 | ## ClientEncoding.decodeResponse() method 6 | 7 | Decode the response object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | decodeResponse(encoded: EncodedResponse): Responses | PromiseLike; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | encoded | EncodedResponse | The encoded response object | 20 | 21 | **Returns:** 22 | 23 | [Responses](./async-call-rpc.responses.md) \| PromiseLike<[Responses](./async-call-rpc.responses.md)> 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.eventbasedchannel.on.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [EventBasedChannel](./async-call-rpc.eventbasedchannel.md) > [on](./async-call-rpc.eventbasedchannel.on.md) 4 | 5 | ## EventBasedChannel.on() method 6 | 7 | Register the message listener. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | on(listener: (data: Data, hint?: 'request' | 'response' | undefined) => void): void | (() => void); 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | listener | (data: Data, hint?: 'request' \| 'response' \| undefined) => void | The message listener. | 20 | 21 | **Returns:** 22 | 23 | void \| (() => void) 24 | 25 | a function that unregister the listener. 26 | 27 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/bad-data.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | 0 7 | ``` 8 | 9 | ## T=1 Message: client => server 10 | 11 | ```php 12 | Object {} 13 | ``` 14 | 15 | ## T=2 Message: client => server 16 | 17 | ```php 18 | Object { 19 | "jsonrpc": "1.0", 20 | } 21 | ``` 22 | 23 | ## T=3 Message: client => server 24 | 25 | ```php 26 | Object { 27 | "jsonrpc": "2.0", 28 | "params": 1, 29 | } 30 | ``` 31 | 32 | ## T=4 Message: client => server 33 | 34 | ```php 35 | Object { 36 | "error": Object { 37 | "code": -1, 38 | "message": "what", 39 | }, 40 | "id": null, 41 | "jsonrpc": "2.0", 42 | } 43 | ``` 44 | 45 | ## T=5 Log: server/log 46 | 47 | ```php 48 | Array [ 49 | "Error: what(-1) %c@null 50 | %c", 51 | "color: gray", 52 | "", 53 | ] 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/async-call-rpc.serialization.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Serialization](./async-call-rpc.serialization.md) 4 | 5 | ## Serialization interface 6 | 7 | > Warning: This API is now obsolete. 8 | > 9 | > Use [IsomorphicEncoder](./async-call-rpc.isomorphicencoder.md) instead. 10 | > 11 | 12 | Serialize and deserialize of the JSON-RPC payload 13 | 14 | **Signature:** 15 | 16 | ```typescript 17 | export interface Serialization 18 | ``` 19 | 20 | ## Methods 21 | 22 | | Method | Description | 23 | | --- | --- | 24 | | [deserialization(serialized)](./async-call-rpc.serialization.deserialization.md) | Deserialize data | 25 | | [serialization(from)](./async-call-rpc.serialization.serialization.md) | Serialize data | 26 | 27 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asyncgeneratorversionof.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncGeneratorVersionOf](./async-call-rpc.asyncgeneratorversionof.md) 4 | 5 | ## AsyncGeneratorVersionOf type 6 | 7 | Make all generator in the type T becomes AsyncGenerator 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export type AsyncGeneratorVersionOf = T extends Record ? 'then' extends keyof T ? Omit, 'then'> : T : _AsyncGeneratorVersionOf; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | Only generics signatures on function that returning an AsyncGenerator will be preserved due to the limitation of TypeScript. 18 | 19 | Method called `then` are intentionally removed because it is very likely to be a foot gun in promise auto-unwrap. 20 | 21 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errorresponsedetail.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorResponseDetail](./async-call-rpc.errorresponsedetail.md) 4 | 5 | ## ErrorResponseDetail interface 6 | 7 | The "error" record on the JSON-RPC ErrorResponse object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface ErrorResponseDetail 13 | ``` 14 | 15 | ## Properties 16 | 17 | | Property | Modifiers | Type | Description | 18 | | --- | --- | --- | --- | 19 | | [code](./async-call-rpc.errorresponsedetail.code.md) | readonly | number | | 20 | | [data?](./async-call-rpc.errorresponsedetail.data.md) | readonly | Error | _(Optional)_ | 21 | | [message](./async-call-rpc.errorresponsedetail.message.md) | readonly | string | | 22 | 23 | -------------------------------------------------------------------------------- /utils-src/web/websocket.client.ts: -------------------------------------------------------------------------------- 1 | import type { EventBasedChannel } from 'async-call-rpc' 2 | 3 | /** 4 | * WebSocket support for AsyncCall. 5 | * Please make sure your serializer can convert JSON RPC payload into one of the following data types: 6 | * 7 | * - string 8 | * - ArrayBuffer 9 | * - SharedArrayBuffer 10 | * - Blob 11 | * - ArrayBufferView 12 | */ 13 | export class WebSocketMessageChannel extends WebSocket implements EventBasedChannel { 14 | on(listener: (data: unknown) => void) { 15 | const f = (e: MessageEvent) => listener(e.data) 16 | this.addEventListener('message', f) 17 | return () => this.removeEventListener('message', f) 18 | } 19 | send(data: any): void { 20 | if (this.readyState === this.CONNECTING) { 21 | this.addEventListener('open', () => this.send(data), { once: true }) 22 | } else super.send(data) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-default-logger.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "In other tests we provide logger.", 8 | "So we'd test no logger here.", 9 | "It should use globalThis.logger", 10 | ] 11 | ``` 12 | 13 | ## T=1 Message: client => server 14 | 15 | ```php 16 | Object { 17 | "id": 0, 18 | "jsonrpc": "2.0", 19 | "method": "add", 20 | "params": Array [ 21 | 1, 22 | 2, 23 | ], 24 | } 25 | ``` 26 | 27 | ## T=2 Log: testRunner/log 28 | 29 | ```php 30 | Array [ 31 | "rpc.%cadd%c(%o, %o%c) 32 | %o %c@0", 33 | "color:#d2c057", 34 | "", 35 | 1, 36 | 2, 37 | "", 38 | Promise {}, 39 | "color:gray;font-style:italic;", 40 | ] 41 | ``` 42 | 43 | ## T=3 Message: server => client 44 | 45 | ```php 46 | Object { 47 | "id": 0, 48 | "jsonrpc": "2.0", 49 | "result": 3, 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-non-strict.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client sent 4 | 5 | ```php 6 | "unknown message" 7 | ``` 8 | 9 | ## T=1 Message: client sent 10 | 11 | ```php 12 | Object { 13 | "id": 0, 14 | "jsonrpc": "2.0", 15 | "method": "not_defined_method", 16 | "params": Array [], 17 | } 18 | ``` 19 | 20 | ## T=2 Message: server received 21 | 22 | ```php 23 | "unknown message" 24 | ``` 25 | 26 | ## T=3 Message: server received 27 | 28 | ```php 29 | Object { 30 | "id": 0, 31 | "jsonrpc": "2.0", 32 | "method": "not_defined_method", 33 | "params": Array [], 34 | } 35 | ``` 36 | 37 | ## T=4 Log: server/log 38 | 39 | ```php 40 | Array [ 41 | "Missing method", 42 | "not_defined_method", 43 | Object { 44 | "id": 0, 45 | "jsonrpc": "2.0", 46 | "method": "not_defined_method", 47 | "params": Array [], 48 | }, 49 | ] 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/async-call-rpc.errorresponse.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ErrorResponse](./async-call-rpc.errorresponse.md) 4 | 5 | ## ErrorResponse interface 6 | 7 | JSON-RPC ErrorResponse object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface ErrorResponse 13 | ``` 14 | 15 | ## Properties 16 | 17 | | Property | Modifiers | Type | Description | 18 | | --- | --- | --- | --- | 19 | | [error](./async-call-rpc.errorresponse.error.md) | readonly | [ErrorResponseDetail](./async-call-rpc.errorresponsedetail.md)<Error> | | 20 | | [id?](./async-call-rpc.errorresponse.id.md) | readonly | [ID](./async-call-rpc.id.md) | _(Optional)_ | 21 | | [jsonrpc](./async-call-rpc.errorresponse.jsonrpc.md) | readonly | '2.0' | | 22 | 23 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-log-false.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Message: server => client 18 | 19 | ```php 20 | Object { 21 | "id": 0, 22 | "jsonrpc": "2.0", 23 | "result": 3, 24 | } 25 | ``` 26 | 27 | ## T=2 Message: client => server 28 | 29 | ```php 30 | Object { 31 | "id": 1, 32 | "jsonrpc": "2.0", 33 | "method": "throws", 34 | "params": Array [], 35 | } 36 | ``` 37 | 38 | ## T=3 Message: server => client 39 | 40 | ```php 41 | Object { 42 | "error": Object { 43 | "code": -1, 44 | "data": Object { 45 | "type": "Error", 46 | }, 47 | "message": "impl error", 48 | }, 49 | "id": 1, 50 | "jsonrpc": "2.0", 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/async-call-rpc.isomorphicencoder.encode.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [IsomorphicEncoder](./async-call-rpc.isomorphicencoder.md) > [encode](./async-call-rpc.isomorphicencoder.encode.md) 4 | 5 | ## IsomorphicEncoder.encode() method 6 | 7 | Encode the request or response object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | encode(data: Requests | Responses): EncodedRequest | EncodedResponse | PromiseLike; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | data | [Requests](./async-call-rpc.requests.md) \| [Responses](./async-call-rpc.responses.md) | The request or response object | 20 | 21 | **Returns:** 22 | 23 | EncodedRequest \| EncodedResponse \| PromiseLike<EncodedRequest \| EncodedResponse> 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.successresponse.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [SuccessResponse](./async-call-rpc.successresponse.md) 4 | 5 | ## SuccessResponse interface 6 | 7 | JSON-RPC SuccessResponse object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface SuccessResponse 13 | ``` 14 | 15 | ## Properties 16 | 17 | | Property | Modifiers | Type | Description | 18 | | --- | --- | --- | --- | 19 | | [id?](./async-call-rpc.successresponse.id.md) | readonly | [ID](./async-call-rpc.id.md) | _(Optional)_ | 20 | | [jsonrpc](./async-call-rpc.successresponse.jsonrpc.md) | readonly | '2.0' | | 21 | | [result](./async-call-rpc.successresponse.result.md) | | unknown | | 22 | | [undef?](./async-call-rpc.successresponse.undef.md) | | unknown | _(Optional)_ Non-standard property | 23 | 24 | -------------------------------------------------------------------------------- /docs/async-call-rpc.eventbasedchannel.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [EventBasedChannel](./async-call-rpc.eventbasedchannel.md) 4 | 5 | ## EventBasedChannel interface 6 | 7 | This interface represents a "on message" - "send response" model. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface EventBasedChannel 13 | ``` 14 | 15 | ## Remarks 16 | 17 | Usually used for there is only 1 remote (act like a client). Example: [Example for EventBasedChannel](https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/node/websocket.server.ts) 18 | 19 | ## Methods 20 | 21 | | Method | Description | 22 | | --- | --- | 23 | | [on(listener)](./async-call-rpc.eventbasedchannel.on.md) | Register the message listener. | 24 | | [send(data)](./async-call-rpc.eventbasedchannel.send.md) | Send the data to the remote side. | 25 | 26 | -------------------------------------------------------------------------------- /docs/async-call-rpc.isomorphicencoderfull.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [IsomorphicEncoderFull](./async-call-rpc.isomorphicencoderfull.md) 4 | 5 | ## IsomorphicEncoderFull interface 6 | 7 | Encoder that work for both server and client. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface IsomorphicEncoderFull extends ClientEncoding, ServerEncoding, Partial> 13 | ``` 14 | **Extends:** [ClientEncoding](./async-call-rpc.clientencoding.md)<EncodedRequest, EncodedResponse>, [ServerEncoding](./async-call-rpc.serverencoding.md)<EncodedRequest, EncodedResponse>, Partial<Pick<[IsomorphicEncoder](./async-call-rpc.isomorphicencoder.md), 'decode'>> 15 | 16 | -------------------------------------------------------------------------------- /docs/async-call-rpc.isomorphicencoder.decode.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [IsomorphicEncoder](./async-call-rpc.isomorphicencoder.md) > [decode](./async-call-rpc.isomorphicencoder.decode.md) 4 | 5 | ## IsomorphicEncoder.decode() method 6 | 7 | Decode the request or response object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | decode(encoded: EncodedRequest | EncodedResponse): Requests | Responses | PromiseLike; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | encoded | EncodedRequest \| EncodedResponse | The encoded request or response object | 20 | 21 | **Returns:** 22 | 23 | [Requests](./async-call-rpc.requests.md) \| [Responses](./async-call-rpc.responses.md) \| PromiseLike<[Requests](./async-call-rpc.requests.md) \| [Responses](./async-call-rpc.responses.md)> 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.channel.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [channel](./async-call-rpc.asynccalloptions.channel.md) 4 | 5 | ## AsyncCallOptions.channel property 6 | 7 | The message channel to exchange messages between server and client 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | channel: CallbackBasedChannel | EventBasedChannel | Promise | EventBasedChannel>; 13 | ``` 14 | 15 | ## Example 16 | 17 | [Example for CallbackBasedChannel](https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/web/websocket.client.ts) or [Example for EventBasedChannel](https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/node/websocket.server.ts) 18 | 19 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallstrictoptions.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallStrictOptions](./async-call-rpc.asynccallstrictoptions.md) 4 | 5 | ## AsyncCallStrictOptions interface 6 | 7 | Strict options 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface AsyncCallStrictOptions 13 | ``` 14 | 15 | ## Remarks 16 | 17 | Control the behavior that different from the JSON-RPC specification. 18 | 19 | ## Properties 20 | 21 | | Property | Modifiers | Type | Description | 22 | | --- | --- | --- | --- | 23 | | [methodNotFound?](./async-call-rpc.asynccallstrictoptions.methodnotfound.md) | | boolean | _(Optional)_ Controls if AsyncCall send an ErrorResponse when the requested method is not defined. | 24 | | [unknownMessage?](./async-call-rpc.asynccallstrictoptions.unknownmessage.md) | | boolean | _(Optional)_ Controls if AsyncCall send an ErrorResponse when the message is not valid. | 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | id-token: write 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - uses: pnpm/action-setup@v2 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: "20" 26 | cache: "pnpm" 27 | - run: pnpm install 28 | - run: npm install -g npm 29 | - run: pnpm run build 30 | - run: pnpm run doc:api 31 | - uses: changesets/action@v1 32 | with: 33 | publish: pnpm run ci:release 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | NPM_CONFIG_PROVENANCE: true 39 | -------------------------------------------------------------------------------- /docs/async-call-rpc.callbackbasedchannel.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [CallbackBasedChannel](./async-call-rpc.callbackbasedchannel.md) 4 | 5 | ## CallbackBasedChannel interface 6 | 7 | This interface represents a "callback" model. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface CallbackBasedChannel extends Partial> 13 | ``` 14 | **Extends:** Partial<[EventBasedChannel](./async-call-rpc.eventbasedchannel.md)<Data>> 15 | 16 | ## Remarks 17 | 18 | Usually used for there are many remotes (act like a server). Example: [Example for CallbackBasedChannel](https://github.com/Jack-Works/async-call-rpc/blob/main/utils-src/web/websocket.client.ts) 19 | 20 | ## Methods 21 | 22 | | Method | Description | 23 | | --- | --- | 24 | | [setup(jsonRPCHandlerCallback, isValidJSONRPCPayload)](./async-call-rpc.callbackbasedchannel.setup.md) | Setup the CallbackBasedChannel. | 25 | 26 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-strict-partial.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client sent 4 | 5 | ```php 6 | "unknown message" 7 | ``` 8 | 9 | ## T=1 Message: client sent 10 | 11 | ```php 12 | Object { 13 | "id": 0, 14 | "jsonrpc": "2.0", 15 | "method": "not_defined_method", 16 | "params": Array [], 17 | } 18 | ``` 19 | 20 | ## T=2 Message: server received 21 | 22 | ```php 23 | "unknown message" 24 | ``` 25 | 26 | ## T=3 Message: server received 27 | 28 | ```php 29 | Object { 30 | "id": 0, 31 | "jsonrpc": "2.0", 32 | "method": "not_defined_method", 33 | "params": Array [], 34 | } 35 | ``` 36 | 37 | ## T=4 Message: server => client 38 | 39 | ```php 40 | Object { 41 | "error": Object { 42 | "code": -32601, 43 | "message": "Method not found", 44 | }, 45 | "id": 0, 46 | "jsonrpc": "2.0", 47 | } 48 | ``` 49 | 50 | ## T=5 Log: client/log 51 | 52 | ```php 53 | Array [ 54 | "Error: Method not found(-32601) %c@0 55 | %c", 56 | "color: gray", 57 | "", 58 | ] 59 | ``` 60 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/encoder-json.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"undefined\",\"params\":[]}" 7 | ``` 8 | 9 | ## T=1 Log: server/log 10 | 11 | ```php 12 | Array [ 13 | "rpc.%cundefined%c(%c) 14 | %o %c@0", 15 | "color:#d2c057", 16 | "", 17 | "", 18 | Promise {}, 19 | "color:gray;font-style:italic;", 20 | ] 21 | ``` 22 | 23 | ## T=2 Message: server => client 24 | 25 | ```php 26 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":null}" 27 | ``` 28 | 29 | ## T=3 Message: client => server 30 | 31 | ```php 32 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"deep_undefined\",\"params\":[]}" 33 | ``` 34 | 35 | ## T=4 Log: server/log 36 | 37 | ```php 38 | Array [ 39 | "rpc.%cdeep_undefined%c(%c) 40 | %o %c@1", 41 | "color:#d2c057", 42 | "", 43 | "", 44 | Promise {}, 45 | "color:gray;font-style:italic;", 46 | ] 47 | ``` 48 | 49 | ## T=5 Message: server => client 50 | 51 | ```php 52 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"a\":{\"b\":{}}}}" 53 | ``` 54 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/encoder-json-Default.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"undefined\",\"params\":[]}" 7 | ``` 8 | 9 | ## T=1 Log: server/log 10 | 11 | ```php 12 | Array [ 13 | "rpc.%cundefined%c(%c) 14 | %o %c@0", 15 | "color:#d2c057", 16 | "", 17 | "", 18 | Promise {}, 19 | "color:gray;font-style:italic;", 20 | ] 21 | ``` 22 | 23 | ## T=2 Message: server => client 24 | 25 | ```php 26 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":null}" 27 | ``` 28 | 29 | ## T=3 Message: client => server 30 | 31 | ```php 32 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"deep_undefined\",\"params\":[]}" 33 | ``` 34 | 35 | ## T=4 Log: server/log 36 | 37 | ```php 38 | Array [ 39 | "rpc.%cdeep_undefined%c(%c) 40 | %o %c@1", 41 | "color:#d2c057", 42 | "", 43 | "", 44 | Promise {}, 45 | "color:gray;font-style:italic;", 46 | ] 47 | ``` 48 | 49 | ## T=5 Message: server => client 50 | 51 | ```php 52 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"a\":{\"b\":{}}}}" 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/async-call-rpc.request.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [Request](./async-call-rpc.request.md) 4 | 5 | ## Request interface 6 | 7 | JSON-RPC Request object. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface Request 13 | ``` 14 | 15 | ## Properties 16 | 17 | | Property | Modifiers | Type | Description | 18 | | --- | --- | --- | --- | 19 | | [id?](./async-call-rpc.request.id.md) | readonly | [ID](./async-call-rpc.id.md) | _(Optional)_ | 20 | | [jsonrpc](./async-call-rpc.request.jsonrpc.md) | readonly | '2.0' | | 21 | | [method](./async-call-rpc.request.method.md) | readonly | string | | 22 | | [params](./async-call-rpc.request.params.md) | readonly | readonly unknown\[\] \| object | | 23 | | [remoteStack?](./async-call-rpc.request.remotestack.md) | readonly | string \| undefined | _(Optional)_ Non-standard field. It records the caller's stack of this Request. | 24 | 25 | -------------------------------------------------------------------------------- /docs/async-call-rpc.successresponse.undef.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [SuccessResponse](./async-call-rpc.successresponse.md) > [undef](./async-call-rpc.successresponse.undef.md) 4 | 5 | ## SuccessResponse.undef property 6 | 7 | Non-standard property 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | undef?: unknown; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | This is a non-standard field that used to represent the result field should be `undefined` instead of `null`. 18 | 19 | A field with value `undefined` will be omitted in `JSON.stringify`, and if the `"result"` field is omitted, this is no longer a valid JSON-RPC response object. 20 | 21 | By default, AsyncCall will convert `undefined` to `null` to keep the response valid, but it \_won't\_ add this field. 22 | 23 | Set `keepUndefined` in JSONEncoderOptions to `"keep"` will add this field. 24 | 25 | This field starts with a space, so TypeScript will hide it when providing completion. 26 | 27 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/DOMException.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "DOMException", 10 | "params": Array [], 11 | } 12 | ``` 13 | 14 | ## T=1 Log: server/log 15 | 16 | ```php 17 | Array [ 18 | "rpc.%cDOMException%c(%c) 19 | %o %c@0", 20 | "color:#d2c057", 21 | "", 22 | "", 23 | Promise {}, 24 | "color:gray;font-style:italic;", 25 | ] 26 | ``` 27 | 28 | ## T=2 Log: server/log 29 | 30 | ```php 31 | Array [ 32 | [name: message], 33 | ] 34 | ``` 35 | 36 | ## T=3 Message: server => client 37 | 38 | ```php 39 | Object { 40 | "error": Object { 41 | "code": -1, 42 | "data": Object { 43 | "type": "DOMException:name", 44 | }, 45 | "message": "message", 46 | }, 47 | "id": 0, 48 | "jsonrpc": "2.0", 49 | } 50 | ``` 51 | 52 | ## T=4 Log: client/log 53 | 54 | ```php 55 | Array [ 56 | "DOMException:name: message(-1) %c@0 57 | %c", 58 | "color: gray", 59 | "", 60 | ] 61 | ``` 62 | -------------------------------------------------------------------------------- /__tests__/notify.ts: -------------------------------------------------------------------------------- 1 | import { notify } from '../src/index.js' 2 | import { withSnapshotDefault } from './utils/test.js' 3 | import { expect, it } from 'vitest' 4 | 5 | it( 6 | 'can send notify-only request', 7 | withSnapshotDefault('async-call-notify', async ({ init, log }) => { 8 | const orig_s = init() 9 | const server = notify(orig_s) 10 | log( 11 | 'Before this line should have no response.', 12 | 'Even throwing functions should succeed too.', 13 | 'No response should be sent from the server.', 14 | ) 15 | await expect(server.add(2, 3)).resolves.toBeUndefined() 16 | await expect(server.throws()).resolves.toBeUndefined() 17 | await expect((server as any).not_found()).resolves.toBeUndefined() 18 | 19 | // Should also works for the function on the instance 20 | await expect(notify(server.echo)(1)).resolves.toBeUndefined() 21 | await expect(notify(orig_s.throws)()).resolves.toBeUndefined() 22 | 23 | // Keep identity 24 | expect(server.add).toBe(server.add) 25 | }), 26 | ) 27 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-custom-error-mapper.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "throws", 10 | "params": Array [], 11 | } 12 | ``` 13 | 14 | ## T=1 Log: server/log 15 | 16 | ```php 17 | Array [ 18 | "rpc.%cthrows%c(%c) 19 | %o %c@0", 20 | "color:#d2c057", 21 | "", 22 | "", 23 | Promise {}, 24 | "color:gray;font-style:italic;", 25 | ] 26 | ``` 27 | 28 | ## T=2 Log: server/log 29 | 30 | ```php 31 | Array [ 32 | [Error: impl error], 33 | ] 34 | ``` 35 | 36 | ## T=3 Message: server => client 37 | 38 | ```php 39 | Object { 40 | "error": Object { 41 | "code": -233, 42 | "data": Object { 43 | "custom_data": true, 44 | }, 45 | "message": "Oh my message", 46 | }, 47 | "id": 0, 48 | "jsonrpc": "2.0", 49 | } 50 | ``` 51 | 52 | ## T=4 Log: client/log 53 | 54 | ```php 55 | Array [ 56 | "Error: Oh my message(-233) %c@0 57 | %c", 58 | "color: gray", 59 | "", 60 | ] 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/async-call-rpc.consoleinterface.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [ConsoleInterface](./async-call-rpc.consoleinterface.md) 4 | 5 | ## ConsoleInterface interface 6 | 7 | The minimal Console interface that AsyncCall needs. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface ConsoleInterface 13 | ``` 14 | 15 | ## Remarks 16 | 17 | The method not provided will use "log" as it's fallback. 18 | 19 | ## Methods 20 | 21 | | Method | Description | 22 | | --- | --- | 23 | | [debug(args)?](./async-call-rpc.consoleinterface.debug.md) | _(Optional)_ | 24 | | [error(args)?](./async-call-rpc.consoleinterface.error.md) | _(Optional)_ | 25 | | [groupCollapsed(args)?](./async-call-rpc.consoleinterface.groupcollapsed.md) | _(Optional)_ | 26 | | [groupEnd(args)?](./async-call-rpc.consoleinterface.groupend.md) | _(Optional)_ | 27 | | [log(args)](./async-call-rpc.consoleinterface.log.md) | | 28 | | [warn(args)?](./async-call-rpc.consoleinterface.warn.md) | _(Optional)_ | 29 | 30 | -------------------------------------------------------------------------------- /docs/async-call-rpc.jsonencoderoptions.keepundefined.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [JSONEncoderOptions](./async-call-rpc.jsonencoderoptions.md) > [keepUndefined](./async-call-rpc.jsonencoderoptions.keepundefined.md) 4 | 5 | ## JSONEncoderOptions.keepUndefined property 6 | 7 | How to handle `"undefined"` in the result of [SuccessResponse](./async-call-rpc.successresponse.md). 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | keepUndefined?: false | 'null' | undefined; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | If you need a full support of encoding `undefined`, for example, when the result is `{ field: undefined }` and you want to keep it, you need to find another library to do this. 18 | 19 | If this is not handled properly, `JSON.stringify` will emit an invalid JSON-RPC object (fields with `undefined` value will be omitted). 20 | 21 | Options: - `"null"`(\*\*default\*\*): convert it to `null`. - `false`: do not do anything, let it break. 22 | 23 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/recover-error-with-bad-implementation.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "DOMException", 10 | "params": Array [], 11 | } 12 | ``` 13 | 14 | ## T=1 Log: server/log 15 | 16 | ```php 17 | Array [ 18 | "rpc.%cDOMException%c(%c) 19 | %o %c@0", 20 | "color:#d2c057", 21 | "", 22 | "", 23 | Promise {}, 24 | "color:gray;font-style:italic;", 25 | ] 26 | ``` 27 | 28 | ## T=2 Log: server/log 29 | 30 | ```php 31 | Array [ 32 | [name: message], 33 | ] 34 | ``` 35 | 36 | ## T=3 Message: server => client 37 | 38 | ```php 39 | Object { 40 | "error": Object { 41 | "code": -1, 42 | "data": Object { 43 | "type": "DOMException:name", 44 | }, 45 | "message": "message", 46 | }, 47 | "id": 0, 48 | "jsonrpc": "2.0", 49 | } 50 | ``` 51 | 52 | ## T=4 Log: client/log 53 | 54 | ```php 55 | Array [ 56 | "DOMException:name: message(-1) %c@0 57 | %c", 58 | "color: gray", 59 | "", 60 | ] 61 | ``` 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jack Works 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Project composition 4 | "composite": true, 5 | "incremental": true, 6 | 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "noEmitOnError": true, 11 | 12 | // Build target 13 | "target": "ES2018", 14 | "lib": ["ES2018"], 15 | "module": "NodeNext", 16 | "moduleResolution": "NodeNext", 17 | "skipLibCheck": true, 18 | 19 | // Strict 20 | "strict": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "verbatimModuleSyntax": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noImplicitReturns": true, 25 | // "noPropertyAccessFromIndexSignature": true, 26 | "noUncheckedIndexedAccess": true, 27 | "noUnusedLocals": true, 28 | "noUnusedParameters": true, 29 | 30 | // Spec compliance 31 | "useDefineForClassFields": true 32 | }, 33 | "references": [ 34 | { "path": "./src/tsconfig.json" }, 35 | { "path": "./utils-src/node/tsconfig.json" }, 36 | { "path": "./utils-src/web/tsconfig.json" }, 37 | { "path": "./__tests__/tsconfig.json" } 38 | ], 39 | "files": [] 40 | } 41 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/bad-state-generator-non-strict.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": "a", 8 | "jsonrpc": "2.0", 9 | "method": "rpc.async-iterator.next", 10 | "params": Array [ 11 | "b", 12 | undefined, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%crpc.async-iterator.next%c(%o, %o%c) 22 | %o %c@a", 23 | "color:#d2c057", 24 | "", 25 | "b", 26 | undefined, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: client => server 34 | 35 | ```php 36 | Object { 37 | "id": 0, 38 | "jsonrpc": "2.0", 39 | "method": "rpc.async-iterator.start", 40 | "params": Array [ 41 | "not_found", 42 | Array [], 43 | ], 44 | } 45 | ``` 46 | 47 | ## T=3 Log: server/log 48 | 49 | ```php 50 | Array [ 51 | "rpc.%crpc.async-iterator.start%c(%o, %o%c) 52 | %o %c@0", 53 | "color:#d2c057", 54 | "", 55 | "not_found", 56 | Array [], 57 | "", 58 | Promise {}, 59 | "color:gray;font-style:italic;", 60 | ] 61 | ``` 62 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-batch-notify-3.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "No log before this file", 8 | ] 9 | ``` 10 | 11 | ## T=1 Message: client => server 12 | 13 | ```php 14 | Array [ 15 | Object { 16 | "jsonrpc": "2.0", 17 | "method": "add", 18 | "params": Array [ 19 | 1, 20 | 2, 21 | ], 22 | }, 23 | Object { 24 | "jsonrpc": "2.0", 25 | "method": "throws", 26 | "params": Array [], 27 | }, 28 | ] 29 | ``` 30 | 31 | ## T=2 Log: server/log 32 | 33 | ```php 34 | Array [ 35 | "rpc.%cadd%c(%o, %o%c) 36 | %o %c@undefined", 37 | "color:#d2c057", 38 | "", 39 | 1, 40 | 2, 41 | "", 42 | Promise {}, 43 | "color:gray;font-style:italic;", 44 | ] 45 | ``` 46 | 47 | ## T=3 Log: server/log 48 | 49 | ```php 50 | Array [ 51 | "rpc.%cthrows%c(%c) 52 | %o %c@undefined", 53 | "color:#d2c057", 54 | "", 55 | "", 56 | Promise {}, 57 | "color:gray;font-style:italic;", 58 | ] 59 | ``` 60 | 61 | ## T=4 Log: server/log 62 | 63 | ```php 64 | Array [ 65 | [Error: impl error], 66 | ] 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/async-call-rpc.batch.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [batch](./async-call-rpc.batch.md) 4 | 5 | ## batch() function 6 | 7 | Wrap the AsyncCall instance to use batch call. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export declare function batch(asyncCallInstance: T): [T, () => void, (error?: unknown) => void]; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | asyncCallInstance | T | The AsyncCall instance | 20 | 21 | **Returns:** 22 | 23 | \[T, () => void, (error?: unknown) => void\] 24 | 25 | It will return a tuple. 26 | 27 | The first item is the batched version of AsyncCall instance passed in. 28 | 29 | The second item is a function to send all pending requests. 30 | 31 | The third item is a function to drop and reject all pending requests. 32 | 33 | ## Example 34 | 35 | const \[batched, send, drop\] = batch(AsyncCall(...)) batched.call1() // pending batched.call2() // pending send() // send all pending requests drop() // drop all pending requests 36 | 37 | -------------------------------------------------------------------------------- /docs/async-call-rpc.abortsignallike.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AbortSignalLike](./async-call-rpc.abortsignallike.md) 4 | 5 | ## AbortSignalLike interface 6 | 7 | AbortSignal 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface AbortSignalLike 13 | ``` 14 | 15 | ## Remarks 16 | 17 | This is a subset of the AbortSignal interface defined in the \[WinterCG\](https://wintercg.org/). 18 | 19 | ## Properties 20 | 21 | | Property | Modifiers | Type | Description | 22 | | --- | --- | --- | --- | 23 | | [aborted](./async-call-rpc.abortsignallike.aborted.md) | readonly | boolean | | 24 | | [reason](./async-call-rpc.abortsignallike.reason.md) | | any | | 25 | 26 | ## Methods 27 | 28 | | Method | Description | 29 | | --- | --- | 30 | | [addEventListener(type, listener, options)](./async-call-rpc.abortsignallike.addeventlistener.md) | | 31 | | [removeEventListener(type, listener)](./async-call-rpc.abortsignallike.removeeventlistener.md) | | 32 | | [throwIfAborted()](./async-call-rpc.abortsignallike.throwifaborted.md) | | 33 | 34 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-notify.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "Before this line should have no response.", 8 | "Even throwing functions should succeed too.", 9 | "No response should be sent from the server.", 10 | ] 11 | ``` 12 | 13 | ## T=1 Message: client sent 14 | 15 | ```php 16 | Object { 17 | "jsonrpc": "2.0", 18 | "method": "add", 19 | "params": Array [ 20 | 2, 21 | 3, 22 | ], 23 | } 24 | ``` 25 | 26 | ## T=2 Message: client sent 27 | 28 | ```php 29 | Object { 30 | "jsonrpc": "2.0", 31 | "method": "throws", 32 | "params": Array [], 33 | } 34 | ``` 35 | 36 | ## T=3 Message: client sent 37 | 38 | ```php 39 | Object { 40 | "jsonrpc": "2.0", 41 | "method": "not_found", 42 | "params": Array [], 43 | } 44 | ``` 45 | 46 | ## T=4 Message: client sent 47 | 48 | ```php 49 | Object { 50 | "jsonrpc": "2.0", 51 | "method": "echo", 52 | "params": Array [ 53 | 1, 54 | ], 55 | } 56 | ``` 57 | 58 | ## T=5 Message: client sent 59 | 60 | ```php 61 | Object { 62 | "jsonrpc": "2.0", 63 | "method": "throws", 64 | "params": Array [], 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-impl-rejected.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: server/log 4 | 5 | ```php 6 | Array [ 7 | "AsyncCall failed to start", 8 | [TypeError: Import failed], 9 | ] 10 | ``` 11 | 12 | ## T=1 Log: client/log 13 | 14 | ```php 15 | Array [ 16 | "AsyncCall failed to start", 17 | [TypeError: Import failed], 18 | ] 19 | ``` 20 | 21 | ## T=2 Message: client => server 22 | 23 | ```php 24 | Object { 25 | "id": 0, 26 | "jsonrpc": "2.0", 27 | "method": "add", 28 | "params": Array [ 29 | 1, 30 | 2, 31 | ], 32 | } 33 | ``` 34 | 35 | ## T=3 Log: server/log 36 | 37 | ```php 38 | Array [ 39 | [TypeError: Import failed], 40 | ] 41 | ``` 42 | 43 | ## T=4 Message: server => client 44 | 45 | ```php 46 | Object { 47 | "error": Object { 48 | "code": -1, 49 | "data": Object { 50 | "type": "TypeError", 51 | }, 52 | "message": "Import failed", 53 | }, 54 | "id": 0, 55 | "jsonrpc": "2.0", 56 | } 57 | ``` 58 | 59 | ## T=5 Log: client/log 60 | 61 | ```php 62 | Array [ 63 | "TypeError: Import failed(-1) %c@0 64 | %c", 65 | "color: gray", 66 | "", 67 | ] 68 | ``` 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | es/ 2 | out/ 3 | utils/* 4 | !utils/deno/ 5 | dist/ 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | # next.js build output 66 | .next 67 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-log-object.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.add(1,2) @0", 22 | ] 23 | ``` 24 | 25 | ## T=2 Message: server => client 26 | 27 | ```php 28 | Object { 29 | "id": 0, 30 | "jsonrpc": "2.0", 31 | "result": 3, 32 | } 33 | ``` 34 | 35 | ## T=3 Message: client => server 36 | 37 | ```php 38 | Object { 39 | "id": 1, 40 | "jsonrpc": "2.0", 41 | "method": "throws", 42 | "params": Array [], 43 | } 44 | ``` 45 | 46 | ## T=4 Log: server/log 47 | 48 | ```php 49 | Array [ 50 | "rpc.throws() @1", 51 | ] 52 | ``` 53 | 54 | ## T=5 Message: server => client 55 | 56 | ```php 57 | Object { 58 | "error": Object { 59 | "code": -1, 60 | "data": Object { 61 | "type": "Error", 62 | }, 63 | "message": "impl error", 64 | }, 65 | "id": 1, 66 | "jsonrpc": "2.0", 67 | } 68 | ``` 69 | 70 | ## T=6 Log: client/log 71 | 72 | ```php 73 | Array [ 74 | "Error: impl error(-1) @1 75 | ", 76 | ] 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/async-call-rpc.callbackbasedchannel.setup.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [CallbackBasedChannel](./async-call-rpc.callbackbasedchannel.md) > [setup](./async-call-rpc.callbackbasedchannel.setup.md) 4 | 5 | ## CallbackBasedChannel.setup() method 6 | 7 | Setup the CallbackBasedChannel. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | setup(jsonRPCHandlerCallback: (jsonRPCPayload: unknown, hint?: undefined | 'request' | 'response') => Promise, isValidJSONRPCPayload: (data: unknown, hint?: undefined | 'request' | 'response') => boolean | Promise): (() => void) | void; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | jsonRPCHandlerCallback | (jsonRPCPayload: unknown, hint?: undefined \| 'request' \| 'response') => Promise<unknown \| undefined> | A function that will execute the JSON RPC request then give the result back. If the result is undefined, it means no response is created. | 20 | | isValidJSONRPCPayload | (data: unknown, hint?: undefined \| 'request' \| 'response') => boolean \| Promise<boolean> | | 21 | 22 | **Returns:** 23 | 24 | (() => void) \| void 25 | 26 | a function that unregister the setup. 27 | 28 | -------------------------------------------------------------------------------- /__tests__/special-names.ts: -------------------------------------------------------------------------------- 1 | import { withSnapshotDefault } from './utils/test.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it( 5 | 'be called with symbol-keyed methods', 6 | withSnapshotDefault('async-call-symbols', async ({ init }) => { 7 | const server: any = init() 8 | await expect(server[Symbol.iterator]()).rejects.toThrowErrorMatchingInlineSnapshot( 9 | `[TypeError: Not start with rpc.]`, 10 | ) 11 | }), 12 | ) 13 | 14 | it( 15 | 'be called with "rpc.*" methods', 16 | withSnapshotDefault('async-call-preserved-names', async ({ init }) => { 17 | const server: any = init() 18 | await expect(server['rpc.test']()).rejects.toThrowErrorMatchingInlineSnapshot( 19 | `[TypeError: Error 2: https://github.com/Jack-Works/async-call-rpc/wiki/Errors#2]`, 20 | ) 21 | expect(() => server.then()).toThrowErrorMatchingInlineSnapshot(`[TypeError: server.then is not a function]`) 22 | }), 23 | ) 24 | 25 | it( 26 | '(generator) be called with symbol methods', 27 | withSnapshotDefault('async-call-generator-symbols', async ({ initIterator }) => { 28 | const server: any = initIterator() 29 | expect(() => server[Symbol.toStringTag]()).toThrowErrorMatchingInlineSnapshot( 30 | `[TypeError: Error 1: https://github.com/Jack-Works/async-call-rpc/wiki/Errors#1]`, 31 | ) 32 | }), 33 | ) 34 | -------------------------------------------------------------------------------- /src/utils/normalizeOptions.ts: -------------------------------------------------------------------------------- 1 | import type { AsyncCallOptions } from '../Async-Call.ts' 2 | import { isBoolean } from './constants.ts' 3 | const undefinedToTrue = (x: undefined | boolean) => (x === void 0 ? true : x) 4 | type NormalizedLogOptions = readonly [ 5 | beCalled: boolean, 6 | localError: boolean, 7 | remoteError: boolean, 8 | isPretty?: boolean, 9 | requestReplay?: boolean, 10 | sendLocalStack?: boolean, 11 | ] 12 | 13 | export const normalizeLogOptions = (log: NonNullable): NormalizedLogOptions | [] => { 14 | if (log === 'all') return [true, true, true, true, true, true] 15 | if (!isBoolean(log)) { 16 | const { beCalled, localError, remoteError, type, requestReplay, sendLocalStack } = log 17 | return [ 18 | undefinedToTrue(beCalled), 19 | undefinedToTrue(localError), 20 | undefinedToTrue(remoteError), 21 | type !== 'basic', 22 | requestReplay, 23 | sendLocalStack, 24 | ] 25 | } 26 | if (log) return [true, true, true, true] as const 27 | return [] 28 | } 29 | 30 | export const normalizeStrictOptions = (strict: NonNullable) => { 31 | if (!isBoolean(strict)) { 32 | const { methodNotFound, unknownMessage } = strict 33 | return [methodNotFound, unknownMessage] as const 34 | } 35 | return [strict, strict] as const 36 | } 37 | -------------------------------------------------------------------------------- /__tests__/DOMException.ts: -------------------------------------------------------------------------------- 1 | import { reproduceDOMException, reproduceError } from './utils/reproduce.js' 2 | import { withSnapshotDefault } from './utils/test.js' 3 | import { expect, it } from 'vitest' 4 | 5 | it( 6 | 'able to serialize DOMException', 7 | withSnapshotDefault('DOMException', async ({ init }) => { 8 | await reproduceDOMException(false, async () => { 9 | const server = init() 10 | const promise: Promise = server.DOMException() 11 | try { 12 | await promise 13 | expect.fail('this request should not success') 14 | } catch (error: any) { 15 | if (!(error instanceof DOMException)) expect.fail('should be an instance of DOMException') 16 | expect(error.message).toMatchInlineSnapshot('"message"') 17 | expect(error.name).toMatchInlineSnapshot('"name"') 18 | } 19 | }) 20 | }), 21 | ) 22 | 23 | it( 24 | 'able to recover from Error constructor with a bad implementation', 25 | withSnapshotDefault('recover-error-with-bad-implementation', async ({ init }) => { 26 | await reproduceError(() => 27 | reproduceDOMException(true, async () => { 28 | const server = init() 29 | const promise = server.DOMException() 30 | await expect(promise).rejects.toThrowError() 31 | }), 32 | ) 33 | }), 34 | ) 35 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccallloglevel.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallLogLevel](./async-call-rpc.asynccallloglevel.md) 4 | 5 | ## AsyncCallLogLevel interface 6 | 7 | Log options 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface AsyncCallLogLevel 13 | ``` 14 | 15 | ## Remarks 16 | 17 | This option controls how AsyncCall log requests to the console. 18 | 19 | ## Properties 20 | 21 | | Property | Modifiers | Type | Description | 22 | | --- | --- | --- | --- | 23 | | [beCalled?](./async-call-rpc.asynccallloglevel.becalled.md) | | boolean | _(Optional)_ Log all incoming requests | 24 | | [localError?](./async-call-rpc.asynccallloglevel.localerror.md) | | boolean | _(Optional)_ Log all errors when responding requests | 25 | | [remoteError?](./async-call-rpc.asynccallloglevel.remoteerror.md) | | boolean | _(Optional)_ Log errors from the remote | 26 | | [requestReplay?](./async-call-rpc.asynccallloglevel.requestreplay.md) | | boolean | _(Optional)_ If log a function that can replay the request | 27 | | [sendLocalStack?](./async-call-rpc.asynccallloglevel.sendlocalstack.md) | | boolean | _(Optional)_ Send the stack to the remote when making requests | 28 | | [type?](./async-call-rpc.asynccallloglevel.type.md) | | 'basic' \| 'pretty' | _(Optional)_ Style of the log | 29 | 30 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccalloptions.thenable.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) > [thenable](./async-call-rpc.asynccalloptions.thenable.md) 4 | 5 | ## AsyncCallOptions.thenable property 6 | 7 | If the instance is "thenable". 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | thenable?: boolean; 13 | ``` 14 | 15 | ## Remarks 16 | 17 | If this options is set to `true`, it will return a `then` method normally (forwards the request to the remote). 18 | 19 | If this options is set to `false`, it will return `undefined`, which means a method named "then" on the remote is not reachable. 20 | 21 | If this options is set to `undefined`, it will return `undefined` and show a warning. You must explicitly set this option to `true` or `false` to dismiss the warning. 22 | 23 | This option is used to resolve the problem caused by Promise auto-unwrapping. 24 | 25 | Consider this code: 26 | 27 | ```ts 28 | async function getRPC() { 29 | return AsyncCall(...) 30 | } 31 | ``` 32 | According to the JS semantics, it will invoke the "then" method immediately on the returning instance which is unwanted in most scenarios. 33 | 34 | To avoid this problem, methods called "then" are omitted from the type signatures. Strongly suggest to not use "then" as your RPC method name. 35 | 36 | -------------------------------------------------------------------------------- /src/core/notify.ts: -------------------------------------------------------------------------------- 1 | import { AsyncCallNotify } from '../utils/internalSymbol.ts' 2 | import { isFunction } from '../utils/constants.ts' 3 | 4 | /** 5 | * Make the returning type to `Promise` 6 | * @internal 7 | * @remarks 8 | * Due to the limitation of TypeScript, generic signatures cannot be preserved 9 | * if the function is the top level parameter of this utility type, 10 | * or the function is not returning `Promise`. 11 | */ 12 | export type _IgnoreResponse = T extends (...args: infer Args) => unknown 13 | ? (...args: Args) => Promise 14 | : { 15 | [key in keyof T as T[key] extends Function ? key : never]: T[key] extends ( 16 | ...args: infer Args 17 | ) => infer Return 18 | ? Return extends Promise 19 | ? T[key] 20 | : (...args: Args) => Promise 21 | : never 22 | } 23 | /** 24 | * Wrap the AsyncCall instance to send notification. 25 | * @param instanceOrFnOnInstance - The AsyncCall instance or function on the AsyncCall instance 26 | * @example 27 | * const notifyOnly = notify(AsyncCall(...)) 28 | * @public 29 | */ 30 | 31 | export function notify(instanceOrFnOnInstance: T): _IgnoreResponse { 32 | if (isFunction(instanceOrFnOnInstance)) return (instanceOrFnOnInstance as any)[AsyncCallNotify] 33 | return new Proxy(instanceOrFnOnInstance, { get: notifyTrap }) as any 34 | } 35 | const notifyTrap = (target: any, p: string | number | symbol) => { 36 | return target[p][AsyncCallNotify] 37 | } 38 | -------------------------------------------------------------------------------- /docs/async-call-rpc.jsonencoderoptions.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [JSONEncoderOptions](./async-call-rpc.jsonencoderoptions.md) 4 | 5 | ## JSONEncoderOptions interface 6 | 7 | Options of [JSONEncoder()](./async-call-rpc.jsonencoder.md) 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export interface JSONEncoderOptions 13 | ``` 14 | 15 | ## Properties 16 | 17 | | Property | Modifiers | Type | Description | 18 | | --- | --- | --- | --- | 19 | | [keepUndefined?](./async-call-rpc.jsonencoderoptions.keepundefined.md) | | false \| 'null' \| undefined | _(Optional)_ How to handle "undefined" in the result of [SuccessResponse](./async-call-rpc.successresponse.md). | 20 | | [replacer?](./async-call-rpc.jsonencoderoptions.replacer.md) | | ((this: any, key: string, value: any) => any) \| undefined | _(Optional)_ A function that transforms the results. | 21 | | [reviver?](./async-call-rpc.jsonencoderoptions.reviver.md) | | ((this: any, key: string, value: any) => any) \| undefined | _(Optional)_ A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is. | 22 | | [space?](./async-call-rpc.jsonencoderoptions.space.md) | | string \| number \| undefined | _(Optional)_ Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. | 23 | 24 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/encode-isomorphic-hinted.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "encode", 8 | Object { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "method": "undefined", 12 | "params": Array [], 13 | }, 14 | ] 15 | ``` 16 | 17 | ## T=1 Message: client => server 18 | 19 | ```php 20 | Object { 21 | "id": 0, 22 | "jsonrpc": "2.0", 23 | "method": "undefined", 24 | "params": Array [], 25 | } 26 | ``` 27 | 28 | ## T=2 Log: testRunner/log 29 | 30 | ```php 31 | Array [ 32 | "decode", 33 | Object { 34 | "id": 0, 35 | "jsonrpc": "2.0", 36 | "method": "undefined", 37 | "params": Array [], 38 | }, 39 | ] 40 | ``` 41 | 42 | ## T=3 Log: server/log 43 | 44 | ```php 45 | Array [ 46 | "rpc.%cundefined%c(%c) 47 | %o %c@0", 48 | "color:#d2c057", 49 | "", 50 | "", 51 | Promise {}, 52 | "color:gray;font-style:italic;", 53 | ] 54 | ``` 55 | 56 | ## T=4 Log: testRunner/log 57 | 58 | ```php 59 | Array [ 60 | "encode", 61 | Object { 62 | "id": 0, 63 | "jsonrpc": "2.0", 64 | "result": undefined, 65 | }, 66 | ] 67 | ``` 68 | 69 | ## T=5 Message: server => client 70 | 71 | ```php 72 | Object { 73 | "id": 0, 74 | "jsonrpc": "2.0", 75 | "result": undefined, 76 | } 77 | ``` 78 | 79 | ## T=6 Log: testRunner/log 80 | 81 | ```php 82 | Array [ 83 | "decode", 84 | Object { 85 | "id": 0, 86 | "jsonrpc": "2.0", 87 | "result": undefined, 88 | }, 89 | ] 90 | ``` 91 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/encoder-json-no-keep.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client sent 4 | 5 | ```php 6 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"undefined\",\"params\":[]}" 7 | ``` 8 | 9 | ## T=1 Message: client sent 10 | 11 | ```php 12 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"deep_undefined\",\"params\":[]}" 13 | ``` 14 | 15 | ## T=2 Message: server received 16 | 17 | ```php 18 | "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"undefined\",\"params\":[]}" 19 | ``` 20 | 21 | ## T=3 Log: server/log 22 | 23 | ```php 24 | Array [ 25 | "rpc.%cundefined%c(%c) 26 | %o %c@0", 27 | "color:#d2c057", 28 | "", 29 | "", 30 | Promise {}, 31 | "color:gray;font-style:italic;", 32 | ] 33 | ``` 34 | 35 | ## T=4 Message: server sent 36 | 37 | ```php 38 | "{\"jsonrpc\":\"2.0\",\"id\":0}" 39 | ``` 40 | 41 | ## T=5 Message: server received 42 | 43 | ```php 44 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"deep_undefined\",\"params\":[]}" 45 | ``` 46 | 47 | ## T=6 Log: server/log 48 | 49 | ```php 50 | Array [ 51 | "rpc.%cdeep_undefined%c(%c) 52 | %o %c@1", 53 | "color:#d2c057", 54 | "", 55 | "", 56 | Promise {}, 57 | "color:gray;font-style:italic;", 58 | ] 59 | ``` 60 | 61 | ## T=7 Message: server sent 62 | 63 | ```php 64 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"a\":{\"b\":{}}}}" 65 | ``` 66 | 67 | ## T=8 Message: client received 68 | 69 | ```php 70 | "{\"jsonrpc\":\"2.0\",\"id\":0}" 71 | ``` 72 | 73 | ## T=9 Message: client received 74 | 75 | ```php 76 | "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"a\":{\"b\":{}}}}" 77 | ``` 78 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/encode-isomorphic-not-hinted.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "encode", 8 | Object { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "method": "undefined", 12 | "params": Array [], 13 | }, 14 | ] 15 | ``` 16 | 17 | ## T=1 Message: client => server 18 | 19 | ```php 20 | Object { 21 | "id": 0, 22 | "jsonrpc": "2.0", 23 | "method": "undefined", 24 | "params": Array [], 25 | } 26 | ``` 27 | 28 | ## T=2 Log: testRunner/log 29 | 30 | ```php 31 | Array [ 32 | "decode", 33 | Object { 34 | "id": 0, 35 | "jsonrpc": "2.0", 36 | "method": "undefined", 37 | "params": Array [], 38 | }, 39 | ] 40 | ``` 41 | 42 | ## T=3 Log: server/log 43 | 44 | ```php 45 | Array [ 46 | "rpc.%cundefined%c(%c) 47 | %o %c@0", 48 | "color:#d2c057", 49 | "", 50 | "", 51 | Promise {}, 52 | "color:gray;font-style:italic;", 53 | ] 54 | ``` 55 | 56 | ## T=4 Log: testRunner/log 57 | 58 | ```php 59 | Array [ 60 | "encode", 61 | Object { 62 | "id": 0, 63 | "jsonrpc": "2.0", 64 | "result": undefined, 65 | }, 66 | ] 67 | ``` 68 | 69 | ## T=5 Message: server => client 70 | 71 | ```php 72 | Object { 73 | "id": 0, 74 | "jsonrpc": "2.0", 75 | "result": undefined, 76 | } 77 | ``` 78 | 79 | ## T=6 Log: testRunner/log 80 | 81 | ```php 82 | Array [ 83 | "decode", 84 | Object { 85 | "id": 0, 86 | "jsonrpc": "2.0", 87 | "result": undefined, 88 | }, 89 | ] 90 | ``` 91 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/encode-isomorphic-full-not-hinted.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "encodeRequest", 8 | Object { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "method": "undefined", 12 | "params": Array [], 13 | }, 14 | ] 15 | ``` 16 | 17 | ## T=1 Message: client => server 18 | 19 | ```php 20 | Object { 21 | "id": 0, 22 | "jsonrpc": "2.0", 23 | "method": "undefined", 24 | "params": Array [], 25 | } 26 | ``` 27 | 28 | ## T=2 Log: testRunner/log 29 | 30 | ```php 31 | Array [ 32 | "decode", 33 | Object { 34 | "id": 0, 35 | "jsonrpc": "2.0", 36 | "method": "undefined", 37 | "params": Array [], 38 | }, 39 | ] 40 | ``` 41 | 42 | ## T=3 Log: server/log 43 | 44 | ```php 45 | Array [ 46 | "rpc.%cundefined%c(%c) 47 | %o %c@0", 48 | "color:#d2c057", 49 | "", 50 | "", 51 | Promise {}, 52 | "color:gray;font-style:italic;", 53 | ] 54 | ``` 55 | 56 | ## T=4 Log: testRunner/log 57 | 58 | ```php 59 | Array [ 60 | "encodeResponse", 61 | Object { 62 | "id": 0, 63 | "jsonrpc": "2.0", 64 | "result": undefined, 65 | }, 66 | ] 67 | ``` 68 | 69 | ## T=5 Message: server => client 70 | 71 | ```php 72 | Object { 73 | "id": 0, 74 | "jsonrpc": "2.0", 75 | "result": undefined, 76 | } 77 | ``` 78 | 79 | ## T=6 Log: testRunner/log 80 | 81 | ```php 82 | Array [ 83 | "decode", 84 | Object { 85 | "id": 0, 86 | "jsonrpc": "2.0", 87 | "result": undefined, 88 | }, 89 | ] 90 | ``` 91 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/encode-isomorphic-full-hinted.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "encodeRequest", 8 | Object { 9 | "id": 0, 10 | "jsonrpc": "2.0", 11 | "method": "undefined", 12 | "params": Array [], 13 | }, 14 | ] 15 | ``` 16 | 17 | ## T=1 Message: client => server 18 | 19 | ```php 20 | Object { 21 | "id": 0, 22 | "jsonrpc": "2.0", 23 | "method": "undefined", 24 | "params": Array [], 25 | } 26 | ``` 27 | 28 | ## T=2 Log: testRunner/log 29 | 30 | ```php 31 | Array [ 32 | "decodeRequest", 33 | Object { 34 | "id": 0, 35 | "jsonrpc": "2.0", 36 | "method": "undefined", 37 | "params": Array [], 38 | }, 39 | ] 40 | ``` 41 | 42 | ## T=3 Log: server/log 43 | 44 | ```php 45 | Array [ 46 | "rpc.%cundefined%c(%c) 47 | %o %c@0", 48 | "color:#d2c057", 49 | "", 50 | "", 51 | Promise {}, 52 | "color:gray;font-style:italic;", 53 | ] 54 | ``` 55 | 56 | ## T=4 Log: testRunner/log 57 | 58 | ```php 59 | Array [ 60 | "encodeResponse", 61 | Object { 62 | "id": 0, 63 | "jsonrpc": "2.0", 64 | "result": undefined, 65 | }, 66 | ] 67 | ``` 68 | 69 | ## T=5 Message: server => client 70 | 71 | ```php 72 | Object { 73 | "id": 0, 74 | "jsonrpc": "2.0", 75 | "result": undefined, 76 | } 77 | ``` 78 | 79 | ## T=6 Log: testRunner/log 80 | 81 | ```php 82 | Array [ 83 | "decodeResponse", 84 | Object { 85 | "id": 0, 86 | "jsonrpc": "2.0", 87 | "result": undefined, 88 | }, 89 | ] 90 | ``` 91 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-batch-notify-1.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "No log before this line", 8 | ] 9 | ``` 10 | 11 | ## T=1 Message: client => server 12 | 13 | ```php 14 | Array [ 15 | Object { 16 | "jsonrpc": "2.0", 17 | "method": "add", 18 | "params": Array [ 19 | 1, 20 | 2, 21 | ], 22 | }, 23 | Object { 24 | "jsonrpc": "2.0", 25 | "method": "throws", 26 | "params": Array [], 27 | }, 28 | Object { 29 | "jsonrpc": "2.0", 30 | "method": "add", 31 | "params": Array [ 32 | 3, 33 | 4, 34 | ], 35 | }, 36 | ] 37 | ``` 38 | 39 | ## T=2 Log: server/log 40 | 41 | ```php 42 | Array [ 43 | "rpc.%cadd%c(%o, %o%c) 44 | %o %c@undefined", 45 | "color:#d2c057", 46 | "", 47 | 1, 48 | 2, 49 | "", 50 | Promise {}, 51 | "color:gray;font-style:italic;", 52 | ] 53 | ``` 54 | 55 | ## T=3 Log: server/log 56 | 57 | ```php 58 | Array [ 59 | "rpc.%cthrows%c(%c) 60 | %o %c@undefined", 61 | "color:#d2c057", 62 | "", 63 | "", 64 | Promise {}, 65 | "color:gray;font-style:italic;", 66 | ] 67 | ``` 68 | 69 | ## T=4 Log: server/log 70 | 71 | ```php 72 | Array [ 73 | "rpc.%cadd%c(%o, %o%c) 74 | %o %c@undefined", 75 | "color:#d2c057", 76 | "", 77 | 3, 78 | 4, 79 | "", 80 | Promise {}, 81 | "color:gray;font-style:italic;", 82 | ] 83 | ``` 84 | 85 | ## T=5 Log: server/log 86 | 87 | ```php 88 | Array [ 89 | [Error: impl error], 90 | ] 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asynccall.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncCall](./async-call-rpc.asynccall.md) 4 | 5 | ## AsyncCall() function 6 | 7 | Create a RPC server & client. 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export declare function AsyncCall(thisSideImplementation: null | undefined | object | Promise, options: AsyncCallOptions): AsyncVersionOf; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | thisSideImplementation | null \| undefined \| object \| Promise<object> | The implementation when this AsyncCall acts as a JSON RPC server. Can be a Promise. | 20 | | options | [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) | [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) | 21 | 22 | **Returns:** 23 | 24 | [AsyncVersionOf](./async-call-rpc.asyncversionof.md)<OtherSideImplementedFunctions> 25 | 26 | Same as the `OtherSideImplementedFunctions` type parameter, but every function in that interface becomes async and non-function value is removed. Method called "then" are also removed. 27 | 28 | ## Remarks 29 | 30 | See [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) 31 | 32 | thisSideImplementation can be a Promise so you can write: 33 | 34 | ```ts 35 | export const service = AsyncCall(typeof window === 'object' ? {} : import('./backend/service.js'), {}) 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /__tests__/options-normalize.ts: -------------------------------------------------------------------------------- 1 | import { normalizeLogOptions, normalizeStrictOptions } from '../src/utils/normalizeOptions.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it('should normalize log options', () => { 5 | expect(normalizeLogOptions(true)).toMatchInlineSnapshot(` 6 | [ 7 | true, 8 | true, 9 | true, 10 | true, 11 | ] 12 | `) 13 | expect(normalizeLogOptions(false)).toMatchInlineSnapshot('[]') 14 | expect(normalizeLogOptions('all')).toMatchInlineSnapshot(` 15 | [ 16 | true, 17 | true, 18 | true, 19 | true, 20 | true, 21 | true, 22 | ] 23 | `) 24 | expect(normalizeLogOptions({ type: 'basic' })).toMatchInlineSnapshot(` 25 | [ 26 | true, 27 | true, 28 | true, 29 | false, 30 | undefined, 31 | undefined, 32 | ] 33 | `) 34 | expect(normalizeLogOptions({ beCalled: false })).toMatchInlineSnapshot(` 35 | [ 36 | false, 37 | true, 38 | true, 39 | true, 40 | undefined, 41 | undefined, 42 | ] 43 | `) 44 | }) 45 | 46 | it('should normalize strict options', () => { 47 | expect(normalizeStrictOptions(true)).toMatchInlineSnapshot(` 48 | [ 49 | true, 50 | true, 51 | ] 52 | `) 53 | expect(normalizeStrictOptions(false)).toMatchInlineSnapshot(` 54 | [ 55 | false, 56 | false, 57 | ] 58 | `) 59 | expect(normalizeStrictOptions({ methodNotFound: false })).toMatchInlineSnapshot(` 60 | [ 61 | false, 62 | undefined, 63 | ] 64 | `) 65 | }) 66 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/generateRandomID-2.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": "ndom-id-1", 8 | "jsonrpc": "2.0", 9 | "method": "rpc.async-iterator.start", 10 | "params": Array [ 11 | "echo", 12 | Array [ 13 | Array [], 14 | ], 15 | ], 16 | } 17 | ``` 18 | 19 | ## T=1 Log: server/log 20 | 21 | ```php 22 | Array [ 23 | "rpc.%crpc.async-iterator.start%c(%o, %o%c) 24 | %o %c@ndom-id-1", 25 | "color:#d2c057", 26 | "", 27 | "echo", 28 | Array [ 29 | Array [], 30 | ], 31 | "", 32 | Promise {}, 33 | "color:gray;font-style:italic;", 34 | ] 35 | ``` 36 | 37 | ## T=2 Message: server => client 38 | 39 | ```php 40 | Object { 41 | "id": "ndom-id-1", 42 | "jsonrpc": "2.0", 43 | "result": "ndom-id-2", 44 | } 45 | ``` 46 | 47 | ## T=3 Message: client => server 48 | 49 | ```php 50 | Object { 51 | "id": "ndom-id-3", 52 | "jsonrpc": "2.0", 53 | "method": "rpc.async-iterator.next", 54 | "params": Array [ 55 | "ndom-id-2", 56 | undefined, 57 | ], 58 | } 59 | ``` 60 | 61 | ## T=4 Log: server/log 62 | 63 | ```php 64 | Array [ 65 | "rpc.%crpc.async-iterator.next%c(%o, %o%c) 66 | %o %c@ndom-id-3", 67 | "color:#d2c057", 68 | "", 69 | "ndom-id-2", 70 | undefined, 71 | "", 72 | Promise {}, 73 | "color:gray;font-style:italic;", 74 | ] 75 | ``` 76 | 77 | ## T=5 Message: server => client 78 | 79 | ```php 80 | Object { 81 | "id": "ndom-id-3", 82 | "jsonrpc": "2.0", 83 | "result": Object { 84 | "done": true, 85 | "value": undefined, 86 | }, 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/async-call-rpc.jsonserialization.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [JSONSerialization](./async-call-rpc.jsonserialization.md) 4 | 5 | ## JSONSerialization() function 6 | 7 | Create a serialization by JSON.parse/stringify 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | JSONSerialization: (replacerAndReceiver?: [(((key: string, value: any) => any) | undefined)?, (((key: string, value: any) => any) | undefined)?], space?: string | number | undefined, undefinedKeepingBehavior?: 'keep' | 'null' | false) => Serialization 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | replacerAndReceiver | \[(((key: string, value: any) => any) \| undefined)?, (((key: string, value: any) => any) \| undefined)?\] | _(Optional)_ Replacer and receiver of JSON.parse/stringify | 20 | | space | string \| number \| undefined | _(Optional)_ Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. | 21 | | undefinedKeepingBehavior | 'keep' \| 'null' \| false |

_(Optional)_ How to keep "undefined" in result of SuccessResponse?

If it is not handled properly, JSON.stringify will emit an invalid JSON RPC object.

Options: - "null"(\*\*default\*\*): convert it to null. - "keep": try to keep it by additional property "undef". - false: Don't keep it, let it break.

| 22 | 23 | **Returns:** 24 | 25 | [Serialization](./async-call-rpc.serialization.md) 26 | 27 | ## Remarks 28 | 29 | [Serialization](./async-call-rpc.serialization.md) 30 | 31 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-log-true.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cadd%c(%o, %o%c) 22 | %o %c@0", 23 | "color:#d2c057", 24 | "", 25 | 1, 26 | 2, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: server => client 34 | 35 | ```php 36 | Object { 37 | "id": 0, 38 | "jsonrpc": "2.0", 39 | "result": 3, 40 | } 41 | ``` 42 | 43 | ## T=3 Message: client => server 44 | 45 | ```php 46 | Object { 47 | "id": 1, 48 | "jsonrpc": "2.0", 49 | "method": "throws", 50 | "params": Array [], 51 | } 52 | ``` 53 | 54 | ## T=4 Log: server/log 55 | 56 | ```php 57 | Array [ 58 | "rpc.%cthrows%c(%c) 59 | %o %c@1", 60 | "color:#d2c057", 61 | "", 62 | "", 63 | Promise {}, 64 | "color:gray;font-style:italic;", 65 | ] 66 | ``` 67 | 68 | ## T=5 Log: server/log 69 | 70 | ```php 71 | Array [ 72 | [Error: impl error], 73 | ] 74 | ``` 75 | 76 | ## T=6 Message: server => client 77 | 78 | ```php 79 | Object { 80 | "error": Object { 81 | "code": -1, 82 | "data": Object { 83 | "type": "Error", 84 | }, 85 | "message": "impl error", 86 | }, 87 | "id": 1, 88 | "jsonrpc": "2.0", 89 | } 90 | ``` 91 | 92 | ## T=7 Log: client/log 93 | 94 | ```php 95 | Array [ 96 | "Error: impl error(-1) %c@1 97 | %c", 98 | "color: gray", 99 | "", 100 | ] 101 | ``` 102 | -------------------------------------------------------------------------------- /utils/deno/websocket.server.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'https://deno.land/std@0.61.0/http/server.ts' 2 | import { acceptWebSocket, WebSocket } from 'https://deno.land/std@0.61.0/ws/mod.ts' 3 | import { CallbackBasedChannel } from '../../src/types.ts' 4 | 5 | export class WebSocketChannel extends EventTarget implements CallbackBasedChannel { 6 | constructor(public server: Server) { 7 | super() 8 | } 9 | private async acceptRequest(callback: (data: unknown) => Promise, signal: AbortController) { 10 | for await (const req of this.server) { 11 | if (signal.signal.aborted) return 12 | const { conn, r: bufReader, w: bufWriter, headers } = req 13 | const ws = await acceptWebSocket({ 14 | conn, 15 | bufReader, 16 | bufWriter, 17 | headers, 18 | }) 19 | signal.signal.addEventListener('abort', () => ws.close(), { once: true }) 20 | this.handledWebSocket(ws, callback, signal).catch(this.error) 21 | } 22 | } 23 | private async handledWebSocket( 24 | websocket: WebSocket, 25 | callback: (data: unknown) => Promise, 26 | signal: AbortController, 27 | ) { 28 | for await (const event of websocket) { 29 | if (signal.signal.aborted || websocket.isClosed) return 30 | callback(event).then((x) => x && !websocket.isClosed && websocket.send(x as any), this.error) 31 | } 32 | } 33 | private error = (error: any) => { 34 | this.dispatchEvent(new ErrorEvent('error', { error })) 35 | } 36 | setup(callback: (data: unknown) => Promise) { 37 | const signal = new AbortController() 38 | this.acceptRequest(callback, signal).catch(this.error) 39 | return () => signal.abort() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /__tests__/generator.ts: -------------------------------------------------------------------------------- 1 | import { withSnapshotDefault } from './utils/test.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it( 5 | '(generator) behavior correct', 6 | withSnapshotDefault('generator-detailed', async ({ initIterator }) => { 7 | const server = initIterator() 8 | const gen = server.endless() 9 | expect(await gen.next()).toMatchInlineSnapshot(` 10 | { 11 | "done": false, 12 | "value": 1, 13 | } 14 | `) 15 | expect(await gen.return({} as never)).toMatchInlineSnapshot(` 16 | { 17 | "done": false, 18 | "value": 1, 19 | } 20 | `) 21 | expect(await gen.throw({})).toMatchInlineSnapshot(` 22 | { 23 | "done": false, 24 | "value": 2, 25 | } 26 | `) 27 | expect(await gen.next({})).toMatchInlineSnapshot(` 28 | { 29 | "done": false, 30 | "value": 1, 31 | } 32 | `) 33 | expect(await gen.next({})).toMatchInlineSnapshot(` 34 | { 35 | "done": false, 36 | "value": 1, 37 | } 38 | `) 39 | 40 | const gen2 = server.echo([]) 41 | expect(await gen2.next()).toMatchInlineSnapshot(` 42 | { 43 | "done": true, 44 | "value": undefined, 45 | } 46 | `) 47 | expect(await gen2.next()).toMatchInlineSnapshot(` 48 | { 49 | "done": true, 50 | "value": undefined, 51 | } 52 | `) 53 | expect(await gen2.return(1 as any)).toMatchInlineSnapshot(` 54 | { 55 | "done": true, 56 | "value": 1, 57 | } 58 | `) 59 | await expect(gen2.throw(1 as any)).rejects.toThrowErrorMatchingInlineSnapshot('1') 60 | }), 61 | ) 62 | -------------------------------------------------------------------------------- /__tests__/non-strict.ts: -------------------------------------------------------------------------------- 1 | import { delay, withSnapshotDefault } from './utils/test.js' 2 | import { expect, it } from 'vitest' 3 | it( 4 | 'non strict behaviors', 5 | withSnapshotDefault( 6 | 'async-call-non-strict', 7 | async ({ init, channel }) => { 8 | const server: any = init({ options: { strict: false } }) 9 | if (!('send' in channel.client)) throw new Error('test error') 10 | 11 | // send unknown message should not return an error 12 | channel.client.send!('unknown message') 13 | // this promise should never resolves 14 | const promise: Promise = server.not_defined_method() 15 | let fail = false 16 | promise.finally(() => (fail = true)) 17 | await delay(800) 18 | if (fail) throw new Error('This promise should never resolves') 19 | }, 20 | { timeout: 1000 }, 21 | ), 22 | ) 23 | 24 | it( 25 | 'strict behaviors', 26 | withSnapshotDefault('async-call-strict', async ({ init, channel }) => { 27 | const server: any = init() 28 | if (!('send' in channel.client)) throw new Error('test error') 29 | channel.client.send!('unknown message') 30 | await expect(server.not_defined_method()).rejects.toThrowErrorMatchingInlineSnapshot( 31 | `[Error: Method not found]`, 32 | ) 33 | }), 34 | ) 35 | 36 | it( 37 | 'partial strict behaviors', 38 | withSnapshotDefault('async-call-strict-partial', async ({ init, channel }) => { 39 | const server: any = init({ options: { strict: { methodNotFound: true } } }) 40 | if (!('send' in channel.client)) throw new Error('test error') 41 | channel.client.send!('unknown message') 42 | await expect(server.not_defined_method()).rejects.toThrowErrorMatchingInlineSnapshot( 43 | `[Error: Method not found]`, 44 | ) 45 | }), 46 | ) 47 | -------------------------------------------------------------------------------- /__tests__/batch-and-notify.ts: -------------------------------------------------------------------------------- 1 | import { batch, notify } from '../src/index.js' 2 | import { delay, withSnapshotDefault } from './utils/test.js' 3 | import { expect, it } from 'vitest' 4 | 5 | // try those orders: 6 | // notify(batch(server)[0]) 7 | // notify(batch(server)[0].fn) 8 | // batch(notify(server)) 9 | 10 | it( 11 | 'batch and notify can be used with style 1', 12 | withSnapshotDefault('async-call-batch-notify-1', async ({ init, log }) => { 13 | const server = init() 14 | const [b, send] = batch(server) 15 | const n = notify(b) 16 | await expect(Promise.all([n.add(1, 2), n.throws(), notify(b.add)(3, 4)])).resolves.toMatchInlineSnapshot(` 17 | [ 18 | undefined, 19 | undefined, 20 | undefined, 21 | ] 22 | `) 23 | log('No log before this line') 24 | send() 25 | await delay(50) 26 | }), 27 | ) 28 | 29 | it( 30 | 'batch and notify can be used with style 2', 31 | withSnapshotDefault('async-call-batch-notify-2', async ({ init, log }) => { 32 | const server = init() 33 | const [b, _send, drop] = batch(server) 34 | const n = notify(b) 35 | await expect(Promise.all([n.add(1, 2), n.throws()])).resolves.toMatchInlineSnapshot(` 36 | [ 37 | undefined, 38 | undefined, 39 | ] 40 | `) 41 | log('No log in this file') 42 | drop() 43 | await delay(50) 44 | }), 45 | ) 46 | 47 | it( 48 | 'batch and notify can be used with style 3', 49 | withSnapshotDefault('async-call-batch-notify-3', async ({ init, log }) => { 50 | const server = init() 51 | const [b, send] = batch(notify(server)) 52 | await expect(Promise.all([b.add(1, 2), b.throws()])).resolves.toMatchInlineSnapshot(` 53 | [ 54 | undefined, 55 | undefined, 56 | ] 57 | `) 58 | log('No log before this file') 59 | send() 60 | await delay(50) 61 | }), 62 | ) 63 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-strict.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client sent 4 | 5 | ```php 6 | "unknown message" 7 | ``` 8 | 9 | ## T=1 Message: client sent 10 | 11 | ```php 12 | Object { 13 | "id": 0, 14 | "jsonrpc": "2.0", 15 | "method": "not_defined_method", 16 | "params": Array [], 17 | } 18 | ``` 19 | 20 | ## T=2 Message: server received 21 | 22 | ```php 23 | "unknown message" 24 | ``` 25 | 26 | ## T=3 Message: server sent 27 | 28 | ```php 29 | Object { 30 | "error": Object { 31 | "code": -32600, 32 | "message": "Invalid Request", 33 | }, 34 | "id": null, 35 | "jsonrpc": "2.0", 36 | } 37 | ``` 38 | 39 | ## T=4 Message: server received 40 | 41 | ```php 42 | Object { 43 | "id": 0, 44 | "jsonrpc": "2.0", 45 | "method": "not_defined_method", 46 | "params": Array [], 47 | } 48 | ``` 49 | 50 | ## T=5 Message: server sent 51 | 52 | ```php 53 | Object { 54 | "error": Object { 55 | "code": -32601, 56 | "message": "Method not found", 57 | }, 58 | "id": 0, 59 | "jsonrpc": "2.0", 60 | } 61 | ``` 62 | 63 | ## T=6 Message: client received 64 | 65 | ```php 66 | Object { 67 | "error": Object { 68 | "code": -32600, 69 | "message": "Invalid Request", 70 | }, 71 | "id": null, 72 | "jsonrpc": "2.0", 73 | } 74 | ``` 75 | 76 | ## T=7 Log: client/log 77 | 78 | ```php 79 | Array [ 80 | "Error: Invalid Request(-32600) %c@null 81 | %c", 82 | "color: gray", 83 | "", 84 | ] 85 | ``` 86 | 87 | ## T=8 Message: client received 88 | 89 | ```php 90 | Object { 91 | "error": Object { 92 | "code": -32601, 93 | "message": "Method not found", 94 | }, 95 | "id": 0, 96 | "jsonrpc": "2.0", 97 | } 98 | ``` 99 | 100 | ## T=9 Log: client/log 101 | 102 | ```php 103 | Array [ 104 | "Error: Method not found(-32601) %c@0 105 | %c", 106 | "color: gray", 107 | "", 108 | ] 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/async-call-rpc.asyncgeneratorcall.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [Home](./index.md) > [async-call-rpc](./async-call-rpc.md) > [AsyncGeneratorCall](./async-call-rpc.asyncgeneratorcall.md) 4 | 5 | ## AsyncGeneratorCall() function 6 | 7 | The async generator version of the AsyncCall 8 | 9 | **Signature:** 10 | 11 | ```typescript 12 | export declare function AsyncGeneratorCall(thisSideImplementation: null | undefined | object | Promise, options: AsyncCallOptions): AsyncGeneratorVersionOf; 13 | ``` 14 | 15 | ## Parameters 16 | 17 | | Parameter | Type | Description | 18 | | --- | --- | --- | 19 | | thisSideImplementation | null \| undefined \| object \| Promise<object> | The implementation when this AsyncCall acts as a JSON RPC server. | 20 | | options | [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) | [AsyncCallOptions](./async-call-rpc.asynccalloptions.md) | 21 | 22 | **Returns:** 23 | 24 | [AsyncGeneratorVersionOf](./async-call-rpc.asyncgeneratorversionof.md)<OtherSideImplementedFunctions> 25 | 26 | ## Remarks 27 | 28 | Warning: Due to technical limitation, AsyncGeneratorCall will leak memory. Use it at your own risk. 29 | 30 | To use AsyncGeneratorCall, the server and the client MUST support the following JSON RPC internal methods which is pre ECMAScript async generator semantics: 31 | 32 | - `rpc.async-iterator.start` 33 | 34 | - `rpc.async-iterator.next` 35 | 36 | - `rpc.async-iterator.return` 37 | 38 | - `rpc.async-iterator.throw` 39 | 40 | ## Example 41 | 42 | 43 | ```ts 44 | const server = { 45 | async *generator() { 46 | let last = 0 47 | while (true) yield last++ 48 | }, 49 | } 50 | type Server = typeof server 51 | const serverRPC = AsyncGeneratorCall({}, { channel }) 52 | async function main() { 53 | for await (const x of serverRPC.generator()) { 54 | console.log('Server yielded number', x) 55 | } 56 | } 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-batch.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Log: testRunner/log 4 | 5 | ```php 6 | Array [ 7 | "Before this line no request was sent", 8 | ] 9 | ``` 10 | 11 | ## T=1 Message: client => server 12 | 13 | ```php 14 | Array [ 15 | Object { 16 | "id": 0, 17 | "jsonrpc": "2.0", 18 | "method": "add", 19 | "params": Array [ 20 | 2, 21 | 3, 22 | ], 23 | }, 24 | Object { 25 | "id": 1, 26 | "jsonrpc": "2.0", 27 | "method": "echo", 28 | "params": Array [ 29 | 1, 30 | ], 31 | }, 32 | ] 33 | ``` 34 | 35 | ## T=2 Log: server/log 36 | 37 | ```php 38 | Array [ 39 | "rpc.%cadd%c(%o, %o%c) 40 | %o %c@0", 41 | "color:#d2c057", 42 | "", 43 | 2, 44 | 3, 45 | "", 46 | Promise {}, 47 | "color:gray;font-style:italic;", 48 | ] 49 | ``` 50 | 51 | ## T=3 Log: server/log 52 | 53 | ```php 54 | Array [ 55 | "rpc.%cecho%c(%o%c) 56 | %o %c@1", 57 | "color:#d2c057", 58 | "", 59 | 1, 60 | "", 61 | Promise {}, 62 | "color:gray;font-style:italic;", 63 | ] 64 | ``` 65 | 66 | ## T=4 Message: server => client 67 | 68 | ```php 69 | Array [ 70 | Object { 71 | "id": 0, 72 | "jsonrpc": "2.0", 73 | "result": 5, 74 | }, 75 | Object { 76 | "id": 1, 77 | "jsonrpc": "2.0", 78 | "result": 1, 79 | }, 80 | ] 81 | ``` 82 | 83 | ## T=5 Log: testRunner/log 84 | 85 | ```php 86 | Array [ 87 | "After this line no request was sent", 88 | ] 89 | ``` 90 | 91 | ## T=6 Log: testRunner/log 92 | 93 | ```php 94 | Array [ 95 | "Part 1 end", 96 | ] 97 | ``` 98 | 99 | ## T=7 Log: testRunner/log 100 | 101 | ```php 102 | Array [ 103 | "Part 2 start", 104 | ] 105 | ``` 106 | 107 | ## T=8 Log: testRunner/log 108 | 109 | ```php 110 | Array [ 111 | "In this part it should has no log", 112 | ] 113 | ``` 114 | 115 | ## T=9 Log: testRunner/log 116 | 117 | ```php 118 | Array [ 119 | "Part 2 end", 120 | ] 121 | ``` 122 | -------------------------------------------------------------------------------- /__tests__/serialization.ts: -------------------------------------------------------------------------------- 1 | import { JSONSerialization, NoSerialization } from '../src/index.js' 2 | import { withSnapshotDefault } from './utils/test.js' 3 | import { BSON_Serialization } from '../utils-src/node/bson.js' 4 | import { expect, it } from 'vitest' 5 | 6 | it( 7 | 'json', 8 | withSnapshotDefault('serialization-json-default', async ({ init }) => { 9 | const server = init({ options: { serializer: JSONSerialization() } }) 10 | expect(await server.undefined()).toMatchInlineSnapshot(`null`) 11 | }), 12 | ) 13 | 14 | it( 15 | 'json with keep undefined', 16 | withSnapshotDefault('serialization-json-keep-undefined', async ({ init }) => { 17 | const server = init({ options: { serializer: JSONSerialization(undefined, undefined, 'keep') } }) 18 | expect(await server.undefined()).toMatchInlineSnapshot(`undefined`) 19 | }), 20 | ) 21 | 22 | it( 23 | 'json with no keep', 24 | withSnapshotDefault('serialization-json-no-keep', async ({ init }) => { 25 | const server = init({ options: { serializer: JSONSerialization(undefined, undefined, false) } }) 26 | expect(await server.undefined()).toMatchInlineSnapshot(`undefined`) 27 | }), 28 | ) 29 | 30 | it( 31 | 'bson', 32 | withSnapshotDefault('serialization-bson', async ({ init }) => { 33 | const server = init({ options: { serializer: BSON_Serialization(require('bson')) } }) 34 | expect(await server.add(1, 2)).toMatchInlineSnapshot(`3`) 35 | expect(await server.undefined()).toMatchInlineSnapshot(`undefined`) 36 | await expect(server.throws()).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: impl error]`) 37 | }), 38 | ) 39 | 40 | it( 41 | 'no-serialization', 42 | withSnapshotDefault('serialization-no-serialization', async ({ init }) => { 43 | const server = init({ options: { serializer: NoSerialization } }) 44 | expect(await server.add(1, 2)).toMatchInlineSnapshot(`3`) 45 | expect(await server.undefined()).toMatchInlineSnapshot(`undefined`) 46 | await expect(server.throws()).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: impl error]`) 47 | }), 48 | ) 49 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'Code scanning - action' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '0 6 1 * *' 11 | 12 | jobs: 13 | CodeQL-Build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | with: 20 | # We must fetch at least the immediate parents so that if this is 21 | # a pull request then we can checkout the head. 22 | fetch-depth: 2 23 | 24 | # If this run was triggered by a pull request event, then checkout 25 | # the head of the pull request instead of the merge commit. 26 | - run: git checkout HEAD^2 27 | if: ${{ github.event_name == 'pull_request' }} 28 | 29 | # Initializes the CodeQL tools for scanning. 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v1 32 | # Override language selection by uncommenting this and choosing your languages 33 | # with: 34 | # languages: go, javascript, csharp, python, cpp, java 35 | 36 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 37 | # If this step fails, then you should remove it and run the build manually (see below) 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@v1 40 | 41 | # ℹ️ Command-line programs to run using the OS shell. 42 | # 📚 https://git.io/JvXDl 43 | 44 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 45 | # and modify them (or add more) to build your code if your project 46 | # uses a compiled language 47 | 48 | #- run: | 49 | # make bootstrap 50 | # make release 51 | 52 | - name: Perform CodeQL Analysis 53 | uses: github/codeql-action/analyze@v1 54 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-client-abort-signal.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client sent 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "delay", 10 | "params": Array [ 11 | 200, 12 | "first", 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Message: client sent 18 | 19 | ```php 20 | Object { 21 | "id": 1, 22 | "jsonrpc": "2.0", 23 | "method": "delay", 24 | "params": Array [ 25 | 400, 26 | "second", 27 | ], 28 | } 29 | ``` 30 | 31 | ## T=2 Message: server received 32 | 33 | ```php 34 | Object { 35 | "id": 0, 36 | "jsonrpc": "2.0", 37 | "method": "delay", 38 | "params": Array [ 39 | 200, 40 | "first", 41 | ], 42 | } 43 | ``` 44 | 45 | ## T=3 Log: server/log 46 | 47 | ```php 48 | Array [ 49 | "rpc.%cdelay%c(%o, %o%c) 50 | %o %c@0", 51 | "color:#d2c057", 52 | "", 53 | 200, 54 | "first", 55 | "", 56 | Promise {}, 57 | "color:gray;font-style:italic;", 58 | ] 59 | ``` 60 | 61 | ## T=4 Message: server received 62 | 63 | ```php 64 | Object { 65 | "id": 1, 66 | "jsonrpc": "2.0", 67 | "method": "delay", 68 | "params": Array [ 69 | 400, 70 | "second", 71 | ], 72 | } 73 | ``` 74 | 75 | ## T=5 Log: server/log 76 | 77 | ```php 78 | Array [ 79 | "rpc.%cdelay%c(%o, %o%c) 80 | %o %c@1", 81 | "color:#d2c057", 82 | "", 83 | 400, 84 | "second", 85 | "", 86 | Promise {}, 87 | "color:gray;font-style:italic;", 88 | ] 89 | ``` 90 | 91 | ## T=6 Log: testRunner/log 92 | 93 | ```php 94 | Array [ 95 | "soft abort", 96 | ] 97 | ``` 98 | 99 | ## T=7 Message: server => client 100 | 101 | ```php 102 | Object { 103 | "id": 0, 104 | "jsonrpc": "2.0", 105 | "result": undefined, 106 | } 107 | ``` 108 | 109 | ## T=8 Log: testRunner/log 110 | 111 | ```php 112 | Array [ 113 | "hard abort", 114 | ] 115 | ``` 116 | 117 | ## T=9 Message: server => client 118 | 119 | ```php 120 | Object { 121 | "id": 1, 122 | "jsonrpc": "2.0", 123 | "result": undefined, 124 | } 125 | ``` 126 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /__tests__/utils/reproduce.ts: -------------------------------------------------------------------------------- 1 | export function reproduceIDGenerator() { 2 | let id = 0 3 | return () => id++ 4 | } 5 | export async function reproduceRandomID(f: Function) { 6 | const old = Number.prototype.toString 7 | let last = 1 8 | Number.prototype.toString = function (radix) { 9 | if (radix === 36) return 'random-id-' + last++ 10 | return old.call(this, radix) 11 | } 12 | try { 13 | return await f() 14 | } catch (e) { 15 | throw e 16 | } finally { 17 | Number.prototype.toString = old 18 | } 19 | } 20 | export async function reproduceDOMException(badImpl: boolean, f: Function) { 21 | const old = globalThis.DOMException 22 | Object.defineProperty(globalThis, 'DOMException', { 23 | configurable: true, 24 | value: class DOMException extends old { 25 | static nextThrow = false 26 | constructor(...args: any) { 27 | super(...args) 28 | this.stack = '' 29 | if (badImpl) { 30 | if (DOMException.nextThrow) { 31 | DOMException.nextThrow = false 32 | throw new Error('') 33 | } else DOMException.nextThrow = true 34 | } 35 | } 36 | } as any, 37 | }) 38 | try { 39 | return await f() 40 | } catch (e) { 41 | throw e 42 | } finally { 43 | Object.defineProperty(globalThis, 'DOMException', { configurable: true, value: old }) 44 | } 45 | } 46 | export async function reproduceError(f: Function) { 47 | const orig = Error 48 | globalThis.Error = class Error extends orig { 49 | constructor(msg: string) { 50 | super(msg) 51 | this.stack = '' 52 | } 53 | } as any 54 | const old = JSON.parse 55 | JSON.parse = (...args) => { 56 | try { 57 | return old(...args) 58 | } catch (e: any) { 59 | e.stack = '' 60 | throw e 61 | } 62 | } 63 | try { 64 | return await f() 65 | } catch (e) { 66 | throw e 67 | } finally { 68 | globalThis.Error = orig 69 | JSON.parse = old 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { swc, minify } from 'rollup-plugin-swc3' 2 | 3 | /** @type {import('rollup').RollupOptions} */ 4 | const base = { 5 | input: './src/Async-Call.ts', 6 | output: outputMatrix('base'), 7 | plugins: [swc({ sourceMaps: true })], 8 | } 9 | 10 | /** @type {import('rollup').RollupOptions} */ 11 | const full = { 12 | input: './src/index.ts', 13 | output: outputMatrix('full'), 14 | plugins: [swc({ sourceMaps: true })], 15 | } 16 | export default [base, full] 17 | 18 | /** 19 | * @param {string} name 20 | * @param {('es' | 'umd')[]} format 21 | * @returns {import('rollup').OutputOptions[]} 22 | */ 23 | function outputMatrix(name, format = ['es', 'umd']) { 24 | return format.flatMap((f) => { 25 | /** @returns {import('rollup').OutputOptions} */ 26 | const base = (compress = false) => { 27 | const baseName = getBaseName(name, compress) 28 | return { 29 | file: `./out/${baseName}.${f === 'es' ? 'm' : ''}js`, 30 | name: 'AsyncCall', 31 | sourcemap: true, 32 | format: f, 33 | banner: `/// `, 34 | plugins: [ 35 | compress && 36 | minify({ 37 | sourceMap: true, 38 | compress: { 39 | unsafe: true, 40 | ecma: 2018, 41 | // unsafe_arrows: true, 42 | unsafe_symbols: true, 43 | unsafe_undefined: true, 44 | drop_debugger: false, 45 | pure_getters: true, 46 | keep_fargs: false, 47 | module: f === 'es', 48 | }, 49 | ecma: 2018, 50 | module: f === 'es', 51 | }), 52 | ], 53 | } 54 | } 55 | return [base(false), base(true)] 56 | }) 57 | } 58 | /** 59 | * @param {string} name 60 | * @param {boolean} compress 61 | */ 62 | function getBaseName(name, compress) { 63 | if (!compress) return name 64 | return `${name}.min` 65 | } 66 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/async-call-log-all.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | "remoteStack": "", 15 | } 16 | ``` 17 | 18 | ## T=1 Log: server/log 19 | 20 | ```php 21 | Array [ 22 | "rpc.%cadd%c(%o, %o%c) 23 | %o %c@0", 24 | "color:#d2c057", 25 | "", 26 | 1, 27 | 2, 28 | "", 29 | Promise {}, 30 | "color:gray;font-style:italic;", 31 | [Function anonymous], 32 | ] 33 | ``` 34 | 35 | ## T=2 Log: server/log 36 | 37 | ```php 38 | Array [ 39 | "", 40 | ] 41 | ``` 42 | 43 | ## T=3 Log: server/log 44 | 45 | ```php 46 | Array [] 47 | ``` 48 | 49 | ## T=4 Message: server => client 50 | 51 | ```php 52 | Object { 53 | "id": 0, 54 | "jsonrpc": "2.0", 55 | "result": 3, 56 | } 57 | ``` 58 | 59 | ## T=5 Message: client => server 60 | 61 | ```php 62 | Object { 63 | "id": 1, 64 | "jsonrpc": "2.0", 65 | "method": "throws", 66 | "params": Array [], 67 | "remoteStack": "", 68 | } 69 | ``` 70 | 71 | ## T=6 Log: server/log 72 | 73 | ```php 74 | Array [ 75 | "rpc.%cthrows%c(%c) 76 | %o %c@1", 77 | "color:#d2c057", 78 | "", 79 | "", 80 | Promise {}, 81 | "color:gray;font-style:italic;", 82 | [Function anonymous], 83 | ] 84 | ``` 85 | 86 | ## T=7 Log: server/log 87 | 88 | ```php 89 | Array [ 90 | "", 91 | ] 92 | ``` 93 | 94 | ## T=8 Log: server/log 95 | 96 | ```php 97 | Array [] 98 | ``` 99 | 100 | ## T=9 Log: server/log 101 | 102 | ```php 103 | Array [ 104 | [Error: impl error], 105 | ] 106 | ``` 107 | 108 | ## T=10 Message: server => client 109 | 110 | ```php 111 | Object { 112 | "error": Object { 113 | "code": -1, 114 | "data": Object { 115 | "stack": "", 116 | "type": "Error", 117 | }, 118 | "message": "impl error", 119 | }, 120 | "id": 1, 121 | "jsonrpc": "2.0", 122 | } 123 | ``` 124 | 125 | ## T=11 Log: client/log 126 | 127 | ```php 128 | Array [ 129 | "Error: impl error(-1) %c@1 130 | %c", 131 | "color: gray", 132 | "", 133 | ] 134 | ``` 135 | -------------------------------------------------------------------------------- /src/core/batch.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '../utils/constants.ts' 2 | import { AsyncCallBatch, AsyncCallNotify } from '../utils/internalSymbol.ts' 3 | import type { Request } from '../types.ts' 4 | /** 5 | * Wrap the AsyncCall instance to use batch call. 6 | * @param asyncCallInstance - The AsyncCall instance 7 | * @example 8 | * const [batched, send, drop] = batch(AsyncCall(...)) 9 | * batched.call1() // pending 10 | * batched.call2() // pending 11 | * send() // send all pending requests 12 | * drop() // drop all pending requests 13 | * @returns It will return a tuple. 14 | * 15 | * The first item is the batched version of AsyncCall instance passed in. 16 | * 17 | * The second item is a function to send all pending requests. 18 | * 19 | * The third item is a function to drop and reject all pending requests. 20 | * @public 21 | */ 22 | // TODO: use private field in the future. 23 | export function batch(asyncCallInstance: T): [T, () => void, (error?: unknown) => void] { 24 | const queue: BatchQueue = [] 25 | const getTrap = new Proxy( 26 | {}, 27 | { 28 | get(_, p) { 29 | // @ts-expect-error 30 | const f = (...args: any) => asyncCallInstance[AsyncCallBatch](queue, p, ...args) 31 | // @ts-expect-error 32 | f[AsyncCallNotify] = (...args: any) => 33 | // @ts-expect-error 34 | asyncCallInstance[AsyncCallBatch][AsyncCallNotify](queue, p, ...args) 35 | f[AsyncCallNotify][AsyncCallNotify] = f[AsyncCallNotify] 36 | isString(p) && Object.defineProperty(methodContainer, p, { value: f, configurable: true }) 37 | return f 38 | }, 39 | }, 40 | ) 41 | const methodContainer = { __proto__: getTrap } as any 42 | return [ 43 | new Proxy(methodContainer, { 44 | getPrototypeOf: () => null, 45 | setPrototypeOf: (_, value) => value === null, 46 | }), 47 | () => { 48 | queue.length && queue.r![0]() 49 | queue.length = 0 50 | }, 51 | (error = new Error('Aborted')) => { 52 | queue.length && queue.r![1](error) 53 | queue.length = 0 54 | }, 55 | ] 56 | } 57 | export type BatchQueue = Request[] & { 58 | /** Request handler */ 59 | r?: [emit: () => void, reject: (error?: unknown) => void] 60 | } 61 | -------------------------------------------------------------------------------- /__tests__/__file_snapshots__/serialization-no-serialization.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | ## T=0 Message: client => server 4 | 5 | ```php 6 | Object { 7 | "id": 0, 8 | "jsonrpc": "2.0", 9 | "method": "add", 10 | "params": Array [ 11 | 1, 12 | 2, 13 | ], 14 | } 15 | ``` 16 | 17 | ## T=1 Log: server/log 18 | 19 | ```php 20 | Array [ 21 | "rpc.%cadd%c(%o, %o%c) 22 | %o %c@0", 23 | "color:#d2c057", 24 | "", 25 | 1, 26 | 2, 27 | "", 28 | Promise {}, 29 | "color:gray;font-style:italic;", 30 | ] 31 | ``` 32 | 33 | ## T=2 Message: server => client 34 | 35 | ```php 36 | Object { 37 | "id": 0, 38 | "jsonrpc": "2.0", 39 | "result": 3, 40 | } 41 | ``` 42 | 43 | ## T=3 Message: client => server 44 | 45 | ```php 46 | Object { 47 | "id": 1, 48 | "jsonrpc": "2.0", 49 | "method": "undefined", 50 | "params": Array [], 51 | } 52 | ``` 53 | 54 | ## T=4 Log: server/log 55 | 56 | ```php 57 | Array [ 58 | "rpc.%cundefined%c(%c) 59 | %o %c@1", 60 | "color:#d2c057", 61 | "", 62 | "", 63 | Promise {}, 64 | "color:gray;font-style:italic;", 65 | ] 66 | ``` 67 | 68 | ## T=5 Message: server => client 69 | 70 | ```php 71 | Object { 72 | "id": 1, 73 | "jsonrpc": "2.0", 74 | "result": undefined, 75 | } 76 | ``` 77 | 78 | ## T=6 Message: client => server 79 | 80 | ```php 81 | Object { 82 | "id": 2, 83 | "jsonrpc": "2.0", 84 | "method": "throws", 85 | "params": Array [], 86 | } 87 | ``` 88 | 89 | ## T=7 Log: server/log 90 | 91 | ```php 92 | Array [ 93 | "rpc.%cthrows%c(%c) 94 | %o %c@2", 95 | "color:#d2c057", 96 | "", 97 | "", 98 | Promise {}, 99 | "color:gray;font-style:italic;", 100 | ] 101 | ``` 102 | 103 | ## T=8 Log: server/log 104 | 105 | ```php 106 | Array [ 107 | [Error: impl error], 108 | ] 109 | ``` 110 | 111 | ## T=9 Message: server => client 112 | 113 | ```php 114 | Object { 115 | "error": Object { 116 | "code": -1, 117 | "data": Object { 118 | "type": "Error", 119 | }, 120 | "message": "impl error", 121 | }, 122 | "id": 2, 123 | "jsonrpc": "2.0", 124 | } 125 | ``` 126 | 127 | ## T=10 Log: client/log 128 | 129 | ```php 130 | Array [ 131 | "Error: impl error(-1) %c@2 132 | %c", 133 | "color: gray", 134 | "", 135 | ] 136 | ``` 137 | -------------------------------------------------------------------------------- /src/utils/serialization.ts: -------------------------------------------------------------------------------- 1 | import { isObject, undefined } from './constants.ts' 2 | import type { Serialization } from '../types.ts' 3 | 4 | /** 5 | * Serialization implementation that do nothing 6 | * @remarks {@link Serialization} 7 | * @public 8 | * @deprecated Will be removed in next major version 9 | */ 10 | export const NoSerialization: Serialization = { 11 | serialization(from) { 12 | return from 13 | }, 14 | deserialization(serialized) { 15 | return serialized 16 | }, 17 | } 18 | 19 | /** 20 | * Create a serialization by JSON.parse/stringify 21 | * 22 | * @param replacerAndReceiver - Replacer and receiver of JSON.parse/stringify 23 | * @param space - Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. 24 | * @param undefinedKeepingBehavior - How to keep "undefined" in result of SuccessResponse? 25 | * 26 | * If it is not handled properly, JSON.stringify will emit an invalid JSON RPC object. 27 | * 28 | * Options: 29 | * - `"null"`(**default**): convert it to null. 30 | * - `"keep"`: try to keep it by additional property "undef". 31 | * - `false`: Don't keep it, let it break. 32 | * @remarks {@link Serialization} 33 | * @public 34 | */ 35 | export const JSONSerialization = ( 36 | replacerAndReceiver: [((key: string, value: any) => any)?, ((key: string, value: any) => any)?] = [ 37 | undefined, 38 | undefined, 39 | ], 40 | space?: string | number | undefined, 41 | undefinedKeepingBehavior: 'keep' | 'null' | false = 'null', 42 | ): Serialization => ({ 43 | serialization(from) { 44 | if (undefinedKeepingBehavior && isObject(from) && 'result' in from && from.result === undefined) { 45 | const alt = { ...from } 46 | alt.result = null 47 | if (undefinedKeepingBehavior === 'keep') (alt as any).undef = true 48 | from = alt 49 | } 50 | return JSON.stringify(from, replacerAndReceiver[0], space) 51 | }, 52 | deserialization(serialized) { 53 | const result = JSON.parse(serialized as string, replacerAndReceiver[1]) 54 | if ( 55 | isObject(result) && 56 | 'result' in result && 57 | result.result === null && 58 | 'undef' in result && 59 | result.undef === true 60 | ) { 61 | result.result = undefined 62 | delete result.undef 63 | } 64 | return result 65 | }, 66 | }) 67 | -------------------------------------------------------------------------------- /__tests__/brand.ts: -------------------------------------------------------------------------------- 1 | import { withSnapshotDefault } from './utils/test.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it( 5 | 'methods and prototype brand check', 6 | withSnapshotDefault('async-call-brand', async ({ init }) => { 7 | const server = init() 8 | 9 | // Prototype check 10 | expect(Object.getPrototypeOf(server)).toBeNull() 11 | Reflect.setPrototypeOf(server, {}) 12 | expect(Object.getPrototypeOf(server)).toBeNull() 13 | 14 | // Method equality check 15 | expect(server.add).toBe(server.add) 16 | 17 | // Method name check 18 | expect(server.add.name).toBe('add') 19 | 20 | // [[GetOwnPropertyDescriptor]] check 21 | { 22 | const method = Object.getOwnPropertyDescriptor(server, 'missing-method') 23 | expect(method).toBeTypeOf('object') 24 | expect(Reflect.has(server, 'missing-method')).toBeTruthy() 25 | expect((server as any)['missing-method']).toBe(method!.value!) 26 | } 27 | 28 | // Result check 29 | const q = server.add(0, 1) 30 | expect(q).toBeInstanceOf(Promise) 31 | await q 32 | }), 33 | ) 34 | 35 | it( 36 | '(generator) methods and prototype brand check', 37 | withSnapshotDefault('async-call-generator-brand', async ({ initIterator }) => { 38 | const server = initIterator() 39 | 40 | // Prototype check 41 | expect(Object.getPrototypeOf(server)).toBeNull() 42 | Reflect.setPrototypeOf(server, {}) 43 | expect(Object.getPrototypeOf(server)).toBeNull() 44 | 45 | // Method equality check 46 | expect(server.echo).toBe(server.echo) 47 | 48 | // Method name check 49 | expect(server.echo.name).toBe('echo') 50 | 51 | // [[GetOwnPropertyDescriptor]] check 52 | { 53 | const method = Object.getOwnPropertyDescriptor(server, 'missing-method') 54 | expect(method).toBeTypeOf('object') 55 | expect(Reflect.has(server, 'missing-method')).toBeTruthy() 56 | expect((server as any)['missing-method']).toBe(method!.value!) 57 | } 58 | 59 | // Result check 60 | const iter = server.echo([]) 61 | async function* __() {} 62 | const proto = Object.getPrototypeOf(Object.getPrototypeOf(__())) 63 | const testSymbol = Symbol('test') 64 | Object.defineProperty(proto, testSymbol, { configurable: true, value: 1 }) 65 | expect(Reflect.get(iter, testSymbol)).toBe(1) 66 | Reflect.deleteProperty(proto, testSymbol) 67 | }), 68 | ) 69 | -------------------------------------------------------------------------------- /src/utils/encoder.ts: -------------------------------------------------------------------------------- 1 | import type { IsomorphicEncoder, Request, Requests, Response, Responses, SuccessResponse } from '../types.ts' 2 | import { isArray, undefined } from './constants.ts' 3 | 4 | /** 5 | * @public 6 | * Options of {@link (JSONEncoder:function)} 7 | */ 8 | export interface JSONEncoderOptions { 9 | /** Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. */ 10 | space?: string | number | undefined 11 | /** 12 | * How to handle `"undefined"` in the result of {@link SuccessResponse}. 13 | * 14 | * @remarks 15 | * If you need a full support of encoding `undefined`, for example, when the result is `{ field: undefined }` and you want to keep it, 16 | * you need to find another library to do this. 17 | * 18 | * If this is not handled properly, `JSON.stringify` will emit an invalid JSON-RPC object (fields with `undefined` value will be omitted). 19 | * 20 | * Options: 21 | * - `"null"`(**default**): convert it to `null`. 22 | * - `false`: do not do anything, let it break. 23 | */ 24 | keepUndefined?: false | 'null' | undefined 25 | /** A function that transforms the results. */ 26 | replacer?: ((this: any, key: string, value: any) => any) | undefined 27 | /** A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is. */ 28 | reviver?: ((this: any, key: string, value: any) => any) | undefined 29 | } 30 | /** 31 | * Create a encoder by JSON.parse/stringify 32 | * 33 | * @public 34 | * @param options - Options for this encoder. 35 | * @remarks {@link IsomorphicEncoder} 36 | */ 37 | export function JSONEncoder({ 38 | keepUndefined = 'null', 39 | replacer, 40 | reviver, 41 | space, 42 | }: JSONEncoderOptions = {}): IsomorphicEncoder { 43 | return { 44 | encode(data) { 45 | if (keepUndefined) { 46 | isArray(data) ? data.forEach(undefinedEncode) : undefinedEncode(data) 47 | } 48 | return JSON.stringify(data, replacer, space) 49 | }, 50 | decode(encoded) { 51 | const data: Requests | Responses = JSON.parse(encoded as string, reviver) 52 | return data 53 | }, 54 | } 55 | } 56 | 57 | const undefinedEncode = (i: Response | Request) => { 58 | if ('result' in i && i.result === undefined) { 59 | i.result = null 60 | } 61 | } 62 | 63 | /** @public */ 64 | export namespace JSONEncoder { 65 | export const Default: IsomorphicEncoder = JSONEncoder() 66 | } 67 | -------------------------------------------------------------------------------- /__tests__/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import format from 'pretty-format' 2 | import type { Console } from '../../src/index.js' 3 | import { toMatchFile } from 'jest-file-snapshot' 4 | import { expect } from 'vitest' 5 | 6 | expect.extend({ toMatchFile }) 7 | 8 | export type TimelineItem = 9 | | { type: 'message'; sender?: string; receivers: string[]; message: unknown } 10 | | { type: 'log'; message: unknown[]; kind: string; from: string } 11 | export type Logger = { 12 | send(msg: unknown): void 13 | receive(msg: unknown): void 14 | log: Console 15 | } 16 | export function createLogger(roles: readonly T[]) { 17 | const timeline: TimelineItem[] = [] 18 | function emit() { 19 | let str = ['# Timeline\n'] 20 | let count = 0 21 | for (const line of timeline) { 22 | if (line.type === 'log') { 23 | str.push(`## T=${count++} Log: ${line.from}/${line.type}\n` + formatValue(line.message)) 24 | } else { 25 | let head = '' 26 | if (line.sender && line.receivers.length) head = `${line.sender} => ${line.receivers.join(', ')}` 27 | else if (!line.sender && line.receivers.length) head = `${line.receivers.join(', ')} received` 28 | else if (line.sender && !line.receivers.length) head = `${line.sender} sent` 29 | else head = '???????????????????????' 30 | str.push(`## T=${count++} Message: ${head}\n` + formatValue(line.message)) 31 | } 32 | } 33 | return str.join('\n') 34 | } 35 | const log: Record = {} as any 36 | for (const role of roles) { 37 | log[role] = { 38 | send(message) { 39 | timeline.push({ message, receivers: [], sender: role, type: 'message' }) 40 | }, 41 | receive(message) { 42 | const last = timeline[timeline.length - 1] 43 | if (last?.type === 'message' && JSON.stringify(message) === JSON.stringify(last.message)) 44 | last.receivers.push(role) 45 | else timeline.push({ message, receivers: [role], type: 'message' }) 46 | }, 47 | log: new Proxy({} as Console, { 48 | get(_, kind: string) { 49 | return (...message: any[]) => timeline.push({ message, type: 'log', kind, from: role }) 50 | }, 51 | }), 52 | } 53 | } 54 | return { emit, log } 55 | } 56 | 57 | function code(lang: string, str: string) { 58 | return '\n```' + lang + '\n' + str + '\n```\n' 59 | } 60 | function formatValue(value: unknown) { 61 | return code('php', (format.default || format)(value, { indent: 4 })) 62 | } 63 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Playground 6 | 7 | 8 | See document 9 |
10 | The AsyncCall, AsyncGeneratorCall, JSONSerialization, NoSerialization is available in globalThis 11 | are available as the global variable 12 |
13 | Call define(fns) to define a new demo JSON RPC server. Call await sleep(ms) to slow 14 | down the server. 15 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/utils/error.ts: -------------------------------------------------------------------------------- 1 | import type { AbortSignalLike } from '../types.ts' 2 | 3 | class CustomError extends Error { 4 | // TODO: support cause 5 | constructor( 6 | public name: string, 7 | message: string, 8 | public code: number, 9 | public stack: string, 10 | ) { 11 | super(message) 12 | } 13 | } 14 | export const Err_Cannot_find_a_running_iterator_with_given_ID: unique symbol = {} as any 15 | export const Err_Only_string_can_be_the_RPC_method_name: unique symbol = {} as any 16 | export const Err_Cannot_call_method_starts_with_rpc_dot_directly: unique symbol = {} as any 17 | export const Err_Then_is_accessed_on_local_implementation_Please_explicitly_mark_if_it_is_thenable_in_the_options: unique symbol = 18 | {} as any 19 | const Messages = [ 20 | Err_Cannot_find_a_running_iterator_with_given_ID, 21 | Err_Only_string_can_be_the_RPC_method_name, 22 | Err_Cannot_call_method_starts_with_rpc_dot_directly, 23 | Err_Then_is_accessed_on_local_implementation_Please_explicitly_mark_if_it_is_thenable_in_the_options, 24 | ] as const 25 | // https://github.com/Jack-Works/async-call-rpc/wiki/Error-messages 26 | export const makeHostedMessage = (id: (typeof Messages)[number], error: Error) => { 27 | const n = Messages.indexOf(id) 28 | error.message += `Error ${n}: https://github.com/Jack-Works/async-call-rpc/wiki/Errors#` + n 29 | return error 30 | } 31 | // ! side effect 32 | /** These Error is defined in ECMAScript spec */ 33 | const errors: Record = { 34 | // @ts-expect-error 35 | __proto__: null, 36 | Error, 37 | EvalError, 38 | RangeError, 39 | ReferenceError, 40 | SyntaxError, 41 | TypeError, 42 | URIError, 43 | } 44 | export const DOMExceptionHeader = 'DOMException:' 45 | /** 46 | * AsyncCall support somehow transfer ECMAScript Error 47 | */ 48 | export const RecoverError = (type: string, message: string, code: number, stack: string): Error => { 49 | try { 50 | let E 51 | if (type.startsWith(DOMExceptionHeader) && (E = globalDOMException())) { 52 | const name = type.slice(DOMExceptionHeader.length) 53 | return new E(message, name) 54 | } else if (type in errors) { 55 | const e = new errors[type]!(message) 56 | e.stack = stack 57 | // @ts-expect-error 58 | e.code = code 59 | return e 60 | } else { 61 | return new CustomError(type, message, code, stack) 62 | } 63 | } catch { 64 | return new Error(`E${code} ${type}: ${message}\n${stack}`) 65 | } 66 | } 67 | export const removeStackHeader = (stack: unknown) => String(stack).replace(/^.+\n.+\n/, '') 68 | // ! side effect 69 | export const globalDOMException = (() => { 70 | try { 71 | // @ts-expect-error 72 | return DOMException 73 | } catch {} 74 | }) as () => DOMException | undefined 75 | type DOMException = { new (message: string, name: string): any } 76 | export function onAbort(signal: AbortSignalLike | undefined, callback: () => void) { 77 | signal && signal.addEventListener('abort', callback, { once: true }) 78 | } 79 | -------------------------------------------------------------------------------- /__tests__/batch.ts: -------------------------------------------------------------------------------- 1 | import { batch } from '../src/index.js' 2 | import { withSnapshotDefault } from './utils/test.js' 3 | import { expect, it } from 'vitest' 4 | 5 | it( 6 | 'can send batch request', 7 | withSnapshotDefault('async-call-batch', async ({ init, log }) => { 8 | const [server, send, drop] = batch(init()) 9 | 10 | // brand check 11 | expect(Object.getPrototypeOf(server)).toBeNull() 12 | expect(() => Object.setPrototypeOf(server, {})).toThrow() 13 | expect(Object.getPrototypeOf(server)).toBeNull() 14 | Object.setPrototypeOf(server, null) 15 | 16 | // should not send anything 17 | send() 18 | { 19 | const a = server.add(2, 3) 20 | const b = server.echo(1) 21 | log('Before this line no request was sent') 22 | send() 23 | expect(await a).toMatchInlineSnapshot(`5`) 24 | expect(await b).toMatchInlineSnapshot(`1`) 25 | log('After this line no request was sent') 26 | send() 27 | log('Part 1 end') 28 | } 29 | { 30 | log('Part 2 start') 31 | const a = server.add(2, 3) 32 | const b = server.echo(1) 33 | log('In this part it should has no log') 34 | drop() 35 | expect(a).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Aborted]`) 36 | expect(b).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Aborted]`) 37 | log('Part 2 end') 38 | } 39 | // Keep identity 40 | expect(server.add).toBe(server.add) 41 | }), 42 | ) 43 | 44 | it( 45 | 'can create multiple batch', 46 | withSnapshotDefault('async-call-multiple-batch', async ({ init, log }) => { 47 | const _server = init() 48 | const [server, send, drop] = batch(_server) 49 | const [server2, send2] = batch(_server) 50 | 51 | // should not send anything 52 | send() 53 | { 54 | const a = server.add(2, 3) 55 | const b = server.echo(1) 56 | const a2 = server2.add(4, 5) 57 | const b2 = server2.echo(2) 58 | 59 | log('Before this line no request from batch 1 was sent') 60 | send() 61 | await a 62 | await b 63 | log('After this line no request from batch 1 was sent') 64 | send() 65 | 66 | log('Before this line no request from batch 2 was sent') 67 | send2() 68 | await a2 69 | await b2 70 | log('After this line no request from batch 2 was sent') 71 | send2() 72 | 73 | log('Part 1 end') 74 | } 75 | { 76 | log('Part 2 start') 77 | const a = server.add(2, 3) 78 | const b = server.echo(1) 79 | const a2 = server2.add(4, 5) 80 | const b2 = server2.echo(2) 81 | 82 | drop() 83 | await expect(a).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Aborted]`) 84 | await expect(b).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Aborted]`) 85 | 86 | send2() 87 | await a2 88 | await b2 89 | 90 | log('Part 2 end') 91 | } 92 | }), 93 | ) 94 | --------------------------------------------------------------------------------