koota-kit 0.5.0 → 0.6.2
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 +34 -0
- package/CONTRIBUTING.md +2 -1
- package/README.md +28 -4
- package/dist/cjs/index.cjs +5 -2
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/index.d.cts.map +1 -1
- package/dist/cjs/schedule.cjs +131 -0
- package/dist/cjs/schedule.d.cts +69 -0
- package/dist/cjs/schedule.d.cts.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/schedule.d.ts +69 -0
- package/dist/esm/schedule.d.ts.map +1 -0
- package/dist/esm/schedule.js +128 -0
- package/docs/API.md +57 -0
- package/docs/ARCHITECTURE.md +15 -2
- package/docs/contributing.md +3 -2
- package/package.json +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.2 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
|
6
|
+
|
|
7
|
+
## What's Changed
|
|
8
|
+
* fix(release): accept generated GitHub changelog by @jbdevprimary in https://github.com/jbcom/koota-kit/pull/52
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
**Full Changelog**: https://github.com/jbcom/koota-kit/compare/v0.6.1...v0.6.2
|
|
12
|
+
|
|
13
|
+
## 0.6.1 (2026-10-14)
|
|
14
|
+
|
|
15
|
+
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
|
16
|
+
|
|
17
|
+
## What's Changed
|
|
18
|
+
* chore(deps): bump actions/setup-node from 7.0.0 to 7.1.0 in the non-major group by @dependabot[bot] in https://github.com/jbcom/koota-kit/pull/45
|
|
19
|
+
* fix(release): deduplicate changelog pull requests by @jbdevprimary in https://github.com/jbcom/koota-kit/pull/50
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
**Full Changelog**: https://github.com/jbcom/koota-kit/compare/v0.6.0...v0.6.1
|
|
23
|
+
|
|
24
|
+
## [0.6.0](https://github.com/jbcom/koota-kit/compare/v0.5.0...v0.6.0) (2026-10-10)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* **schedule:** add createSchedule, a fixed-step system schedule ([bb69bd6](https://github.com/jbcom/koota-kit/commit/bb69bd638e7a3dc6315ede827e4c58820facea3a))
|
|
30
|
+
* **schedule:** add createSchedule, a fixed-step system schedule ([21f6ffb](https://github.com/jbcom/koota-kit/commit/21f6ffbb2a6f4fae819f88197d02e30f50cc98ea))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Bug Fixes
|
|
34
|
+
|
|
35
|
+
* **schedule:** pause means no steps, and a refused step is never debited ([e2fbfad](https://github.com/jbcom/koota-kit/commit/e2fbfadb363764bc8c43232e19862223ef250ee6))
|
|
36
|
+
|
|
3
37
|
## [0.5.0](https://github.com/jbcom/koota-kit/compare/v0.4.0...v0.5.0) (2026-10-10)
|
|
4
38
|
|
|
5
39
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -31,7 +31,8 @@ pnpm verify
|
|
|
31
31
|
3. Run `pnpm verify`. A change is not ready while any part of that is red.
|
|
32
32
|
4. Commit with [Conventional Commits](https://www.conventionalcommits.org):
|
|
33
33
|
`fix:`, `feat:`, `docs:`, `refactor:`, `test:`, `chore:`. Release Please uses
|
|
34
|
-
these commits to
|
|
34
|
+
these commits to select the next version; GitHub release notes list each
|
|
35
|
+
merged pull request once in the changelog.
|
|
35
36
|
5. Open a pull request describing what changed and why.
|
|
36
37
|
|
|
37
38
|
## What gets reviewed
|
package/README.md
CHANGED
|
@@ -137,6 +137,26 @@ defineTrait({ storage: { logs: 0 } }); // TypeError: shared object footg
|
|
|
137
137
|
Declare traits once in a leaf module and export each trait from one location.
|
|
138
138
|
Koota identifies traits by reference, not by a string name.
|
|
139
139
|
|
|
140
|
+
### A fixed-step schedule removes the frame rate from the sim
|
|
141
|
+
|
|
142
|
+
`createSchedule(systems, { step, maxSteps })` returns a `tick(handle, frameDt)`
|
|
143
|
+
for your frame loop. The frame's dt fills an accumulator kept in `handle.scratch`
|
|
144
|
+
(so it dies with the run); each whole `step` advances the clock, then calls every
|
|
145
|
+
system in order with `dt === step`. At most `maxSteps` run per tick and any
|
|
146
|
+
excess is dropped, so one huge dt after a backgrounded tab cannot cause a
|
|
147
|
+
catch-up storm. `tick(handle, 0)` is a valid pause and returns the steps run.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { createSchedule } from "koota-kit";
|
|
151
|
+
|
|
152
|
+
const schedule = createSchedule([stove, mixer, serving], { step: 1 / 60, maxSteps: 6 });
|
|
153
|
+
// once per rendered frame:
|
|
154
|
+
const steps = schedule.tick(sim, frameDt);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Sixty frames of 1/60 s and thirty of 1/30 s run the same sixty steps.
|
|
158
|
+
`schedule.remainder(sim) / schedule.step` is the interpolation alpha.
|
|
159
|
+
|
|
140
160
|
### Event logs have one consuming owner
|
|
141
161
|
|
|
142
162
|
`push` appends, `drain` returns every pending event and empties the log, and
|
|
@@ -205,6 +225,7 @@ are also available:
|
|
|
205
225
|
| `koota-kit/rng` | Seeded streams, named substreams, bounded draws, snapshots |
|
|
206
226
|
| `koota-kit/seed` | Seed derivation and 128-bit master seeds |
|
|
207
227
|
| `koota-kit/traits` | Safe trait declaration |
|
|
228
|
+
| `koota-kit/schedule` | Fixed-step system schedule with a step cap |
|
|
208
229
|
| `koota-kit/eventLog` | World-scoped publish/drain logs |
|
|
209
230
|
| `koota-kit/react` | `SimWorldProvider` and `useSimWorld` (optional React peer) |
|
|
210
231
|
|
|
@@ -224,6 +245,8 @@ For task-oriented usage, read the Sourcey guides on
|
|
|
224
245
|
- `createMasterSeed` throws when no entropy source exists or the source leaves
|
|
225
246
|
the buffer all zero.
|
|
226
247
|
- `advanceClock` accepts finite, non-negative `dt`; `0` is a valid paused tick.
|
|
248
|
+
- `createSchedule` needs a finite `step > 0` and a safe-integer `maxSteps >= 1`;
|
|
249
|
+
a negative, `NaN` or infinite `frameDt` counts as `0`.
|
|
227
250
|
- `nextInt` requires safe-integer bounds with `maxExclusive > minInclusive` and
|
|
228
251
|
a range no larger than `2**32`.
|
|
229
252
|
- `chance` accepts only finite probabilities in `[0, 1]`.
|
|
@@ -263,10 +286,11 @@ pnpm audit --prod # registry-backed production dependency audit
|
|
|
263
286
|
|
|
264
287
|
## Releases and support
|
|
265
288
|
|
|
266
|
-
Release Please
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
289
|
+
Release Please uses Conventional Commits to select versions and GitHub's
|
|
290
|
+
merged-pull-request release notes to maintain `CHANGELOG.md`. A release tag
|
|
291
|
+
triggers a clean verification and npm publication with provenance. The latest
|
|
292
|
+
`0.x` release receives fixes; pre-1.0 APIs may evolve, with breaking changes
|
|
293
|
+
called out in the changelog.
|
|
270
294
|
|
|
271
295
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a pull request. Report
|
|
272
296
|
security issues privately according to [SECURITY.md](./SECURITY.md).
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// koota-kit — thin koota ECS conventions layer.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// Six modules, also importable as subpaths:
|
|
5
5
|
// ./world — WorldHandle lifecycle (world + dual-stream rng + clock + scratch)
|
|
6
|
+
// ./schedule — fixed-step system schedule (accumulator in scratch, step cap)
|
|
6
7
|
// ./rng — dual-layer deterministic seedrandom PRNG and named substreams
|
|
7
8
|
// with byte-exact snapshot/restore
|
|
8
9
|
// ./seed — length-prefixed SHA-256 seed derivation and 128-bit master
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
// ./eventLog — scratch-backed publish/drain event logs with a peek seam for
|
|
13
14
|
// observers (harnesses, tests, HUDs)
|
|
14
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.defineTrait = exports.defineEventLog = exports.MASTER_SEED_BYTES = exports.isMasterSeed = exports.deriveSeed = exports.createMasterSeed = exports.substream = exports.snapshotSubstreams = exports.snapshotStream = exports.snapshotLayers = exports.shuffle = exports.restoreSubstreams = exports.restoreStream = exports.restoreLayers = exports.pick = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.getSubstream = exports.createSubstreams = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
|
|
16
|
+
exports.defineTrait = exports.defineEventLog = exports.createSchedule = exports.MASTER_SEED_BYTES = exports.isMasterSeed = exports.deriveSeed = exports.createMasterSeed = exports.substream = exports.snapshotSubstreams = exports.snapshotStream = exports.snapshotLayers = exports.shuffle = exports.restoreSubstreams = exports.restoreStream = exports.restoreLayers = exports.pick = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.getSubstream = exports.createSubstreams = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
|
|
16
17
|
var world_js_1 = require("./world.cjs");
|
|
17
18
|
Object.defineProperty(exports, "advanceClock", { enumerable: true, get: function () { return world_js_1.advanceClock; } });
|
|
18
19
|
Object.defineProperty(exports, "createActions", { enumerable: true, get: function () { return world_js_1.createActions; } });
|
|
@@ -44,6 +45,8 @@ Object.defineProperty(exports, "createMasterSeed", { enumerable: true, get: func
|
|
|
44
45
|
Object.defineProperty(exports, "deriveSeed", { enumerable: true, get: function () { return seed_js_1.deriveSeed; } });
|
|
45
46
|
Object.defineProperty(exports, "isMasterSeed", { enumerable: true, get: function () { return seed_js_1.isMasterSeed; } });
|
|
46
47
|
Object.defineProperty(exports, "MASTER_SEED_BYTES", { enumerable: true, get: function () { return seed_js_1.MASTER_SEED_BYTES; } });
|
|
48
|
+
var schedule_js_1 = require("./schedule.cjs");
|
|
49
|
+
Object.defineProperty(exports, "createSchedule", { enumerable: true, get: function () { return schedule_js_1.createSchedule; } });
|
|
47
50
|
var eventLog_js_1 = require("./eventLog.cjs");
|
|
48
51
|
Object.defineProperty(exports, "defineEventLog", { enumerable: true, get: function () { return eventLog_js_1.defineEventLog; } });
|
|
49
52
|
var index_js_1 = require("./traits/index.cjs");
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -4,6 +4,8 @@ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt,
|
|
|
4
4
|
export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, Substreams, SubstreamsSnapshot, } from "./rng.cjs";
|
|
5
5
|
export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.cjs";
|
|
6
6
|
export type { EntropySource, SeedPart } from "./seed.cjs";
|
|
7
|
+
export { createSchedule } from "./schedule.cjs";
|
|
8
|
+
export type { Schedule, ScheduleOptions, System } from "./schedule.cjs";
|
|
7
9
|
export { defineEventLog } from "./eventLog.cjs";
|
|
8
10
|
export type { EventLog } from "./eventLog.cjs";
|
|
9
11
|
export { defineTrait } from "./traits/index.cjs";
|
package/dist/cjs/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,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,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,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"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Fixed-step system schedule — the loop that turns "a frame passed" into
|
|
3
|
+
// "the sim advanced by whole steps".
|
|
4
|
+
//
|
|
5
|
+
// A simulation that integrates the frame's own dt drifts with the frame rate:
|
|
6
|
+
// a 120 Hz phone and a test that advances three seconds in one call disagree,
|
|
7
|
+
// and anything counted in real time (a save every 0.25 s) lands on different
|
|
8
|
+
// ticks. A fixed-step schedule removes the frame rate from the sim: the frame's
|
|
9
|
+
// dt fills an accumulator, and the sim only ever advances by whole `step`s.
|
|
10
|
+
//
|
|
11
|
+
// The loop is generic sim plumbing, so it lives here and not in the game: it
|
|
12
|
+
// knows nothing about React, a renderer, or what a system does. A renderer's
|
|
13
|
+
// frame callback calls `schedule.tick(handle, dt)` and that is the entire
|
|
14
|
+
// integration. The accumulator lives in the handle's `scratch`, so it dies with
|
|
15
|
+
// the run: a new world starts with nothing owed, and two schedules (or two
|
|
16
|
+
// worlds) never share one.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.createSchedule = createSchedule;
|
|
19
|
+
const world_js_1 = require("./world.cjs");
|
|
20
|
+
let nextScheduleId = 0;
|
|
21
|
+
// Keys are process-wide because schedules are declared once at module scope and
|
|
22
|
+
// used on many handles. An explicit key may be shared on purpose; a generated
|
|
23
|
+
// one must stay private to its schedule.
|
|
24
|
+
const claimedKeys = new Set();
|
|
25
|
+
const generatedKeys = new Set();
|
|
26
|
+
// Float accumulation: 432 frames of 1/144 add up to a hair under 3 s, and
|
|
27
|
+
// without a tolerance the sim would owe — and run — one step fewer than the
|
|
28
|
+
// same time delivered at 60 Hz. A billionth of a step is far below anything a
|
|
29
|
+
// frame clock can resolve, so treating "within it" as whole costs nothing.
|
|
30
|
+
const STEP_TOLERANCE = 1e-9;
|
|
31
|
+
/**
|
|
32
|
+
* Build a fixed-step schedule over `systems`, run in the order given.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* const schedule = createSchedule([intro, stove, mixer, serving], {
|
|
36
|
+
* step: 1 / 60,
|
|
37
|
+
* maxSteps: 6,
|
|
38
|
+
* });
|
|
39
|
+
* // once per rendered frame, with the frame's dt:
|
|
40
|
+
* schedule.tick(run, frameDt);
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* The systems array is copied; later changes to it do not affect the schedule.
|
|
44
|
+
*/
|
|
45
|
+
function createSchedule(systems, options) {
|
|
46
|
+
if (!Array.isArray(systems)) {
|
|
47
|
+
throw new TypeError("createSchedule: systems must be an array of functions.");
|
|
48
|
+
}
|
|
49
|
+
const { step, maxSteps } = options;
|
|
50
|
+
if (!Number.isFinite(step) || step <= 0) {
|
|
51
|
+
throw new RangeError(`createSchedule: step must be finite and greater than zero; received ${step}.`);
|
|
52
|
+
}
|
|
53
|
+
if (!Number.isSafeInteger(maxSteps) || maxSteps < 1) {
|
|
54
|
+
throw new RangeError(`createSchedule: maxSteps must be a safe integer of at least 1; received ${maxSteps}.`);
|
|
55
|
+
}
|
|
56
|
+
if (options.key !== undefined && (typeof options.key !== "string" || options.key === "")) {
|
|
57
|
+
throw new TypeError("createSchedule: key must be a non-empty string when provided.");
|
|
58
|
+
}
|
|
59
|
+
const ordered = Object.freeze([...systems]);
|
|
60
|
+
for (const system of ordered) {
|
|
61
|
+
if (typeof system !== "function") {
|
|
62
|
+
throw new TypeError("createSchedule: every system must be a function.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let key;
|
|
66
|
+
if (options.key === undefined) {
|
|
67
|
+
// Skip any key a caller has already named, so a default never lands on one.
|
|
68
|
+
do {
|
|
69
|
+
key = `schedule:${nextScheduleId++}`;
|
|
70
|
+
} while (claimedKeys.has(key));
|
|
71
|
+
generatedKeys.add(key);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (generatedKeys.has(options.key)) {
|
|
75
|
+
throw new TypeError(`createSchedule: key "${options.key}" is already the generated key of another schedule.`);
|
|
76
|
+
}
|
|
77
|
+
key = options.key;
|
|
78
|
+
}
|
|
79
|
+
claimedKeys.add(key);
|
|
80
|
+
const tolerance = step * STEP_TOLERANCE;
|
|
81
|
+
function read(handle) {
|
|
82
|
+
const stored = handle.scratch.get(key);
|
|
83
|
+
if (stored === undefined)
|
|
84
|
+
return 0;
|
|
85
|
+
if (typeof stored !== "number" || !Number.isFinite(stored) || stored < 0) {
|
|
86
|
+
throw new TypeError(`createSchedule: scratch key "${key}" already holds something other than a schedule accumulator.`);
|
|
87
|
+
}
|
|
88
|
+
return stored;
|
|
89
|
+
}
|
|
90
|
+
function tick(handle, frameDt) {
|
|
91
|
+
const previous = read(handle);
|
|
92
|
+
// A pause (and any frame dt that counts as 0) runs nothing, even if a
|
|
93
|
+
// failed tick left whole steps owed: those wait for a frame that moves time.
|
|
94
|
+
if (!(Number.isFinite(frameDt) && frameDt > 0))
|
|
95
|
+
return 0;
|
|
96
|
+
const sum = previous + frameDt;
|
|
97
|
+
// Two huge finite frames can sum past the double range; saturate instead of
|
|
98
|
+
// letting Infinity poison the stored accumulator.
|
|
99
|
+
let owed = Number.isFinite(sum) ? sum : Number.MAX_VALUE;
|
|
100
|
+
let ran = 0;
|
|
101
|
+
try {
|
|
102
|
+
while (owed + tolerance >= step && ran < maxSteps) {
|
|
103
|
+
// The clock moves first, so a step it refuses (an exhausted clock) is
|
|
104
|
+
// never debited. After that the step is spent before any system runs:
|
|
105
|
+
// if one throws, this step's time is gone with the tick it caused.
|
|
106
|
+
(0, world_js_1.advanceClock)(handle, step);
|
|
107
|
+
owed = Math.max(0, owed - step);
|
|
108
|
+
ran++;
|
|
109
|
+
for (const system of ordered)
|
|
110
|
+
system(handle, step);
|
|
111
|
+
}
|
|
112
|
+
if (owed + tolerance >= step) {
|
|
113
|
+
// Capped with whole steps still owed: drop them, keep the fraction.
|
|
114
|
+
owed %= step;
|
|
115
|
+
if (owed + tolerance >= step)
|
|
116
|
+
owed = 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
handle.scratch.set(key, owed);
|
|
121
|
+
}
|
|
122
|
+
return ran;
|
|
123
|
+
}
|
|
124
|
+
return Object.freeze({
|
|
125
|
+
step,
|
|
126
|
+
maxSteps,
|
|
127
|
+
key,
|
|
128
|
+
tick,
|
|
129
|
+
remainder: read,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { WorldHandle } from "./world.cjs";
|
|
2
|
+
/**
|
|
3
|
+
* One unit of sim work, run once per fixed step.
|
|
4
|
+
*
|
|
5
|
+
* `dt` is always the schedule's `step`, never the frame's dt, so a system can
|
|
6
|
+
* integrate it without knowing the frame rate. Systems that need randomness
|
|
7
|
+
* should draw from a named substream rather than the handle's positional `rng`,
|
|
8
|
+
* so adding a system does not shift another system's draws.
|
|
9
|
+
*/
|
|
10
|
+
export type System = (handle: WorldHandle, dt: number) => void;
|
|
11
|
+
export type ScheduleOptions = {
|
|
12
|
+
/** Seconds of sim time one step represents. Finite and greater than zero. */
|
|
13
|
+
step: number;
|
|
14
|
+
/**
|
|
15
|
+
* The most steps a single `tick` may run. A safe integer of at least 1. Time
|
|
16
|
+
* owed beyond this is dropped, not carried, so one huge frame dt (a hidden
|
|
17
|
+
* tab returning after a minute) cannot trigger a catch-up storm.
|
|
18
|
+
*/
|
|
19
|
+
maxSteps: number;
|
|
20
|
+
/**
|
|
21
|
+
* The `scratch` key holding this schedule's accumulator, for callers who want
|
|
22
|
+
* to namespace it (`"run:schedule"`). Defaults to a key unique to this
|
|
23
|
+
* schedule. Two schedules given the same key share one accumulator.
|
|
24
|
+
*/
|
|
25
|
+
key?: string;
|
|
26
|
+
};
|
|
27
|
+
export type Schedule = {
|
|
28
|
+
readonly step: number;
|
|
29
|
+
readonly maxSteps: number;
|
|
30
|
+
/** The `scratch` key holding this schedule's accumulator on a handle. */
|
|
31
|
+
readonly key: string;
|
|
32
|
+
/**
|
|
33
|
+
* Add `frameDt` seconds to `handle`'s accumulator and run every whole step it
|
|
34
|
+
* now covers, up to `maxSteps`: each step advances the clock by `step`, then
|
|
35
|
+
* calls every system in order with `dt === step`. Returns how many steps ran.
|
|
36
|
+
*
|
|
37
|
+
* A `frameDt` that is negative, `NaN` or infinite counts as `0`, and `0` is a
|
|
38
|
+
* valid pause: no step runs and the clock does not move. Whole steps beyond
|
|
39
|
+
* `maxSteps` are dropped; the sub-step remainder is kept.
|
|
40
|
+
*
|
|
41
|
+
* If a system throws, the steps that had already started are consumed (the
|
|
42
|
+
* clock moved for them) and the rest stay owed, so a retry does not replay
|
|
43
|
+
* time the sim already spent.
|
|
44
|
+
*/
|
|
45
|
+
tick: (handle: WorldHandle, frameDt: number) => number;
|
|
46
|
+
/**
|
|
47
|
+
* Seconds currently owed to the next step, in `[0, step)` between ticks.
|
|
48
|
+
* Dividing it by `step` gives the interpolation alpha for a renderer drawing
|
|
49
|
+
* between the previous and current sim state. `0` on a handle that has not
|
|
50
|
+
* ticked.
|
|
51
|
+
*/
|
|
52
|
+
remainder: (handle: WorldHandle) => number;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Build a fixed-step schedule over `systems`, run in the order given.
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* const schedule = createSchedule([intro, stove, mixer, serving], {
|
|
59
|
+
* step: 1 / 60,
|
|
60
|
+
* maxSteps: 6,
|
|
61
|
+
* });
|
|
62
|
+
* // once per rendered frame, with the frame's dt:
|
|
63
|
+
* schedule.tick(run, frameDt);
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* The systems array is copied; later changes to it do not affect the schedule.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createSchedule(systems: readonly System[], options: ScheduleOptions): Readonly<Schedule>;
|
|
69
|
+
//# sourceMappingURL=schedule.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../../src/schedule.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;AAE/D,MAAM,MAAM,eAAe,GAAG;IAC5B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;;OAYG;IACH,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACvD;;;;;OAKG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC;CAC5C,CAAC;AAeF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,eAAe,GACvB,QAAQ,CAAC,QAAQ,CAAC,CA2FpB"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt,
|
|
|
4
4
|
export type { RngLayers, RngLayersSnapshot, RngSeeds, RngStream, RngStreamSnapshot, Substreams, SubstreamsSnapshot, } from "./rng.js";
|
|
5
5
|
export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.js";
|
|
6
6
|
export type { EntropySource, SeedPart } from "./seed.js";
|
|
7
|
+
export { createSchedule } from "./schedule.js";
|
|
8
|
+
export type { Schedule, ScheduleOptions, System } from "./schedule.js";
|
|
7
9
|
export { defineEventLog } from "./eventLog.js";
|
|
8
10
|
export type { EventLog } from "./eventLog.js";
|
|
9
11
|
export { defineTrait } from "./traits/index.js";
|
package/dist/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,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,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,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"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// koota-kit — thin koota ECS conventions layer.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Six modules, also importable as subpaths:
|
|
4
4
|
// ./world — WorldHandle lifecycle (world + dual-stream rng + clock + scratch)
|
|
5
|
+
// ./schedule — fixed-step system schedule (accumulator in scratch, step cap)
|
|
5
6
|
// ./rng — dual-layer deterministic seedrandom PRNG and named substreams
|
|
6
7
|
// with byte-exact snapshot/restore
|
|
7
8
|
// ./seed — length-prefixed SHA-256 seed derivation and 128-bit master
|
|
@@ -13,5 +14,6 @@
|
|
|
13
14
|
export { advanceClock, createActions, createSimWorld, destroySimWorld, relation, restoreWorldHeader, snapshotWorld, trait, } from "./world.js";
|
|
14
15
|
export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt, nextU32, pick, restoreLayers, restoreStream, restoreSubstreams, shuffle, snapshotLayers, snapshotStream, snapshotSubstreams, substream, } from "./rng.js";
|
|
15
16
|
export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.js";
|
|
17
|
+
export { createSchedule } from "./schedule.js";
|
|
16
18
|
export { defineEventLog } from "./eventLog.js";
|
|
17
19
|
export { defineTrait } from "./traits/index.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { WorldHandle } from "./world.js";
|
|
2
|
+
/**
|
|
3
|
+
* One unit of sim work, run once per fixed step.
|
|
4
|
+
*
|
|
5
|
+
* `dt` is always the schedule's `step`, never the frame's dt, so a system can
|
|
6
|
+
* integrate it without knowing the frame rate. Systems that need randomness
|
|
7
|
+
* should draw from a named substream rather than the handle's positional `rng`,
|
|
8
|
+
* so adding a system does not shift another system's draws.
|
|
9
|
+
*/
|
|
10
|
+
export type System = (handle: WorldHandle, dt: number) => void;
|
|
11
|
+
export type ScheduleOptions = {
|
|
12
|
+
/** Seconds of sim time one step represents. Finite and greater than zero. */
|
|
13
|
+
step: number;
|
|
14
|
+
/**
|
|
15
|
+
* The most steps a single `tick` may run. A safe integer of at least 1. Time
|
|
16
|
+
* owed beyond this is dropped, not carried, so one huge frame dt (a hidden
|
|
17
|
+
* tab returning after a minute) cannot trigger a catch-up storm.
|
|
18
|
+
*/
|
|
19
|
+
maxSteps: number;
|
|
20
|
+
/**
|
|
21
|
+
* The `scratch` key holding this schedule's accumulator, for callers who want
|
|
22
|
+
* to namespace it (`"run:schedule"`). Defaults to a key unique to this
|
|
23
|
+
* schedule. Two schedules given the same key share one accumulator.
|
|
24
|
+
*/
|
|
25
|
+
key?: string;
|
|
26
|
+
};
|
|
27
|
+
export type Schedule = {
|
|
28
|
+
readonly step: number;
|
|
29
|
+
readonly maxSteps: number;
|
|
30
|
+
/** The `scratch` key holding this schedule's accumulator on a handle. */
|
|
31
|
+
readonly key: string;
|
|
32
|
+
/**
|
|
33
|
+
* Add `frameDt` seconds to `handle`'s accumulator and run every whole step it
|
|
34
|
+
* now covers, up to `maxSteps`: each step advances the clock by `step`, then
|
|
35
|
+
* calls every system in order with `dt === step`. Returns how many steps ran.
|
|
36
|
+
*
|
|
37
|
+
* A `frameDt` that is negative, `NaN` or infinite counts as `0`, and `0` is a
|
|
38
|
+
* valid pause: no step runs and the clock does not move. Whole steps beyond
|
|
39
|
+
* `maxSteps` are dropped; the sub-step remainder is kept.
|
|
40
|
+
*
|
|
41
|
+
* If a system throws, the steps that had already started are consumed (the
|
|
42
|
+
* clock moved for them) and the rest stay owed, so a retry does not replay
|
|
43
|
+
* time the sim already spent.
|
|
44
|
+
*/
|
|
45
|
+
tick: (handle: WorldHandle, frameDt: number) => number;
|
|
46
|
+
/**
|
|
47
|
+
* Seconds currently owed to the next step, in `[0, step)` between ticks.
|
|
48
|
+
* Dividing it by `step` gives the interpolation alpha for a renderer drawing
|
|
49
|
+
* between the previous and current sim state. `0` on a handle that has not
|
|
50
|
+
* ticked.
|
|
51
|
+
*/
|
|
52
|
+
remainder: (handle: WorldHandle) => number;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Build a fixed-step schedule over `systems`, run in the order given.
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* const schedule = createSchedule([intro, stove, mixer, serving], {
|
|
59
|
+
* step: 1 / 60,
|
|
60
|
+
* maxSteps: 6,
|
|
61
|
+
* });
|
|
62
|
+
* // once per rendered frame, with the frame's dt:
|
|
63
|
+
* schedule.tick(run, frameDt);
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* The systems array is copied; later changes to it do not affect the schedule.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createSchedule(systems: readonly System[], options: ScheduleOptions): Readonly<Schedule>;
|
|
69
|
+
//# sourceMappingURL=schedule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../../src/schedule.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;AAE/D,MAAM,MAAM,eAAe,GAAG;IAC5B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;;OAYG;IACH,IAAI,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACvD;;;;;OAKG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC;CAC5C,CAAC;AAeF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,eAAe,GACvB,QAAQ,CAAC,QAAQ,CAAC,CA2FpB"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Fixed-step system schedule — the loop that turns "a frame passed" into
|
|
2
|
+
// "the sim advanced by whole steps".
|
|
3
|
+
//
|
|
4
|
+
// A simulation that integrates the frame's own dt drifts with the frame rate:
|
|
5
|
+
// a 120 Hz phone and a test that advances three seconds in one call disagree,
|
|
6
|
+
// and anything counted in real time (a save every 0.25 s) lands on different
|
|
7
|
+
// ticks. A fixed-step schedule removes the frame rate from the sim: the frame's
|
|
8
|
+
// dt fills an accumulator, and the sim only ever advances by whole `step`s.
|
|
9
|
+
//
|
|
10
|
+
// The loop is generic sim plumbing, so it lives here and not in the game: it
|
|
11
|
+
// knows nothing about React, a renderer, or what a system does. A renderer's
|
|
12
|
+
// frame callback calls `schedule.tick(handle, dt)` and that is the entire
|
|
13
|
+
// integration. The accumulator lives in the handle's `scratch`, so it dies with
|
|
14
|
+
// the run: a new world starts with nothing owed, and two schedules (or two
|
|
15
|
+
// worlds) never share one.
|
|
16
|
+
import { advanceClock } from "./world.js";
|
|
17
|
+
let nextScheduleId = 0;
|
|
18
|
+
// Keys are process-wide because schedules are declared once at module scope and
|
|
19
|
+
// used on many handles. An explicit key may be shared on purpose; a generated
|
|
20
|
+
// one must stay private to its schedule.
|
|
21
|
+
const claimedKeys = new Set();
|
|
22
|
+
const generatedKeys = new Set();
|
|
23
|
+
// Float accumulation: 432 frames of 1/144 add up to a hair under 3 s, and
|
|
24
|
+
// without a tolerance the sim would owe — and run — one step fewer than the
|
|
25
|
+
// same time delivered at 60 Hz. A billionth of a step is far below anything a
|
|
26
|
+
// frame clock can resolve, so treating "within it" as whole costs nothing.
|
|
27
|
+
const STEP_TOLERANCE = 1e-9;
|
|
28
|
+
/**
|
|
29
|
+
* Build a fixed-step schedule over `systems`, run in the order given.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const schedule = createSchedule([intro, stove, mixer, serving], {
|
|
33
|
+
* step: 1 / 60,
|
|
34
|
+
* maxSteps: 6,
|
|
35
|
+
* });
|
|
36
|
+
* // once per rendered frame, with the frame's dt:
|
|
37
|
+
* schedule.tick(run, frameDt);
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* The systems array is copied; later changes to it do not affect the schedule.
|
|
41
|
+
*/
|
|
42
|
+
export function createSchedule(systems, options) {
|
|
43
|
+
if (!Array.isArray(systems)) {
|
|
44
|
+
throw new TypeError("createSchedule: systems must be an array of functions.");
|
|
45
|
+
}
|
|
46
|
+
const { step, maxSteps } = options;
|
|
47
|
+
if (!Number.isFinite(step) || step <= 0) {
|
|
48
|
+
throw new RangeError(`createSchedule: step must be finite and greater than zero; received ${step}.`);
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isSafeInteger(maxSteps) || maxSteps < 1) {
|
|
51
|
+
throw new RangeError(`createSchedule: maxSteps must be a safe integer of at least 1; received ${maxSteps}.`);
|
|
52
|
+
}
|
|
53
|
+
if (options.key !== undefined && (typeof options.key !== "string" || options.key === "")) {
|
|
54
|
+
throw new TypeError("createSchedule: key must be a non-empty string when provided.");
|
|
55
|
+
}
|
|
56
|
+
const ordered = Object.freeze([...systems]);
|
|
57
|
+
for (const system of ordered) {
|
|
58
|
+
if (typeof system !== "function") {
|
|
59
|
+
throw new TypeError("createSchedule: every system must be a function.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let key;
|
|
63
|
+
if (options.key === undefined) {
|
|
64
|
+
// Skip any key a caller has already named, so a default never lands on one.
|
|
65
|
+
do {
|
|
66
|
+
key = `schedule:${nextScheduleId++}`;
|
|
67
|
+
} while (claimedKeys.has(key));
|
|
68
|
+
generatedKeys.add(key);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (generatedKeys.has(options.key)) {
|
|
72
|
+
throw new TypeError(`createSchedule: key "${options.key}" is already the generated key of another schedule.`);
|
|
73
|
+
}
|
|
74
|
+
key = options.key;
|
|
75
|
+
}
|
|
76
|
+
claimedKeys.add(key);
|
|
77
|
+
const tolerance = step * STEP_TOLERANCE;
|
|
78
|
+
function read(handle) {
|
|
79
|
+
const stored = handle.scratch.get(key);
|
|
80
|
+
if (stored === undefined)
|
|
81
|
+
return 0;
|
|
82
|
+
if (typeof stored !== "number" || !Number.isFinite(stored) || stored < 0) {
|
|
83
|
+
throw new TypeError(`createSchedule: scratch key "${key}" already holds something other than a schedule accumulator.`);
|
|
84
|
+
}
|
|
85
|
+
return stored;
|
|
86
|
+
}
|
|
87
|
+
function tick(handle, frameDt) {
|
|
88
|
+
const previous = read(handle);
|
|
89
|
+
// A pause (and any frame dt that counts as 0) runs nothing, even if a
|
|
90
|
+
// failed tick left whole steps owed: those wait for a frame that moves time.
|
|
91
|
+
if (!(Number.isFinite(frameDt) && frameDt > 0))
|
|
92
|
+
return 0;
|
|
93
|
+
const sum = previous + frameDt;
|
|
94
|
+
// Two huge finite frames can sum past the double range; saturate instead of
|
|
95
|
+
// letting Infinity poison the stored accumulator.
|
|
96
|
+
let owed = Number.isFinite(sum) ? sum : Number.MAX_VALUE;
|
|
97
|
+
let ran = 0;
|
|
98
|
+
try {
|
|
99
|
+
while (owed + tolerance >= step && ran < maxSteps) {
|
|
100
|
+
// The clock moves first, so a step it refuses (an exhausted clock) is
|
|
101
|
+
// never debited. After that the step is spent before any system runs:
|
|
102
|
+
// if one throws, this step's time is gone with the tick it caused.
|
|
103
|
+
advanceClock(handle, step);
|
|
104
|
+
owed = Math.max(0, owed - step);
|
|
105
|
+
ran++;
|
|
106
|
+
for (const system of ordered)
|
|
107
|
+
system(handle, step);
|
|
108
|
+
}
|
|
109
|
+
if (owed + tolerance >= step) {
|
|
110
|
+
// Capped with whole steps still owed: drop them, keep the fraction.
|
|
111
|
+
owed %= step;
|
|
112
|
+
if (owed + tolerance >= step)
|
|
113
|
+
owed = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
handle.scratch.set(key, owed);
|
|
118
|
+
}
|
|
119
|
+
return ran;
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
step,
|
|
123
|
+
maxSteps,
|
|
124
|
+
key,
|
|
125
|
+
tick,
|
|
126
|
+
remainder: read,
|
|
127
|
+
});
|
|
128
|
+
}
|
package/docs/API.md
CHANGED
|
@@ -174,6 +174,63 @@ const Pose = defineTrait(() => ({ position: { x: 0, y: 0 } }));
|
|
|
174
174
|
Bare object and array fields are rejected by `SafeSchema` and by the runtime
|
|
175
175
|
guard.
|
|
176
176
|
|
|
177
|
+
## `schedule`
|
|
178
|
+
|
|
179
|
+
Import from `koota-kit/schedule` or the root.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
type System = (handle: WorldHandle, dt: number) => void;
|
|
183
|
+
|
|
184
|
+
type ScheduleOptions = {
|
|
185
|
+
step: number; // seconds per step; finite, > 0
|
|
186
|
+
maxSteps: number; // safe integer >= 1: most steps one tick may run
|
|
187
|
+
key?: string; // scratch key for the accumulator; unique by default
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
type Schedule = {
|
|
191
|
+
readonly step: number;
|
|
192
|
+
readonly maxSteps: number;
|
|
193
|
+
readonly key: string;
|
|
194
|
+
tick(handle: WorldHandle, frameDt: number): number;
|
|
195
|
+
remainder(handle: WorldHandle): number;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
function createSchedule(systems: readonly System[], options: ScheduleOptions): Readonly<Schedule>;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`createSchedule` copies `systems` and validates `step`, `maxSteps`, `key` and
|
|
202
|
+
every system before returning; bad configuration throws `TypeError` or
|
|
203
|
+
`RangeError` naming `createSchedule`.
|
|
204
|
+
|
|
205
|
+
`tick(handle, frameDt)` adds `frameDt` to an accumulator stored in
|
|
206
|
+
`handle.scratch` under `key`, then, while at least one whole `step` is owed and
|
|
207
|
+
fewer than `maxSteps` have run, calls `advanceClock(handle, step)` and then
|
|
208
|
+
every system in order with `dt === step`. It returns the number of steps run.
|
|
209
|
+
Systems read `dt`, never the frame's dt, so the same elapsed time gives the same
|
|
210
|
+
run at any frame rate, provided no single tick owes more than `maxSteps` (the
|
|
211
|
+
excess is dropped, so one long frame and many short ones then differ).
|
|
212
|
+
|
|
213
|
+
- A `frameDt` that is negative, `NaN` or infinite counts as `0`. `0` is a valid
|
|
214
|
+
pause: no step runs and the clock does not move, even if a failed tick left
|
|
215
|
+
whole steps owed; those wait for the next frame that moves time.
|
|
216
|
+
- When `maxSteps` is reached with whole steps still owed, those steps are
|
|
217
|
+
dropped, not carried. The sub-step fraction is kept, so phase is preserved.
|
|
218
|
+
- Owed time saturates at the largest finite double instead of overflowing.
|
|
219
|
+
- A billionth of a step of tolerance absorbs float drift, so 432 frames of
|
|
220
|
+
1/144 s run the same 180 steps as 180 frames of 1/60 s.
|
|
221
|
+
- The accumulator lives in `scratch`, so it dies with the world and two
|
|
222
|
+
handles never share one. A `key` already holding something other than a
|
|
223
|
+
schedule accumulator throws `TypeError` before the clock moves. Two schedules
|
|
224
|
+
given the same explicit `key` share one accumulator, but an explicit `key`
|
|
225
|
+
may not equal another schedule's generated key, and a generated key skips any
|
|
226
|
+
explicit one.
|
|
227
|
+
- If a system throws, the step it was in is already spent (the clock moved) and
|
|
228
|
+
the steps not yet started stay owed, so a retry does not replay spent time.
|
|
229
|
+
|
|
230
|
+
`remainder(handle)` returns the seconds currently owed, in `[0, step)` between
|
|
231
|
+
ticks (`0` on a handle that has not ticked). `remainder / step` is the
|
|
232
|
+
interpolation alpha for a renderer drawing between sim states.
|
|
233
|
+
|
|
177
234
|
## `eventLog`
|
|
178
235
|
|
|
179
236
|
```ts
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -11,6 +11,7 @@ traits ───────────────> koota
|
|
|
11
11
|
seed ─────────────────> (leaf: built-in SHA-256)
|
|
12
12
|
rng ──────────────────> seedrandom + seed
|
|
13
13
|
world ────────────────> koota + rng
|
|
14
|
+
schedule ─────────────> world (advanceClock) + WorldHandle scratch
|
|
14
15
|
eventLog ─────────────> WorldHandle scratch
|
|
15
16
|
react ────────────────> koota/react + react + world (type only)
|
|
16
17
|
index ────────────────> public re-exports only (never react)
|
|
@@ -34,6 +35,12 @@ text) is a stable contract: changing it would change every derived world.
|
|
|
34
35
|
`world` owns allocation, teardown, time, and facade-owned persistence. It does
|
|
35
36
|
not own application entity serialization.
|
|
36
37
|
|
|
38
|
+
`schedule` turns a frame's dt into whole fixed steps. Its accumulator lives in
|
|
39
|
+
the handle's scratch map, so time owed dies with the run and two schedules or
|
|
40
|
+
two worlds never share it. It imports no renderer: a frame callback calls
|
|
41
|
+
`tick` and that is the whole integration. The step cap drops excess time rather
|
|
42
|
+
than carrying it, so one huge dt cannot cause a catch-up storm.
|
|
43
|
+
|
|
37
44
|
`eventLog` uses the handle's scratch map so event lifetime matches world
|
|
38
45
|
lifetime. It does not use a process-global queue.
|
|
39
46
|
|
|
@@ -59,6 +66,10 @@ it reaches `react` or `koota/react`.
|
|
|
59
66
|
8. A named substream's sequence depends only on its seed and name; creating or
|
|
60
67
|
consuming another substream never shifts it.
|
|
61
68
|
9. Only `koota-kit/react` imports React; every other entry stays React-free.
|
|
69
|
+
10. A schedule's systems only ever see `dt === step`; the same elapsed time
|
|
70
|
+
delivered at any frame rate runs the same steps in the same order, as long
|
|
71
|
+
as no single tick owes more than `maxSteps` (beyond that, the excess is
|
|
72
|
+
dropped by design, so a one-second frame and sixty 1/60 s frames differ).
|
|
62
73
|
|
|
63
74
|
## Performance choices
|
|
64
75
|
|
|
@@ -73,8 +84,10 @@ it reaches `react` or `koota/react`.
|
|
|
73
84
|
## Intentional limits
|
|
74
85
|
|
|
75
86
|
- No entity/trait serializer: applications need versioned domain schemas.
|
|
76
|
-
- No
|
|
77
|
-
|
|
87
|
+
- No system graph: `createSchedule` runs an ordered list once per fixed step,
|
|
88
|
+
and nothing more. There is no dependency resolution, parallelism, or
|
|
89
|
+
per-system rate; a system that wants a slower cadence counts steps itself.
|
|
90
|
+
Koota's queries/actions remain the execution model inside each system.
|
|
78
91
|
- No implicit derived keys: `deriveSeed` hashes only the parts the application
|
|
79
92
|
passes, so the application still owns stable domain keys.
|
|
80
93
|
- No multi-consumer queue semantics: publish separate logs when two systems
|
package/docs/contributing.md
CHANGED
|
@@ -20,8 +20,9 @@ Sourcey site.
|
|
|
20
20
|
Branch from `main`, make a focused Conventional Commit, open an upstream pull
|
|
21
21
|
request, and keep the branch current by merging `main` into it when necessary.
|
|
22
22
|
The protected path uses automated checks rather than a routine human approval;
|
|
23
|
-
merge commits preserve the constituent history
|
|
24
|
-
|
|
23
|
+
merge commits preserve the constituent history and GitHub release notes list
|
|
24
|
+
each merged pull request once. Do not hand-edit versions or `CHANGELOG.md`:
|
|
25
|
+
Release Please owns them.
|
|
25
26
|
|
|
26
27
|
Read the repository [contribution guide](https://github.com/jbcom/koota-kit/blob/main/CONTRIBUTING.md)
|
|
27
28
|
and [agent instructions](https://github.com/jbcom/koota-kit/blob/main/AGENTS.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koota-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Deterministic Koota simulation conventions: world lifecycle, dual RNG streams, safe object traits, and world-scoped event logs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,6 +63,16 @@
|
|
|
63
63
|
"default": "./dist/cjs/traits/index.cjs"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
+
"./schedule": {
|
|
67
|
+
"import": {
|
|
68
|
+
"types": "./dist/esm/schedule.d.ts",
|
|
69
|
+
"default": "./dist/esm/schedule.js"
|
|
70
|
+
},
|
|
71
|
+
"require": {
|
|
72
|
+
"types": "./dist/cjs/schedule.d.cts",
|
|
73
|
+
"default": "./dist/cjs/schedule.cjs"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
66
76
|
"./eventLog": {
|
|
67
77
|
"import": {
|
|
68
78
|
"types": "./dist/esm/eventLog.d.ts",
|
|
@@ -188,6 +198,9 @@
|
|
|
188
198
|
"traits": [
|
|
189
199
|
"./dist/esm/traits/index.d.ts"
|
|
190
200
|
],
|
|
201
|
+
"schedule": [
|
|
202
|
+
"./dist/esm/schedule.d.ts"
|
|
203
|
+
],
|
|
191
204
|
"eventLog": [
|
|
192
205
|
"./dist/esm/eventLog.d.ts"
|
|
193
206
|
],
|