Skip to content

Commit

Permalink
Changed Preact thread utilities to be class-based instead of being a …
Browse files Browse the repository at this point in the history
…collection of utility functions (#818)
  • Loading branch information
lemonmade committed Aug 16, 2024
1 parent ecd7322 commit 8669216
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 211 deletions.
40 changes: 40 additions & 0 deletions .changeset/unlucky-spies-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
'@quilted/threads': major
'@quilted/events': patch
'@quilted/quilt': patch
---

Changed Preact thread utilities to be class-based instead of being a collection of utility functions.

Previously, you used `createThreadSignal()` to serialize a Preact signal to pass over a thread, and `acceptThreadSignal()` to turn it into a "live" signal. With the new API, you will do the same steps, but with `ThreadSignal.serialize()` and `new ThreadSignal()`:

```js
import {signal, computed} from '@preact/signals-core';
import {ThreadWebWorker, ThreadSignal} from '@quilted/threads';

// Old API:
const result = signal(32);
const serializedSignal = createThreadSignal(result);
await thread.imports.calculateResult(serializedSignal);

// New API:
const result = signal(32);
const serializedSignal = ThreadSignal.serialize(result);
await thread.imports.calculateResult(serializedSignal);

// In the target thread:

// Old API:
function calculateResult(resultThreadSignal) {
const result = acceptThreadSignal(resultThreadSignal);
const computedSignal = computed(() => `Result from thread: ${result.value}`);
// ...
}

// New API:
function calculateResult(resultThreadSignal) {
const result = new ThreadSignal(resultThreadSignal);
const computedSignal = computed(() => `Result from thread: ${result.value}`);
// ...
}
```
4 changes: 3 additions & 1 deletion packages/events/source/abort/NestedAbortController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
* nested.signal.aborted; // true
*/
export class NestedAbortController extends AbortController {
constructor(...parents: AbortSignal[]) {
constructor(
...parents: Pick<AbortSignal, 'aborted' | 'reason' | 'addEventListener'>[]
) {
super();

const abortedSignal = parents.find((signal) => signal.aborted);
Expand Down
8 changes: 4 additions & 4 deletions packages/quilt/source/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ export type {
ServiceWorkerClientThreads,
} from '@quilted/threads';
export {
isThreadSignal,
createThreadSignal,
acceptThreadSignal,
type ThreadSignal,
threadSignal,
ThreadSignal,
type ThreadSignalOptions,
type ThreadSignalSerialization,
} from '@quilted/threads/signals';
export {
on,
Expand Down
69 changes: 58 additions & 11 deletions packages/threads/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ function calculateResult({
}
```

If you are using `@quilted/threads`’ manual memory management approach, you must explicitly pass `retain` and `release` functions to `ThreadAbortSignal.serialize()` and `new ThreadAbortSignal()` methods:
If you are using `@quilted/threads`’ manual memory management approach, you must explicitly pass `retain` and `release` functions to `ThreadAbortSignal.serialize()` and `new ThreadAbortSignal()` functions:

```ts
import {
Expand Down Expand Up @@ -320,37 +320,84 @@ function calculateResult({

[Preact signals](https://github.com/preactjs/signals) are a powerful tool for managing state in JavaScript applications. Signals represent mutable state that can be subscribed to, so they can be useful for sharing state between JavaScript environments connected by `@quilted/threads`. This library provides a collection of helpers for working with signals across threads.

Like the `AbortSignal` utilities documented above, a pair of utilities is provided to create a "thread-safe" Preact signal on one thread, and "accepting" that signal on another thread. In the thread sending a signal, use the `createThreadSignal()` function from this library, passing it a Preact signal:
Like the `AbortSignal` utilities documented above, a class is provided for creating a "thread-safe" Preact signal on one thread, and accepting that signal on another thread. In the thread sending a signal, use the `ThreadSignal.serialize()` method to serialize your Preact signal:

```ts
import {signal} from '@preact/signals-core';
import {createThreadFromWebWorker} from '@quilted/threads';
import {createThreadSignal} from '@quilted/threads/signals';
import {ThreadSignal} from '@quilted/threads/signals';

const result = signal(32);

const worker = new Worker('worker.js');
const thread = createThreadFromWebWorker(worker);

await thread.calculateResult(createThreadSignal(result));
await thread.calculateResult(ThreadSignal.serialize(result));
```

On the receiving thread, use the `acceptThreadSignal()` to turn it back into a "live" Preact signal, in the current thread’s JavaScript environment:
If you want a Preact signal to be writable in the target environment, and have that value propagate to the original signal, you must pass a `writable: true` option to the `ThreadSignal.serialize()` function:

```ts
import {signal} from '@preact/signals-core';
import {createThreadFromWebWorker} from '@quilted/threads';
import {acceptThreadSignal, type ThreadSignal} from '@quilted/threads/signals';
import {ThreadSignal} from '@quilted/threads/signals';

const result = signal(32);

const worker = new Worker('worker.js');
const thread = createThreadFromWebWorker(worker);

await thread.calculateResult(
ThreadSignal.serialize(result, {
// Allow the target environment to write back to this signal.
writable: true,
}),
);
```

On the receiving thread, use `new ThreadSignal()` (or, equivalently, `threadSignal()`) to turn the serialized version back into a "live" Preact signal, in the current thread’s JavaScript environment:

```ts
import {signal} from '@preact/signals-core';
import {createThreadFromWebWorker} from '@quilted/threads';
import {
ThreadSignal,
type ThreadSignalSerialization,
} from '@quilted/threads/signals';

const thread = createThreadFromWebWorker(self, {
expose: {calculateResult},
});

function calculateResult(serializedSignal: ThreadSignalSerialization<number>) {
const result = new ThreadSignal(serializedSignal); // or threadSignal(serializedSignal)
const computedSignal = computed(() => `Result from thread: ${result.value}`);
}
```

Like with `ThreadAbortSignal` documented above, if you are using `@quilted/threads`’ manual memory management approach, you must explicitly pass `retain` and `release` functions to `ThreadSignal.serialize()` and `new ThreadSignal()` functions:

```ts
import {signal} from '@preact/signals-core';
import {createThreadFromWebWorker} from '@quilted/threads';
import {
retain,
release,
ThreadSignal,
type ThreadSignalSerialization,
} from '@quilted/threads/signals';

const thread = createThreadFromWebWorker(self, {
expose: {calculateResult},
});

function calculateResult(resultThreadSignal: ThreadSignal<number>) {
const result = acceptThreadSignal(resultThreadSignal);
const correctedResult = await figureOutResult();
result.value = correctedAge;
function calculateResult(serializedSignal: ThreadSignalSerialization<number>) {
const result = new ThreadSignal(serializedSignal, {
retain,
release,
});
const computedSignal = computed(() => `Result from thread: ${result.value}`);
}
```

Both `createThreadSignal()` and `acceptThreadSignal()` accept an optional second argument, which must be an options object. The only option accepted is `signal`, which is an `AbortSignal` that allows you to stop synchronizing the Preact signal’s value between threads.
Both `new ThreadSignal()` and `ThreadSignal.serialize()` also accept an optional `signal` option, which is an `AbortSignal` that allows you to stop synchronizing the Preact signal’s value between threads.
20 changes: 18 additions & 2 deletions packages/threads/source/ThreadAbortSignal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export interface ThreadAbortSignalSerialization {
}

export interface ThreadAbortSignalOptions {
/**
* An optional AbortSignal that can cancel synchronizing the
* (Preact) signal to its paired thread.
*/
signal?: AbortSignal;

/**
* An optional function to call in order to manually retain the memory
* associated with the `start` function of the serialized signal.
Expand Down Expand Up @@ -73,7 +79,7 @@ export class ThreadAbortSignal implements AbortSignal {

constructor(
signal: AbortSignal | ThreadAbortSignalSerialization | undefined,
{retain, release}: ThreadAbortSignalOptions = {},
{signal: killswitchSignal, retain, release}: ThreadAbortSignalOptions = {},
) {
if (isAbortSignal(signal)) {
this.#abortSignal = signal;
Expand All @@ -93,8 +99,18 @@ export class ThreadAbortSignal implements AbortSignal {
});

if (release) {
killswitchSignal?.addEventListener(
'abort',
() => () => release(start),
{
once: true,
signal: this.#abortSignal,
},
);

this.#abortSignal.addEventListener('abort', () => release(start), {
once: true,
signal: killswitchSignal,
});
}
}
Expand Down Expand Up @@ -126,7 +142,7 @@ export class ThreadAbortSignal implements AbortSignal {
* `AbortSignal`.
*/
static serialize(
signal: AbortSignal,
signal: Pick<AbortSignal, 'aborted' | 'addEventListener'>,
{retain, release}: ThreadAbortSignalOptions = {},
): ThreadAbortSignalSerialization {
if (signal.aborted) {
Expand Down
Loading

0 comments on commit 8669216

Please sign in to comment.