Skip to content

Commit 20d3933

Browse files
committed
Update CHANGELOG for version 0.26.0 and deprecate old context options in Kernel
1 parent a151e83 commit 20d3933

4 files changed

Lines changed: 92 additions & 43 deletions

File tree

packages/shadow-objects/CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to [@spearwolf/shadow-objects](https://github.com/spearwolf/
55
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.26.0] - 2026-01-08
9+
10+
- `provideContext()` and `provideGlobalContext()` expect as third argument now an option object `{compare?, clearOnDestroy?}`
11+
- the old way (third argument as `isEqual` callback) will continue to work but is now deprecated!
12+
- both `provide*Context()` features will now clear the context value to _undefined_ on shadow-object destruction by default
13+
- opt out via `{clearOnDestroy: false}`
14+
815
## [0.25.0] - 2025-12-31
916

1017
- use `display: contents` style for all shadow object host elements to avoid layout issues
@@ -20,4 +27,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2027
- added the `useProperties()` function
2128
- added the `useResource()` function
2229
- added lots of new tests and improved code coverage
23-

packages/shadow-objects/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@spearwolf/shadow-objects",
33
"description": "a reactive entity-component framework that feels at home in the shadows",
4-
"version": "0.25.0",
4+
"version": "0.26.0",
55
"author": {
66
"name": "Wolfger Schramm",
77
"email": "wolfger@spearwolf.de",
@@ -91,4 +91,4 @@
9191
"@spearwolf/eventize": "^4.0.2",
9292
"@spearwolf/signalize": "^0.25.0"
9393
}
94-
}
94+
}

packages/shadow-objects/src/in-the-dark/Kernel.ts

Lines changed: 75 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
import {emit, eventize, off, on, once, Priority} from '@spearwolf/eventize';
22
import {
33
batch,
4+
type CompareFunc,
45
createEffect,
56
createMemo,
67
createSignal,
78
destroySignal,
89
isSignal,
910
link,
10-
Signal,
11-
type CompareFunc,
11+
type Signal,
1212
type SignalReader,
1313
} from '@spearwolf/signalize';
1414
import {ComponentChangeType, MessageToView} from '../constants.js';
15-
import type {IComponentChangeType, IComponentEvent, ShadowObjectConstructor, ShadowObjectType, SyncEvent} from '../types.js';
15+
import type {
16+
IComponentChangeType,
17+
IComponentEvent,
18+
ProvideContextOptions,
19+
ShadowObjectConstructor,
20+
ShadowObjectType,
21+
SyncEvent,
22+
} from '../types.js';
1623
import {ConsoleLogger} from '../utils/ConsoleLogger.js';
1724
import {Entity} from './Entity.js';
18-
import {onCreate, onDestroy, onParentChanged, type OnCreate, type OnDestroy} from './events.js';
25+
import {type OnCreate, onCreate, type OnDestroy, onDestroy, onParentChanged} from './events.js';
1926
import {Registry} from './Registry.js';
2027
import {SignalsPath} from './SignalsPath.js';
2128

@@ -46,7 +53,9 @@ enum ShadowObjectAction {
4653
DestroyOnly,
4754
}
4855

49-
const getDisplayName = (constructor: ShadowObjectConstructor) => constructor.displayName || constructor.name;
56+
const getDisplayName = (construct: ShadowObjectConstructor) => construct.displayName || construct.name;
57+
58+
let provideContextOptionsDeprecatedShown = false;
5059

