Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .changeset/event-bus-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@solid-primitives/event-bus": major
---

Migrate to Solid.js v2.0 (beta.10)

## Breaking Changes

**Peer dependency**: `solid-js@^2.0.0-beta.10` is now required.

### `batchEmits`

In Solid 2.0, all signal writes are automatically batched via microtasks. `batchEmits` is now a no-op that returns the bus unchanged. The `batch` wrapper has been removed.

### `toEffect`

Updated to use the Solid 2.0 split `createEffect(compute, apply)` signature. The `on()` helper (removed in Solid 2.0) has been replaced with the split effect pattern. The reactive owner is now explicitly captured at creation time and forwarded via `runWithOwner` in the apply phase, since the apply phase is unowned in Solid 2.0.

### `createEventStack`

The internal stack signal is created with `{ ownedWrite: true }` to allow writes from event listeners called within reactive contexts (e.g. `createRoot`, effects). Signal reads are now deferred in Solid 2.0, so the stack value emitted in event payloads is now computed inside the setter callback to ensure it reflects the updated state.

### Tests

Tests that read signal values after `emit()` now require `flush()` to commit pending signal updates before assertions.
16 changes: 3 additions & 13 deletions packages/event-bus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pnpm add @solid-primitives/event-bus
yarn add @solid-primitives/event-bus
```

Requires `solid-js` `^2.0.0-beta.10`.

## `createEventBus`

Provides all the base functions of an event-emitter, plus additional functions for managing listeners, it's behavior could be customized with an config object. Good for advanced usage.
Expand Down Expand Up @@ -276,26 +278,14 @@ emitInEffect(); // listener will log an owner object

### `batchEmits`

Wraps `emit` calls inside a `batch` call. It causes that listeners execute in a single batch, so they are not executed in sepatate queue ticks.
In Solid 2.0, all signal writes are automatically batched via microtasks, so this function is now a no-op that returns the bus unchanged. It is kept for backwards compatibility.

```ts
import { createEventBus, batchEmits } from "@solid-primitives/event-bus";

const bus = batchEmits(createEventBus());

const [a, setA] = createSignal(0);
const [b, setB] = createSignal(0);

bus.listen(setA);
bus.listen(setB);

