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
15 changes: 5 additions & 10 deletions js/hang/src/publish/audio/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,9 @@ export class Encoder {
}

#runSource(effect: Effect): void {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const source = effect.get(this.source);
if (!source) return;
const values = effect.getAll([this.enabled, this.source]);
if (!values) return;
const [_, source] = values;

const settings = source.getSettings();

Expand Down Expand Up @@ -153,12 +151,9 @@ export class Encoder {
}

serve(track: Moq.Track, effect: Effect): void {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const values = effect.getAll([this.#worklet, this.#config]);
const values = effect.getAll([this.enabled, this.#worklet, this.#config]);
if (!values) return;
const [worklet, config] = values;
const [_, worklet, config] = values;

effect.set(this.active, true, false);

Expand Down
10 changes: 5 additions & 5 deletions js/hang/src/publish/broadcast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ export class Broadcast {
}

#run(effect: Effect) {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const values = effect.getAll([this.connection, this.path]);
const values = effect.getAll([this.enabled, this.connection]);
if (!values) return;
const [connection, path] = values;
const [_enabled, connection] = values;

const path = effect.get(this.path);
if (path === undefined) return;
Comment on lines +60 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against null path values explicitly.

Now that path is no longer part of getAll, null would slip past path === undefined and can reach connection.publish, which could throw. Consider guarding both null and undefined to preserve the previous “non-null” behavior.

🔧 Suggested fix
-		const path = effect.get(this.path);
-		if (path === undefined) return;
+		const path = effect.get(this.path);
+		if (path == null) return;
🤖 Prompt for AI Agents
In `@js/hang/src/publish/broadcast.ts` around lines 60 - 61, The current check
only returns when path === undefined, but effect.get(this.path) can now return
null and that null may reach connection.publish; update the guard around the
path obtained from effect.get(this.path) (the local variable named path in the
method where you call connection.publish) to treat both null and undefined as
missing (e.g., use a nullish check like path == null or path === null || path
=== undefined) and return early to preserve the previous non-null behavior
before calling connection.publish.


const broadcast = new Moq.Broadcast();
effect.cleanup(() => broadcast.close());
Expand Down
8 changes: 3 additions & 5 deletions js/hang/src/publish/location/peers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,9 @@ export class Peers {
}

serve(track: Moq.Track, effect: Effect): void {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const positions = effect.get(this.positions);
if (!positions) return;
const values = effect.getAll([this.enabled, this.positions]);
if (!values) return;
const [_, positions] = values;

Zod.write(track, positions, Catalog.PeersSchema);
}
Expand Down
8 changes: 3 additions & 5 deletions js/hang/src/publish/location/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,9 @@ export class Window {
}

serve(track: Moq.Track, effect: Effect): void {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const position = effect.get(this.position);
if (!position) return;
const values = effect.getAll([this.enabled, this.position]);
if (!values) return;
const [_, position] = values;

Zod.write(track, position, Catalog.PositionSchema);
}
Expand Down
7 changes: 3 additions & 4 deletions js/hang/src/publish/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ export class Preview {
}

serve(track: Moq.Track, effect: Effect): void {
if (!effect.get(this.enabled)) return;

const info = effect.get(this.info);
if (!info) return;
const values = effect.getAll([this.enabled, this.info]);
if (!values) return;
const [_, info] = values;

track.writeJson(info);
}
Expand Down
9 changes: 3 additions & 6 deletions js/hang/src/publish/source/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ export class File {
this.file = Signal.from(config.file);

this.signals.effect((effect) => {
const file = effect.get(this.file);
const enabled = effect.get(this.enabled);

if (!file || !enabled) {
return;
}
const values = effect.getAll([this.file, this.enabled]);
if (!values) return;
const [file] = values;

this.#decode(file, effect).catch((err) => {
console.error("Failed to decode file:", err);
Expand Down
15 changes: 5 additions & 10 deletions js/hang/src/publish/video/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,9 @@ export class Encoder {

// Returns the catalog for the configured settings.
#runCatalog(effect: Effect): void {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const config = effect.get(this.#config);
if (!config) return;
const values = effect.getAll([this.enabled, this.#config]);
if (!values) return;
const [_, config] = values;

const catalog: Catalog.VideoConfig = {
codec: config.codec,
Expand All @@ -147,14 +145,11 @@ export class Encoder {
}

#runConfig(effect: Effect): void {
const enabled = effect.get(this.enabled);
if (!enabled) return;

// NOTE: dimensions already factors in user provided maxPixels.
// It's a separate effect in order to deduplicate.
const values = effect.getAll([this.source, this.#dimensions]);
const values = effect.getAll([this.enabled, this.source, this.#dimensions]);
if (!values) return;
const [source, dimensions] = values;
const [_, source, dimensions] = values;

const settings = source.getSettings();
const framerate = settings.frameRate ?? 30;
Expand Down
15 changes: 5 additions & 10 deletions js/hang/src/watch/audio/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,19 @@ export class Source {
}

#runEnabled(effect: Effect): void {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const context = effect.get(this.#context);
if (!context) return;
const values = effect.getAll([this.enabled, this.#context]);
if (!values) return;
const [_enabled, context] = values;

context.resume();

// NOTE: You should disconnect/reconnect the worklet to save power when disabled.
}

#runDecoder(effect: Effect): void {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const values = effect.getAll([this.catalog, this.broadcast, this.config, this.active]);
const values = effect.getAll([this.enabled, this.catalog, this.broadcast, this.config, this.active]);
if (!values) return;
const [catalog, broadcast, config, active] = values;
const [_enabled, catalog, broadcast, config, active] = values;

const sub = broadcast.subscribe(active, catalog.priority);
effect.cleanup(() => sub.close());
Expand Down
16 changes: 8 additions & 8 deletions js/hang/src/watch/broadcast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,12 @@ export class Broadcast {
}

#runBroadcast(effect: Effect): void {
if (!effect.get(this.enabled) || !effect.get(this.#active)) return;

const values = effect.getAll([this.connection, this.path]);
const values = effect.getAll([this.enabled, this.#active, this.connection]);
if (!values) return;
const [conn, path] = values;
const [_enabled, _active, conn] = values;

const path = effect.get(this.path);
if (path === undefined) return;

const broadcast = conn.consume(path);
effect.cleanup(() => broadcast.close());
Expand All @@ -128,10 +129,9 @@ export class Broadcast {
}

#runCatalog(effect: Effect): void {
if (!effect.get(this.enabled)) return;

const broadcast = effect.get(this.#broadcast);
if (!broadcast) return;
const values = effect.getAll([this.enabled, this.#broadcast]);
if (!values) return;
const [_enabled, broadcast] = values;

this.status.set("loading");

Expand Down
10 changes: 3 additions & 7 deletions js/hang/src/watch/chat/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,9 @@ export class Message {
}

#run(effect: Effect) {
if (!effect.get(this.enabled)) return;

const catalog = effect.get(this.#catalog);
if (!catalog) return;

const broadcast = effect.get(this.broadcast);
if (!broadcast) return;
const values = effect.getAll([this.enabled, this.#catalog, this.broadcast]);
if (!values) return;
const [_, catalog, broadcast] = values;

const track = broadcast.subscribe(catalog.name, catalog.priority);
effect.cleanup(() => track.close());
Expand Down
10 changes: 3 additions & 7 deletions js/hang/src/watch/chat/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,9 @@ export class Typing {
}

#run(effect: Effect) {
if (!effect.get(this.enabled)) return;

const catalog = effect.get(this.#catalog);
if (!catalog) return;

const broadcast = effect.get(this.broadcast);
if (!broadcast) return;
const values = effect.getAll([this.enabled, this.#catalog, this.broadcast]);
if (!values) return;
const [_, catalog, broadcast] = values;

const track = broadcast.subscribe(catalog.name, catalog.priority);
effect.cleanup(() => track.close());
Expand Down
11 changes: 3 additions & 8 deletions js/hang/src/watch/location/peers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,9 @@ export class Peers {
}

#run(effect: Effect) {
const enabled = effect.get(this.enabled);
if (!enabled) return;

const catalog = effect.get(this.#catalog);
if (!catalog) return;

const broadcast = effect.get(this.broadcast);
if (!broadcast) return;
const values = effect.getAll([this.enabled, this.#catalog, this.broadcast]);
if (!values) return;
const [_, catalog, broadcast] = values;

const track = broadcast.subscribe(catalog.name, catalog.priority);
effect.cleanup(() => track.close());
Expand Down
10 changes: 3 additions & 7 deletions js/hang/src/watch/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,9 @@ export class Preview {
});

this.#signals.effect((effect) => {
if (!effect.get(this.enabled)) return;

const broadcast = effect.get(this.broadcast);
if (!broadcast) return;

const catalog = effect.get(this.#catalog);
if (!catalog) return;
const values = effect.getAll([this.enabled, this.broadcast, this.#catalog]);
if (!values) return;
const [_, broadcast, catalog] = values;

// Subscribe to the preview.json track directly
const track = broadcast.subscribe(catalog.name, catalog.priority);
Expand Down
2 changes: 1 addition & 1 deletion js/signals/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@moq/signals",
"type": "module",
"version": "0.1.1",
"version": "0.1.2",
"description": "Reactive and safe signals",
"license": "(MIT OR Apache-2.0)",
"repository": "github:moq-dev/moq",
Expand Down
12 changes: 8 additions & 4 deletions js/signals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ export class Signal<T> implements Getter<T>, Setter<T> {
type SetterType<S> = S extends Setter<infer T> ? T : never;
type GetterType<G> = G extends Getter<infer T> ? T : never;

// Excludes common falsy values from a type
type Falsy = false | 0 | "" | null | undefined;
type Truthy<T> = Exclude<T, Falsy>;

// TODO Make this a single instance of an Effect, so close() can work correctly from async code.
export class Effect {
// Sanity check to make sure roots are being disposed on dev.
Expand Down Expand Up @@ -414,17 +418,17 @@ export class Effect {
this.#dispose.push(() => effect.close());
}

// Get the values of multiple signals, returning undefined if any are null/undefined.
// Get the values of multiple signals, returning undefined if any are falsy.
getAll<S extends readonly Getter<unknown>[]>(
signals: [...S],
): { [K in keyof S]: NonNullable<GetterType<S[K]>> } | undefined {
): { [K in keyof S]: Truthy<GetterType<S[K]>> } | undefined {
const values: unknown[] = [];
for (const signal of signals) {
const value = this.get(signal);
if (value == null) return undefined;
if (!value) return undefined;
values.push(value);
}
return values as { [K in keyof S]: NonNullable<GetterType<S[K]>> };
return values as { [K in keyof S]: Truthy<GetterType<S[K]>> };
}
Comment on lines +421 to 432
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd js/signals && find . -name "*.ts" -o -name "*.tsx" | head -20

Repository: moq-dev/moq

Length of output: 138


🏁 Script executed:

cd js/signals && cat -n src/index.ts | sed -n '415,440p'

Repository: moq-dev/moq

Length of output: 1022


🏁 Script executed:

cd js/signals && grep -n "type Falsy\|type Truthy" src/index.ts | head -20

Repository: moq-dev/moq

Length of output: 146


🏁 Script executed:

cd js/signals && rg "Falsy|Truthy" --type ts -B 2 -A 2

Repository: moq-dev/moq

Length of output: 786


Runtime truthiness is stricter than the Falsy type definition.

!value also treats NaN and 0n as falsy, which aren't included in Falsy/Truthy. If only the explicit falsy values should be excluded, use explicit comparisons:

Suggested fix
-			if (!value) return undefined;
+			if (value === false || value === 0 || value === "" || value === null || value === undefined) {
+				return undefined;
+			}

Alternatively, document that the function also excludes NaN and 0n.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Get the values of multiple signals, returning undefined if any are falsy.
getAll<S extends readonly Getter<unknown>[]>(
signals: [...S],
): { [K in keyof S]: NonNullable<GetterType<S[K]>> } | undefined {
): { [K in keyof S]: Truthy<GetterType<S[K]>> } | undefined {
const values: unknown[] = [];
for (const signal of signals) {
const value = this.get(signal);
if (value == null) return undefined;
if (!value) return undefined;
values.push(value);
}
return values as { [K in keyof S]: NonNullable<GetterType<S[K]>> };
return values as { [K in keyof S]: Truthy<GetterType<S[K]>> };
}
// Get the values of multiple signals, returning undefined if any are falsy.
getAll<S extends readonly Getter<unknown>[]>(
signals: [...S],
): { [K in keyof S]: Truthy<GetterType<S[K]>> } | undefined {
const values: unknown[] = [];
for (const signal of signals) {
const value = this.get(signal);
if (value === false || value === 0 || value === "" || value === null || value === undefined) {
return undefined;
}
values.push(value);
}
return values as { [K in keyof S]: Truthy<GetterType<S[K]>> };
}
🤖 Prompt for AI Agents
In `@js/signals/src/index.ts` around lines 421 - 432, The runtime check in getAll
uses `if (!value)` which wrongly treats `0`, `NaN`, `0n`, etc. as falsy and
narrows values more than the Falsy/Truthy types; change the check to only reject
null/undefined (e.g. `if (value === undefined || value === null) return
undefined;`) so GetterType/Truthy semantics are preserved; update the check in
getAll (and any related helper) instead of using `!value`.


// A helper to call a function when a signal changes.
Expand Down
Loading