async-call-rpc
Advanced tools
Comparing version 1.0.0 to 1.0.2
@@ -5,2 +5,11 @@ # Changelog | ||
### [1.0.2](https://github.com/Jack-Works/async-call/compare/v1.0.1...v1.0.2) (2019-12-23) | ||
### [1.0.1](https://github.com/Jack-Works/async-call/compare/v1.0.0...v1.0.1) (2019-12-23) | ||
### Bug Fixes | ||
* change the package name ([234440c](https://github.com/Jack-Works/async-call/commit/234440c2a63d01aeea4f213ee5c07a7ecf9cb29b)) | ||
## 1.0.0 (2019-09-21) | ||
@@ -7,0 +16,0 @@ |
{ | ||
"name": "async-call-rpc", | ||
"version": "1.0.0", | ||
"version": "1.0.2", | ||
"type": "module", | ||
"description": "A lightweight JSON RPC server & client", | ||
"main": "out/index.js", | ||
"module": "out/index.js", | ||
"scripts": { | ||
"release": "standard-version", | ||
"start": "tsc --watch", | ||
"prepublishOnly": "rimraf ./out && tsc" | ||
"start": "node ./watch.js", | ||
"prepublishOnly": "rimraf ./out && tsc", | ||
"doc:api": "api-extractor run --local --verbose", | ||
"doc:md": "api-documenter markdown -o docs -i temp", | ||
"doc:preview": "docsify serve ." | ||
}, | ||
@@ -15,3 +20,5 @@ "repository": { | ||
}, | ||
"keywords": ["jsonrpc"], | ||
"keywords": [ | ||
"jsonrpc" | ||
], | ||
"author": "Jack Works <zjwpeter@gmail.com>", | ||
@@ -24,7 +31,12 @@ "license": "MIT", | ||
"devDependencies": { | ||
"@microsoft/api-extractor": "latest", | ||
"@microsoft/api-documenter": "latest", | ||
"rimraf": "^3.0.0", | ||
"standard-version": "^7.0.0", | ||
"typescript": "^3.6.3" | ||
"standard-version": "^7.0.1", | ||
"typescript": "^3.7.2" | ||
}, | ||
"files": ["src", "out"] | ||
"files": [ | ||
"src", | ||
"out" | ||
] | ||
} |
@@ -0,28 +1,33 @@ | ||
/** | ||
* See the document at https://github.com/Jack-Works/async-call/ | ||
*/ | ||
import { | ||
AsyncCallOptions, | ||
AsyncCall, | ||
calcStrictOptions, | ||
generateRandomID, | ||
$AsyncIteratorStart, | ||
$AsyncIteratorNext, | ||
$AsyncIteratorReturn, | ||
$AsyncIteratorThrow, | ||
$AsyncCallIgnoreResponse, | ||
_calcStrictOptions, | ||
_generateRandomID, | ||
_AsyncIteratorStart, | ||
_AsyncIteratorNext, | ||
_AsyncIteratorReturn, | ||
_AsyncIteratorThrow, | ||
_AsyncCallIgnoreResponse, | ||
} from './Async-Call' | ||
interface AsyncGeneratorInternalMethods { | ||
[$AsyncIteratorStart](method: string, params: unknown[]): Promise<string> | ||
[$AsyncIteratorNext](id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
[$AsyncIteratorReturn](id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
[$AsyncIteratorThrow](id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
[_AsyncIteratorStart](method: string, params: unknown[]): Promise<string> | ||
[_AsyncIteratorNext](id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
[_AsyncIteratorReturn](id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
[_AsyncIteratorThrow](id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
} | ||
/** | ||
* Unbox the Promise<T> into T if possible | ||
* @internal | ||
*/ | ||
export type UnboxPromise<T> = T extends PromiseLike<infer U> ? U : T | ||
export type _UnboxPromise<T> = T extends PromiseLike<infer U> ? U : T | ||
/** | ||
* Make all generator in the type T becomes AsyncGenerator | ||
* @internal | ||
*/ | ||
export type MakeAllGeneratorFunctionsAsync<T> = { | ||
export type _AsyncGeneratorVersionOf<T> = { | ||
[key in keyof T]: T[key] extends ( | ||
@@ -33,4 +38,4 @@ ...args: infer Args | ||
...args: Args | ||
) => AsyncIterator<UnboxPromise<Yield>, UnboxPromise<Return>, UnboxPromise<Next>> & { | ||
[Symbol.asyncIterator](): AsyncIterator<UnboxPromise<Yield>, UnboxPromise<Return>, UnboxPromise<Next>> | ||
) => AsyncIterator<_UnboxPromise<Yield>, _UnboxPromise<Return>, _UnboxPromise<Next>> & { | ||
[Symbol.asyncIterator](): AsyncIterator<_UnboxPromise<Yield>, _UnboxPromise<Return>, _UnboxPromise<Next>> | ||
} | ||
@@ -40,10 +45,45 @@ : T[key] | ||
/** | ||
* This function provides the async generator version of the AsyncCall | ||
* The async generator version of the AsyncCall | ||
* @param thisSideImplementation - The implementation when this AsyncCall acts as a JSON RPC server. | ||
* @param options - {@link AsyncCallOptions} | ||
* @typeParam OtherSideImplementedFunctions - The type of the API that server expose. For any function on this interface, AsyncCall will convert it to the Promised type. | ||
* @remarks | ||
* | ||
* To use AsyncGeneratorCall, the server and the client MUST support the following JSON RPC internal methods: | ||
* | ||
* Warning: Due to technical limitation, AsyncGeneratorCall will leak memory before | ||
* {@link https://github.com/tc39/proposal-weakrefs | the ECMAScript WeakRef proposal} shipped. | ||
* | ||
* - `rpc.async-iterator.start` | ||
* | ||
* - `rpc.async-iterator.next` | ||
* | ||
* - `rpc.async-iterator.return` | ||
* | ||
* - `rpc.async-iterator.throw` | ||
* | ||
* @example | ||
* ```ts | ||
* const server = { | ||
* async *generator() { | ||
* let last = 0 | ||
* while (true) yield last++ | ||
* }, | ||
* } | ||
* type Server = typeof server | ||
* const serverRPC = AsyncGeneratorCall<Server>({}, { messageChannel }) | ||
* async function main() { | ||
* for await (const x of serverRPC.generator()) { | ||
* console.log('Server yielded number', x) | ||
* } | ||
* } | ||
* ``` | ||
* @public | ||
*/ | ||
export function AsyncGeneratorCall<OtherSideImplementedFunctions = {}>( | ||
implementation: object = {}, | ||
thisSideImplementation: object = {}, | ||
options: Partial<AsyncCallOptions> & Pick<AsyncCallOptions, 'messageChannel'>, | ||
): MakeAllGeneratorFunctionsAsync<OtherSideImplementedFunctions> { | ||
): _AsyncGeneratorVersionOf<OtherSideImplementedFunctions> { | ||
const iterators = new Map<string, Iterator<unknown> | AsyncIterator<unknown>>() | ||
const strict = calcStrictOptions(options.strict || false) | ||
const strict = _calcStrictOptions(options.strict || false) | ||
function findIterator(id: string, label: string) { | ||
@@ -53,3 +93,3 @@ const it = iterators.get(id) | ||
if (strict.methodNotFound) throw new Error(`Remote iterator not found while executing ${label}`) | ||
else return $AsyncCallIgnoreResponse | ||
else return _AsyncCallIgnoreResponse | ||
} | ||
@@ -59,27 +99,27 @@ return it | ||
const server = { | ||
[$AsyncIteratorStart](method, args) { | ||
const iteratorGenerator: unknown = Reflect.get(implementation, method) | ||
[_AsyncIteratorStart](method, args) { | ||
const iteratorGenerator: unknown = Reflect.get(thisSideImplementation, method) | ||
if (typeof iteratorGenerator !== 'function') { | ||
if (strict.methodNotFound) throw new Error(method + ' is not a function') | ||
else return $AsyncCallIgnoreResponse | ||
else return _AsyncCallIgnoreResponse | ||
} | ||
const iterator = iteratorGenerator(...args) | ||
const id = generateRandomID() | ||
const id = _generateRandomID() | ||
iterators.set(id, iterator) | ||
return Promise.resolve(id) | ||
}, | ||
[$AsyncIteratorNext](id, val) { | ||
[_AsyncIteratorNext](id, val) { | ||
const it = findIterator(id, 'next') | ||
if (it !== $AsyncCallIgnoreResponse) return it.next(val as any) | ||
if (it !== _AsyncCallIgnoreResponse) return it.next(val as any) | ||
return it | ||
}, | ||
[$AsyncIteratorReturn](id, val) { | ||
[_AsyncIteratorReturn](id, val) { | ||
const it = findIterator(id, 'return') | ||
if (it !== $AsyncCallIgnoreResponse) return it.return!(val) | ||
return $AsyncCallIgnoreResponse | ||
if (it !== _AsyncCallIgnoreResponse) return it.return!(val) | ||
return _AsyncCallIgnoreResponse | ||
}, | ||
[$AsyncIteratorThrow](id, val) { | ||
[_AsyncIteratorThrow](id, val) { | ||
const it = findIterator(id, 'throw') | ||
if (it !== $AsyncCallIgnoreResponse) return it.throw!(val) | ||
return $AsyncCallIgnoreResponse | ||
if (it !== _AsyncCallIgnoreResponse) return it.throw!(val) | ||
return _AsyncCallIgnoreResponse | ||
}, | ||
@@ -94,12 +134,12 @@ } as AsyncGeneratorInternalMethods | ||
return function(...args: unknown[]) { | ||
const id = remote[$AsyncIteratorStart](key, args) | ||
const id = remote[_AsyncIteratorStart](key, args) | ||
return new (class implements AsyncIterableIterator<unknown>, AsyncIterator<unknown, unknown, unknown> { | ||
async return(val: unknown) { | ||
return remote[$AsyncIteratorReturn](await id, val) | ||
return remote[_AsyncIteratorReturn](await id, val) | ||
} | ||
async next(val?: unknown) { | ||
return remote[$AsyncIteratorNext](await id, val) | ||
return remote[_AsyncIteratorNext](await id, val) | ||
} | ||
async throw(val?: unknown) { | ||
return remote[$AsyncIteratorThrow](await id, val) | ||
return remote[_AsyncIteratorThrow](await id, val) | ||
} | ||
@@ -113,3 +153,3 @@ [Symbol.asyncIterator]() { | ||
} | ||
return new Proxy({}, { get: proxyTrap }) as MakeAllGeneratorFunctionsAsync<OtherSideImplementedFunctions> | ||
return new Proxy({}, { get: proxyTrap }) as _AsyncGeneratorVersionOf<OtherSideImplementedFunctions> | ||
} |
/** | ||
* This is a light implementation of JSON RPC 2.0 | ||
* | ||
* https://www.jsonrpc.org/specification | ||
* | ||
* ----------------------------------------------------------------------------- | ||
* Extends to the specification: | ||
* | ||
* Request object: | ||
* remoteStack?: string | ||
* This property will help server print the log better. | ||
* | ||
* Error object: | ||
* data?: { stack?: string, type?: string } | ||
* This property will help client to build a better Error object. | ||
* Supported value for "type" field (Defined in ECMAScript standard): | ||
* Error, EvalError, RangeError, ReferenceError, | ||
* SyntaxError, TypeError, URIError | ||
* | ||
* Response object: | ||
* resultIsUndefined?: boolean | ||
* This property is a hint. If the client is run in JavaScript, | ||
* it should treat "result: null" as "result: undefined" | ||
* ----------------------------------------------------------------------------- | ||
* Implemented JSON RPC extension (internal methods): | ||
* None | ||
* See the document at https://github.com/Jack-Works/async-call/ | ||
*/ | ||
//#region Serialization | ||
/** | ||
* Define how to do serialization and deserialization of remote procedure call | ||
* Serialization and deserialization of the JSON RPC payload | ||
* @public | ||
*/ | ||
@@ -45,4 +21,7 @@ export interface Serialization { | ||
} | ||
/** | ||
* Serialization implementation that do nothing | ||
* @remarks {@link Serialization} | ||
* @public | ||
*/ | ||
@@ -57,9 +36,13 @@ export const NoSerialization: Serialization = { | ||
} | ||
/** | ||
* Serialization implementation by JSON.parse/stringify | ||
* Create a serialization by JSON.parse/stringify | ||
* | ||
* @param replacerAndReceiver - Replacer of JSON.parse/stringify | ||
* @param replacerAndReceiver - Replacer and receiver of JSON.parse/stringify | ||
* @param space - Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. | ||
* @remarks {@link Serialization} | ||
* @public | ||
*/ | ||
export const JSONSerialization = ( | ||
[replacer, receiver]: [Parameters<JSON['stringify']>[1], Parameters<JSON['parse']>[1]] = [undefined, undefined], | ||
replacerAndReceiver: [Parameters<JSON['stringify']>[1], Parameters<JSON['parse']>[1]] = [undefined, undefined], | ||
space?: string | number | undefined, | ||
@@ -69,29 +52,114 @@ ) => | ||
async serialization(from) { | ||
return JSON.stringify(from, replacer, space) | ||
return JSON.stringify(from, replacerAndReceiver[0], space) | ||
}, | ||
async deserialization(serialized) { | ||
return JSON.parse(serialized as string, receiver) | ||
return JSON.parse(serialized as string, replacerAndReceiver[1]) | ||
}, | ||
} as Serialization) | ||
/** | ||
* What should AsyncCall log to console. | ||
* @public | ||
*/ | ||
export interface AsyncCallLogLevel { | ||
/** | ||
* Print the log from the client when act as server | ||
* @defaultValue true | ||
*/ | ||
beCalled?: boolean | ||
/** | ||
* Print errors of self when act as a server | ||
* @defaultValue true | ||
*/ | ||
localError?: boolean | ||
/** | ||
* Print remote errors when act as a client | ||
* @defaultValue true | ||
*/ | ||
remoteError?: boolean | ||
/** | ||
* Send the local call stack to remote when act as a client | ||
* @defaultValue false | ||
*/ | ||
sendLocalStack?: boolean | ||
/** | ||
* How to print the log, 'pretty' is recommended in browser. | ||
* @defaultValue 'pretty' | ||
*/ | ||
type?: 'basic' | 'pretty' | ||
} | ||
/** | ||
* Control the behavior that different from the JSON RPC spec. | ||
* @public | ||
*/ | ||
export interface AsyncCallStrictJSONRPC { | ||
/** | ||
* Return an error when the requested method is not defined | ||
* @defaultValue false | ||
*/ | ||
methodNotFound?: boolean | ||
/** | ||
* don't try to keep `undefined` result (then it will be `null`) | ||
* @defaultValue false | ||
*/ | ||
noUndefined?: boolean | ||
/** | ||
* send an error when receive invalid JSON RPC payload | ||
* @defaultValue false | ||
*/ | ||
unknownMessage?: boolean | ||
} | ||
//#endregion | ||
/** | ||
* Options for {@link AsyncCall} | ||
* @public | ||
*/ | ||
export interface AsyncCallOptions { | ||
/** | ||
* A key to prevent collision with other AsyncCalls. Can be anything, but need to be the same on the both side. | ||
* A key to prevent collision with other AsyncCalls. | ||
* | ||
* @remarks | ||
* The value can be anything, but need to be same on both sides. | ||
* | ||
* This option is useful when you want to run multiple AsyncCall instances on the same message channel. | ||
* | ||
* @example | ||
* these two AsyncCall run on the same channel but they won't affect each other. | ||
* ```ts | ||
* AsyncCall({}, { messageChannel, key: 'app1' }) | ||
* AsyncCall({}, { messageChannel, key: 'app2' }) | ||
* ``` | ||
* | ||
* @defaultValue `default-jsonrpc` | ||
*/ | ||
key: string | ||
/** | ||
* How to serialization and deserialization parameters and return values | ||
* How to serialization and deserialization JSON RPC payload | ||
* | ||
* @remarks | ||
* We offer some built-in serializer: | ||
* - NoSerialization (Do not do any serialization) | ||
* - JSONSerialization (Use JSON.parse/stringify) | ||
* See {@link Serialization}. | ||
* There is some built-in serializer: | ||
* | ||
* - {@link NoSerialization} (Do not do any serialization) | ||
* | ||
* - {@link JSONSerialization} (Use JSON.parse/stringify) | ||
* | ||
* @defaultValue {@link NoSerialization} | ||
*/ | ||
serializer: Serialization | ||
/** | ||
* A class that can let you transfer messages between two sides | ||
* The message channel can let you transport messages between server and client | ||
* @example | ||
* ```ts | ||
* const messageChannel = { | ||
* on(event, callback) { | ||
* document.addEventListener('remote-data', x => callback(x.details)) | ||
* } | ||
* emit(event, data) { | ||
* fetch('/server', { body: data }) | ||
* } | ||
* } | ||
* ``` | ||
*/ | ||
@@ -102,26 +170,16 @@ messageChannel: { | ||
} | ||
/** Log what to console */ | ||
log: | ||
| { | ||
beCalled?: boolean | ||
localError?: boolean | ||
remoteError?: boolean | ||
sendLocalStack?: boolean | ||
type?: 'basic' | 'pretty' | ||
} | ||
| boolean | ||
/** Strict options */ | ||
strict: | ||
| { | ||
/** if method not found, return an error */ | ||
methodNotFound?: boolean | ||
/** do not try to keep `undefined` during transfer (if true, undefined will become null) */ | ||
noUndefined?: boolean | ||
/** send an error when receive unknown message on the channel */ | ||
unknownMessage?: boolean | ||
} | ||
| boolean | ||
/** | ||
* Choose log level. See {@link AsyncCallLogLevel} | ||
* @defaultValue true | ||
*/ | ||
log: AsyncCallLogLevel | boolean | ||
/** | ||
* Strict options. See {@link AsyncCallStrictJSONRPC} | ||
* @defaultValue false | ||
*/ | ||
strict: AsyncCallStrictJSONRPC | boolean | ||
/** | ||
* How parameters passed to remote | ||
* https://www.jsonrpc.org/specification#parameter_structures | ||
* @remarks | ||
* See {@link https://www.jsonrpc.org/specification#parameter_structures} | ||
* @defaultValue "by-position" | ||
@@ -131,3 +189,5 @@ */ | ||
/** | ||
* If `implementation` has the function required, call it directly instead of send it to remote. | ||
* Prefer local implementation than remote. | ||
* @remarks | ||
* 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. | ||
* @defaultValue false | ||
@@ -137,6 +197,8 @@ */ | ||
} | ||
/** | ||
* Make all function in the type T Async | ||
* @internal | ||
*/ | ||
export type MakeAllFunctionsAsync<T> = { | ||
export type _AsyncVersionOf<T> = { | ||
[key in keyof T]: T[key] extends (...args: infer Args) => infer Return | ||
@@ -146,3 +208,3 @@ ? Return extends PromiseLike<infer U> | ||
: (...args: Args) => Promise<Return> | ||
: T[key] | ||
: never | ||
} | ||
@@ -158,67 +220,19 @@ | ||
} as const) | ||
/** | ||
* Async call between different context. | ||
* Create a RPC server & client. | ||
* | ||
* @remarks | ||
* Async call is a high level abstraction of MessageCenter. | ||
* See {@link AsyncCallOptions} | ||
* | ||
* # Shared code | ||
* | ||
* - How to stringify/parse parameters/returns should be shared, defaults to NoSerialization. | ||
* | ||
* - `key` should be shared. | ||
* | ||
* # One side | ||
* | ||
* - Should provide some functions then export its type (for example, `BackgroundCalls`) | ||
* | ||
* - `const call = AsyncCall<ForegroundCalls>(backgroundCalls)` | ||
* | ||
* - Then you can `call` any method on `ForegroundCalls` | ||
* | ||
* # Other side | ||
* | ||
* - Should provide some functions then export its type (for example, `ForegroundCalls`) | ||
* | ||
* - `const call = AsyncCall<BackgroundCalls>(foregroundCalls)` | ||
* | ||
* - Then you can `call` any method on `BackgroundCalls` | ||
* | ||
* Note: Two sides can implement the same function | ||
* | ||
* @example | ||
* For example, here is a mono repo. | ||
* | ||
* Code for UI part: | ||
* ```ts | ||
* const UI = { | ||
* async dialog(text: string) { | ||
* alert(text) | ||
* }, | ||
* } | ||
* export type UI = typeof UI | ||
* const callsClient = AsyncCall<Server>(UI) | ||
* callsClient.sendMail('hello world', 'what') | ||
* ``` | ||
* | ||
* Code for server part | ||
* ```ts | ||
* const Server = { | ||
* async sendMail(text: string, to: string) { | ||
* return true | ||
* } | ||
* } | ||
* export type Server = typeof Server | ||
* const calls = AsyncCall<UI>(Server) | ||
* calls.dialog('hello') | ||
* ``` | ||
* | ||
* @param implementation - Implementation of this side. | ||
* @param options - Define your own serializer, MessageCenter or other options. | ||
* | ||
* @param thisSideImplementation - The implementation when this AsyncCall acts as a JSON RPC server. | ||
* @param options - {@link AsyncCallOptions} | ||
* @typeParam OtherSideImplementedFunctions - The type of the API that server expose. For any function on this interface, AsyncCall will convert it to the Promised type. | ||
* @returns Same as the `OtherSideImplementedFunctions` type parameter, but every function in that interface becomes async and non-function value is removed. | ||
* @public | ||
*/ | ||
export function AsyncCall<OtherSideImplementedFunctions = {}>( | ||
implementation: object = {}, | ||
thisSideImplementation: object = {}, | ||
options: Partial<AsyncCallOptions> & Pick<AsyncCallOptions, 'messageChannel'>, | ||
): MakeAllFunctionsAsync<OtherSideImplementedFunctions> { | ||
): _AsyncVersionOf<OtherSideImplementedFunctions> { | ||
const { serializer, key, strict, log, parameterStructures, preferLocalImplementation } = { | ||
@@ -233,3 +247,3 @@ ...AsyncCallDefaultOptions, | ||
unknownMessage: banUnknownMessage = false, | ||
} = calcStrictOptions(strict) | ||
} = _calcStrictOptions(strict) | ||
const { | ||
@@ -241,5 +255,5 @@ beCalled: logBeCalled = true, | ||
sendLocalStack = false, | ||
} = calcLogOptions(log) | ||
} = _calcLogOptions(log) | ||
const console = getConsole() | ||
type PromiseParam = Parameters<(ConstructorParameters<typeof Promise>)[0]> | ||
type PromiseParam = Parameters<ConstructorParameters<typeof Promise>[0]> | ||
const requestContext = new Map<string | number, { f: PromiseParam; stack: string }>() | ||
@@ -252,4 +266,4 @@ async function onRequest(data: Request): Promise<Response | undefined> { | ||
? Symbol.for(data.method) | ||
: data.method) as keyof typeof implementation | ||
const executor: unknown = implementation[key] | ||
: data.method) as keyof typeof thisSideImplementation | ||
const executor: unknown = thisSideImplementation[key] | ||
if (!executor || typeof executor !== 'function') { | ||
@@ -293,3 +307,3 @@ if (!banMethodNotFound) { | ||
const result = await promise | ||
if (result === $AsyncCallIgnoreResponse) return | ||
if (result === _AsyncCallIgnoreResponse) return | ||
return new SuccessResponse(data.id, await promise, !!noUndefinedKeeping) | ||
@@ -404,3 +418,3 @@ } else { | ||
if (preferLocalImplementation && typeof method === 'string') { | ||
const localImpl: unknown = implementation[method as keyof typeof implementation] | ||
const localImpl: unknown = thisSideImplementation[method as keyof typeof thisSideImplementation] | ||
if (localImpl && typeof localImpl === 'function') { | ||
@@ -417,3 +431,3 @@ return new Promise((resolve, reject) => { | ||
return new Promise((resolve, reject) => { | ||
const id = generateRandomID() | ||
const id = _generateRandomID() | ||
const param0 = params[0] | ||
@@ -437,3 +451,3 @@ const sendingStack = sendLocalStack ? stack : '' | ||
}, | ||
) as MakeAllFunctionsAsync<OtherSideImplementedFunctions> | ||
) as _AsyncVersionOf<OtherSideImplementedFunctions> | ||
@@ -455,12 +469,15 @@ async function handleSingleMessage(data: SuccessResponse | ErrorResponse | Request) { | ||
export const $AsyncIteratorStart = Symbol.for('rpc.async-iterator.start') | ||
export const $AsyncIteratorNext = Symbol.for('rpc.async-iterator.next') | ||
export const $AsyncIteratorReturn = Symbol.for('rpc.async-iterator.return') | ||
export const $AsyncIteratorThrow = Symbol.for('rpc.async-iterator.throw') | ||
export const $AsyncCallIgnoreResponse = Symbol.for('AsyncCall: This response should be ignored.') | ||
/** @internal */ | ||
export const _AsyncIteratorStart = Symbol.for('rpc.async-iterator.start') | ||
/** @internal */ | ||
export const _AsyncIteratorNext = Symbol.for('rpc.async-iterator.next') | ||
/** @internal */ | ||
export const _AsyncIteratorReturn = Symbol.for('rpc.async-iterator.return') | ||
/** @internal */ | ||
export const _AsyncIteratorThrow = Symbol.for('rpc.async-iterator.throw') | ||
/** @internal */ | ||
export const _AsyncCallIgnoreResponse = Symbol.for('AsyncCall: This response should be ignored.') | ||
/** | ||
* @internal | ||
*/ | ||
export function generateRandomID() { | ||
/** @internal */ | ||
export function _generateRandomID() { | ||
return Math.random() | ||
@@ -474,3 +491,3 @@ .toString(36) | ||
*/ | ||
export function calcLogOptions(log: AsyncCallOptions['log']): Exclude<typeof log, boolean> { | ||
function _calcLogOptions(log: AsyncCallOptions['log']): AsyncCallLogLevel { | ||
const logAllOn = { beCalled: true, localError: true, remoteError: true, type: 'pretty' } as const | ||
@@ -484,3 +501,3 @@ const logAllOff = { beCalled: false, localError: false, remoteError: false, type: 'basic' } as const | ||
*/ | ||
export function calcStrictOptions(strict: AsyncCallOptions['strict']): Exclude<typeof strict, boolean> { | ||
export function _calcStrictOptions(strict: AsyncCallOptions['strict']): AsyncCallStrictJSONRPC { | ||
const strictAllOn = { methodNotFound: true, unknownMessage: true, noUndefined: true } | ||
@@ -487,0 +504,0 @@ const strictAllOff = { methodNotFound: false, unknownMessage: false, noUndefined: false } |
147
src/index.ts
@@ -0,2 +1,149 @@ | ||
/** | ||
* A light implementation of {@link https://www.jsonrpc.org/specification | JSON RPC 2.0} | ||
* | ||
* @remarks | ||
* Runtime requirement: At least ECMAScript 6, `globalThis`, well known Symbol `Symbol.asyncIterator` if you use Async Call remote generator function support. | ||
* | ||
* ==================================== | ||
* | ||
* Install | ||
* | ||
* Node: `npm install async-call` | ||
* | ||
* Browser: `import { AsyncCall } from 'https://unpkg.com/async-call-rpc@latest/out/index.js?module'` | ||
* | ||
* Deno: `import { AsyncCall } from 'https://unpkg.com/async-call-rpc@latest/src/Async-Call.ts'` | ||
* | ||
* Other environment: You may copy the `out/Async-Call.js` or `src/Async-Call.ts` to your project. | ||
* | ||
* Async Call has no dependency so you can run it on any modern ECMAScript platform. | ||
* | ||
* ==================================== | ||
* | ||
* Extends to the specification | ||
* | ||
* ```ts | ||
* interface JSONRPC_Request_object { | ||
* // This property will help server print the log better. | ||
* remoteStack?: string | ||
* } | ||
* ``` | ||
* | ||
* ```ts | ||
* interface JSONRPC_Error_object { | ||
* // This property will help client to build a better Error object. | ||
* data?: { | ||
* stack?: string, | ||
* // Supported value for "type" field (Defined in ECMAScript standard): | ||
* type?: string | 'Error' | 'EvalError' | 'RangeError' | ||
* | 'ReferenceError' | 'SyntaxError' | 'TypeError' | 'URIError' | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* ```ts | ||
* interface JSONRPC_Response_object { | ||
* // This property is a hint. | ||
* // If the client is run in JavaScript, | ||
* // it should treat "result: null" as "result: undefined" | ||
* resultIsUndefined?: boolean | ||
* } | ||
* ``` | ||
* | ||
* ==================================== | ||
* | ||
* Implemented internal JSON RPC methods | ||
* | ||
* ```ts | ||
* interface JSONRPC_Internal_Methods { | ||
* // These 4 methods represent the Async Iterator protocol in ECMAScript | ||
* // this method starts an async iterator, return the id | ||
* 'rpc.async-iterator.start'(method: string, params: unknown[]): Promise<string> | ||
* // this method executes `next` method on the previous iterator started by `rpc.async-iterator.start` | ||
* 'rpc.async-iterator.next'(id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
* // this method executes `return` method on the previous iterator started by `rpc.async-iterator.start` | ||
* 'rpc.async-iterator.return'(id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
* // this method executes `throw` method on the previous iterator started by `rpc.async-iterator.start` | ||
* 'rpc.async-iterator.throw'(id: string, value: unknown): Promise<IteratorResult<unknown>> | ||
* } | ||
* ``` | ||
* | ||
* @example | ||
* - Simple usage: | ||
* | ||
* As a Server | ||
* ```ts | ||
* AsyncCall({ | ||
* async add(a: number, b: number) { | ||
* return a + b | ||
* } | ||
* }, { messageChannel }) | ||
* ``` | ||
* | ||
* As a client | ||
* ```ts | ||
* const server = AsyncCall<{ | ||
* add(a: number, b: number): Promise<number> | ||
* }>({}, { messageChannel }) | ||
* server.add(1, 5).then(console.log) | ||
* ``` | ||
* | ||
* - Share type in a mono repo | ||
* | ||
* Code for UI: | ||
* ```ts | ||
* const UI = { | ||
* async dialog(text: string) { | ||
* alert(text) | ||
* }, | ||
* } | ||
* export type UI = typeof UI | ||
* const callsClient = AsyncCall<Server>(UI, { messageChannel }) | ||
* callsClient.sendMail('hello world', 'what') | ||
* ``` | ||
* | ||
* Code for server | ||
* | ||
* ```ts | ||
* const Server = { | ||
* async sendMail(text: string, to: string) { | ||
* return true | ||
* } | ||
* } | ||
* export type Server = typeof Server | ||
* const calls = AsyncCall<UI>(Server, { messageChannel }) | ||
* calls.dialog('hello') | ||
* ``` | ||
* | ||
* - A demo implementation of `messageChannel` | ||
* | ||
* ```ts | ||
class PlayGroundChannel { | ||
static server = document.createElement('a') | ||
static client = document.createElement('a') | ||
// actor: 'server' | 'client' | ||
constructor(actor) { | ||
PlayGroundChannel[actor].addEventListener('targetEventChannel', e => { | ||
const detail = e.detail | ||
for (const f of this.listener) { | ||
try { | ||
f(detail) | ||
} catch {} | ||
} // | ||
}) | ||
} // | ||
listener = [] | ||
on(_, cb) { this.listener.push(cb) } | ||
emit(_, data) { | ||
PlayGroundChannel[this.actor === 'client' ? 'server' : 'client'].dispatchEvent( | ||
new CustomEvent('targetEventChannel', { detail: data }), | ||
) // | ||
} // | ||
} // | ||
``` | ||
* | ||
* @packageDocumentation | ||
*/ | ||
export * from './Async-Call' | ||
export * from './Async-Call-Generator' |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
0
4
Yes
36191
5
7
887
1