@jgengine/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +11 -0
  3. package/dist/assets/spriteSheet.d.ts +46 -0
  4. package/dist/assets/spriteSheet.js +86 -0
  5. package/dist/combat/death.d.ts +61 -0
  6. package/dist/combat/death.js +61 -0
  7. package/dist/combat/effects.d.ts +61 -0
  8. package/dist/combat/effects.js +108 -0
  9. package/dist/combat/projectiles.d.ts +45 -0
  10. package/dist/combat/projectiles.js +156 -0
  11. package/dist/commands/commandRegistry.d.ts +23 -0
  12. package/dist/commands/commandRegistry.js +26 -0
  13. package/dist/economy/currency.d.ts +26 -0
  14. package/dist/economy/currency.js +28 -0
  15. package/dist/economy/wallet.d.ts +16 -0
  16. package/dist/economy/wallet.js +49 -0
  17. package/dist/game/defineGame.d.ts +41 -0
  18. package/dist/game/defineGame.js +7 -0
  19. package/dist/game/events.d.ts +87 -0
  20. package/dist/game/events.js +24 -0
  21. package/dist/game/feed.d.ts +19 -0
  22. package/dist/game/feed.js +47 -0
  23. package/dist/game/leaderboard.d.ts +46 -0
  24. package/dist/game/leaderboard.js +60 -0
  25. package/dist/game/loadout.d.ts +47 -0
  26. package/dist/game/loadout.js +36 -0
  27. package/dist/game/lootTable.d.ts +27 -0
  28. package/dist/game/lootTable.js +78 -0
  29. package/dist/game/playableGame.d.ts +55 -0
  30. package/dist/game/playableGame.js +1 -0
  31. package/dist/game/quest.d.ts +93 -0
  32. package/dist/game/quest.js +228 -0
  33. package/dist/game/social.d.ts +78 -0
  34. package/dist/game/social.js +227 -0
  35. package/dist/game/trade.d.ts +35 -0
  36. package/dist/game/trade.js +53 -0
  37. package/dist/game/unlocks.d.ts +13 -0
  38. package/dist/game/unlocks.js +34 -0
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.js +6 -0
  41. package/dist/input/actionBindings.d.ts +41 -0
  42. package/dist/input/actionBindings.js +115 -0
  43. package/dist/input/lookChannel.d.ts +29 -0
  44. package/dist/input/lookChannel.js +44 -0
  45. package/dist/input/touchGestures.d.ts +31 -0
  46. package/dist/input/touchGestures.js +72 -0
  47. package/dist/interaction/proximityPrompt.d.ts +51 -0
  48. package/dist/interaction/proximityPrompt.js +76 -0
  49. package/dist/inventory/inventoryModel.d.ts +57 -0
  50. package/dist/inventory/inventoryModel.js +208 -0
  51. package/dist/item/use.d.ts +25 -0
  52. package/dist/item/use.js +40 -0
  53. package/dist/item/weapon.d.ts +8 -0
  54. package/dist/item/weapon.js +18 -0
  55. package/dist/movement/avatarGait.d.ts +14 -0
  56. package/dist/movement/avatarGait.js +20 -0
  57. package/dist/movement/cameraRig.d.ts +15 -0
  58. package/dist/movement/cameraRig.js +43 -0
  59. package/dist/movement/movementModel.d.ts +96 -0
  60. package/dist/movement/movementModel.js +171 -0
  61. package/dist/movement/poseState.d.ts +23 -0
  62. package/dist/movement/poseState.js +61 -0
  63. package/dist/multiplayer/poseSyncGate.d.ts +18 -0
  64. package/dist/multiplayer/poseSyncGate.js +30 -0
  65. package/dist/multiplayer/presenceContract.d.ts +60 -0
  66. package/dist/multiplayer/presenceContract.js +19 -0
  67. package/dist/multiplayer/presenceModel.d.ts +60 -0
  68. package/dist/multiplayer/presenceModel.js +51 -0
  69. package/dist/runtime/adapter.d.ts +24 -0
  70. package/dist/runtime/adapter.js +12 -0
  71. package/dist/runtime/commandRunner.d.ts +15 -0
  72. package/dist/runtime/commandRunner.js +25 -0
  73. package/dist/runtime/gameContext.d.ts +138 -0
  74. package/dist/runtime/gameContext.js +371 -0
  75. package/dist/runtime/gameRuntime.d.ts +42 -0
  76. package/dist/runtime/gameRuntime.js +95 -0
  77. package/dist/runtime/hostPersistence.d.ts +105 -0
  78. package/dist/runtime/hostPersistence.js +117 -0
  79. package/dist/runtime/save.d.ts +9 -0
  80. package/dist/runtime/save.js +29 -0
  81. package/dist/runtime/snapshot.d.ts +87 -0
  82. package/dist/runtime/snapshot.js +75 -0
  83. package/dist/runtime/transport.d.ts +60 -0
  84. package/dist/runtime/transport.js +1 -0
  85. package/dist/scene/assetCatalog.d.ts +10 -0
  86. package/dist/scene/assetCatalog.js +17 -0
  87. package/dist/scene/assetPreload.d.ts +7 -0
  88. package/dist/scene/assetPreload.js +22 -0
  89. package/dist/scene/behaviors.d.ts +19 -0
  90. package/dist/scene/behaviors.js +18 -0
  91. package/dist/scene/entityStats.d.ts +38 -0
  92. package/dist/scene/entityStats.js +65 -0
  93. package/dist/scene/entityStore.d.ts +52 -0
  94. package/dist/scene/entityStore.js +84 -0
  95. package/dist/scene/objectStore.d.ts +28 -0
  96. package/dist/scene/objectStore.js +69 -0
  97. package/dist/scene/spatial.d.ts +34 -0
  98. package/dist/scene/spatial.js +96 -0
  99. package/dist/scene/targeting.d.ts +20 -0
  100. package/dist/scene/targeting.js +56 -0
  101. package/dist/stats/statModifiers.d.ts +18 -0
  102. package/dist/stats/statModifiers.js +56 -0
  103. package/dist/store/changeSignal.d.ts +7 -0
  104. package/dist/store/changeSignal.js +32 -0
  105. package/dist/store/engineStore.d.ts +22 -0
  106. package/dist/store/engineStore.js +50 -0
  107. package/dist/store/features.d.ts +10 -0
  108. package/dist/store/features.js +23 -0
  109. package/dist/store/observableKeyedStore.d.ts +10 -0
  110. package/dist/store/observableKeyedStore.js +42 -0
  111. package/dist/time/calendarClock.d.ts +17 -0
  112. package/dist/time/calendarClock.js +42 -0
  113. package/dist/time/gameClock.d.ts +9 -0
  114. package/dist/time/gameClock.js +20 -0
  115. package/dist/time/serverTick.d.ts +16 -0
  116. package/dist/time/serverTick.js +18 -0
  117. package/dist/world/features.d.ts +39 -0
  118. package/dist/world/features.js +15 -0
  119. package/package.json +32 -0
