Skip to content
Merged
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
2 changes: 1 addition & 1 deletion behavior-subject/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# [@observable/behavior-subject](https://jsr.io/@observable/behavior-subject)

A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that keeps track of its
current value and replays it to [`consumers`](https://jsr.io/@observable/core#consumer) upon
current `value` and replays it to [`consumers`](https://jsr.io/@observable/core#consumer) upon
[`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe).

## Build
Expand Down
2 changes: 1 addition & 1 deletion behavior-subject/deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@observable/behavior-subject",
"version": "0.11.0",
"version": "0.12.0",
"license": "MIT",
"exports": "./mod.ts",
"publish": { "exclude": ["*.test.ts"] }
Expand Down
38 changes: 31 additions & 7 deletions behavior-subject/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ Deno.test("BehaviorSubject.toString should be '[object BehaviorSubject]'", () =>
assertStrictEquals(`${new BehaviorSubject(2)}`, "[object BehaviorSubject]");
});

Deno.test("AsyncSubject.constructor should be frozen", () => {
Deno.test("BehaviorSubject.constructor should be frozen", () => {
// Arrange / Act / Assert
assertStrictEquals(Object.isFrozen(BehaviorSubject), true);
});

Deno.test("AsyncSubject should be frozen", () => {
Deno.test("BehaviorSubject should be frozen", () => {
// Arrange / Act / Assert
assertStrictEquals(Object.isFrozen(new BehaviorSubject(2)), true);
});

Deno.test("AsyncSubject.prototype should be frozen", () => {
Deno.test("BehaviorSubject.prototype should be frozen", () => {
// Arrange / Act / Assert
assertStrictEquals(Object.isFrozen(BehaviorSubject.prototype), true);
});
Expand Down Expand Up @@ -106,12 +106,27 @@ Deno.test("BehaviorSubject.next should emit value to observers", () => {
subject.next("foo");

// Assert
assertEquals(notifications, [
["next", "initial"],
["next", "foo"],
]);
assertEquals(notifications, [["next", "initial"], ["next", "foo"]]);
});

Deno.test(
"BehaviorSubject.next should allow empty next when created with void type",
() => {
// Arrange
const subject = new BehaviorSubject<void>(undefined);
const notifications: Array<ObserverNotification<void>> = [];
pipe(subject, materialize()).subscribe(
new Observer((notification) => notifications.push(notification)),
);

// Act
subject.next();

// Assert
assertEquals(notifications, [["next", undefined], ["next", undefined]]);
},
);

Deno.test(
"BehaviorSubject.next should store value for late observers",
() => {
Expand All @@ -130,6 +145,15 @@ Deno.test(
},
);

Deno.test("BehaviorSubject.throw should throw if called with no arguments", () => {
// Arrange / Act / Assert
assertThrows(
() => new BehaviorSubject("initial").throw(...([] as unknown as Parameters<Observer["throw"]>)),
TypeError,
"1 argument required but 0 present",
);
});

Deno.test("BehaviorSubject.throw should pass through this subject", () => {
// Arrange
const error = new Error("test error");
Expand Down
17 changes: 12 additions & 5 deletions behavior-subject/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type BehaviorSubject<Value = unknown> = Subject<Value>;
export interface BehaviorSubjectConstructor {
/**
* Creates and returns an object that acts as a [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that keeps track of its current
* value and replays it to [`consumers`](https://jsr.io/@observable/core#consumer) upon
* {@linkcode value} and replays it to [`consumers`](https://jsr.io/@observable/core#consumer) upon
* [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe).
* @example
* ```ts
Expand Down Expand Up @@ -74,8 +74,12 @@ export const BehaviorSubject: BehaviorSubjectConstructor = class<Value> {
}

next(value: Value): void {
if (this instanceof BehaviorSubject) this.#subject.next(value);
else throw new TypeError(`'this' is not instanceof '${stringTag}'`);
if (!(this instanceof BehaviorSubject)) {
throw new TypeError(`'this' is not instanceof '${stringTag}'`);
}
// No arguments.length check because Value may be void, making next() with no args valid.

this.#subject.next(value);
}

return(): void {
Expand All @@ -84,8 +88,11 @@ export const BehaviorSubject: BehaviorSubjectConstructor = class<Value> {
}

throw(value: unknown): void {
if (this instanceof BehaviorSubject) this.#subject.throw(value);
else throw new TypeError(`'this' is not instanceof '${stringTag}'`);
if (!(this instanceof BehaviorSubject)) {
throw new TypeError(`'this' is not instanceof '${stringTag}'`);
}
if (!arguments.length) throw new TypeError("1 argument required but 0 present");
this.#subject.throw(value);
}

subscribe(observer: Observer<Value>): void {
Expand Down
15 changes: 6 additions & 9 deletions broadcast-subject/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# [@observable/broadcast-subject](https://jsr.io/@observable/broadcast-subject)

A variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject) whose
[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values are
[`structured cloned`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and sent
only to [consumers](https://jsr.io/@observable/core#consumer) of _other_
[`BroadcastSubject`](https://jsr.io/@observable/broadcast-subject/doc/~/BroadcastSubject) instances
with the same name even if they are in different browsing contexts (e.g. browser tabs). Logically,
[consumers](https://jsr.io/@observable/core#consumer) of the
[`BroadcastSubject`](https://jsr.io/@observable/broadcast-subject/doc/~/BroadcastSubject) do not
receive its _own_ [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values.
A variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject) whose values are
[`structured cloned`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and
[pushed](https://jsr.io/@observable/core#push) only to
[consumers](https://jsr.io/@observable/core#consumer) of _other_
[`BroadcastSubject`](https://jsr.io/@observable/broadcast-subject/doc/~/BroadcastSubject)s with the
same `name` even if they are in different browsing contexts (e.g. browser tabs).

## Build

Expand Down
2 changes: 1 addition & 1 deletion broadcast-subject/deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@observable/broadcast-subject",
"version": "0.11.0",
"version": "0.12.0",
"license": "MIT",
"exports": "./mod.ts",
"publish": { "exclude": ["*.test.ts"] }
Expand Down
37 changes: 37 additions & 0 deletions broadcast-subject/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,30 @@ Deno.test(
},
);

Deno.test(
"BroadcastSubject.next should allow empty next when created with void type",
() => {
// Arrange
const subject = new BroadcastSubject<void>("test");
const postMessageCalls: Array<Parameters<BroadcastChannel["postMessage"]>> = [];
Object.defineProperty(BroadcastChannel.prototype, "postMessage", {
value: new Proxy(BroadcastChannel.prototype.postMessage, {
apply: (target, thisArg, argumentsList: [message: unknown]) => {
postMessageCalls.push(argumentsList);
return target.apply(thisArg, argumentsList);
},
}),
});

// Act
subject.next();

// Assert
assertEquals(postMessageCalls, [[undefined]]);
subject.return();
},
);

Deno.test("BroadcastSubject.next should not abort signal", () => {
// Arrange
const subject = new BroadcastSubject<string>("test");
Expand Down Expand Up @@ -262,6 +286,19 @@ Deno.test(
},
);

Deno.test("BroadcastSubject.throw should throw if called with no arguments", () => {
// Arrange
const subject = new BroadcastSubject("test");
subject.return(); // Prevent memory leaks

// Act / Assert
assertThrows(
() => subject.throw(...([] as unknown as Parameters<Observer["throw"]>)),
TypeError,
"1 argument required but 0 present",
);
});

Deno.test("BroadcastSubject.throw should pass through this subject", () => {
// Arrange
const error = new Error("test error");
Expand Down
55 changes: 47 additions & 8 deletions broadcast-subject/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ export type BroadcastSubject<Value = unknown> = Subject<Value>;
*/
export interface BroadcastSubjectConstructor {
/**
* Creates and returns an object that acts as a variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject) whose [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed
* values are [`structured cloned`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and sent only to [consumers](https://jsr.io/@observable/core#consumer)
* of _other_ {@linkcode BroadcastSubject} instances with the same {@linkcode name} even if they are in different browsing contexts (e.g. browser tabs). Logically,
* [consumers](https://jsr.io/@observable/core#consumer) of the {@linkcode BroadcastSubject} do not receive its _own_
* [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values.
* Creates and returns an object that acts as a variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject) whose values
* are [`structured cloned`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and [pushed](https://jsr.io/@observable/core#push)
* only to [consumers](https://jsr.io/@observable/core#consumer) of _other_ {@linkcode BroadcastSubject}s with the same
* {@linkcode name} even if they are in different browsing contexts (e.g. browser tabs).
* @example
* ```ts
* import { BroadcastSubject } from "@observable/broadcast-subject";
Expand Down Expand Up @@ -46,6 +45,41 @@ export interface BroadcastSubjectConstructor {
* ```
*/
new (name: string): BroadcastSubject;
/**
* Creates and returns an object that acts as a variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject) whose {@linkcode Value|values}
* are [`structured cloned`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and [pushed](https://jsr.io/@observable/core#push)
* only to [consumers](https://jsr.io/@observable/core#consumer) of _other_ {@linkcode BroadcastSubject}s with the same
* {@linkcode name} even if they are in different browsing contexts (e.g. browser tabs).
* @example
* ```ts
* import { BroadcastSubject } from "@observable/broadcast-subject";
*
* // Setup subjects
* const name = "test";
* const controller = new AbortController();
* const subject1 = new BroadcastSubject<number>(name);
* const subject2 = new BroadcastSubject<number>(name);
*
* // Subscribe to subjects
* subject1.subscribe({
* signal: controller.signal,
* next: (value) => console.log("subject1 received", value, "from subject1"),
* return: () => console.log("subject1 returned"),
* throw: (value) => console.log("subject1 threw", value),
* });
* subject2.subscribe({
* signal: controller.signal,
* next: (value) => console.log("subject2 received", value, "from subject2"),
* return: () => console.log("subject2 returned"),
* throw: (value) => console.log("subject2 threw", value),
* });
*
* subject1.next(1); // subject2 received 1 from subject1
* subject2.next(2); // subject1 received 2 from subject2
* subject2.return(); // subject2 returned
* subject1.next(3); // No console output since subject2 is already returned
* ```
*/
new <Value>(name: string): BroadcastSubject<Value>;
readonly prototype: BroadcastSubject;
}
Expand Down Expand Up @@ -83,10 +117,12 @@ export const BroadcastSubject: BroadcastSubjectConstructor = class<Value> {
if (!(this instanceof BroadcastSubject)) {
throw new TypeError(`'this' is not instanceof '${stringTag}'`);
}
// No arguments.length check because Value may be void, making next() with no args valid.

try {
this.#channel.postMessage(value);
} catch (error) {
this.#subject.throw(error);
this.throw(error);
}
}

Expand All @@ -96,8 +132,11 @@ export const BroadcastSubject: BroadcastSubjectConstructor = class<Value> {
}

throw(value: unknown): void {
if (this instanceof BroadcastSubject) this.#subject.throw(value);
else throw new TypeError(`'this' is not instanceof '${stringTag}'`);
if (!(this instanceof BroadcastSubject)) {
throw new TypeError(`'this' is not instanceof '${stringTag}'`);
}
if (!arguments.length) throw new TypeError("1 argument required but 0 present");
this.#subject.throw(value);
}

subscribe(observer: Observer<Value>): void {
Expand Down
2 changes: 1 addition & 1 deletion core/deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@observable/core",
"version": "0.11.0",
"version": "0.12.0",
"license": "MIT",
"exports": "./mod.ts",
"publish": { "exclude": ["*.test.ts"] }
Expand Down
12 changes: 3 additions & 9 deletions core/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,7 @@ export const Observer: ObserverConstructor = class<Value> {
}

next(value: Value): void {
if (!(this instanceof Observer)) {
throw new TypeError(`'this' is not instanceof '${stringTag}'`);
}
if (!(this instanceof Observer)) throw new TypeError(`'this' is not instanceof '${stringTag}'`);
// No arguments.length check because Value may be void, making next() with no args valid.

// If this observer has been aborted there is nothing to do.
Expand All @@ -112,9 +110,7 @@ export const Observer: ObserverConstructor = class<Value> {
}

return(): void {
if (!(this instanceof Observer)) {
throw new TypeError(`'this' is not instanceof '${stringTag}'`);
}
if (!(this instanceof Observer)) throw new TypeError(`'this' is not instanceof '${stringTag}'`);

// If this observer has been aborted there is nothing to do.
if (this.signal.aborted) return;
Expand All @@ -135,9 +131,7 @@ export const Observer: ObserverConstructor = class<Value> {
}

throw(value: unknown): void {
if (!(this instanceof Observer)) {
throw new TypeError(`'this' is not instanceof '${stringTag}'`);
}
if (!(this instanceof Observer)) throw new TypeError(`'this' is not instanceof '${stringTag}'`);
if (!arguments.length) throw new TypeError("1 argument required but 0 present");

// If this observer has been aborted there is nothing to do.
Expand Down
9 changes: 9 additions & 0 deletions core/subject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,15 @@ Deno.test("Subject should handle reentrant observers when returning", () => {
]);
});

Deno.test("Subject.throw should throw if called with no arguments", () => {
// Arrange / Act / Assert
assertThrows(
() => new Subject().throw(...([] as unknown as Parameters<Observer["throw"]>)),
TypeError,
"1 argument required but 0 present",
);
});

Deno.test("Subject should handle reentrant observers when throwing", () => {
// Arrange
const error = new Error("test");
Expand Down
Loading
Loading