Skip to content

Commit

Permalink
feat(infra): introduce op pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo committed Nov 7, 2024
1 parent add8c56 commit 1e0a762
Show file tree
Hide file tree
Showing 11 changed files with 1,278 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/common/infra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@affine/templates": "workspace:*",
"@blocksuite/affine": "0.17.26",
"@datastructures-js/binary-search-tree": "^5.3.2",
"eventemitter2": "^6.4.9",
"foxact": "^0.2.33",
"fractional-indexing": "^3.2.0",
"fuse.js": "^7.0.0",
Expand Down
180 changes: 180 additions & 0 deletions packages/common/infra/src/op/README.md
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);
```
Loading

0 comments on commit 1e0a762

Please sign in to comment.