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/ui/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './core/api.ts';
export * from './core/context.ts';
export * from './core/lifecycle.ts';
export * from './core/node.ts';
84 changes: 84 additions & 0 deletions packages/ui/src/core/lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, test } from 'vitest';
import { lifecycle, LifecycleApi } from './lifecycle.ts';
import type { Node } from './node.ts';

describe('lifecycle', () => {
describe('create', () => {
test('with no parent, roots the node in the global scope', () => {
const node = lifecycle.create();
expect(node.parent).toBeDefined();
});

test('wires the child into its parent', () => {
const parent = lifecycle.create();
const child = lifecycle.create(parent);
expect(child.parent).toBe(parent);
expect(parent.children.has(child)).toBe(true);
});

test('autogenerates an id', () => {
const node = lifecycle.create();
expect(typeof lifecycle.id(node)).toBe('string');
expect(lifecycle.id(node).length).toBeGreaterThan(0);
});

test('autogenerated ids are distinct', () => {
expect(lifecycle.id(lifecycle.create())).not.toBe(lifecycle.id(lifecycle.create()));
});

test('a child owns its id rather than inheriting its parent’s', () => {
const parent = lifecycle.create();
const child = lifecycle.create(parent);
expect(lifecycle.id(child)).not.toBe(lifecycle.id(parent));
});

test('hooks fire through create middleware', () => {
const created: Node[] = [];
const root = lifecycle.create();
LifecycleApi.decorate(root, () => ({
create(parent) {
const node = super.create(parent);
created.push(node);
return node;
},
}));
const a = LifecycleApi.at(root).create(root);
const b = LifecycleApi.at(a).create(a);
expect(created).toEqual([a, b]);
});
});

describe('destroy', () => {
test('detaches a node from its parent', () => {
const parent = lifecycle.create();
const child = lifecycle.create(parent);
lifecycle.destroy(child);
expect(parent.children.has(child)).toBe(false);
});

test('cascades to descendants', () => {
const root = lifecycle.create();
const parent = lifecycle.create(root);
const child = lifecycle.create(parent);
lifecycle.destroy(root);
expect(root.children.has(parent)).toBe(false);
expect(parent.children.has(child)).toBe(false);
});

test('tears down bottom-up, each node running its destroy middleware', () => {
const order: Node[] = [];
const root = lifecycle.create();
LifecycleApi.decorate(root, () => ({
destroy(node) {
const result = super.destroy(node);
order.push(node);
return result;
},
}));
const parent = lifecycle.create(root);
const child = lifecycle.create(parent);
LifecycleApi.at(root).destroy(root);
expect(order).toEqual([child, parent, root]);
});
});
});
35 changes: 35 additions & 0 deletions packages/ui/src/core/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Node } from './node.ts';
import { createContext } from './context.ts';
import { Api } from './api.ts';

export class LifecycleApi extends Api {
ids = 0;
global: Node;
constructor(global: Node) {
super(global);
this.global = global;
IdContext.set(this.global, 'root');
}

create(parent: Node = this.global) {
const node = new Node(parent);
parent.children.add(node);
IdContext.set(node, `${this.ids++}`);
return node;
}

destroy(node: Node) {
for (const child of node.children) {
this.destroy(child);
}
node.parent?.children.delete(node);
}

id(node: Node): string {
return IdContext.expect(node);
}
}

const IdContext = createContext<string>('id');

export const lifecycle = LifecycleApi.init(new Node());
Loading