Skip to content

Commit e06f76d

Browse files
committed
♻️ core: refactor internals to follow ref-instance pattern
1 parent 02a3651 commit e06f76d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+606
-421
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ function RocketView({ entity }) {
9595
Use actions to safely modify Koota from inside of React in either effects or events.
9696
9797
```js
98-
import { createActions } from 'koota'
98+
import { defineActions } from 'koota'
9999
import { useActions } from 'koota/react'
100100

101-
const actions = createActions((world) => ({
101+
const actions = defineActions((world) => ({
102102
spawnShip: (position) => world.spawn(Position(position), Velocity),
103103
destroyAllShips: () => {
104104
world.query(Position, Velocity).forEach((entity) => {
@@ -808,7 +808,7 @@ const positions = getStore(world, Position)
808808
809809
A Koota query is a lot like a database query. Parameters define how to find entities and efficiently process them in batches. Queries are the primary way to update and transform your app state, similar to how you'd use SQL to filter and modify database records.
810810
811-
#### Caching queries
811+
#### Defining queries
812812
813813
Inline queries are great for readability and are optimized to be as fast as possible, but there is still some small overhead in hashing the query each time it is called.
814814
@@ -820,13 +820,13 @@ function updateMovement(world) {
820820
}
821821
```
822822
823-
While this is not likely to be a bottleneck in your code compared to the actual update function, if you want to save these CPU cycles you can cache the query ahead of time and use the returned key. This will have the additional effect of creating the internal query immediately on a worlds, otherwise it will get created the first time it is run.
823+
While this is not likely to be a bottleneck in your code compared to the actual update function, if you want to save these CPU cycles you can cache the query ahead of time and use the returned ref. This will have the additional effect of creating the internal query immediately on all worlds, otherwise it will get created the first time it is run.
824824
825825
```js
826826
// The internal query is created immediately before it is invoked
827-
const movementQuery = cacheQuery(Position, Velocity)
827+
const movementQuery = defineQuery(Position, Velocity)
828828

829-
// They query key is hashed ahead of time and we just use it
829+
// The query ref is used for fast array-based lookup
830830
function updateMovement(world) {
831831
world.query(movementQuery).updateEach(([pos, vel]) => {})
832832
}
@@ -1016,11 +1016,11 @@ useTraitEffect(world, GameState, (state) => {
10161016
10171017
### `useActions`
10181018
1019-
Returns actions bound to the world that is context. Use actions created by `createActions`.
1019+
Returns actions bound to the world that is in context. Use actions created by `defineActions`.
10201020
10211021
```js
10221022
// Create actions
1023-
const actions = createActions((world) => ({
1023+
const actions = defineActions((world) => ({
10241024
spawnPlayer: () => world.spawn(IsPlayer).
10251025
destroyAllPlayers: () => {
10261026
world.query(IsPlayer).forEach((player) => {

packages/core/architecture.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
A work in progress document for the architecture of Koota.
2+
3+
Koota allows for many worlds. To make this experience simple there are global, stateless **refs** that get lazily **instantiated** on a world whenever it is used. Examples of this are:
4+
5+
- Traits
6+
- Relations
7+
- Queries
8+
- Actions
9+
- Tracking modifiers
10+
11+
A world is the context and holds the underlying storage, manages entities and the general lifecycle for data changes. Refs get instantiated on a world and use the id as a key for its instance.
12+
13+
Traits are a user-facing handle for storage. The user never interacts with stores directly and isntead deals with the mental model of traits -- composable pieces of semantic data.
14+
15+
## Glossary
16+
17+
**Ref.** A stateless, global definition returned by factory functions (`trait()`, `relation()`, `createQuery()`, `createActions()`). Refs are world-agnostic and contain only definition data (schema, configuration) plus a unique ID for fast lookups. Users interact primarily with refs. Since the user is not aware of internals like instances and only see the ref, the ref type is usually named as the target concept, such as `Trait` or `Query`.
18+
19+
**Instance.** Per-world state created from a ref. Contains world-specific data like stores, subscriptions, query results, and bitmasks. Examples: `TraitInstance`, `QueryInstance`. Instances are internal — users don't interact with them directly.
20+
21+
**Register.** The process of creating an instance for a ref on a world. Happens lazily on first use. Allocates storage, sets up bitmasks, and integrates with the world's query system.
22+
23+
**Create.** The verb used for all factory functions. `create*` functions return refs (`createQuery`, `createActions`, `createAdded`) or instances (`createWorld`). The primitives `trait()` and `relation()` omit the verb for brevity. We used to use `define*` to differentiate creating a ref and creating an instance, but we now juse use `create*` in all cases and try to make this process hidden from the user.
24+
25+
**World.** The context that holds all per-world state. Contains storage, trait instances, query instances, action instances, and manages the lifecycle of data changes.
26+
27+
**Schema.** The shape definition for trait data. Can be SoA (struct of arrays), AoS (array of structs via factory function), or empty (tag trait).
28+
29+
**Store.** The actual per-world storage for trait data, created from a schema. SoA stores have one array per property; AoS stores have one array of objects.
Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
1+
import { $internal } from '../common';
12
import type { World } from '../world';
2-
import type { ActionGetter, ActionInitializer, Actions } from './types';
3+
import type { Actions, ActionsInitializer, ActionRecord } from './types';
34

4-
const actionCache = new WeakMap<World, Map<(...args: any[]) => any, Actions>>();
5+
let actionsId = 0;
56

6-
export function createActions<T extends Actions>(initializer: ActionInitializer<T>): ActionGetter<T> {
7-
return (world: World): T => {
8-
let worldCache = actionCache.get(world);
7+
export function createActions<T extends ActionRecord>(
8+
initializer: ActionsInitializer<T>
9+
): Actions<T> {
10+
const id = actionsId++;
911

10-
if (!worldCache) {
11-
worldCache = new Map();
12-
actionCache.set(world, worldCache);
13-
}
12+
const actions = Object.assign(
13+
(world: World): T => {
14+
const ctx = world[$internal];
15+
16+
// Try array lookup first (faster)
17+
let instance = ctx.actionInstances[id];
1418

15-
let actions = worldCache.get(initializer);
19+
if (!instance) {
20+
// Create and cache actions instance
21+
instance = initializer(world);
1622

17-
if (!actions) {
18-
actions = initializer(world);
19-
worldCache.set(initializer, actions);
23+
// Ensure array is large enough
24+
if (id >= ctx.actionInstances.length) {
25+
ctx.actionInstances.length = id + 1;
26+
}
27+
ctx.actionInstances[id] = instance;
28+
}
29+
30+
return instance as T;
31+
},
32+
{
33+
initializer,
2034
}
35+
) as Actions<T>;
36+
37+
// Add public read-only id property
38+
Object.defineProperty(actions, 'id', {
39+
value: id,
40+
writable: false,
41+
enumerable: true,
42+
configurable: false,
43+
});
2144

22-
return actions as T;
23-
};
45+
return actions;
2446
}

packages/core/src/actions/types.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import type { World } from '../world';
22

3-
export type Actions = Record<string, (...args: any[]) => void>;
4-
export type ActionInitializer<T extends Actions> = (world: World) => T;
5-
export type ActionGetter<T extends Actions> = (world: World) => T;
3+
export type ActionRecord = Record<string, (...args: any[]) => void>;
4+
export type ActionsInitializer<T extends ActionRecord> = (world: World) => T;
5+
export type Actions<T extends ActionRecord> = {
6+
/** Public read-only ID for fast array lookups */
7+
readonly id: number;
8+
/** Initializer function */
9+
readonly initializer: ActionsInitializer<T>;
10+
} & ((world: World) => T);
11+
12+
export type ActionInstance = ActionRecord;

packages/core/src/common.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export const $internal = Symbol.for('koota.internal');
2+
3+
/**
4+
* Type utility for symbol-branded runtime type checks.
5+
* Allows accessing a symbol property while maintaining type safety.
6+
*/
7+
export type Brand<S extends symbol> = { readonly [K in S]?: true };

packages/core/src/entity/entity-methods-patch.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55

66
import { $internal } from '../common';
77
import { setChanged } from '../query/modifiers/changed';
8-
import {
9-
getFirstRelationTarget,
10-
getRelationTargets,
11-
hasRelationPair,
12-
isRelationPair,
13-
} from '../relation/relation';
8+
import { getFirstRelationTarget, getRelationTargets, hasRelationPair } from '../relation/relation';
149
import type { Relation, RelationPair } from '../relation/types';
10+
import { isRelationPair } from '../relation/utils/is-relation';
1511
import { addTrait, getTrait, hasTrait, removeTrait, setTrait } from '../trait/trait';
1612
import type { ConfigurableTrait, Trait } from '../trait/types';
1713
import { destroyEntity, getEntityWorld } from './entity';

packages/core/src/index.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { createActions } from './actions/create-actions';
2-
export type { ActionGetter, ActionInitializer, Actions } from './actions/types';
2+
export type { Actions, ActionsInitializer, ActionRecord } from './actions/types';
33
export { $internal } from './common';
44
export type { Entity } from './entity/types';
55
export { unpackEntity } from './entity/utils/pack-entity';
@@ -8,24 +8,26 @@ export { createChanged } from './query/modifiers/changed';
88
export { Not } from './query/modifiers/not';
99
export { Or } from './query/modifiers/or';
1010
export { createRemoved } from './query/modifiers/removed';
11-
export { IsExcluded } from './query/query';
11+
export { $modifier } from './query/modifier';
12+
export { createQuery, IsExcluded } from './query/query';
1213
export type {
1314
EventType,
1415
InstancesFromParameters,
1516
IsNotModifier,
16-
ModifierData,
17+
Modifier,
1718
Query,
18-
QueryHash,
1919
QueryModifier,
2020
QueryParameter,
2121
QueryResult,
2222
QueryResultOptions,
2323
QuerySubscriber,
2424
QueryUnsubscriber,
25+
QueryHash,
2526
StoresFromParameters,
2627
} from './query/types';
27-
export { cacheQuery } from './query/utils/cache-query';
28-
export { isRelation, Pair, relation } from './relation/relation';
28+
export { $queryRef } from './query/symbols';
29+
export { relation } from './relation/relation';
30+
export { $relationPair, $relation } from './relation/symbols';
2931
export type { Relation, RelationPair, RelationTarget } from './relation/types';
3032
export { getStore, trait } from './trait/trait';
3133
export type {
@@ -37,13 +39,30 @@ export type {
3739
SetTraitCallback,
3840
TagTrait,
3941
Trait,
40-
TraitData,
4142
TraitRecord,
4243
TraitTuple,
4344
TraitValue,
4445
} from './trait/types';
4546
export type { AoSFactory, Norm, Schema, Store, StoreType } from './storage/types';
4647
export type { TraitType } from './trait/types';
4748
export { universe } from './universe/universe';
48-
export type { World, WorldInternal, WorldOptions } from './world';
49+
export type { World, WorldOptions } from './world';
4950
export { createWorld } from './world';
51+
52+
/**
53+
* Deprecations. To be removed in v0.7.0.
54+
*/
55+
56+
import { createQuery } from './query/query';
57+
/** @deprecated Use createQuery instead */
58+
export const cacheQuery = createQuery;
59+
60+
import type { TraitInstance } from './trait/types';
61+
/** @deprecated Use TraitInstance instead */
62+
export type TraitData = TraitInstance;
63+
64+
/** @deprecated Will remove this internal type entirely */
65+
export type { TraitInstance } from './trait/types';
66+
67+
/** @deprecated Will remove this internal type entirely */
68+
export type { QueryInstance } from './query/types';
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import { $internal } from '../common';
1+
import { Brand } from '../common';
22
import { Trait } from '../trait/types';
3-
import { ModifierData, QueryParameter } from './types';
3+
import { Modifier, QueryParameter } from './types';
44

55
export const $modifier = Symbol('modifier');
66

77
export function createModifier<TTrait extends Trait[] = Trait[], TType extends string = string>(
88
type: TType,
99
id: number,
1010
traits: TTrait
11-
): ModifierData<TTrait, TType> {
11+
): Modifier<TTrait, TType> {
1212
return {
1313
[$modifier]: true,
1414
type,
1515
id,
1616
traits,
17-
traitIds: traits.map((trait) => trait[$internal].id),
17+
traitIds: traits.map((trait) => trait.id),
1818
} as const;
1919
}
2020

21-
export function isModifier(param: QueryParameter): param is ModifierData {
22-
return $modifier in param;
21+
export /* @inline @pure */ function isModifier(param: QueryParameter): param is Modifier {
22+
return (param as Brand<typeof $modifier> | null | undefined)?.[$modifier] as unknown as boolean;
2323
}

packages/core/src/query/modifiers/added.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { $internal } from '../../common';
2-
import { isRelation } from '../../relation/relation';
2+
import { isRelation } from '../../relation/utils/is-relation';
33
import type { Trait, TraitOrRelation } from '../../trait/types';
44
import { universe } from '../../universe/universe';
55
import { createModifier } from '../modifier';
6-
import type { ModifierData } from '../types';
6+
import type { Modifier } from '../types';
77
import { createTrackingId, setTrackingMasks } from '../utils/tracking-cursor';
88

99
export function createAdded() {
@@ -16,7 +16,7 @@ export function createAdded() {
1616

1717
return <T extends TraitOrRelation[] = TraitOrRelation[]>(
1818
...inputs: T
19-
): ModifierData<Trait[], `added-${number}`> => {
19+
): Modifier<Trait[], `added-${number}`> => {
2020
const traits = inputs.map((input) => (isRelation(input) ? input[$internal].trait : input));
2121
return createModifier(`added-${id}`, id, traits);
2222
};

packages/core/src/query/modifiers/changed.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { $internal } from '../../common';
22
import type { Entity } from '../../entity/types';
33
import { getEntityId } from '../../entity/utils/pack-entity';
4-
import { isRelation } from '../../relation/relation';
4+
import { isRelation } from '../../relation/utils/is-relation';
55
import { hasTrait, registerTrait } from '../../trait/trait';
6-
import { getTraitData, hasTraitData } from '../../trait/trait-data';
6+
import { getTraitInstance, hasTraitInstance } from '../../trait/trait-instance';
77
import type { Trait, TraitOrRelation } from '../../trait/types';
88
import { universe } from '../../universe/universe';
99
import type { World } from '../../world';
1010
import { createModifier } from '../modifier';
11-
import type { ModifierData } from '../types';
11+
import type { Modifier } from '../types';
1212
import { checkQueryTrackingWithRelations } from '../utils/check-query-tracking-with-relations';
1313
import { createTrackingId, setTrackingMasks } from '../utils/tracking-cursor';
1414

@@ -22,7 +22,7 @@ export function createChanged() {
2222

2323
return <T extends TraitOrRelation[] = TraitOrRelation[]>(
2424
...inputs: T
25-
): ModifierData<Trait[], `changed-${number}`> => {
25+
): Modifier<Trait[], `changed-${number}`> => {
2626
const traits = inputs.map((input) => (isRelation(input) ? input[$internal].trait : input));
2727
return createModifier(`changed-${id}`, id, traits);
2828
};
@@ -35,14 +35,14 @@ export function setChanged(world: World, entity: Entity, trait: Trait) {
3535
if (!hasTrait(world, entity, trait)) return;
3636

3737
// Register the trait if it's not already registered.
38-
if (!hasTraitData(ctx.traitData, trait)) registerTrait(world, trait);
39-
const data = getTraitData(ctx.traitData, trait)!;
38+
if (!hasTraitInstance(ctx.traitInstances, trait)) registerTrait(world, trait);
39+
const data = getTraitInstance(ctx.traitInstances, trait)!;
4040

4141
// Mark the trait as changed for the entity.
4242
// This is used for filling initial values for Changed modifiers.
4343
for (const changedMask of ctx.changedMasks.values()) {
4444
const eid = getEntityId(entity);
45-
const data = getTraitData(ctx.traitData, trait)!;
45+
const data = getTraitInstance(ctx.traitInstances, trait)!;
4646
const { generationId, bitflag } = data;
4747

4848
// Ensure the generation array exists

0 commit comments

Comments
 (0)