5160
/**
5261
* The entity kernel manages the lifecycle of all entities and shadow-objects.
@@ -107,12 +116,13 @@ export class Kernel {
107116
}
108117
};
109118

110-
this.#rootEntities.forEach((uuid) => traverse(uuid, 0));
119+
this.#rootEntities.forEach((uuid) => {
120+
traverse(uuid, 0);
121+
});
111122

112123
this.#allEntities = Array.from(lvl.entries())
113124
.sort((a, b) => a[0] - b[0])
114-
.map(([, entities]) => entities)
115-
.flat();
125+
.flatMap(([, entities]) => entities);
116126

117127
this.#allEntitiesReversed = this.#allEntities.slice().reverse();
118128
this.#allEntitiesNeedUpdate = false;
@@ -311,9 +321,9 @@ export class Kernel {
311321
// destroy all shadow-objects created by constructors no longer in the list
312322
//
313323
if (shouldDestroy) {
314-
for (const [constructor, shadowObjects] of entry.usedConstructors) {
315-
if (!nextConstructors.has(constructor)) {
316-
entry.usedConstructors.delete(constructor);
324+
for (const [construct, shadowObjects] of entry.usedConstructors) {
325+
if (!nextConstructors.has(construct)) {
326+
entry.usedConstructors.delete(construct);
317327
for (const obj of shadowObjects) {
318328
this.destroyShadowObject(obj, entry.entity);
319329
}
@@ -324,17 +334,17 @@ export class Kernel {
324334
// shadow-objects for new constructors are now created using the updated constructor list
325335
//
326336
if (shouldCreate) {
327-
for (const constructor of nextConstructors) {
328-
if (!entry.usedConstructors.has(constructor)) {
329-
this.constructShadowObject(constructor, entry);
337+
for (const construct of nextConstructors) {
338+
if (!entry.usedConstructors.has(construct)) {
339+
this.constructShadowObject(construct, entry);
330340
}
331341
}
332342
}
333343

334344
return nextConstructors;
335345
}
336346

337-
private constructShadowObject(constructor: ShadowObjectConstructor, entry: EntityEntry): ShadowObjectType {
347+
private constructShadowObject(construct: ShadowObjectConstructor, entry: EntityEntry): ShadowObjectType {
338348
const unsubscribePrimary = new Set<() => any>();
339349
const unsubscribeSecondary = new Set<() => any>();
340350

@@ -357,17 +367,30 @@ export class Kernel {
357367
};
358368

359369
const shadowObject = eventize(
360-
new constructor({
370+
new construct({
361371
entity: entry.entity,
362372

363-
provideContext<T = unknown>(name: string | symbol, sourceOrInitialValue?: T | SignalReader<T>, isEqual?: CompareFunc<T>) {
373+
provideContext<T = unknown>(
374+
name: string | symbol,
375+
sourceOrInitialValue?: T | SignalReader<T>,
376+
options?: ProvideContextOptions<T> | CompareFunc<T>,
377+
) {
364378
let ctxProvider = contextProviders.get(name);
365379

380+
if (!provideContextOptionsDeprecatedShown && typeof options !== 'function') {
381+
console.warn(
382+
'[shadow-objects] Deprecation Warning: The "isEqual" option of "provideContext()" and "provideGlobalContext()" is now passed as {compare} argument. Please update your code accordingly.',
383+
);
384+
provideContextOptionsDeprecatedShown = true;
385+
}
386+
387+
const opts = typeof options === 'function' ? {compare: options} : options;
388+
366389
if (ctxProvider == null) {
367390
const isSig = isSignal(sourceOrInitialValue);
368391
const initialValue = isSig ? undefined : (sourceOrInitialValue as T);
369392

370-
ctxProvider = createSignal(initialValue, isEqual ? {compare: isEqual} : undefined);
393+
ctxProvider = createSignal(initialValue, opts?.compare ? {compare: opts.compare} : undefined);
371394

372395
if (isSig) {
373396
const ln = link(sourceOrInitialValue as SignalReader<T>, ctxProvider);
@@ -379,21 +402,36 @@ export class Kernel {
379402
contextProviders.set(name, ctxProvider);
380403
}
381404

405+
if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
406+
unsubscribePrimary.add(() => {
407+
ctxProvider.set(undefined);
408+
});
409+
}
410+
382411
return ctxProvider;
383412
},
384413

385414
provideGlobalContext<T = unknown>(
386415
name: string | symbol,
387416
sourceOrInitialValue?: T | SignalReader<T>,
388-
isEqual?: CompareFunc<T>,
417+
options?: ProvideContextOptions<T> | CompareFunc<T>,
389418
) {
390419
let ctxProvider = contextRootProviders.get(name);
391420

421+
if (!provideContextOptionsDeprecatedShown && typeof options !== 'function') {
422+
console.warn(
423+
'[shadow-objects] Deprecation Warning: The "isEqual" option of "provideContext()" and "provideGlobalContext()" is now passed as {compare} argument. Please update your code accordingly.',
424+
);
425+
provideContextOptionsDeprecatedShown = true;
426+
}
427+
428+
const opts = typeof options === 'function' ? {compare: options} : options;
429+
392430
if (ctxProvider == null) {
393431
const isSig = isSignal(sourceOrInitialValue);
394432
const initialValue = isSig ? undefined : (sourceOrInitialValue as T);
395433

396-
ctxProvider = createSignal(initialValue, isEqual ? {compare: isEqual} : undefined);
434+
ctxProvider = createSignal(initialValue, opts?.compare ? {compare: opts.compare} : undefined);
397435

398436
if (isSig) {
399437
const ln = link(sourceOrInitialValue as SignalReader<T>, ctxProvider);
@@ -405,6 +443,12 @@ export class Kernel {
405443
contextRootProviders.set(name, ctxProvider);
406444
}
407445

446+
if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
447+
unsubscribePrimary.add(() => {
448+
ctxProvider.set(undefined);
449+
});
450+
}
451+
408452
return ctxProvider;
409453
},
410454

@@ -435,7 +479,7 @@ export class Kernel {
435479
useProperties<K extends string>(props: Record<K, string>): Record<K, SignalReader<any>> {
436480
const result = {} as Record<K, SignalReader<any>>;
437481
for (const key in props) {
438-
if (Object.prototype.hasOwnProperty.call(props, key)) {
482+
if (Object.hasOwn(props, key)) {
439483
result[key] = getUseProperty(props[key]);
440484
}
441485
}
@@ -508,12 +552,12 @@ export class Kernel {
508552
);
509553

510554
if (this.logger.isInfo) {
511-
this.logger.info('create shadow-object', getDisplayName(constructor), {shadowObject, entity: entry.entity});
555+
this.logger.info('create shadow-object', getDisplayName(construct), {shadowObject, entity: entry.entity});
512556
}
513557

514558
once(entry.entity, onDestroy, Priority.Low, () => {
515559
if (this.logger.isInfo) {
516-
this.logger.info('destroy shadow-object', getDisplayName(constructor), {shadowObject, entity: entry.entity});
560+
this.logger.info('destroy shadow-object', getDisplayName(construct), {shadowObject, entity: entry.entity});
517561
}
518562

519563
for (const callback of unsubscribePrimary) {
@@ -552,22 +596,22 @@ export class Kernel {
552596
contextProviders.clear();
553597
contextRootProviders.clear();
554598

555-
const otherShadowObjects = entry.usedConstructors.get(constructor);
599+
const otherShadowObjects = entry.usedConstructors.get(construct);
556600
if (otherShadowObjects) {
557601
otherShadowObjects.delete(shadowObject);
558602
if (otherShadowObjects.size === 0) {
559-
entry.usedConstructors.delete(constructor);
603+
entry.usedConstructors.delete(construct);
560604
}
561605
}
562606
});
563607

564608
// We want to keep track which shadow-objects are created by which constructors.
565609
// This will all
566610
//
567-
if (entry.usedConstructors.has(constructor)) {
568-
entry.usedConstructors.get(constructor).add(shadowObject);
611+
if (entry.usedConstructors.has(construct)) {
612+
entry.usedConstructors.get(construct).add(shadowObject);
569613
} else {
570-
entry.usedConstructors.set(constructor, new Set([shadowObject]));
614+
entry.usedConstructors.set(construct, new Set([shadowObject]));
571615
}
572616

573617
this.attachShadowObject(shadowObject, entry.entity);
@@ -578,8 +622,8 @@ export class Kernel {
578622
private createShadowObjects(uuid: string): void {
579623
const entry = this.#entities.get(uuid);
580624

581-
this.registry.findConstructors(entry.token, entry.entity.truthyProps())?.forEach((constructor) => {
582-
this.constructShadowObject(constructor, entry);
625+
this.registry.findConstructors(entry.token, entry.entity.truthyProps())?.forEach((construct) => {
626+
this.constructShadowObject(construct, entry);
583627
});
584628
}
585629

@@ -588,13 +632,7 @@ export class Kernel {
588632

589633
const {usedConstructors} = this.#entities.get(uuid);
590634

591-
return Array.from(
592-
new Set(
593-
Array.from(usedConstructors.values())
594-
.map((objs) => Array.from(objs))
595-
.flat(),
596-
),
597-
);
635+
return Array.from(new Set(Array.from(usedConstructors.values()).flatMap((objs) => Array.from(objs))));
598636
}
599637

600638
private attachShadowObject(shadowObject: object, entity: Entity): void {

packages/shadow-objects/src/types.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {EventizedObject, on, once} from '@spearwolf/eventize';
22
import type {CompareFunc, createEffect, createMemo, createSignal, Signal, SignalReader} from '@spearwolf/signalize';
3-
import {AppliedChangeTrail, ImportedModule, type ComponentChangeType} from './constants.js';
3+
import type {AppliedChangeTrail, ComponentChangeType, ImportedModule} from './constants.js';
44
import type {Entity} from './in-the-dark/Entity.js';
55
import type {Kernel, Registry} from './shadow-objects.js';
66

@@ -93,18 +93,23 @@ export type EntityApi = Pick<
9393
traverse(callback: (entity: EntityApi) => any): void;
9494
};
9595

96+
export interface ProvideContextOptions<T> {
97+
compare?: CompareFunc<T>;
98+
clearOnDestroy?: boolean;
99+
}
100+
96101
export interface ShadowObjectCreationAPI {
97102
entity: EntityApi;
98103

99104
provideContext<T = unknown>(
100105
name: string | symbol,
101106
sourceOrInitialValue?: T | SignalReader<T>,
102-
isEqual?: CompareFunc<T>,
107+
isEqual?: ProvideContextOptions<T> | CompareFunc<T>,
103108
): Signal<T>;
104109
provideGlobalContext<T = unknown>(
105110
name: string | symbol,
106111
sourceOrInitialValue?: T | SignalReader<T>,
107-
isEqual?: CompareFunc<T>,
112+
isEqual?: ProvideContextOptions<T> | CompareFunc<T>,
108113
): Signal<T>;
109114

110115
useContext<T = any>(name: string | symbol, isEqual?: CompareFunc<T>): SignalReader<T>;

0 commit comments

Comments
 (0)