Skip to content
Closed
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
1 change: 1 addition & 0 deletions packages/shadow-objects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- improve `useProperties()` type inference with key-to-type maps
- **API Update:** `on()` and `once()` in `ShadowObjectCreationAPI` now support an implicit event source.
- If the first argument is a `string`, `symbol`, or `[]`, the `entity` is automatically used as the event source.
- Example: `on('eventName', callback)` is equivalent to `on(entity, 'eventName', callback)`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ The recommended way to define a Shadow Object is a simple function. This functio
```typescript
import { ShadowObjectCreationAPI } from "@spearwolf/shadow-objects";

export function UserProfileLogic({
useProperty,
createEffect
export function UserProfileLogic({
useProperties,
createEffect,
}: ShadowObjectCreationAPI) {

// 1. Setup Phase: Define your reactive graph here
const userId = useProperty('userId');
const { userId } = useProperties<{userId: string}>({
userId: 'userId',
});
Comment on lines +18 to +20
Copy link
Owner

Choose a reason for hiding this comment

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

please revert changes - do not use setProprties() with a single property. thats for setProperty

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted the guide example back to useProperty instead of setProperties for a single field. Commit: ad9339d.


createEffect(() => {
// 2. Runtime Phase: This runs whenever userId changes
Expand Down
10 changes: 7 additions & 3 deletions packages/shadow-objects/docs/03-api/01-shadow-object-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,16 @@ createEffect(() => {

A convenience helper to create multiple property signals at once.

* **Signature:** `useProperties(map: Record<string, any>): Record<string, () => any>`
* **Signature:** `useProperties<T>(map: { [K in keyof T]: string }): { [K in keyof T]: () => T[K] | undefined }`
* **Returns:** An object where keys match the input map, and values are signal readers.
Comment on lines +42 to 43
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

useProperty is documented above as returning () => T, but the actual API type is SignalReader<Maybe<T>> (i.e., T | undefined for consumers with strictNullChecks). Since useProperties is now documented as returning T[K] | undefined, please update the useProperty signature (and ideally its example text) to also reflect the possible undefined value for consistency and correctness.

Copilot uses AI. Check for mistakes.

```typescript
const { x, y } = useProperties({ x: 0, y: 0 });
// x() and y() are now signals
const { foo, bar } = useProperties<{ foo: number; bar: string }>({
foo: 'prop.name.foo',
bar: 'prop.bar',
});
// foo(): number | undefined
// bar(): string | undefined
```

---
Expand Down
9 changes: 4 additions & 5 deletions packages/shadow-objects/docs/03-api/03-view-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,15 @@ class GameEntity {
});

// Sync position to Shadow World
this.viewComponent.setProperties({
x: this.x,
y: this.y
});
this.viewComponent.setProperty('x', this.x);
this.viewComponent.setProperty('y', this.y);
Comment on lines +121 to +122
Copy link
Owner

Choose a reason for hiding this comment

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

please revert changes - the use of setProperties() and setProperty() should not change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted the second setProperties example in the view-components doc. Commit: ad9339d.

}

update() {
// Send updates every frame (or optimally, only on change)
if (this.moved) {
this.viewComponent.setProperties({ x: this.x, y: this.y });
this.viewComponent.setProperty('x', this.x);
this.viewComponent.setProperty('y', this.y);
Comment on lines +128 to +129
Copy link
Owner

Choose a reason for hiding this comment

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

please revert changes - the use of setProperties() and setProperty() should not change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted the view-components doc example to use setProperties again as requested. Commit: ad9339d.

}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/shadow-objects/docs/04-patterns/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ const myMeshResource = createResource(
If you need multiple properties, avoid calling `useProperty` multiple times. Use `useProperties` to get a structured object of signals.

```typescript
const { x, y, visible } = useProperties({
const { x, y, visible } = useProperties<{ x: number; y: number; visible: boolean }>({
x: "position-x",
y: "position-y",
visible: "is-visible"
visible: "is-visible",
});
```

Expand Down
36 changes: 36 additions & 0 deletions packages/shadow-objects/src/in-the-dark/Kernel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,42 @@ describe('Kernel', () => {

kernel.destroy();
});

it('should support typed property maps', () => {
const registry = new Registry();
const kernel = new Kernel(registry);

let capturedProps:
| {
foo: SignalReader<number | undefined>;
bar: SignalReader<string | undefined>;
}
| undefined;

@ShadowObject({registry, token: 'testTypedUseProperties'})
class TestTypedUseProperties {
constructor({useProperties}: ShadowObjectCreationAPI) {
const props = useProperties<{foo: number; bar: string}>({
foo: 'propA',
bar: 'propB',
});
capturedProps = props;
}
}
expect(TestTypedUseProperties).toBeDefined();

const uuid = generateUUID();
kernel.createEntity(uuid, 'testTypedUseProperties', undefined, 0, [
['propA', 123],
['propB', 'valueB'],
]);

expect(capturedProps).toBeDefined();
expect(value(capturedProps!.foo)).toBe(123);
expect(value(capturedProps!.bar)).toBe('valueB');

kernel.destroy();
});
});

describe('provideContext and useContext', () => {
Expand Down
18 changes: 10 additions & 8 deletions packages/shadow-objects/src/in-the-dark/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,12 +360,12 @@ export class Kernel {
const contextProviders = new Map<string | symbol, Signal<any>>();
const contextRootProviders = new Map<string | symbol, Signal<any>>();

const propertyReaders = new Map<string, SignalReader<any>>();
const propertyReaders = new Map<string, SignalReader<unknown>>();

const getUseProperty = <T = any>(
const getUseProperty = <T = unknown>(
name: string,
options?: SignalValueOptions<T> | CompareFunc<T | undefined>,
): SignalReader<T> => {
): SignalReader<Maybe<T>> => {
if (!usePropertyOptionsDeprecatedShown && options != null && typeof options === 'function') {
console.warn(
'[shadow-objects] Deprecation Warning: The "isEqual" option of "useProperty()" is now passed as {compare} argument. Please update your code accordingly.',
Expand All @@ -375,11 +375,11 @@ export class Kernel {

const opts = typeof options === 'function' ? {compare: options} : options;

let propReader = propertyReaders.get(name);
let propReader = propertyReaders.get(name) as SignalReader<Maybe<T>> | undefined;

if (propReader === undefined) {
propReader = createSignal<any>(undefined, opts).get;
propertyReaders.set(name, propReader);
propReader = createSignal<Maybe<T>>(undefined, opts).get;
propertyReaders.set(name, propReader as SignalReader<unknown>);
const con = link(entry.entity.getPropertyReader(name), propReader);
unsubscribeSecondary.add(con.destroy.bind(con));
}
Expand Down Expand Up @@ -523,8 +523,10 @@ export class Kernel {

useProperty: getUseProperty,

useProperties<K extends string>(props: Record<K, string>): Record<K, SignalReader<any>> {
const result = {} as Record<K, SignalReader<any>>;
useProperties<T extends Record<string, unknown> = Record<string, unknown>>(
props: {[K in keyof T]: string},
): {[K in keyof T]: SignalReader<Maybe<T[K]>>} {
const result = {} as {[K in keyof T]: SignalReader<Maybe<T[K]>>};
for (const key in props) {
if (Object.hasOwn(props, key)) {
result[key] = getUseProperty(props[key]);
Expand Down
4 changes: 3 additions & 1 deletion packages/shadow-objects/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ export interface ShadowObjectCreationAPI {

useProperty<T = unknown>(name: string, options?: SignalValueOptions<T> | CompareFunc<T | undefined>): SignalReader<Maybe<T>>;

useProperties<K extends string>(props: Record<K, string>): Record<K, SignalReader<any>>;
useProperties<T extends Record<string, unknown> = Record<string, unknown>>(
props: {[K in keyof T]: string},
): {[K in keyof T]: SignalReader<Maybe<T[K]>>};

createResource<T = unknown>(factory: () => T | undefined, cleanup?: (resource: NonNullable<T>) => unknown): Signal<Maybe<T>>;

Expand Down
Loading