bus.emit(1); // will set both a and b to 1 in a single batch
```

## Demo

https://codesandbox.io/s/solid-primitives-event-bus-6fp4h?file=/index.tsx

## Changelog

See [CHANGELOG.md](./CHANGELOG.md)
6 changes: 3 additions & 3 deletions packages/event-bus/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solid-primitives/event-bus",
"version": "1.1.3",
"version": "2.0.0",
"description": "A collection of SolidJS primitives providing various features of a pubsub/event-emitter/event-bus.",
"author": "Damian Tarnawski @thetarnav <gthetarnav@gmail.com>",
"license": "MIT",
Expand Down Expand Up @@ -56,10 +56,10 @@
"@solid-primitives/utils": "workspace:^"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"solid-js": "^2.0.0-beta.10"
},
"typesVersions": {},
"devDependencies": {
"solid-js": "^1.9.7"
"solid-js": "2.0.0-beta.10"
}
}
15 changes: 9 additions & 6 deletions packages/event-bus/src/eventStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ export type EventStackConfig<E, V = E> = {
/**
* Provides all the base functions of an event-emitter, functions for managing listeners, it's behavior could be customized with an config object.
* Additionally it provides the emitted events in a list/history form, with tools to manage it.
*
*
* @returns event stack: `{listen, emit, remove, clear, value, setValue}`
*
*
* @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/event-bus#createEventStack
*
*
* @example
const bus = createEventStack<{ message: string }>();
// can be destructured:
Expand Down Expand Up @@ -61,19 +61,22 @@ export function createEventStack<E, V>(
): EventStack<E, V> {
const { toValue = (e: any) => e as V, length = 0 } = config;

const [stack, setValue] = /*#__PURE__*/ createSignal<V[]>([]);
const [stack, setValue] = /*#__PURE__*/ createSignal<V[]>([], { ownedWrite: true });
const eventEventBus = createEventBus<E>();
const valueEventBus = createEventBus<EventStackPayload<V>>();

eventEventBus.listen(event => {
const value = toValue(event, stack());
// Capture the new stack inside the setter because signal reads are deferred
// in Solid — stack() after setValue() still returns the old committed value.
let newStack: V[];
setValue(prev => {
const list = push(prev, value);
return length && list.length > length ? drop(list) : list;
return (newStack = length && list.length > length ? drop(list) : list);
});
valueEventBus.emit({
event: value,
stack: stack(),
stack: newStack!,
remove: () => remove(value),
});
});
Expand Down
18 changes: 9 additions & 9 deletions packages/event-bus/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { push } from "@solid-primitives/utils/immutable";
import { type AnyFunction } from "@solid-primitives/utils";
import { batch, createEffect, createSignal, on } from "solid-js";
import { createEffect, createSignal, getOwner, runWithOwner } from "solid-js";
import type { Listen, Listener, Emit } from "./eventBus.js";

/**
Expand Down Expand Up @@ -62,25 +62,25 @@ export function once<T>(subscribe: Listen<T>, listener: Listener<T>): VoidFuncti
* emitInEffect() // listener will log an owner object
*/
export function toEffect<T>(emit: Emit<T>): Emit<T> {
const owner = getOwner();
const [stack, setStack] = createSignal<T[]>([]);
createEffect(
on(stack, stack => {
() => stack(),
stack => {
if (!stack.length) return;
setStack([]);
stack.forEach(emit as Emit<any>);
}),
runWithOwner(owner, () => stack.forEach(emit as Emit<any>));
},
);
return (payload?: any) => void setStack(p => push(p, payload));
}

/**
* Wraps `emit` calls inside a `batch` call. It causes that listeners execute in a single batch, so they are not executed in sepatate queue ticks.
* In Solid 2.0 all signal writes are automatically batched via microtask. This function
* is kept for backwards compatibility but is now a no-op — it simply returns the bus unchanged.
*
* @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/event-bus#batchEmits
*/
export function batchEmits<T extends { emit: AnyFunction }>(bus: T): T {
return {
...bus,
emit: (...args) => batch(() => bus.emit(...args)),
};
return bus;
}
3 changes: 2 additions & 1 deletion packages/event-bus/test/eventHub.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect, vi } from "vitest";
import { createRoot } from "solid-js";
import { createRoot, flush } from "solid-js";
import { createEventBus, createEventHub, createEventStack } from "../src/index.js";

const syncTest = (name: string, fn: (dispose: () => void) => void) =>
Expand Down Expand Up @@ -57,6 +57,7 @@ describe("createEventHub", () => {

hub.emit("busA");
hub.emit("busB", { text: "bar" });
flush();

expect(hub.value.busA).toBe(undefined);
expect(hub.value.busB).toEqual([{ text: "bar" }]);
Expand Down
21 changes: 13 additions & 8 deletions packages/event-bus/test/eventStack.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect } from "vitest";
import { createComputed, createRoot } from "solid-js";
import { createRoot, flush } from "solid-js";
import { createEventStack } from "../src/index.js";

describe("createEventStack", () => {
Expand All @@ -18,17 +18,20 @@ describe("createEventStack", () => {
});

emit(["foo"]);
flush();
expect(captured[0]).toBe("foo");
expect(capturedStack.length).toBe(1);
expect(value().length).toBe(1);

emit(["bar"]);
flush();
expect(captured[1]).toBe("bar");
expect(capturedStack.length).toBe(2);
expect(value().length).toBe(2);

allowRemove = true;
emit(["baz"]);
flush();
expect(captured[2]).toBe("baz");
expect(capturedStack.length).toBe(3);
expect(value().length).toBe(2);
Expand Down Expand Up @@ -70,37 +73,39 @@ describe("createEventStack", () => {
expect(value()).toEqual([]);

emit(["foo"]);
flush();
expect(value()).toEqual([["foo"]]);

const x: [string] = ["bar"];

emit(x);
flush();
expect(value()).toEqual([["foo"], ["bar"]]);

expect(remove(x)).toBe(true);
flush();
expect(remove(["hello"])).toBe(false);
expect(value().length).toBe(1);

const y: [string][] = [["0"], ["1"]];
setValue(y);
flush();
expect(value()).toEqual(y);
});

test("stack is reactive", () =>
createRoot(dispose => {
const { emit, value } = createEventStack<{ t: string }>();
let captured: any;
createComputed(() => {
captured = value();
});

expect(captured).toEqual([]);
expect(value()).toEqual([]);

emit({ t: "foo" });
expect(captured).toEqual([{ t: "foo" }]);
flush();
expect(value()).toEqual([{ t: "foo" }]);

emit({ t: "bar" });
expect(captured).toEqual([{ t: "foo" }, { t: "bar" }]);
flush();
expect(value()).toEqual([{ t: "foo" }, { t: "bar" }]);

dispose();
}));
Expand Down
56 changes: 30 additions & 26 deletions packages/event-bus/test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect } from "vitest";
import { createRoot, getOwner } from "solid-js";
import { createRoot, flush, getOwner } from "solid-js";
import { createEventBus, once, toEffect, toPromise } from "../src/index.js";

describe("toPromise", () => {
Expand Down Expand Up @@ -38,31 +38,35 @@ describe("once", () => {

describe("toEffect", () => {
test("toEffect()", () =>
createRoot(dispose => {
const captured: any[] = [];
let capturedOwner: any;
const { listen, emit } = createEventBus<string>();
const emitInEffect = toEffect(emit);
listen(e => {
captured.push(e);
capturedOwner = getOwner();
});
new Promise<void>(resolve =>
createRoot(dispose => {
const captured: any[] = [];
let capturedOwner: any;
const { listen, emit } = createEventBus<string>();
const emitInEffect = toEffect(emit);
listen(e => {
captured.push(e);
capturedOwner = getOwner();
});

// owner gets set to null synchronously after root executes
setTimeout(() => {
emit("foo");
expect(
capturedOwner,
"owner will should not be available inside listener after using normal emit",
).toBe(null);
// owner is null after the synchronous root callback returns
setTimeout(() => {
emit("foo");
expect(
capturedOwner,
"owner should not be available inside listener after using normal emit",
).toBe(null);

emitInEffect("bar");
expect(captured).toEqual(["foo", "bar"]);
expect(
capturedOwner,
"owner will should be available inside listener after using emitInEffect",
).not.toBe(null);
dispose();
}, 0);
}));
emitInEffect("bar");
flush();
expect(captured).toEqual(["foo", "bar"]);
expect(
capturedOwner,
"owner should be available inside listener after using emitInEffect",
).not.toBe(null);
dispose();
resolve();
}, 0);
}),
));
});
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.