koota-kit 0.1.1
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.
- package/CHANGELOG.md +28 -0
- package/CONTRIBUTING.md +51 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/SECURITY.md +25 -0
- package/dist/cjs/eventLog.cjs +81 -0
- package/dist/cjs/eventLog.d.cts +49 -0
- package/dist/cjs/eventLog.d.cts.map +1 -0
- package/dist/cjs/index.cjs +36 -0
- package/dist/cjs/index.d.cts +9 -0
- package/dist/cjs/index.d.cts.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/rng.cjs +168 -0
- package/dist/cjs/rng.d.cts +86 -0
- package/dist/cjs/rng.d.cts.map +1 -0
- package/dist/cjs/traits/index.cjs +54 -0
- package/dist/cjs/traits/index.d.cts +48 -0
- package/dist/cjs/traits/index.d.cts.map +1 -0
- package/dist/cjs/world.cjs +111 -0
- package/dist/cjs/world.d.cts +83 -0
- package/dist/cjs/world.d.cts.map +1 -0
- package/dist/esm/eventLog.d.ts +49 -0
- package/dist/esm/eventLog.d.ts.map +1 -0
- package/dist/esm/eventLog.js +78 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/rng.d.ts +86 -0
- package/dist/esm/rng.d.ts.map +1 -0
- package/dist/esm/rng.js +154 -0
- package/dist/esm/traits/index.d.ts +48 -0
- package/dist/esm/traits/index.d.ts.map +1 -0
- package/dist/esm/traits/index.js +51 -0
- package/dist/esm/world.d.ts +83 -0
- package/dist/esm/world.d.ts.map +1 -0
- package/dist/esm/world.js +101 -0
- package/docs/API.md +119 -0
- package/docs/ARCHITECTURE.md +57 -0
- package/docs/assets/favicon.svg +1 -0
- package/docs/assets/koota-kit-hero.webp +0 -0
- package/docs/changelog.md +13 -0
- package/docs/contributing.md +28 -0
- package/examples/basic.mjs +27 -0
- package/examples/commonjs.cjs +8 -0
- package/package.json +173 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/) and uses
|
|
5
|
+
[Conventional Commits](https://www.conventionalcommits.org/) to drive releases.
|
|
6
|
+
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Release-ready `@jbdevprimary/koota-kit` package with ESM and CommonJS entry
|
|
12
|
+
points.
|
|
13
|
+
- Idempotent world teardown and validated, atomic snapshot restoration.
|
|
14
|
+
- Independent deterministic `gen` and `events` random streams.
|
|
15
|
+
- Compile-time and runtime guards for object-valued Koota traits.
|
|
16
|
+
- World-scoped event logs with consuming and observational read paths.
|
|
17
|
+
- Runnable examples, package-consumer checks, API/architecture documentation,
|
|
18
|
+
and Node 22/24 Linux plus Node 24 Windows CI coverage.
|
|
19
|
+
- Repository-level LF normalization so formatter checks behave identically on
|
|
20
|
+
Windows, macOS, and Linux checkouts.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `WorldHandle.world`, `WorldHandle.seeds`, and `WorldHandle.scratch` are
|
|
25
|
+
readonly references; seed values are copied and frozen at creation.
|
|
26
|
+
- Invalid RNG bounds, probabilities, clock values, event-log keys, and
|
|
27
|
+
persisted headers now fail with `TypeError` or `RangeError` instead of
|
|
28
|
+
producing invalid state.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking the time to contribute.
|
|
4
|
+
|
|
5
|
+
## Getting set up
|
|
6
|
+
|
|
7
|
+
With [mise](https://mise.jdx.dev) (recommended — installs the Node and pnpm
|
|
8
|
+
versions pinned in `mise.toml`):
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
mise install
|
|
12
|
+
pnpm install
|
|
13
|
+
pnpm verify # lint, typecheck, test, build — the same gate CI runs
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Without mise, use `corepack` so pnpm matches the version pinned in
|
|
17
|
+
`package.json#packageManager`, on any Node release in the `engines.node`
|
|
18
|
+
range (`>=22`; CI verifies 22, 24, and 26 with the official
|
|
19
|
+
`actions/setup-node` + `pnpm/action-setup`):
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
corepack enable
|
|
23
|
+
pnpm install
|
|
24
|
+
pnpm verify
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Making a change
|
|
28
|
+
|
|
29
|
+
1. Branch off `main`.
|
|
30
|
+
2. Write the test first. A bug fix should come with a test that fails without it.
|
|
31
|
+
3. Run `pnpm verify`. A change is not ready while any part of that is red.
|
|
32
|
+
4. Commit with [Conventional Commits](https://www.conventionalcommits.org):
|
|
33
|
+
`fix:`, `feat:`, `docs:`, `refactor:`, `test:`, `chore:`. Release Please uses
|
|
34
|
+
these commits to drive the changelog and next version number.
|
|
35
|
+
5. Open a pull request describing what changed and why.
|
|
36
|
+
|
|
37
|
+
## What gets reviewed
|
|
38
|
+
|
|
39
|
+
- Does it do what it says, and is there a test proving it?
|
|
40
|
+
- Does it keep the public API honest? A breaking change needs a `!` or a
|
|
41
|
+
`BREAKING CHANGE:` footer.
|
|
42
|
+
- Are the types right for consumers? CI runs `publint` and
|
|
43
|
+
`arethetypeswrong` because broken types only surface at integration time.
|
|
44
|
+
- Do invalid inputs fail before changing clock, RNG, or restored state?
|
|
45
|
+
- Does the change keep Koota-facing behavior aligned with the supported peer?
|
|
46
|
+
|
|
47
|
+
## Releases
|
|
48
|
+
|
|
49
|
+
Releases are automated. Merging a conventional commit to `main` opens a
|
|
50
|
+
release pull request; merging that publishes to npm with provenance. Do not
|
|
51
|
+
hand-edit versions or the changelog.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jon Bogaty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# koota-kit
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[](https://github.com/jbcom/koota-kit/actions/workflows/ci.yml)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
Small, production-derived conventions for deterministic simulations built with
|
|
9
|
+
[Koota](https://github.com/pmndrs/koota). koota-kit gives a simulation one
|
|
10
|
+
lifecycle boundary, two independent seeded random streams, safe object-valued
|
|
11
|
+
traits, and world-scoped event logs.
|
|
12
|
+
|
|
13
|
+
It is deliberately **not a game engine**. It is the narrow layer that prevents
|
|
14
|
+
recurring classes of simulation bug without hiding Koota's queries,
|
|
15
|
+
entities, relations, or actions.
|
|
16
|
+
|
|
17
|
+
Full documentation: **[jonbogaty.com/koota-kit](https://jonbogaty.com/koota-kit/)**
|
|
18
|
+
|
|
19
|
+
## Why use it?
|
|
20
|
+
|
|
21
|
+
| Problem | koota-kit convention |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| World generation changes after unrelated gameplay draws | Separate `gen` and `events` RNG streams |
|
|
24
|
+
| A loaded save no longer reproduces the next random result | Byte-exact, JSON-safe RNG snapshots |
|
|
25
|
+
| Object state leaks between entities | `defineTrait` requires per-entity factories |
|
|
26
|
+
| A HUD or test consumes an event before its real system sees it | Single-consumer `drain`, non-consuming `peek` |
|
|
27
|
+
| Replaced worlds retain caches or process-global Koota IDs | `createSimWorld` / idempotent `destroySimWorld` lifecycle |
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm add koota-kit koota
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requirements:
|
|
36
|
+
|
|
37
|
+
- Node.js 22 or newer (CI covers Node 22/24/26 on Linux and Node 24 on Windows)
|
|
38
|
+
- Koota `^0.6.6`, installed by the application as a peer dependency
|
|
39
|
+
|
|
40
|
+
The package ships native ESM and CommonJS entry points with format-correct
|
|
41
|
+
TypeScript declarations.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import {
|
|
47
|
+
advanceClock,
|
|
48
|
+
createSimWorld,
|
|
49
|
+
defineEventLog,
|
|
50
|
+
defineTrait,
|
|
51
|
+
destroySimWorld,
|
|
52
|
+
nextInt,
|
|
53
|
+
snapshotWorld,
|
|
54
|
+
} from "koota-kit";
|
|
55
|
+
|
|
56
|
+
const Position = defineTrait({ x: 0, y: 0 });
|
|
57
|
+
const Inventory = defineTrait({ items: () => [] as string[] });
|
|
58
|
+
const moved = defineEventLog<{ entity: number; distance: number }>("movement:completed");
|
|
59
|
+
|
|
60
|
+
const sim = createSimWorld({ gen: "map-42", events: "run-1" });
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const scout = sim.world.spawn(Position({ x: 4, y: 9 }), Inventory());
|
|
64
|
+
|
|
65
|
+
advanceClock(sim, 1 / 60);
|
|
66
|
+
const distance = nextInt(sim.rng.events, 1, 5);
|
|
67
|
+
const position = scout.get(Position);
|
|
68
|
+
if (position) scout.set(Position, { ...position, x: position.x + distance });
|
|
69
|
+
moved.push(sim, { entity: Number(scout), distance });
|
|
70
|
+
|
|
71
|
+
// The owning system consumes each event once.
|
|
72
|
+
for (const event of moved.drain(sim)) {
|
|
73
|
+
console.log(event);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Save this beside your own serialized entity/trait state.
|
|
77
|
+
const header = snapshotWorld(sim);
|
|
78
|
+
const serializedHeader = JSON.stringify(header);
|
|
79
|
+
console.log(serializedHeader);
|
|
80
|
+
} finally {
|
|
81
|
+
destroySimWorld(sim);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Runnable ESM and CommonJS versions live in [`examples/`](./examples).
|
|
86
|
+
|
|
87
|
+
## Core concepts
|
|
88
|
+
|
|
89
|
+
### One handle owns one simulation
|
|
90
|
+
|
|
91
|
+
`createSimWorld` creates a Koota world, immutable seed values, independent RNG
|
|
92
|
+
streams, a fixed-step clock, and a scratch map. `destroySimWorld` clears the
|
|
93
|
+
scratch map and releases Koota's world ID; it is safe to call more than once.
|
|
94
|
+
|
|
95
|
+
The scratch map is for derived, non-serializable caches and short-lived
|
|
96
|
+
coordination state. Namespace keys by system, for example `pathfinding:grid`
|
|
97
|
+
or `combat:hits`.
|
|
98
|
+
|
|
99
|
+
### Randomness has two jobs
|
|
100
|
+
|
|
101
|
+
- `rng.gen` defines stable world content: terrain, layouts, initial spawns.
|
|
102
|
+
- `rng.events` defines play history: combat rolls, weather, runtime events.
|
|
103
|
+
|
|
104
|
+
Never key per-entity randomness on packed Koota entity IDs or consume a shared
|
|
105
|
+
stream in query iteration order. Entity IDs contain process-global world and
|
|
106
|
+
generation bits. Derive a local stream from a stable domain key instead:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { createRng, nextU32 } from "koota-kit/rng";
|
|
110
|
+
|
|
111
|
+
const tileRng = createRng({
|
|
112
|
+
gen: `${String(sim.seeds.gen)}/tile:12,8`,
|
|
113
|
+
events: 0,
|
|
114
|
+
});
|
|
115
|
+
const terrainVariant = nextU32(tileRng.gen);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Object-valued traits need factories
|
|
119
|
+
|
|
120
|
+
Koota's structure-of-arrays layout can alias a bare object literal across
|
|
121
|
+
entities. koota-kit rejects that form at compile time and runtime:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
defineTrait({ storage: () => ({ logs: 0 }) }); // correct: one object per entity
|
|
125
|
+
defineTrait({ storage: { logs: 0 } }); // TypeError: shared object footgun
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Declare traits once in a leaf module and export each trait from one location.
|
|
129
|
+
Koota identifies traits by reference, not by a string name.
|
|
130
|
+
|
|
131
|
+
### Event logs have one consuming owner
|
|
132
|
+
|
|
133
|
+
`push` appends, `drain` returns every pending event and empties the log, and
|
|
134
|
+
`peek` returns a copy without consuming. A devtool, HUD, or test should peek;
|
|
135
|
+
the system responsible for acting on the event should drain.
|
|
136
|
+
|
|
137
|
+
## Persistence boundary
|
|
138
|
+
|
|
139
|
+
`snapshotWorld` stores the clock and both RNG states. It intentionally does not
|
|
140
|
+
serialize entities or traits: those are application data with application-
|
|
141
|
+
specific schemas and migrations. `restoreWorldHeader` validates a parsed save
|
|
142
|
+
before changing the handle and restores atomically.
|
|
143
|
+
|
|
144
|
+
Persist a version beside the header and your ECS state so the application can
|
|
145
|
+
migrate its own schema.
|
|
146
|
+
|
|
147
|
+
## API and architecture
|
|
148
|
+
|
|
149
|
+
The root package exports the full API. Focused entry points are also available:
|
|
150
|
+
|
|
151
|
+
| Entry point | Purpose |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `koota-kit/world` | World lifecycle, clock, Koota facade, world snapshots |
|
|
154
|
+
| `koota-kit/rng` | Seeded streams, bounded draws, snapshots |
|
|
155
|
+
| `koota-kit/traits` | Safe trait declaration |
|
|
156
|
+
| `koota-kit/eventLog` | World-scoped publish/drain logs |
|
|
157
|
+
|
|
158
|
+
See the [API reference](./docs/API.md) for signatures and validation behavior,
|
|
159
|
+
and [architecture notes](./docs/ARCHITECTURE.md) for module boundaries and
|
|
160
|
+
invariants.
|
|
161
|
+
|
|
162
|
+
## Errors and edge cases
|
|
163
|
+
|
|
164
|
+
- Seeds must be strings or finite numbers.
|
|
165
|
+
- `advanceClock` accepts finite, non-negative `dt`; `0` is a valid paused tick.
|
|
166
|
+
- `nextInt` requires safe-integer bounds with `maxExclusive > minInclusive` and
|
|
167
|
+
a range no larger than `2**32`.
|
|
168
|
+
- `chance` accepts only finite probabilities in `[0, 1]`.
|
|
169
|
+
- Restores reject malformed RNG or clock state before mutating the handle.
|
|
170
|
+
- Event-log keys must be non-empty and cannot reuse a scratch key holding a
|
|
171
|
+
non-array cache.
|
|
172
|
+
|
|
173
|
+
Invalid caller input raises `TypeError` or `RangeError` with the API name in the
|
|
174
|
+
message. No validation failure consumes RNG state or partially restores a
|
|
175
|
+
world header.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
mise install # or: corepack enable
|
|
181
|
+
pnpm install --frozen-lockfile
|
|
182
|
+
pnpm verify
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Node and pnpm versions live in `mise.toml`; CI verifies against the full
|
|
186
|
+
supported Node range (22, 24, 26).
|
|
187
|
+
|
|
188
|
+
`pnpm verify` runs formatting/lint checks, strict TypeScript, the full test
|
|
189
|
+
suite with 100% coverage thresholds, dual-format builds, both runnable
|
|
190
|
+
examples, `publint`, Are The Types Wrong, and package-content/runtime checks.
|
|
191
|
+
|
|
192
|
+
Useful individual commands:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
pnpm test # fast behavior suite
|
|
196
|
+
pnpm test:watch # local watch mode
|
|
197
|
+
pnpm coverage # tests plus enforced coverage thresholds
|
|
198
|
+
pnpm build # dist/esm and dist/cjs
|
|
199
|
+
pnpm package:check # exports, declarations, packed files, ESM/CJS parity
|
|
200
|
+
pnpm audit --prod # registry-backed production dependency audit
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Releases and support
|
|
204
|
+
|
|
205
|
+
Release Please maintains versions and `CHANGELOG.md` from Conventional
|
|
206
|
+
Commits. A release tag triggers a clean verification and npm publication with
|
|
207
|
+
provenance. The latest `0.x` release receives fixes; pre-1.0 APIs may evolve,
|
|
208
|
+
with breaking changes called out in the changelog.
|
|
209
|
+
|
|
210
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a pull request. Report
|
|
211
|
+
security issues privately according to [SECURITY.md](./SECURITY.md).
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
[MIT](./LICENSE) © Jon Bogaty.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please do not open a public issue for a security problem.
|
|
6
|
+
|
|
7
|
+
Report it privately through
|
|
8
|
+
[GitHub Security Advisories](https://github.com/jbcom/koota-kit/security/advisories/new),
|
|
9
|
+
which lets us discuss and fix the issue before it is disclosed.
|
|
10
|
+
|
|
11
|
+
You can expect an acknowledgement within a few days. If a fix is warranted, we
|
|
12
|
+
will prepare it privately, publish a patched release, and credit you in the
|
|
13
|
+
advisory unless you would rather remain anonymous.
|
|
14
|
+
|
|
15
|
+
## Supported versions
|
|
16
|
+
|
|
17
|
+
The latest `0.x` release receives security fixes. Older pre-1.0 releases are
|
|
18
|
+
not patched unless a coordinated disclosure requires an exceptional backport.
|
|
19
|
+
|
|
20
|
+
## In scope
|
|
21
|
+
|
|
22
|
+
Examples include malicious persisted snapshot handling, package supply-chain
|
|
23
|
+
issues, unexpected code execution during install/build, and vulnerabilities in
|
|
24
|
+
the package's runtime dependencies. Application authorization and game logic
|
|
25
|
+
built on top of koota-kit are outside this repository's security boundary.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Scratch-backed event logs — the publish/drain seam between sim systems.
|
|
3
|
+
//
|
|
4
|
+
// A fixed-timestep sim needs one system to tell another that something HAPPENED,
|
|
5
|
+
// not merely that some state now differs. The distinction is load-bearing: a
|
|
6
|
+
// system cannot infer an event from state a lower priority band has already
|
|
7
|
+
// consumed, so "a demon was struck this tick" has to be published as a fact
|
|
8
|
+
// rather than reconstructed from health that has already changed.
|
|
9
|
+
//
|
|
10
|
+
// Every such log is the same shape: an array under a well-known scratch key,
|
|
11
|
+
// appended by the owner, drained by exactly one consumer. Consuming systems
|
|
12
|
+
// drain (the log must empty, or a cue plays twice); OBSERVERS — a dev harness, a
|
|
13
|
+
// test, a HUD — must peek instead, because a reader that consumes what a consumer
|
|
14
|
+
// needs is a race, and whichever runs first wins.
|
|
15
|
+
//
|
|
16
|
+
// That distinction is why this is a package module rather than a snippet. It was
|
|
17
|
+
// re-derived ELEVEN times in one consuming game, and three of those sites had no
|
|
18
|
+
// peek at all until an observer silently stole records from the audio bridge and
|
|
19
|
+
// the missing sound was misdiagnosed as a missing swing. The pattern is not hard;
|
|
20
|
+
// getting it wrong is quiet, and it costs a debugging session every time.
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.defineEventLog = defineEventLog;
|
|
23
|
+
/**
|
|
24
|
+
* Declare an event log under `key`.
|
|
25
|
+
*
|
|
26
|
+
* The array is created lazily on first use and lives in `handle.scratch`, so it
|
|
27
|
+
* lifecycles with the world: a new run starts with empty logs and cannot inherit
|
|
28
|
+
* the last one's events. That is not a detail — a log at module scope survives
|
|
29
|
+
* the world that produced it, and stale events replay into a fresh run.
|
|
30
|
+
*
|
|
31
|
+
* Keys should be namespaced by system (`"combat:hits"`, `"voices:events"`) for
|
|
32
|
+
* the same reason trait names are: scratch is one flat map shared by every
|
|
33
|
+
* system in the world. Empty keys are rejected, as is a collision with an
|
|
34
|
+
* existing non-array scratch value.
|
|
35
|
+
*/
|
|
36
|
+
function defineEventLog(key) {
|
|
37
|
+
if (typeof key !== "string" || key.trim().length === 0) {
|
|
38
|
+
throw new TypeError("defineEventLog: key must be a non-empty string.");
|
|
39
|
+
}
|
|
40
|
+
const existingFor = (handle) => {
|
|
41
|
+
const existing = handle.scratch.get(key);
|
|
42
|
+
if (existing === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (!Array.isArray(existing)) {
|
|
45
|
+
throw new TypeError(`Event log "${key}" cannot use its scratch key because it already holds a non-array value.`);
|
|
46
|
+
}
|
|
47
|
+
return existing;
|
|
48
|
+
};
|
|
49
|
+
const arrayFor = (handle) => {
|
|
50
|
+
const existing = existingFor(handle);
|
|
51
|
+
if (existing)
|
|
52
|
+
return existing;
|
|
53
|
+
const log = [];
|
|
54
|
+
handle.scratch.set(key, log);
|
|
55
|
+
return log;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
key,
|
|
59
|
+
push: (handle, event) => {
|
|
60
|
+
arrayFor(handle).push(event);
|
|
61
|
+
},
|
|
62
|
+
drain: (handle) => {
|
|
63
|
+
const log = existingFor(handle);
|
|
64
|
+
if (!log)
|
|
65
|
+
return [];
|
|
66
|
+
// Copy, then truncate IN PLACE rather than replacing the array: another
|
|
67
|
+
// system may already hold this reference, and swapping in a new array
|
|
68
|
+
// would leave that holder appending into an orphan nobody drains.
|
|
69
|
+
const out = log.slice();
|
|
70
|
+
log.length = 0;
|
|
71
|
+
return out;
|
|
72
|
+
},
|
|
73
|
+
peek: (handle) => existingFor(handle)?.slice() ?? [],
|
|
74
|
+
clear: (handle) => {
|
|
75
|
+
const log = existingFor(handle);
|
|
76
|
+
if (log)
|
|
77
|
+
log.length = 0;
|
|
78
|
+
},
|
|
79
|
+
size: (handle) => existingFor(handle)?.length ?? 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { WorldHandle } from "./world.cjs";
|
|
2
|
+
/**
|
|
3
|
+
* A published event log: append here, drain or peek from the other side.
|
|
4
|
+
*
|
|
5
|
+
* Returned as an object rather than loose functions so a system declares its log
|
|
6
|
+
* ONCE at module scope and the key is written in exactly one place. Two string
|
|
7
|
+
* literals for the same log is the typo that produces an event stream nobody
|
|
8
|
+
* receives, and no type checker can catch it.
|
|
9
|
+
*/
|
|
10
|
+
export type EventLog<T> = {
|
|
11
|
+
/** The scratch key, exposed for debugging and for snapshot tooling. */
|
|
12
|
+
readonly key: string;
|
|
13
|
+
/** Append an event. Called by the system that owns the fact. */
|
|
14
|
+
push: (handle: WorldHandle, event: T) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Take every event and EMPTY the log — for the single consuming system.
|
|
17
|
+
*
|
|
18
|
+
* If two systems both need the same events, that is a design question and not
|
|
19
|
+
* a reason to drain twice: publish two logs, or have the second peek. A log
|
|
20
|
+
* drained by two consumers delivers each event to whichever ran first.
|
|
21
|
+
*/
|
|
22
|
+
drain: (handle: WorldHandle) => T[];
|
|
23
|
+
/**
|
|
24
|
+
* Read every event WITHOUT emptying — for observers: harnesses, tests, HUDs.
|
|
25
|
+
*
|
|
26
|
+
* Returns a readonly copy, so a caller cannot mutate the live array and
|
|
27
|
+
* cannot hold a reference that changes underneath them.
|
|
28
|
+
*/
|
|
29
|
+
peek: (handle: WorldHandle) => readonly T[];
|
|
30
|
+
/** Drop everything without reading. For run teardown and test setup. */
|
|
31
|
+
clear: (handle: WorldHandle) => void;
|
|
32
|
+
/** How many events are pending, without allocating a copy. */
|
|
33
|
+
size: (handle: WorldHandle) => number;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Declare an event log under `key`.
|
|
37
|
+
*
|
|
38
|
+
* The array is created lazily on first use and lives in `handle.scratch`, so it
|
|
39
|
+
* lifecycles with the world: a new run starts with empty logs and cannot inherit
|
|
40
|
+
* the last one's events. That is not a detail — a log at module scope survives
|
|
41
|
+
* the world that produced it, and stale events replay into a fresh run.
|
|
42
|
+
*
|
|
43
|
+
* Keys should be namespaced by system (`"combat:hits"`, `"voices:events"`) for
|
|
44
|
+
* the same reason trait names are: scratch is one flat map shared by every
|
|
45
|
+
* system in the world. Empty keys are rejected, as is a collision with an
|
|
46
|
+
* existing non-array scratch value.
|
|
47
|
+
*/
|
|
48
|
+
export declare function defineEventLog<T>(key: string): EventLog<T>;
|
|
49
|
+
//# sourceMappingURL=eventLog.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventLog.d.ts","sourceRoot":"","sources":["../../src/eventLog.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;IACxB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9C;;;;;;OAMG;IACH,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,CAAC,EAAE,CAAC;IACpC;;;;;OAKG;IACH,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,SAAS,CAAC,EAAE,CAAC;IAC5C,wEAAwE;IACxE,KAAK,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACrC,8DAA8D;IAC9D,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC;CACvC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CA6C1D"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// koota-kit — thin koota ECS conventions layer.
|
|
3
|
+
//
|
|
4
|
+
// Four modules, also importable as subpaths:
|
|
5
|
+
// ./world — WorldHandle lifecycle (world + dual-stream rng + clock + scratch)
|
|
6
|
+
// ./rng — dual-layer deterministic seedrandom PRNG with byte-exact
|
|
7
|
+
// snapshot/restore
|
|
8
|
+
// ./traits — defineTrait AoS-aliasing guard (leaf module, zero sibling
|
|
9
|
+
// imports)
|
|
10
|
+
// ./eventLog — scratch-backed publish/drain event logs with a peek seam for
|
|
11
|
+
// observers (harnesses, tests, HUDs)
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.defineTrait = exports.defineEventLog = exports.snapshotStream = exports.snapshotLayers = exports.restoreStream = exports.restoreLayers = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
|
|
14
|
+
var world_js_1 = require("./world.cjs");
|
|
15
|
+
Object.defineProperty(exports, "advanceClock", { enumerable: true, get: function () { return world_js_1.advanceClock; } });
|
|
16
|
+
Object.defineProperty(exports, "createActions", { enumerable: true, get: function () { return world_js_1.createActions; } });
|
|
17
|
+
Object.defineProperty(exports, "createSimWorld", { enumerable: true, get: function () { return world_js_1.createSimWorld; } });
|
|
18
|
+
Object.defineProperty(exports, "destroySimWorld", { enumerable: true, get: function () { return world_js_1.destroySimWorld; } });
|
|
19
|
+
Object.defineProperty(exports, "relation", { enumerable: true, get: function () { return world_js_1.relation; } });
|
|
20
|
+
Object.defineProperty(exports, "restoreWorldHeader", { enumerable: true, get: function () { return world_js_1.restoreWorldHeader; } });
|
|
21
|
+
Object.defineProperty(exports, "snapshotWorld", { enumerable: true, get: function () { return world_js_1.snapshotWorld; } });
|
|
22
|
+
Object.defineProperty(exports, "trait", { enumerable: true, get: function () { return world_js_1.trait; } });
|
|
23
|
+
var rng_js_1 = require("./rng.cjs");
|
|
24
|
+
Object.defineProperty(exports, "chance", { enumerable: true, get: function () { return rng_js_1.chance; } });
|
|
25
|
+
Object.defineProperty(exports, "createRng", { enumerable: true, get: function () { return rng_js_1.createRng; } });
|
|
26
|
+
Object.defineProperty(exports, "nextFloat", { enumerable: true, get: function () { return rng_js_1.nextFloat; } });
|
|
27
|
+
Object.defineProperty(exports, "nextInt", { enumerable: true, get: function () { return rng_js_1.nextInt; } });
|
|
28
|
+
Object.defineProperty(exports, "nextU32", { enumerable: true, get: function () { return rng_js_1.nextU32; } });
|
|
29
|
+
Object.defineProperty(exports, "restoreLayers", { enumerable: true, get: function () { return rng_js_1.restoreLayers; } });
|
|
30
|
+
Object.defineProperty(exports, "restoreStream", { enumerable: true, get: function () { return rng_js_1.restoreStream; } });
|
|
31
|
+
Object.defineProperty(exports, "snapshotLayers", { enumerable: true, get: function () { return rng_js_1.snapshotLayers; } });
|
|
32
|
+
Object.defineProperty(exports, "snapshotStream", { enumerable: true, get: function () { return rng_js_1.snapshotStream; } });
|
|
33
|
+
var eventLog_js_1 = require("./eventLog.cjs");
|
|
34
|
+
Object.defineProperty(exports, "defineEventLog", { enumerable: true, get: function () { return eventLog_js_1.defineEventLog; } });
|
|
35
|
+
var index_js_1 = require("./traits/index.cjs");
|
|
36
|
+
Object.defineProperty(exports, "defineTrait", { enumerable: true, get: function () { return index_js_1.defineTrait; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { advanceClock, createActions, createSimWorld, destroySimWorld, relation, restoreWorldHeader, snapshotWorld, trait, } from "./world.cjs";
|
|
2
|
+
export type { Entity, World, WorldHandle, WorldSnapshot } from "./world.cjs";
|
|
3
|
+
export { chance, createRng, nextFloat, nextInt, nextU32, restoreLayers, restoreStream, snapshotLayers, snapshotStream, } from "./rng.cjs";
|
|
4
|
+
export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, } from "./rng.cjs";
|
|
5
|
+
export { defineEventLog } from "./eventLog.cjs";
|
|
6
|
+
export type { EventLog } from "./eventLog.cjs";
|
|
7
|
+
export { defineTrait } from "./traits/index.cjs";
|
|
8
|
+
export type { SafeSchema } from "./traits/index.cjs";
|
|
9
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,cAAc,EACd,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
|