Skip to content

Commit 89c59c2

Browse files
committed
entity: auto destruction on parent removal
1 parent 9f0411c commit 89c59c2

6 files changed

Lines changed: 70 additions & 5 deletions

File tree

packages/shadow-objects/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ 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.30.2] - 2026-02-26
9+
10+
- **API Update:** When calling the `kernel.createEntity()` function, there is now a new parameter `autoDestructionOnParentRemoval`. This makes it easier to create entities yourself from inside a shadow-object.
11+
- These entities created _internally_ or _in the dark_ are then cleaned up when the original entity is deleted from the view.
12+
- Allow access to `entity.kernel` from inside a shadow-object.
13+
814
## [0.30.1] - 2026-02-04
915

1016
- update dependencies

packages/shadow-objects/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@spearwolf/shadow-objects",
33
"description": "a reactive entity-component framework that feels at home in the shadows",
44
"homepage": "https://github.com/spearwolf/shadow-objects/",
5-
"version": "0.30.1",
5+
"version": "0.30.2",
66
"author": {
77
"name": "Wolfger Schramm",
88
"email": "wolfger@spearwolf.de",

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ export class Entity {
143143
this.#props.clear();
144144
off(this);
145145

146+
this.#autoDestructionSubscription?.();
147+
146148
for (const rootCtx of this.#rootContexts.values()) {
147149
rootCtx.cleanup();
148150
rootCtx.signal.destroy();
@@ -224,6 +226,25 @@ export class Entity {
224226
}
225227
}
226228

229+
#autoDestructionSubscription?: () => void;
230+
231+
get autoDestructionOnParentRemoval(): boolean {
232+
return !!this.#autoDestructionSubscription;
233+
}
234+
235+
set autoDestructionOnParentRemoval(autoDestruct: boolean) {
236+
if (autoDestruct) {
237+
if (!this.#autoDestructionSubscription && this.parentUuid) {
238+
this.#autoDestructionSubscription = once(this.parent, onDestroy, Priority.Max, () => {
239+
this.kernel.destroyEntity(this.uuid);
240+
});
241+
}
242+
} else if (this.#autoDestructionSubscription) {
243+
this.#autoDestructionSubscription();
244+
this.#autoDestructionSubscription = undefined;
245+
}
246+
}
247+
227248
reSubscribeToParentContexts() {
228249
for (const [, ctx] of this.#context) {
229250
this.#subscribeToParent(ctx);

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {emit, on} from '@spearwolf/eventize';
2-
import {createSignal, type Signal, type SignalReader, value} from '@spearwolf/signalize';
2+
import {createSignal, value, type Signal, type SignalReader} from '@spearwolf/signalize';
33
import {afterEach, describe, expect, it, vi} from 'vitest';
44
import {MessageToView} from '../constants.js';
55
import type {ShadowObjectCreationAPI} from '../types.js';
@@ -1048,4 +1048,33 @@ describe('Kernel', () => {
10481048
kernel.destroy();
10491049
});
10501050
});
1051+
1052+
describe('Entity destruction', () => {
1053+
it('Entity destruction should destroy children', () => {
1054+
const registry = new Registry();
1055+
const kernel = new Kernel(registry);
1056+
1057+
const destroyCallback = vi.fn();
1058+
1059+
@ShadowObject({registry, token: 'testOnDestroy'})
1060+
class TestOnDestroy {
1061+
constructor({onDestroy, entity}: ShadowObjectCreationAPI) {
1062+
onDestroy(() => destroyCallback(entity.uuid));
1063+
}
1064+
}
1065+
expect(TestOnDestroy).toBeDefined();
1066+
1067+
const [parentUuid, childUuid] = [generateUUID(), generateUUID()];
1068+
1069+
kernel.createEntity(parentUuid, 'testOnDestroy');
1070+
kernel.createEntity(childUuid, 'testOnDestroy', parentUuid, 0, undefined, true);
1071+
1072+
expect(destroyCallback).not.toHaveBeenCalled();
1073+
1074+
kernel.destroyEntity(parentUuid);
1075+
1076+
expect(destroyCallback).toHaveBeenNthCalledWith(1, childUuid);
1077+
expect(destroyCallback).toHaveBeenNthCalledWith(2, parentUuid);
1078+
});
1079+
});
10511080
});

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,14 @@ export class Kernel {
215215
}
216216
}
217217

218-
createEntity(uuid: string, token: string, parentUuid?: string, order = 0, properties?: [string, unknown][]): void {
218+
createEntity(
219+
uuid: string,
220+
token: string,
221+
parentUuid?: string,
222+
order = 0,
223+
properties?: [string, unknown][],
224+
autoDestructionOnParentRemoval = false,
225+
): void {
219226
const e = new Entity(this, uuid);
220227

221228
e.order = order;
@@ -228,6 +235,8 @@ export class Kernel {
228235
e.parentUuid = parentUuid;
229236
}
230237

238+
e.autoDestructionOnParentRemoval = autoDestructionOnParentRemoval;
239+
231240
if (!e.hasParent) {
232241
this.#rootEntities.add(uuid);
233242
}

packages/shadow-objects/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {AnyEventNames, EventArgs, EventizedObject, SubscribeArgs, on, once} from '@spearwolf/eventize';
1+
import type {AnyEventNames, EventArgs, EventizedObject, on, once, SubscribeArgs} from '@spearwolf/eventize';
22
import type {CompareFunc, createEffect, createMemo, createSignal, Signal, SignalReader} from '@spearwolf/signalize';
33
import type {AppliedChangeTrail, ComponentChangeType, ImportedModule} from './constants.js';
44
import type {Entity} from './in-the-dark/Entity.js';
@@ -86,7 +86,7 @@ export interface AppliedChangeTrailEvent {
8686
}
8787

8888
export type EntityApi = Readonly<
89-
Pick<Entity, 'uuid' | 'order' | 'hasParent' | 'propKeys' | 'propEntries'> & {
89+
Pick<Entity, 'uuid' | 'order' | 'hasParent' | 'propKeys' | 'propEntries' | 'kernel'> & {
9090
parent?: EntityApi;
9191
children: readonly EntityApi[];
9292
traverse(callback: (entity: EntityApi) => unknown): void;

0 commit comments

Comments
 (0)