@@ -0,0 +1,84 @@
1
+ import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
2
+ function toEntityPosition(position) {
3
+ if (position === undefined)
4
+ return [0, 0, 0];
5
+ if ("x" in position)
6
+ return [position.x, position.y, position.z];
7
+ return position;
8
+ }
9
+ export function createEntityStore() {
10
+ const store = createObservableKeyedStore();
11
+ let nextCounter = 1;
12
+ function generateId() {
13
+ let id = `entity-${nextCounter}`;
14
+ while (store.has(id)) {
15
+ nextCounter += 1;
16
+ id = `entity-${nextCounter}`;
17
+ }
18
+ nextCounter += 1;
19
+ return id;
20
+ }
21
+ return {
22
+ spawn(name, options = {}) {
23
+ if (options.id !== undefined && store.has(options.id)) {
24
+ throw new Error(`Scene entity id "${options.id}" is already spawned.`);
25
+ }
26
+ const id = options.id ?? generateId();
27
+ store.set(id, {
28
+ id,
29
+ name,
30
+ position: toEntityPosition(options.position),
31
+ rotationY: options.rotationY ?? 0,
32
+ rotationX: options.rotationX ?? 0,
33
+ rotationZ: options.rotationZ ?? 0,
34
+ role: options.role ?? "prop",
35
+ movement: options.movement ?? {},
36
+ behaviors: options.behaviors ?? [],
37
+ meta: options.meta,
38
+ });
39
+ return id;
40
+ },
41
+ despawn(id) {
42
+ const existed = store.has(id);
43
+ store.delete(id);
44
+ return existed;
45
+ },
46
+ update(id, patch) {
47
+ const current = store.get(id);
48
+ if (!current)
49
+ return false;
50
+ store.set(id, { ...current, ...patch });
51
+ return true;
52
+ },
53
+ setPose(id, pose) {
54
+ const current = store.get(id);
55
+ if (!current)
56
+ return false;
57
+ store.set(id, {
58
+ ...current,
59
+ position: toEntityPosition(pose.position),
60
+ rotationY: pose.rotationY ?? current.rotationY,
61
+ rotationX: pose.rotationX ?? current.rotationX,
62
+ rotationZ: pose.rotationZ ?? current.rotationZ,
63
+ });
64
+ return true;
65
+ },
66
+ get(id) {
67
+ return store.get(id) ?? null;
68
+ },
69
+ list() {
70
+ return store.arraySnapshot();
71
+ },
72
+ clear() {
73
+ for (const entity of store.arraySnapshot()) {
74
+ store.delete(entity.id);
75
+ }
76
+ },
77
+ subscribe(listener) {
78
+ return store.subscribe(listener);
79
+ },
80
+ snapshot() {
81
+ return store.arraySnapshot();
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,28 @@
1
+ import type { EntityPosition } from "./entityStore.js";
2
+ export interface SceneObject {
3
+ instanceId: string;
4
+ catalogId: string;
5
+ position: EntityPosition;
6
+ rotationY: number;
7
+ parentSpace?: string;
8
+ }
9
+ export interface PlaceOptions {
10
+ instanceId?: string;
11
+ parentSpace?: string;
12
+ rotation?: number;
13
+ }
14
+ export interface ObjectListFilter {
15
+ parentSpace?: string;
16
+ }
17
+ export interface ObjectStore {
18
+ place(catalogId: string, x: number, y: number, z: number, options?: PlaceOptions): string;
19
+ remove(instanceId: string): boolean;
20
+ move(instanceId: string, x: number, y: number, z: number): boolean;
21
+ rotate(instanceId: string, rotationY: number): boolean;
22
+ get(instanceId: string): SceneObject | null;
23
+ list(filter?: ObjectListFilter): readonly SceneObject[];
24
+ clear(): void;
25
+ subscribe(listener: () => void): () => void;
26
+ snapshot(): readonly SceneObject[];
27
+ }
28
+ export declare function createObjectStore(): ObjectStore;
@@ -0,0 +1,69 @@
1
+ import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
2
+ export function createObjectStore() {
3
+ const store = createObservableKeyedStore();
4
+ let nextCounter = 1;
5
+ function generateId() {
6
+ let id = `object-${nextCounter}`;
7
+ while (store.has(id)) {
8
+ nextCounter += 1;
9
+ id = `object-${nextCounter}`;
10
+ }
11
+ nextCounter += 1;
12
+ return id;
13
+ }
14
+ return {
15
+ place(catalogId, x, y, z, options = {}) {
16
+ if (options.instanceId !== undefined && store.has(options.instanceId)) {
17
+ throw new Error(`Scene object id "${options.instanceId}" is already placed.`);
18
+ }
19
+ const instanceId = options.instanceId ?? generateId();
20
+ store.set(instanceId, {
21
+ instanceId,
22
+ catalogId,
23
+ position: [x, y, z],
24
+ rotationY: options.rotation ?? 0,
25
+ ...(options.parentSpace !== undefined ? { parentSpace: options.parentSpace } : {}),
26
+ });
27
+ return instanceId;
28
+ },
29
+ remove(instanceId) {
30
+ const existed = store.has(instanceId);
31
+ store.delete(instanceId);
32
+ return existed;
33
+ },
34
+ move(instanceId, x, y, z) {
35
+ const current = store.get(instanceId);
36
+ if (!current)
37
+ return false;
38
+ store.set(instanceId, { ...current, position: [x, y, z] });
39
+ return true;
40
+ },
41
+ rotate(instanceId, rotationY) {
42
+ const current = store.get(instanceId);
43
+ if (!current)
44
+ return false;
45
+ store.set(instanceId, { ...current, rotationY });
46
+ return true;
47
+ },
48
+ get(instanceId) {
49
+ return store.get(instanceId) ?? null;
50
+ },
51
+ list(filter) {
52
+ const all = store.arraySnapshot();
53
+ if (filter?.parentSpace === undefined)
54
+ return all;
55
+ return all.filter((object) => object.parentSpace === filter.parentSpace);
56
+ },
57
+ clear() {
58
+ for (const object of store.arraySnapshot()) {
59
+ store.delete(object.instanceId);
60
+ }
61
+ },
62
+ subscribe(listener) {
63
+ return store.subscribe(listener);
64
+ },
65
+ snapshot() {
66
+ return store.arraySnapshot();
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,34 @@
1
+ import type { EntityPosition } from "./entityStore.js";
2
+ export type Aim = {
3
+ origin: EntityPosition;
4
+ direction: EntityPosition;
5
+ } | {
6
+ yaw: number;
7
+ pitch: number;
8
+ spread?: number;
9
+ };
10
+ export interface QueryArcOptions {
11
+ from: string;
12
+ aim: Aim;
13
+ radius: number;
14
+ halfAngleDeg?: number;
15
+ }
16
+ export interface MoveTowardOptions {
17
+ speed: number;
18
+ stopDistance?: number;
19
+ dt: number;
20
+ }
21
+ export interface SpatialApiOptions {
22
+ resolvePosition: (instanceId: string) => EntityPosition | undefined;
23
+ candidates: () => string[];
24
+ occluder?: (from: EntityPosition, to: EntityPosition) => boolean;
25
+ }
26
+ export interface SpatialApi {
27
+ distance(aInstanceId: string, bInstanceId: string): number | null;
28
+ inRadius(center: EntityPosition | string, radius: number, filter?: (instanceId: string) => boolean): string[];
29
+ hasLineOfSight(fromInstanceId: string, toInstanceId: string): boolean;
30
+ queryArc(options: QueryArcOptions): string[];
31
+ moveToward(instanceId: string, target: EntityPosition | string, options: MoveTowardOptions): EntityPosition | null;
32
+ }
33
+ export declare function distanceBetween(a: EntityPosition, b: EntityPosition): number;
34
+ export declare function createSpatialApi(options: SpatialApiOptions): SpatialApi;
@@ -0,0 +1,96 @@
1
+ export function distanceBetween(a, b) {
2
+ const dx = b[0] - a[0];
3
+ const dy = b[1] - a[1];
4
+ const dz = b[2] - a[2];
5
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
6
+ }
7
+ function forwardXZ(aim) {
8
+ if ("origin" in aim) {
9
+ const [x, , z] = aim.direction;
10
+ const length = Math.sqrt(x * x + z * z);
11
+ if (length === 0)
12
+ return null;
13
+ return [x / length, z / length];
14
+ }
15
+ return [Math.sin(aim.yaw), Math.cos(aim.yaw)];
16
+ }
17
+ export function createSpatialApi(options) {
18
+ const { resolvePosition, candidates, occluder } = options;
19
+ function resolveTarget(target) {
20
+ return typeof target === "string" ? resolvePosition(target) : target;
21
+ }
22
+ return {
23
+ distance(aInstanceId, bInstanceId) {
24
+ const a = resolvePosition(aInstanceId);
25
+ const b = resolvePosition(bInstanceId);
26
+ if (a === undefined || b === undefined)
27
+ return null;
28
+ return distanceBetween(a, b);
29
+ },
30
+ inRadius(center, radius, filter) {
31
+ const centerId = typeof center === "string" ? center : null;
32
+ const centerPosition = resolveTarget(center);
33
+ if (centerPosition === undefined)
34
+ return [];
35
+ return candidates().filter((instanceId) => {
36
+ if (instanceId === centerId)
37
+ return false;
38
+ if (filter !== undefined && !filter(instanceId))
39
+ return false;
40
+ const position = resolvePosition(instanceId);
41
+ return position !== undefined && distanceBetween(centerPosition, position) <= radius;
42
+ });
43
+ },
44
+ hasLineOfSight(fromInstanceId, toInstanceId) {
45
+ const from = resolvePosition(fromInstanceId);
46
+ const to = resolvePosition(toInstanceId);
47
+ if (from === undefined || to === undefined)
48
+ return false;
49
+ if (occluder === undefined)
50
+ return true;
51
+ return !occluder(from, to);
52
+ },
53
+ queryArc({ from, aim, radius, halfAngleDeg = 60 }) {
54
+ const origin = "origin" in aim ? aim.origin : resolvePosition(from);
55
+ if (origin === undefined)
56
+ return [];
57
+ const forward = forwardXZ(aim);
58
+ if (forward === null)
59
+ return [];
60
+ const minDot = Math.cos((halfAngleDeg * Math.PI) / 180);
61
+ return candidates().filter((instanceId) => {
62
+ if (instanceId === from)
63
+ return false;
64
+ const position = resolvePosition(instanceId);
65
+ if (position === undefined)
66
+ return false;
67
+ const dx = position[0] - origin[0];
68
+ const dz = position[2] - origin[2];
69
+ const planarDistance = Math.sqrt(dx * dx + dz * dz);
70
+ if (planarDistance > radius)
71
+ return false;
72
+ if (planarDistance === 0)
73
+ return true;
74
+ const dot = (forward[0] * dx + forward[1] * dz) / planarDistance;
75
+ return dot >= minDot;
76
+ });
77
+ },
78
+ moveToward(instanceId, target, { speed, stopDistance = 0, dt }) {
79
+ const current = resolvePosition(instanceId);
80
+ const destination = resolveTarget(target);
81
+ if (current === undefined || destination === undefined)
82
+ return null;
83
+ const total = distanceBetween(current, destination);
84
+ const remaining = total - stopDistance;
85
+ if (remaining <= 0)
86
+ return current;
87
+ const step = Math.min(speed * dt, remaining);
88
+ const scale = step / total;
89
+ return [
90
+ current[0] + (destination[0] - current[0]) * scale,
91
+ current[1] + (destination[1] - current[1]) * scale,
92
+ current[2] + (destination[2] - current[2]) * scale,
93
+ ];
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,20 @@
1
+ export type TargetRelation = "hostile" | "friendly";
2
+ export type TargetFilter = TargetRelation | "any";
3
+ export interface TargetingOptions {
4
+ candidates: () => string[];
5
+ classify?: (fromId: string, toId: string) => TargetRelation;
6
+ orderBy?: (a: string, b: string) => number;
7
+ distance?: (fromId: string, toId: string) => number | null;
8
+ }
9
+ export interface CycleTargetOptions {
10
+ filter?: TargetFilter;
11
+ direction?: "next" | "prev";
12
+ maxDistance?: number;
13
+ }
14
+ export interface Targeting {
15
+ setTarget(fromId: string, toId: string | null): void;
16
+ getTarget(fromId: string): string | null;
17
+ cycleTarget(fromId: string, options?: CycleTargetOptions): string | null;
18
+ clearAll(instanceId: string): void;
19
+ }
20
+ export declare function createTargeting(options: TargetingOptions): Targeting;
@@ -0,0 +1,56 @@
1
+ export function createTargeting(options) {
2
+ const targets = new Map();
3
+ function eligibleCandidates(fromId, filter, maxDistance) {
4
+ const candidates = options.candidates().filter((id) => id !== fromId);
5
+ const filtered = filter === "any" || options.classify === undefined
6
+ ? candidates
7
+ : candidates.filter((id) => options.classify(fromId, id) === filter);
8
+ const inRange = maxDistance === undefined || options.distance === undefined
9
+ ? filtered
10
+ : filtered.filter((id) => {
11
+ const distance = options.distance(fromId, id);
12
+ return distance !== null && distance <= maxDistance;
13
+ });
14
+ if (options.orderBy === undefined)
15
+ return inRange;
16
+ return inRange.slice().sort(options.orderBy);
17
+ }
18
+ return {
19
+ setTarget(fromId, toId) {
20
+ if (toId === null)
21
+ targets.delete(fromId);
22
+ else
23
+ targets.set(fromId, toId);
24
+ },
25
+ getTarget(fromId) {
26
+ return targets.get(fromId) ?? null;
27
+ },
28
+ cycleTarget(fromId, cycleOptions = {}) {
29
+ const candidates = eligibleCandidates(fromId, cycleOptions.filter ?? "any", cycleOptions.maxDistance);
30
+ if (candidates.length === 0) {
31
+ targets.delete(fromId);
32
+ return null;
33
+ }
34
+ const direction = cycleOptions.direction ?? "next";
35
+ const currentIndex = candidates.indexOf(targets.get(fromId) ?? "");
36
+ let nextIndex;
37
+ if (currentIndex === -1) {
38
+ nextIndex = direction === "next" ? 0 : candidates.length - 1;
39
+ }
40
+ else {
41
+ const step = direction === "next" ? 1 : -1;
42
+ nextIndex = (currentIndex + step + candidates.length) % candidates.length;
43
+ }
44
+ const target = candidates[nextIndex];
45
+ targets.set(fromId, target);
46
+ return target;
47
+ },
48
+ clearAll(instanceId) {
49
+ targets.delete(instanceId);
50
+ for (const [fromId, toId] of targets) {
51
+ if (toId === instanceId)
52
+ targets.delete(fromId);
53
+ }
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,18 @@
1
+ export interface StatModifier {
2
+ add?: number;
3
+ multiply?: number;
4
+ }
5
+ export type StatModifierSet<TStat extends string> = Partial<Record<TStat, StatModifier>>;
6
+ export interface Stats<TStat extends string> {
7
+ setBase(stat: TStat, value: number): void;
8
+ getBase(stat: TStat): number;
9
+ addSource(sourceId: string, modifiers: StatModifierSet<TStat>, options?: {
10
+ expiresAtMs?: number;
11
+ }): void;
12
+ removeSource(sourceId: string): void;
13
+ hasSource(sourceId: string): boolean;
14
+ get(stat: TStat, nowMs?: number): number;
15
+ pruneExpired(nowMs: number): string[];
16
+ sources(): string[];
17
+ }
18
+ export declare function createStats<TStat extends string>(base: Record<TStat, number>): Stats<TStat>;
@@ -0,0 +1,56 @@
1
+ export function createStats(base) {
2
+ const baseValues = { ...base };
3
+ const sourceEntries = new Map();
4
+ function isExpired(entry, nowMs) {
5
+ return nowMs !== undefined && entry.expiresAtMs !== undefined && entry.expiresAtMs <= nowMs;
6
+ }
7
+ function resolve(stat, nowMs) {
8
+ let total = baseValues[stat];
9
+ let multiplier = 1;
10
+ for (const entry of sourceEntries.values()) {
11
+ if (isExpired(entry, nowMs))
12
+ continue;
13
+ const modifier = entry.modifiers[stat];
14
+ if (!modifier)
15
+ continue;
16
+ if (modifier.add !== undefined)
17
+ total += modifier.add;
18
+ if (modifier.multiply !== undefined)
19
+ multiplier *= modifier.multiply;
20
+ }
21
+ return total * multiplier;
22
+ }
23
+ return {
24
+ setBase(stat, value) {
25
+ baseValues[stat] = value;
26
+ },
27
+ getBase(stat) {
28
+ return baseValues[stat];
29
+ },
30
+ addSource(sourceId, modifiers, options) {
31
+ sourceEntries.set(sourceId, { modifiers, expiresAtMs: options?.expiresAtMs });
32
+ },
33
+ removeSource(sourceId) {
34
+ sourceEntries.delete(sourceId);
35
+ },
36
+ hasSource(sourceId) {
37
+ return sourceEntries.has(sourceId);
38
+ },
39
+ get(stat, nowMs) {
40
+ return resolve(stat, nowMs);
41
+ },
42
+ pruneExpired(nowMs) {
43
+ const pruned = [];
44
+ for (const [sourceId, entry] of sourceEntries) {
45
+ if (isExpired(entry, nowMs)) {
46
+ sourceEntries.delete(sourceId);
47
+ pruned.push(sourceId);
48
+ }
49
+ }
50
+ return pruned;
51
+ },
52
+ sources() {
53
+ return Array.from(sourceEntries.keys());
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,7 @@
1
+ export interface ChangeSignal {
2
+ subscribe(listener: () => void): () => void;
3
+ notify(): void;
4
+ version(): number;
5
+ }
6
+ export declare function createChangeSignal(): ChangeSignal;
7
+ export declare function notifyAfter<T extends object, K extends keyof T>(target: T, methods: readonly K[], notify: () => void): T;
@@ -0,0 +1,32 @@
1
+ export function createChangeSignal() {
2
+ const listeners = new Set();
3
+ let version = 0;
4
+ return {
5
+ subscribe(listener) {
6
+ listeners.add(listener);
7
+ return () => {
8
+ listeners.delete(listener);
9
+ };
10
+ },
11
+ notify() {
12
+ version += 1;
13
+ for (const listener of listeners)
14
+ listener();
15
+ },
16
+ version() {
17
+ return version;
18
+ },
19
+ };
20
+ }
21
+ export function notifyAfter(target, methods, notify) {
22
+ const wrapped = { ...target };
23
+ for (const method of methods) {
24
+ const fn = target[method];
25
+ wrapped[method] = ((...args) => {
26
+ const result = fn(...args);
27
+ notify();
28
+ return result;
29
+ });
30
+ }
31
+ return wrapped;
32
+ }
@@ -0,0 +1,22 @@
1
+ export interface EngineStoreConfig<TState, TAction, TEventMap extends object> {
2
+ initialState: TState;
3
+ reduce: (state: TState, action: TAction) => TState;
4
+ derive?: (state: TState) => TState;
5
+ onTransition?: (previous: TState, next: TState, emit: <K extends keyof TEventMap>(eventName: K, payload: TEventMap[K]) => void) => void;
6
+ }
7
+ export declare class EngineStore<TState, TAction, TEventMap extends object> {
8
+ private snapshot;
9
+ private readonly stateListeners;
10
+ private readonly eventListeners;
11
+ private readonly reduce;
12
+ private readonly derive;
13
+ private readonly onTransition;
14
+ constructor(config: EngineStoreConfig<TState, TAction, TEventMap>);
15
+ getState(): TState;
16
+ subscribe(listener: (state: TState) => void): () => boolean;
17
+ dispatch(action: TAction): void;
18
+ on<K extends keyof TEventMap>(eventName: K, listener: (payload: TEventMap[K]) => void): () => boolean;
19
+ private setState;
20
+ private emit;
21
+ private getEventBucket;
22
+ }
@@ -0,0 +1,50 @@
1
+ export class EngineStore {
2
+ snapshot;
3
+ stateListeners = new Set();
4
+ eventListeners;
5
+ reduce;
6
+ derive;
7
+ onTransition;
8
+ constructor(config) {
9
+ this.snapshot = config.derive ? config.derive(config.initialState) : config.initialState;
10
+ this.reduce = config.reduce;
11
+ this.derive = config.derive ?? null;
12
+ this.onTransition = config.onTransition;
13
+ this.eventListeners = {};
14
+ }
15
+ getState() {
16
+ return this.snapshot;
17
+ }
18
+ subscribe(listener) {
19
+ this.stateListeners.add(listener);
20
+ return () => this.stateListeners.delete(listener);
21
+ }
22
+ dispatch(action) {
23
+ this.setState((current) => this.reduce(current, action));
24
+ }
25
+ on(eventName, listener) {
26
+ this.getEventBucket(eventName).add(listener);
27
+ return () => this.getEventBucket(eventName).delete(listener);
28
+ }
29
+ setState(updater) {
30
+ const previous = this.snapshot;
31
+ const reduced = updater(previous);
32
+ const next = this.derive ? this.derive(reduced) : reduced;
33
+ this.snapshot = next;
34
+ for (const listener of this.stateListeners)
35
+ listener(next);
36
+ this.onTransition?.(previous, next, (eventName, payload) => this.emit(eventName, payload));
37
+ }
38
+ emit(eventName, payload) {
39
+ for (const listener of this.getEventBucket(eventName))
40
+ listener(payload);
41
+ }
42
+ getEventBucket(eventName) {
43
+ const currentBucket = this.eventListeners[eventName];
44
+ if (currentBucket !== undefined)
45
+ return currentBucket;
46
+ const nextBucket = new Set();
47
+ this.eventListeners[eventName] = nextBucket;
48
+ return nextBucket;
49
+ }
50
+ }
@@ -0,0 +1,10 @@
1
+ export interface StateFeature<TState, TAction, TEventMap extends object> {
2
+ reduce?: (state: TState, action: TAction) => TState;
3
+ derive?: (state: TState) => TState;
4
+ onTransition?: (previous: TState, next: TState, emit: <K extends keyof TEventMap>(eventName: K, payload: TEventMap[K]) => void) => void;
5
+ }
6
+ export declare function composeStateFeatures<TState, TAction, TEventMap extends object>(features: readonly StateFeature<TState, TAction, TEventMap>[]): {
7
+ reduce(state: TState, action: TAction): TState;
8
+ derive(state: TState): TState;
9
+ onTransition(previous: TState, next: TState, emit: <K extends keyof TEventMap>(eventName: K, payload: TEventMap[K]) => void): void;
10
+ };
@@ -0,0 +1,23 @@
1
+ export function composeStateFeatures(features) {
2
+ return {
3
+ reduce(state, action) {
4
+ let nextState = state;
5
+ for (const feature of features) {
6
+ nextState = feature.reduce ? feature.reduce(nextState, action) : nextState;
7
+ }
8
+ return nextState;
9
+ },
10
+ derive(state) {
11
+ let nextState = state;
12
+ for (const feature of features) {
13
+ nextState = feature.derive ? feature.derive(nextState) : nextState;
14
+ }
15
+ return nextState;
16
+ },
17
+ onTransition(previous, next, emit) {
18
+ for (const feature of features) {
19
+ feature.onTransition?.(previous, next, emit);
20
+ }
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,10 @@
1
+ export interface ObservableKeyedStore<T> {
2
+ set(key: string, value: T): void;
3
+ delete(key: string): void;
4
+ get(key: string): T | undefined;
5
+ has(key: string): boolean;
6
+ subscribe(listener: () => void): () => void;
7
+ mapSnapshot(): ReadonlyMap<string, T>;
8
+ arraySnapshot(): readonly T[];
9
+ }
10
+ export declare function createObservableKeyedStore<T>(areEqual?: (previous: T, next: T) => boolean): ObservableKeyedStore<T>;
@@ -0,0 +1,42 @@
1
+ export function createObservableKeyedStore(areEqual) {
2
+ const store = new Map();
3
+ const listeners = new Set();
4
+ const EMPTY = [];
5
+ let arrayCache = EMPTY;
6
+ function emit() {
7
+ arrayCache = store.size === 0 ? EMPTY : Array.from(store.values());
8
+ for (const listener of listeners)
9
+ listener();
10
+ }
11
+ return {
12
+ set(key, value) {
13
+ const previous = store.get(key);
14
+ if (previous !== undefined && areEqual?.(previous, value))
15
+ return;
16
+ store.set(key, value);
17
+ emit();
18
+ },
19
+ delete(key) {
20
+ if (!store.has(key))
21
+ return;
22
+ store.delete(key);
23
+ emit();
24
+ },
25
+ get(key) {
26
+ return store.get(key);
27
+ },
28
+ has(key) {
29
+ return store.has(key);
30
+ },
31
+ subscribe(listener) {
32
+ listeners.add(listener);
33
+ return () => listeners.delete(listener);
34
+ },
35
+ mapSnapshot() {
36
+ return store;
37
+ },
38
+ arraySnapshot() {
39
+ return arrayCache;
40
+ },
41
+ };
42
+ }