@twoabove/cue 0.4.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/LICENSE +21 -0
- package/README.md +410 -0
- package/dist/api/create.d.ts +3 -0
- package/dist/api/create.d.ts.map +1 -0
- package/dist/api/create.js +106 -0
- package/dist/api/define.d.ts +40 -0
- package/dist/api/define.d.ts.map +1 -0
- package/dist/api/define.js +61 -0
- package/dist/api/index.d.ts +5 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +7 -0
- package/dist/api/supervisor.d.ts +22 -0
- package/dist/api/supervisor.d.ts.map +1 -0
- package/dist/api/supervisor.js +39 -0
- package/dist/core/Evolution.d.ts +11 -0
- package/dist/core/Evolution.d.ts.map +1 -0
- package/dist/core/Evolution.js +29 -0
- package/dist/core/StateKernel.d.ts +35 -0
- package/dist/core/StateKernel.d.ts.map +1 -0
- package/dist/core/StateKernel.js +113 -0
- package/dist/errors/index.d.ts +22 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +43 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/persistence/adapters/inMemory.d.ts +21 -0
- package/dist/persistence/adapters/inMemory.d.ts.map +1 -0
- package/dist/persistence/adapters/inMemory.js +41 -0
- package/dist/persistence/types.d.ts +28 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +1 -0
- package/dist/runtime/Entity.d.ts +42 -0
- package/dist/runtime/Entity.d.ts.map +1 -0
- package/dist/runtime/Entity.js +357 -0
- package/dist/runtime/EntityManager.d.ts +15 -0
- package/dist/runtime/EntityManager.d.ts.map +1 -0
- package/dist/runtime/EntityManager.js +46 -0
- package/dist/runtime/Mailbox.d.ts +5 -0
- package/dist/runtime/Mailbox.d.ts.map +1 -0
- package/dist/runtime/Mailbox.js +8 -0
- package/dist/runtime/Passivation.d.ts +12 -0
- package/dist/runtime/Passivation.d.ts.map +1 -0
- package/dist/runtime/Passivation.js +42 -0
- package/dist/runtime/Supervision.d.ts +4 -0
- package/dist/runtime/Supervision.d.ts.map +1 -0
- package/dist/runtime/Supervision.js +20 -0
- package/dist/serde/index.d.ts +7 -0
- package/dist/serde/index.d.ts.map +1 -0
- package/dist/serde/index.js +19 -0
- package/dist/types/internal.d.ts +10 -0
- package/dist/types/internal.d.ts.map +1 -0
- package/dist/types/internal.js +10 -0
- package/dist/types/public.d.ts +168 -0
- package/dist/types/public.d.ts.map +1 -0
- package/dist/types/public.js +1 -0
- package/dist/utils/clock.d.ts +5 -0
- package/dist/utils/clock.d.ts.map +1 -0
- package/dist/utils/clock.js +3 -0
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +2 -0
- package/dist/utils/invariants.d.ts +2 -0
- package/dist/utils/invariants.d.ts.map +1 -0
- package/dist/utils/invariants.js +5 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Seva Maltsev
|
|
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,410 @@
|
|
|
1
|
+
# Cue
|
|
2
|
+
|
|
3
|
+
**Durable stateful entities for TypeScript.**
|
|
4
|
+
|
|
5
|
+
Define entities with state, commands, and queries. Cue handles persistence, concurrency, and schema evolution—so you don't have to.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { define, create } from "cue";
|
|
9
|
+
|
|
10
|
+
const Counter = define("Counter")
|
|
11
|
+
.initialState(() => ({ count: 0 }))
|
|
12
|
+
.commands({
|
|
13
|
+
increment: (s, by = 1) => {
|
|
14
|
+
s.count += by;
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
.queries({
|
|
18
|
+
value: (s) => s.count,
|
|
19
|
+
})
|
|
20
|
+
.build();
|
|
21
|
+
|
|
22
|
+
const app = create({ definition: Counter });
|
|
23
|
+
const counter = app.get("my-counter");
|
|
24
|
+
|
|
25
|
+
await counter.send.increment(5);
|
|
26
|
+
console.log(await counter.read.value()); // 5
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why Cue?
|
|
30
|
+
|
|
31
|
+
- **Durable.** State survives restarts. Plug in Postgres, SQLite, or Redis—or run in-memory for tests.
|
|
32
|
+
- **Safe.** One operation at a time, always. No race conditions, no corrupted state.
|
|
33
|
+
- **Evolvable.** Schema changes are type-checked and automatic. Add a field, rename a property—old entities migrate on load.
|
|
34
|
+
- **Streamable.** Long-running operations yield progress in real-time.
|
|
35
|
+
- **Time-travel.** Query historical state at any point with full type safety.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install cue
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { create, define, memoryStore } from "cue";
|
|
47
|
+
|
|
48
|
+
// 1. Define your entity
|
|
49
|
+
const Character = define("Character")
|
|
50
|
+
.initialState(() => ({
|
|
51
|
+
level: 1,
|
|
52
|
+
hp: 100,
|
|
53
|
+
quests: new Set<string>(),
|
|
54
|
+
}))
|
|
55
|
+
.commands({
|
|
56
|
+
takeDamage: (state, amount: number) => {
|
|
57
|
+
state.hp -= amount;
|
|
58
|
+
if (state.hp <= 0) {
|
|
59
|
+
state.hp = 0;
|
|
60
|
+
return "You have been defeated!";
|
|
61
|
+
}
|
|
62
|
+
return `Ouch! HP is now ${state.hp}.`;
|
|
63
|
+
},
|
|
64
|
+
levelUp: async (state) => {
|
|
65
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
66
|
+
state.level++;
|
|
67
|
+
state.hp += 10;
|
|
68
|
+
return `Ding! Reached level ${state.level}!`;
|
|
69
|
+
},
|
|
70
|
+
// Streaming command using async generator
|
|
71
|
+
startQuest: async function* (state, quest: string) {
|
|
72
|
+
if (state.quests.has(quest)) {
|
|
73
|
+
yield { status: "already_on_quest" };
|
|
74
|
+
return "Quest already started.";
|
|
75
|
+
}
|
|
76
|
+
state.quests.add(quest);
|
|
77
|
+
yield { status: "started", quest };
|
|
78
|
+
await new Promise((res) => setTimeout(res, 100));
|
|
79
|
+
yield { status: "completed", quest };
|
|
80
|
+
state.quests.delete(quest);
|
|
81
|
+
return "Quest complete!";
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.queries({
|
|
85
|
+
getStats: (state) => ({
|
|
86
|
+
level: state.level,
|
|
87
|
+
hp: state.hp,
|
|
88
|
+
}),
|
|
89
|
+
})
|
|
90
|
+
.build();
|
|
91
|
+
|
|
92
|
+
// 2. Create an entity manager
|
|
93
|
+
const manager = create({ definition: Character });
|
|
94
|
+
|
|
95
|
+
// 3. Get a reference to a specific entity by its unique ID
|
|
96
|
+
const playerOne = manager.get("player-one");
|
|
97
|
+
|
|
98
|
+
// 4. Interact with the entity
|
|
99
|
+
const damageResult = await playerOne.send.takeDamage(10);
|
|
100
|
+
console.log(damageResult); // "Ouch! HP is now 90."
|
|
101
|
+
|
|
102
|
+
const levelUpMessage = await playerOne.send.levelUp();
|
|
103
|
+
console.log(levelUpMessage); // "Ding! Reached level 2!"
|
|
104
|
+
|
|
105
|
+
// Read-only queries
|
|
106
|
+
const stats = await playerOne.read.getStats();
|
|
107
|
+
console.log(stats); // { level: 2, hp: 110 }
|
|
108
|
+
|
|
109
|
+
// Stream progress from long-running commands
|
|
110
|
+
console.log("Starting a new quest...");
|
|
111
|
+
for await (const update of playerOne.stream.startQuest("The Lost Amulet")) {
|
|
112
|
+
console.log(`Quest update: ${update.status}`);
|
|
113
|
+
}
|
|
114
|
+
// > Quest update: started
|
|
115
|
+
// > Quest update: completed
|
|
116
|
+
|
|
117
|
+
// Snapshot for debugging
|
|
118
|
+
const snapshot = await playerOne.snapshot();
|
|
119
|
+
console.log(snapshot.state.quests); // Set(0) {}
|
|
120
|
+
|
|
121
|
+
// Shut down when done
|
|
122
|
+
await manager.stop();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Core Concepts
|
|
126
|
+
|
|
127
|
+
### Defining Entities
|
|
128
|
+
|
|
129
|
+
Use the fluent builder to define your entity's shape and behavior:
|
|
130
|
+
|
|
131
|
+
- `.initialState(() => ({...}))`: Sets the default state for new entities.
|
|
132
|
+
- `.commands({...})`: Methods that can modify state (sync, async, or generators for streaming).
|
|
133
|
+
- `.queries({...})`: Read-only methods for safe state access.
|
|
134
|
+
- `.evolve((prevState) => ({...}))`: Migration function for schema changes.
|
|
135
|
+
- `.persistence({...})`: Configure snapshotting behavior.
|
|
136
|
+
- `.build()`: Finalize the definition.
|
|
137
|
+
|
|
138
|
+
### Entity References
|
|
139
|
+
|
|
140
|
+
Get a handle to interact with a specific entity:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const ref = manager.get("entity-id");
|
|
144
|
+
|
|
145
|
+
await ref.send.someCommand(); // Execute a command (may modify state)
|
|
146
|
+
await ref.read.someQuery(); // Execute a query (read-only)
|
|
147
|
+
ref.stream.streamingCommand(); // Get AsyncIterable for streaming commands
|
|
148
|
+
await ref.snapshot(); // Get current state and version
|
|
149
|
+
await ref.stateAt(version); // Get historical state at a specific event version
|
|
150
|
+
await ref.stop(); // Manually stop this entity
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Persistence
|
|
154
|
+
|
|
155
|
+
Provide a store to persist state changes:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { create, memoryStore } from "cue";
|
|
159
|
+
|
|
160
|
+
const manager = create({
|
|
161
|
+
definition: Character,
|
|
162
|
+
store: new memoryStore(), // or your custom PersistenceAdapter
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Implement the `PersistenceAdapter` interface to plug in any database:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface PersistenceAdapter {
|
|
170
|
+
getEvents(entityId: string, fromVersion: bigint): Promise<EventRecord[]>;
|
|
171
|
+
commitEvent(entityId: string, version: bigint, data: string): Promise<void>;
|
|
172
|
+
getLatestSnapshot(entityId: string): Promise<SnapshotRecord | null>;
|
|
173
|
+
commitSnapshot(entityId: string, version: bigint, data: string): Promise<void>;
|
|
174
|
+
clearEntity?(entityId: string): Promise<void>;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Enable snapshotting to avoid replaying long event histories:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const Entity = define("Entity")
|
|
182
|
+
// ...
|
|
183
|
+
.persistence({ snapshotEvery: 100 }) // Snapshot every 100 versions
|
|
184
|
+
.build();
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Schema Evolution
|
|
188
|
+
|
|
189
|
+
Migrate entity state without downtime:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// V1
|
|
193
|
+
const Character = define("Character").initialState(() => ({
|
|
194
|
+
name: "Player",
|
|
195
|
+
hitPoints: 100,
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
// V2 with evolved schema
|
|
199
|
+
const Character = define("Character")
|
|
200
|
+
.initialState(() => ({ name: "Player", hitPoints: 100 }))
|
|
201
|
+
.evolve((v1) => ({
|
|
202
|
+
name: v1.name,
|
|
203
|
+
health: { current: v1.hitPoints, max: 100 },
|
|
204
|
+
mana: 50, // new field
|
|
205
|
+
}))
|
|
206
|
+
.commands({
|
|
207
|
+
takeDamage: (state, amount: number) => {
|
|
208
|
+
state.health.current -= amount;
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
.build();
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
When an old entity loads, Cue automatically runs upcasters to migrate its state.
|
|
215
|
+
|
|
216
|
+
## Temporal Scrubbing
|
|
217
|
+
|
|
218
|
+
Query historical state at any event version with full type safety:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { create, define, type HistoryOf, type VersionState } from "cue";
|
|
222
|
+
|
|
223
|
+
const Character = define("Character")
|
|
224
|
+
.initialState(() => ({ hp: 100 }))
|
|
225
|
+
.evolve((v1) => ({ health: { current: v1.hp, max: 100 } }))
|
|
226
|
+
.evolve((v2) => ({ ...v2, mana: 50 }))
|
|
227
|
+
.commands({
|
|
228
|
+
damage: (s, amount: number) => {
|
|
229
|
+
s.health.current -= amount;
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
.build();
|
|
233
|
+
|
|
234
|
+
const app = create({ definition: Character, store: new memoryStore() });
|
|
235
|
+
const hero = app.get("hero-1");
|
|
236
|
+
|
|
237
|
+
// Make some changes
|
|
238
|
+
await hero.send.damage(10);
|
|
239
|
+
await hero.send.damage(5);
|
|
240
|
+
|
|
241
|
+
// Query historical state
|
|
242
|
+
const atV1 = await hero.stateAt(1n);
|
|
243
|
+
console.log(atV1.schemaVersion); // 3
|
|
244
|
+
console.log(atV1.state.health.current); // 90
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Type-Safe History
|
|
248
|
+
|
|
249
|
+
The return type of `stateAt()` is a discriminated union of all schema versions:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// Extract the full history union
|
|
253
|
+
type CharacterHistory = HistoryOf<typeof Character>;
|
|
254
|
+
// | { schemaVersion: 1; state: { hp: number } }
|
|
255
|
+
// | { schemaVersion: 2; state: { health: { current: number; max: number } } }
|
|
256
|
+
// | { schemaVersion: 3; state: { health: { current: number; max: number }; mana: number } }
|
|
257
|
+
|
|
258
|
+
// Extract a specific version's state type
|
|
259
|
+
type CharacterV1 = VersionState<typeof Character, 1>; // { hp: number }
|
|
260
|
+
type CharacterV2 = VersionState<typeof Character, 2>; // { health: { current: number; max: number } }
|
|
261
|
+
|
|
262
|
+
// TypeScript narrows correctly in switch statements
|
|
263
|
+
function renderHistorical(h: CharacterHistory) {
|
|
264
|
+
switch (h.schemaVersion) {
|
|
265
|
+
case 1:
|
|
266
|
+
return `HP: ${h.state.hp}`;
|
|
267
|
+
case 2:
|
|
268
|
+
return `Health: ${h.state.health.current}/${h.state.health.max}`;
|
|
269
|
+
case 3:
|
|
270
|
+
return `Health: ${h.state.health.current}, Mana: ${h.state.mana}`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
This enables building debug UIs, audit logs, and replay systems with compile-time guarantees.
|
|
276
|
+
|
|
277
|
+
## Supervision
|
|
278
|
+
|
|
279
|
+
Handle errors declaratively:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { create, supervisor } from "cue";
|
|
283
|
+
|
|
284
|
+
const mySupervisor = supervisor({
|
|
285
|
+
stop: (_state, err) => err.name === "CatastrophicError",
|
|
286
|
+
reset: (_state, err) => err.name === "CorruptionError",
|
|
287
|
+
resume: (_state, err) => err.name === "ValidationError",
|
|
288
|
+
default: "resume",
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const manager = create({
|
|
292
|
+
definition: MyEntity,
|
|
293
|
+
supervisor: mySupervisor,
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Strategies:
|
|
298
|
+
|
|
299
|
+
- **resume**: Error bubbles up, entity stays healthy
|
|
300
|
+
- **reset**: Clear persisted history, reinitialize state
|
|
301
|
+
- **stop**: Entity enters failed state, rejects all future messages
|
|
302
|
+
|
|
303
|
+
## Passivation
|
|
304
|
+
|
|
305
|
+
Automatically evict idle entities to save memory:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
const manager = create({
|
|
309
|
+
definition: MyEntity,
|
|
310
|
+
store: myStore,
|
|
311
|
+
passivation: {
|
|
312
|
+
idleAfter: 5 * 60 * 1000, // Evict after 5 minutes
|
|
313
|
+
sweepInterval: 60 * 1000, // Check every minute
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Entities rehydrate transparently when accessed again.
|
|
319
|
+
|
|
320
|
+
## Metrics
|
|
321
|
+
|
|
322
|
+
Hook into entity lifecycle events:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
const manager = create({
|
|
326
|
+
definition: MyEntity,
|
|
327
|
+
metrics: {
|
|
328
|
+
onHydrate: (id) => console.log(`${id} hydrated`),
|
|
329
|
+
onEvict: (id) => console.log(`${id} evicted`),
|
|
330
|
+
onError: (id, error) => console.error(`${id} failed:`, error),
|
|
331
|
+
onSnapshot: (id, version) => console.log(`${id} snapshot at v${version}`),
|
|
332
|
+
onAfterCommit: (id, version, patch) => {
|
|
333
|
+
/* ... */
|
|
334
|
+
},
|
|
335
|
+
onBeforeSnapshot: (id, version) => {
|
|
336
|
+
/* ... */
|
|
337
|
+
},
|
|
338
|
+
onHydrateFallback: (id, reason) => {
|
|
339
|
+
/* ... */
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Built-in Serialization
|
|
346
|
+
|
|
347
|
+
Cue uses SuperJSON under the hood, so `Date`, `Map`, `Set`, `BigInt`, and `RegExp` values survive serialization automatically.
|
|
348
|
+
|
|
349
|
+
## How It Works
|
|
350
|
+
|
|
351
|
+
Under the hood, Cue uses **event sourcing**. Every state change is recorded as a patch. Entities rebuild from their history on load, with periodic snapshots for efficiency.
|
|
352
|
+
|
|
353
|
+
But you don't need to think about events. Write mutations directly—Cue captures them automatically via Immer.
|
|
354
|
+
|
|
355
|
+
## API Reference
|
|
356
|
+
|
|
357
|
+
### `define(name)`
|
|
358
|
+
|
|
359
|
+
Creates a new entity definition builder.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
const Entity = define("Entity")
|
|
363
|
+
.initialState(() => ({ ... }))
|
|
364
|
+
.commands({ ... })
|
|
365
|
+
.queries({ ... })
|
|
366
|
+
.evolve((prev) => ({ ... }))
|
|
367
|
+
.persistence({ snapshotEvery: 100 })
|
|
368
|
+
.build();
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### `create(config)`
|
|
372
|
+
|
|
373
|
+
Creates an entity manager.
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
const manager = create({
|
|
377
|
+
definition: Entity, // Required: entity definition
|
|
378
|
+
store: new memoryStore(), // Optional: persistence adapter
|
|
379
|
+
supervisor: mySupervisor, // Optional: error handling
|
|
380
|
+
passivation: { ... }, // Optional: idle eviction
|
|
381
|
+
metrics: { ... }, // Optional: lifecycle hooks
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### `EntityRef`
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const ref = manager.get("id");
|
|
389
|
+
|
|
390
|
+
ref.send.command(...args); // Execute command, returns Promise
|
|
391
|
+
ref.read.query(...args); // Execute query, returns Promise
|
|
392
|
+
ref.stream.command(...args); // Returns AsyncIterable for streaming commands
|
|
393
|
+
ref.snapshot(); // Returns Promise<{ state, version }>
|
|
394
|
+
ref.stateAt(eventVersion); // Returns Promise<{ schemaVersion, state }>
|
|
395
|
+
ref.stop(); // Stop and release this entity
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Type Utilities
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { HistoryOf, VersionState, StateOf } from "cue";
|
|
402
|
+
|
|
403
|
+
type History = HistoryOf<typeof Entity>; // Discriminated union of all versions
|
|
404
|
+
type V2State = VersionState<typeof Entity, 2>; // State type at schema version 2
|
|
405
|
+
type Current = StateOf<typeof Entity>; // Current state type
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## License
|
|
409
|
+
|
|
410
|
+
MIT License
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/api/create.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACb,mBAAmB,EAOpB,MAAM,iBAAiB,CAAC;AAEzB,wBAAgB,MAAM,CAAC,IAAI,SAAS,mBAAmB,EACrD,MAAM,EAAE,mBAAmB,CAAC,IAAI,CAAC,GAChC,aAAa,CAAC,IAAI,CAAC,CAkJrB"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ManagerShutdownError } from "../errors";
|
|
2
|
+
import { RuntimeEntityManager } from "../runtime/EntityManager";
|
|
3
|
+
export function create(config) {
|
|
4
|
+
const manager = new RuntimeEntityManager(config);
|
|
5
|
+
const entries = new Map();
|
|
6
|
+
function makeSendProxy(id, getEntity, setEntity) {
|
|
7
|
+
return new Proxy({}, {
|
|
8
|
+
get(_t, prop) {
|
|
9
|
+
return async (...args) => {
|
|
10
|
+
if (manager.isShutdown) {
|
|
11
|
+
throw new ManagerShutdownError("EntityManager is shut down. Cannot interact with entities.");
|
|
12
|
+
}
|
|
13
|
+
let entity = getEntity();
|
|
14
|
+
if (entity.isShutdown) {
|
|
15
|
+
entity = manager.getEntity(id);
|
|
16
|
+
setEntity(entity);
|
|
17
|
+
}
|
|
18
|
+
return entity.tell(prop, args);
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function makeReadProxy(id, getEntity, setEntity) {
|
|
24
|
+
return new Proxy({}, {
|
|
25
|
+
get(_t, prop) {
|
|
26
|
+
return async (...args) => {
|
|
27
|
+
if (manager.isShutdown) {
|
|
28
|
+
throw new ManagerShutdownError("EntityManager is shut down. Cannot interact with entities.");
|
|
29
|
+
}
|
|
30
|
+
let entity = getEntity();
|
|
31
|
+
if (entity.isShutdown) {
|
|
32
|
+
entity = manager.getEntity(id);
|
|
33
|
+
setEntity(entity);
|
|
34
|
+
}
|
|
35
|
+
return entity.ask(prop, args);
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function makeStreamProxy(id, getEntity, setEntity) {
|
|
41
|
+
return new Proxy({}, {
|
|
42
|
+
get(_t, prop) {
|
|
43
|
+
return (...args) => {
|
|
44
|
+
if (manager.isShutdown) {
|
|
45
|
+
throw new ManagerShutdownError("EntityManager is shut down. Cannot interact with entities.");
|
|
46
|
+
}
|
|
47
|
+
let entity = getEntity();
|
|
48
|
+
if (entity.isShutdown) {
|
|
49
|
+
entity = manager.getEntity(id);
|
|
50
|
+
setEntity(entity);
|
|
51
|
+
}
|
|
52
|
+
return entity.stream(prop, args);
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
get(id) {
|
|
59
|
+
const existing = entries.get(id);
|
|
60
|
+
if (existing && !existing.entity.isFailed && !existing.entity.isShutdown) {
|
|
61
|
+
return existing.ref;
|
|
62
|
+
}
|
|
63
|
+
let entity = manager.getEntity(id);
|
|
64
|
+
let entry;
|
|
65
|
+
const getEntity = () => entity;
|
|
66
|
+
const setEntity = (e) => {
|
|
67
|
+
entity = e;
|
|
68
|
+
entry.entity = e;
|
|
69
|
+
};
|
|
70
|
+
const ref = {
|
|
71
|
+
send: makeSendProxy(id, getEntity, setEntity),
|
|
72
|
+
read: makeReadProxy(id, getEntity, setEntity),
|
|
73
|
+
stream: makeStreamProxy(id, getEntity, setEntity),
|
|
74
|
+
snapshot: async () => {
|
|
75
|
+
if (manager.isShutdown) {
|
|
76
|
+
throw new ManagerShutdownError("EntityManager is shut down. Cannot interact with entities.");
|
|
77
|
+
}
|
|
78
|
+
if (entity.isShutdown) {
|
|
79
|
+
entity = manager.getEntity(id);
|
|
80
|
+
setEntity(entity);
|
|
81
|
+
}
|
|
82
|
+
return entity.inspect();
|
|
83
|
+
},
|
|
84
|
+
stateAt: async (eventVersion) => {
|
|
85
|
+
if (manager.isShutdown) {
|
|
86
|
+
throw new ManagerShutdownError("EntityManager is shut down. Cannot interact with entities.");
|
|
87
|
+
}
|
|
88
|
+
if (entity.isShutdown) {
|
|
89
|
+
entity = manager.getEntity(id);
|
|
90
|
+
setEntity(entity);
|
|
91
|
+
}
|
|
92
|
+
return entity.stateAt(eventVersion);
|
|
93
|
+
},
|
|
94
|
+
stop: async () => {
|
|
95
|
+
await entity.terminate();
|
|
96
|
+
manager.removeEntity(id);
|
|
97
|
+
entries.delete(id);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
entry = { ref, entity };
|
|
101
|
+
entries.set(id, entry);
|
|
102
|
+
return ref;
|
|
103
|
+
},
|
|
104
|
+
stop: () => manager.terminate(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { _handlers, _initialStateFn, _messages, _name, _persistence, _state, _tag, _upcasters, _versions } from "../types/internal";
|
|
2
|
+
import type { AnyHandler, CreateMessageMap, Draft, HandlerEntry } from "../types/public";
|
|
3
|
+
declare class DefinitionBuilder<TName extends string, TState extends object, TCommands extends Record<string, AnyHandler> = Record<string, never>, TQueries extends Record<string, AnyHandler> = Record<string, never>, TVersions extends object[] = [TState]> {
|
|
4
|
+
private readonly name;
|
|
5
|
+
private readonly initialStateFn;
|
|
6
|
+
private readonly upcasters;
|
|
7
|
+
private readonly commandsConfig;
|
|
8
|
+
private readonly queriesConfig;
|
|
9
|
+
private persistenceConfig?;
|
|
10
|
+
constructor(name: TName, initialStateFn: () => TVersions[0], upcasters: ReadonlyArray<(prevState: any) => any>, commandsConfig: TCommands, queriesConfig: TQueries, persistenceConfig?: {
|
|
11
|
+
snapshotEvery?: number;
|
|
12
|
+
} | undefined);
|
|
13
|
+
evolve<TNewState extends object>(upcaster: (prevState: TState) => TNewState): DefinitionBuilder<TName, TNewState, Record<string, never>, Record<string, never>, [
|
|
14
|
+
...TVersions,
|
|
15
|
+
TNewState
|
|
16
|
+
]>;
|
|
17
|
+
commands<const C extends Record<string, (state: Draft<TState>, ...args: any[]) => unknown>>(newCommands: C): DefinitionBuilder<TName, TState, TCommands & C, TQueries, TVersions>;
|
|
18
|
+
queries<const Q extends Record<string, (state: Readonly<TState>, ...args: any[]) => unknown>>(newQueries: Q): DefinitionBuilder<TName, TState, TCommands, TQueries & Q, TVersions>;
|
|
19
|
+
persistence(config: {
|
|
20
|
+
snapshotEvery?: number;
|
|
21
|
+
}): this;
|
|
22
|
+
build(): {
|
|
23
|
+
[_persistence]?: {
|
|
24
|
+
snapshotEvery?: number;
|
|
25
|
+
};
|
|
26
|
+
[_name]: TName;
|
|
27
|
+
[_state]: TState;
|
|
28
|
+
[_messages]: CreateMessageMap<TCommands & {}, TQueries & {}>;
|
|
29
|
+
[_tag]: "EntityDefinition";
|
|
30
|
+
[_initialStateFn]: () => TVersions[0];
|
|
31
|
+
[_upcasters]: readonly ((prevState: any) => any)[];
|
|
32
|
+
[_handlers]: Record<string, HandlerEntry>;
|
|
33
|
+
[_versions]: TVersions;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare function define<TName extends string>(name: TName): {
|
|
37
|
+
initialState<TState extends object>(initialStateFn: () => TState): DefinitionBuilder<TName, TState, Record<string, never>, Record<string, never>, [TState]>;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/api/define.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,eAAe,EACf,SAAS,EACT,KAAK,EACL,YAAY,EACZ,MAAM,EACN,IAAI,EACJ,UAAU,EACV,SAAS,EACV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,KAAK,EACL,YAAY,EACb,MAAM,iBAAiB,CAAC;AAKzB,cAAM,iBAAiB,CACrB,KAAK,SAAS,MAAM,EACpB,MAAM,SAAS,MAAM,EACrB,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACpE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACnE,SAAS,SAAS,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC;IAGnC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAE/B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,iBAAiB,CAAC;gBANT,IAAI,EAAE,KAAK,EACX,cAAc,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC,EAElC,SAAS,EAAE,aAAa,CAAC,CAAC,SAAS,EAAE,GAAG,KAAK,GAAG,CAAC,EACjD,cAAc,EAAE,SAAS,EACzB,aAAa,EAAE,QAAQ,EAChC,iBAAiB,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,YAAA;IAGjD,MAAM,CAAC,SAAS,SAAS,MAAM,EACpC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,SAAS,GACzC,iBAAiB,CAClB,KAAK,EACL,SAAS,EACT,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACrB,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACrB;QAAC,GAAG,SAAS;QAAE,SAAS;KAAC,CAC1B;IAiBM,QAAQ,CACb,KAAK,CAAC,CAAC,SAAS,MAAM,CACpB,MAAM,EAEN,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAClD,EACD,WAAW,EAAE,CAAC;IAiBT,OAAO,CACZ,KAAK,CAAC,CAAC,SAAS,MAAM,CACpB,MAAM,EAEN,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CACrD,EACD,UAAU,EAAE,CAAC;IAiBR,WAAW,CAAC,MAAM,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE;IAK9C,KAAK;;4BA/EoC,MAAM;;;kBA0FrB,MAAM;qBACH,gBAAgB,CAC9C,SAAS,GAAG,EAAE,EACd,QAAQ,GAAG,EAAE,CACd;;iCAnGoC,SAAS,CAAC,CAAC,CAAC;4CAEG,GAAG,KAAK,GAAG;;qBAsG/B,SAAS;;CAK9C;AAED,wBAAgB,MAAM,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,KAAK;iBAEvC,MAAM,SAAS,MAAM,kBAAkB,MAAM,MAAM;EAUnE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { _handlers, _initialStateFn, _messages, _name, _persistence, _state, _tag, _upcasters, _versions, } from "../types/internal";
|
|
2
|
+
const IsAsyncGen = (fn) => Object.prototype.toString.call(fn) === "[object AsyncGeneratorFunction]";
|
|
3
|
+
class DefinitionBuilder {
|
|
4
|
+
name;
|
|
5
|
+
initialStateFn;
|
|
6
|
+
upcasters;
|
|
7
|
+
commandsConfig;
|
|
8
|
+
queriesConfig;
|
|
9
|
+
persistenceConfig;
|
|
10
|
+
constructor(name, initialStateFn,
|
|
11
|
+
// biome-ignore lint/suspicious/noExplicitAny: Upcasters must handle any previous state shape
|
|
12
|
+
upcasters, commandsConfig, queriesConfig, persistenceConfig) {
|
|
13
|
+
this.name = name;
|
|
14
|
+
this.initialStateFn = initialStateFn;
|
|
15
|
+
this.upcasters = upcasters;
|
|
16
|
+
this.commandsConfig = commandsConfig;
|
|
17
|
+
this.queriesConfig = queriesConfig;
|
|
18
|
+
this.persistenceConfig = persistenceConfig;
|
|
19
|
+
}
|
|
20
|
+
evolve(upcaster) {
|
|
21
|
+
return new DefinitionBuilder(this.name, this.initialStateFn, [...this.upcasters, upcaster], {}, {}, this.persistenceConfig);
|
|
22
|
+
}
|
|
23
|
+
commands(newCommands) {
|
|
24
|
+
return new DefinitionBuilder(this.name, this.initialStateFn, this.upcasters, { ...this.commandsConfig, ...newCommands }, this.queriesConfig, this.persistenceConfig);
|
|
25
|
+
}
|
|
26
|
+
queries(newQueries) {
|
|
27
|
+
return new DefinitionBuilder(this.name, this.initialStateFn, this.upcasters, this.commandsConfig, { ...this.queriesConfig, ...newQueries }, this.persistenceConfig);
|
|
28
|
+
}
|
|
29
|
+
persistence(config) {
|
|
30
|
+
this.persistenceConfig = config;
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
build() {
|
|
34
|
+
const handlers = {};
|
|
35
|
+
for (const [key, fn] of Object.entries(this.commandsConfig)) {
|
|
36
|
+
handlers[key] = { type: IsAsyncGen(fn) ? "stream" : "command", fn };
|
|
37
|
+
}
|
|
38
|
+
for (const [key, fn] of Object.entries(this.queriesConfig)) {
|
|
39
|
+
handlers[key] = { type: "query", fn };
|
|
40
|
+
}
|
|
41
|
+
const definition = {
|
|
42
|
+
[_name]: this.name,
|
|
43
|
+
[_state]: null,
|
|
44
|
+
[_messages]: null,
|
|
45
|
+
[_tag]: "EntityDefinition",
|
|
46
|
+
[_initialStateFn]: this.initialStateFn,
|
|
47
|
+
[_upcasters]: this.upcasters,
|
|
48
|
+
[_handlers]: handlers,
|
|
49
|
+
[_versions]: null,
|
|
50
|
+
...(this.persistenceConfig && { [_persistence]: this.persistenceConfig }),
|
|
51
|
+
};
|
|
52
|
+
return definition;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function define(name) {
|
|
56
|
+
return {
|
|
57
|
+
initialState(initialStateFn) {
|
|
58
|
+
return new DefinitionBuilder(name, initialStateFn, [], {}, {});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAC9E,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { enableMapSet, enablePatches } from "immer";
|
|
2
|
+
enableMapSet();
|
|
3
|
+
enablePatches();
|
|
4
|
+
export { InMemoryPersistenceAdapter } from "../persistence/adapters/inMemory";
|
|
5
|
+
export { create } from "./create";
|
|
6
|
+
export { define } from "./define";
|
|
7
|
+
export { supervisor } from "./supervisor";
|