-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
1,278 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
# Introduction | ||
|
||
Operation Pattern is a tiny `RPC` framework available both in frontend and backend. | ||
|
||
It introduces super simple call and listen signatures to make Worker, cross tabs SharedWorker or BroadcastChannel easier to use and reduce boilerplate. | ||
|
||
# usage | ||
|
||
## Register Op Handlers | ||
|
||
### Function call handler | ||
|
||
```ts | ||
class AddOp extends Op<{ a: number; b: number }, number> {} | ||
|
||
// register | ||
const consumer: OpConsumer; | ||
consumer.register(AddOp, ({ a, b }) => a + b); | ||
|
||
// call | ||
const client: OpClient; | ||
const ret = client.call(new AddOp({ a: 1, b: 2 })); // Promise<3> | ||
``` | ||
|
||
### Stream call handler | ||
|
||
```ts | ||
class SubscribeStatusOp extends Op<string, string> {} | ||
|
||
// register | ||
const consumer: OpConsumer; | ||
consumer.registerSubscribable(SubscribeStatusOp, (name: string) => { | ||
return interval(3000).pipe(map(() => 'connected')); | ||
}); | ||
|
||
// subscribe | ||
const client: OpClient; | ||
client.subscribe(new SubscribeStatusOp('server'), { | ||
next: status => { | ||
ui.setServerStatus(status); | ||
}, | ||
error: error => { | ||
ui.setServerError(error); | ||
}, | ||
complete: () => { | ||
// | ||
}, | ||
}); | ||
``` | ||
|
||
### Transfer variables | ||
|
||
> [Transferable Objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) | ||
#### Client transferables | ||
|
||
```ts | ||
class JobOp extends Op<{ name: string; data: Uint8Array; data2: Uint8Array }, void> {} | ||
|
||
const client: OpClient; | ||
const data = new Uint8Array([1, 2, 3]); | ||
const nonTransferredData = new Uint8Array([1, 2, 3]); | ||
client.call(new JobOp({ name: 'compress', data, data2: nonTransferredData }).transfer([data.buffer])); | ||
|
||
// after transferring, you can not use the transferred variables anymore!!! | ||
// moved | ||
assertEq(data.byteLength, 0); | ||
// copied | ||
assertEq(nonTransferredData.byteLength, 3); | ||
``` | ||
|
||
#### Consumer transferables | ||
|
||
```ts | ||
class JobOp extends Op<{ id: string }, Uint8Array> {} | ||
|
||
const consumer: OpConsumer; | ||
consumer.register(JobOp, ({ id }) => { | ||
const data = new Uint8Array([1, 2, 3]); | ||
return transfer(data, [data.buffer]); | ||
}); | ||
consumer.registerSubscribable(JobOp, ({ id }) => { | ||
return interval(3000).pipe( | ||
map(() => { | ||
const data = new Uint8Array([1, 2, 3]); | ||
transfer(data, [data.buffer]); | ||
}) | ||
); | ||
}); | ||
``` | ||
|
||
## Communication | ||
|
||
### BroadcastChannel | ||
|
||
:::CAUTION | ||
|
||
BroadcastChannel doesn't support transfer transferable objects. All data passed through it's `postMessage` api would be structured cloned | ||
|
||
see [Structured_clone_algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) | ||
|
||
::: | ||
|
||
```ts | ||
const channel = new BroadcastChannel('domain'); | ||
const consumer = new OpConsumer(channel); | ||
consumer.listen(); | ||
|
||
const client = new OpClient(channel); | ||
client.listen(); | ||
``` | ||
|
||
### MessagePort | ||
|
||
```ts | ||
const { port1, port2 } = new MessagePort(); | ||
|
||
const client = new OpClient(port1); | ||
const consumer = new OpConsumer(port2); | ||
``` | ||
|
||
### Worker | ||
|
||
```ts | ||
const worker = new Worker('./xxx-worker'); | ||
const client = new OpClient(worker); | ||
|
||
// in worker | ||
const consumer = new OpConsumer(globalThis); | ||
consumer.listen(); | ||
``` | ||
|
||
### SharedWorker | ||
|
||
```ts | ||
const worker = new SharedWorker('./xxx-worker'); | ||
const client = new OpClient(worker.port); | ||
|
||
// in worker | ||
globalThis.addEventListener('connect', event => { | ||
const port = event.ports[0]; | ||
const consumer = new OpConsumer(port); | ||
consumer.listen(); | ||
}); | ||
``` | ||
|
||
## Why Pass Operations by Classes instead of Runtimeless Interfaces | ||
|
||
### clean code in caller side | ||
|
||
```ts | ||
// class | ||
class XXXOp extends Op<void, void> {} | ||
|
||
call(new XXXOp()); | ||
|
||
// interface | ||
call<Op<void, void>>({}); | ||
``` | ||
|
||
### avoid magic strings & straightforward type checking | ||
|
||
```ts | ||
// class | ||
class AddOp extends Op<{ a: number; b: number }, number> {} | ||
call(new AddOp({ a: 1, b: 2 })); | ||
|
||
register(AddOp, ({ a, b }) => a + b); | ||
|
||
// interface | ||
interface MyOps { | ||
add: [{ a: number; b: number }, number]; | ||
// ^^^^^^^^^^^^^^^^ input ^ output | ||
} | ||
|
||
type OpFromMyOps<T extends keyof MyOps> = Op</* refer Input */, /* refer Output */> | ||
|
||
call<OpFromMyOps<'add'>>('add', { a: 1, b: 2 }); | ||
register<OpFromMyOps<'add'>>('add', ({ a, b }) => a + b); | ||
``` |
Oops, something went wrong.