@cosmicdrift/kumiko-framework 0.12.0 → 0.12.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 +12 -0
- package/package.json +2 -2
- package/src/engine/registry.ts +28 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @cosmicdrift/kumiko-framework
|
|
2
2
|
|
|
3
|
+
## 0.12.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 597de52: `createRegistry` guards all `Object.entries(feature.X)` against undefined slots — bun-bundled features can have optional slots dropped by minification. Pauschal-fix für alle 22 sites in registry.ts (entities, relations, writeHandlers, queryHandlers, configKeys, jobs, notifications, events, translations, searchPayloadExtensions, registrarExtensions, metrics, projections, multiStreamProjections, rawTables, screens, navs, workspaces, handlerEntityMappings, ...).
|
|
8
|
+
|
|
9
|
+
## 0.12.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- f2ad7c4: `mergeHookList` (the entity-hook variant) also tolerates undefined slots — same fix as `mergeHookListQualified` in 0.11.2 but for the second function. defineFeature leaves `entityHooks.postSave`/`preDelete`/`postDelete`/`postQuery` undefined when not declared; `createRegistry` crashed on `Object.entries(undefined)`.
|
|
14
|
+
|
|
3
15
|
## 0.12.0
|
|
4
16
|
|
|
5
17
|
## 0.11.2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
"zod": "^4.4.3"
|
|
173
173
|
},
|
|
174
174
|
"devDependencies": {
|
|
175
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.12.
|
|
175
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.12.2",
|
|
176
176
|
"@types/uuid": "^11.0.0",
|
|
177
177
|
"bun-types": "^1.3.13",
|
|
178
178
|
"drizzle-kit": "^0.31.10",
|
package/src/engine/registry.ts
CHANGED
|
@@ -271,8 +271,11 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
271
271
|
// bookkeeping — just append.
|
|
272
272
|
function mergeHookList<T>(
|
|
273
273
|
map: Map<string, T[]>,
|
|
274
|
-
source: Readonly<Record<string, readonly T[]
|
|
274
|
+
source: Readonly<Record<string, readonly T[]>> | undefined,
|
|
275
275
|
): void {
|
|
276
|
+
// skip: optionaler entityHook-slot — features ohne postSave/preDelete/
|
|
277
|
+
// postDelete/postQuery lassen das slot undefined.
|
|
278
|
+
if (!source) return;
|
|
276
279
|
for (const [name, fns] of Object.entries(source)) {
|
|
277
280
|
const existing = map.get(name) ?? [];
|
|
278
281
|
existing.push(...fns);
|
|
@@ -308,7 +311,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
308
311
|
featureMap.set(feature.name, feature);
|
|
309
312
|
|
|
310
313
|
// Entities: NOT prefixed — entity names must be globally unique
|
|
311
|
-
for (const [name, entity] of Object.entries(feature.entities)) {
|
|
314
|
+
for (const [name, entity] of Object.entries(feature.entities ?? {})) {
|
|
312
315
|
if (entityMap.has(name)) {
|
|
313
316
|
throw new Error(`Duplicate entity: "${name}" (registered by multiple features)`);
|
|
314
317
|
}
|
|
@@ -316,7 +319,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
// Relations: entityName (not prefixed)
|
|
319
|
-
for (const [entityName, rels] of Object.entries(feature.relations)) {
|
|
322
|
+
for (const [entityName, rels] of Object.entries(feature.relations ?? {})) {
|
|
320
323
|
const existing = relationMap.get(entityName) ?? {};
|
|
321
324
|
for (const [relName, relDef] of Object.entries(rels)) {
|
|
322
325
|
if (existing[relName]) {
|
|
@@ -330,7 +333,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
330
333
|
}
|
|
331
334
|
|
|
332
335
|
// Write handlers: scope:write:name
|
|
333
|
-
for (const [name, handler] of Object.entries(feature.writeHandlers)) {
|
|
336
|
+
for (const [name, handler] of Object.entries(feature.writeHandlers ?? {})) {
|
|
334
337
|
const qualified = qualify(feature.name, "write", name);
|
|
335
338
|
if (writeHandlerMap.has(qualified)) {
|
|
336
339
|
throw new Error(
|
|
@@ -342,7 +345,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
342
345
|
}
|
|
343
346
|
|
|
344
347
|
// Query handlers: scope:query:name
|
|
345
|
-
for (const [name, handler] of Object.entries(feature.queryHandlers)) {
|
|
348
|
+
for (const [name, handler] of Object.entries(feature.queryHandlers ?? {})) {
|
|
346
349
|
const qualified = qualify(feature.name, "query", name);
|
|
347
350
|
if (queryHandlerMap.has(qualified)) {
|
|
348
351
|
throw new Error(
|
|
@@ -354,7 +357,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
354
357
|
}
|
|
355
358
|
|
|
356
359
|
// Config keys: scope:config:name
|
|
357
|
-
for (const [key, keyDef] of Object.entries(feature.configKeys)) {
|
|
360
|
+
for (const [key, keyDef] of Object.entries(feature.configKeys ?? {})) {
|
|
358
361
|
const qualifiedKey = qualify(feature.name, "config", key);
|
|
359
362
|
if (configKeyMap.has(qualifiedKey)) {
|
|
360
363
|
throw new Error(
|
|
@@ -365,7 +368,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
365
368
|
}
|
|
366
369
|
|
|
367
370
|
// Jobs: scope:job:name
|
|
368
|
-
for (const [name, jobDef] of Object.entries(feature.jobs)) {
|
|
371
|
+
for (const [name, jobDef] of Object.entries(feature.jobs ?? {})) {
|
|
369
372
|
const qualifiedName = qualify(feature.name, "job", name);
|
|
370
373
|
if (jobMap.has(qualifiedName)) {
|
|
371
374
|
throw new Error(`Duplicate job: "${qualifiedName}" (registered by multiple features)`);
|
|
@@ -387,7 +390,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
387
390
|
}
|
|
388
391
|
|
|
389
392
|
// Notifications: scope:notify:name
|
|
390
|
-
for (const [name, notifDef] of Object.entries(feature.notifications)) {
|
|
393
|
+
for (const [name, notifDef] of Object.entries(feature.notifications ?? {})) {
|
|
391
394
|
const qualifiedName = qualify(feature.name, "notify", name);
|
|
392
395
|
notificationMap.set(qualifiedName, {
|
|
393
396
|
...notifDef,
|
|
@@ -401,13 +404,13 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
401
404
|
// in the FeatureDefinition and get stitched into the eventUpcasterMap
|
|
402
405
|
// below (after ALL features are ingested) so cross-feature validation has
|
|
403
406
|
// the complete picture.
|
|
404
|
-
for (const [eventName, eventDef] of Object.entries(feature.events)) {
|
|
407
|
+
for (const [eventName, eventDef] of Object.entries(feature.events ?? {})) {
|
|
405
408
|
const qualified = qualify(feature.name, "event", eventName);
|
|
406
409
|
eventMap.set(qualified, { ...eventDef, name: qualified });
|
|
407
410
|
}
|
|
408
411
|
|
|
409
412
|
// Translations prefixed with featureName: (i18next namespace convention)
|
|
410
|
-
for (const [key, value] of Object.entries(feature.translations)) {
|
|
413
|
+
for (const [key, value] of Object.entries(feature.translations ?? {})) {
|
|
411
414
|
mergedTranslations[`${feature.name}:${key}`] = value;
|
|
412
415
|
}
|
|
413
416
|
|
|
@@ -428,14 +431,16 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
428
431
|
mergeHookList(entityPostQueryHooks, feature.entityHooks.postQuery);
|
|
429
432
|
|
|
430
433
|
// F3 search-payload-extensions: per-entity contributors merged additively
|
|
431
|
-
for (const [entityName, contributors] of Object.entries(
|
|
434
|
+
for (const [entityName, contributors] of Object.entries(
|
|
435
|
+
feature.searchPayloadExtensions ?? {},
|
|
436
|
+
)) {
|
|
432
437
|
const existing = searchPayloadExtensions.get(entityName) ?? [];
|
|
433
438
|
for (const c of contributors) existing.push(c);
|
|
434
439
|
searchPayloadExtensions.set(entityName, existing);
|
|
435
440
|
}
|
|
436
441
|
|
|
437
442
|
// Registrar extensions: collect definitions and usages
|
|
438
|
-
for (const [extName, extDef] of Object.entries(feature.registrarExtensions)) {
|
|
443
|
+
for (const [extName, extDef] of Object.entries(feature.registrarExtensions ?? {})) {
|
|
439
444
|
if (extensionMap.has(extName)) {
|
|
440
445
|
throw new Error(
|
|
441
446
|
`Duplicate registrar extension: "${extName}" (registered by multiple features)`,
|
|
@@ -452,7 +457,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
452
457
|
// different shapes (labels/type) because the resulting fully qualified
|
|
453
458
|
// names differ, but same short+feature combo would already fail in
|
|
454
459
|
// defineFeature. This loop catches cross-feature/extension edge cases.
|
|
455
|
-
for (const [shortName, def] of Object.entries(feature.metrics)) {
|
|
460
|
+
for (const [shortName, def] of Object.entries(feature.metrics ?? {})) {
|
|
456
461
|
const fullName = buildMetricName(feature.name, shortName);
|
|
457
462
|
validateMetricName(fullName, def.type);
|
|
458
463
|
if (metricMap.has(fullName)) {
|
|
@@ -479,7 +484,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
479
484
|
|
|
480
485
|
// Projections: qualified by feature name. Build the source-entity index so
|
|
481
486
|
// the event-store-executor can fetch matching projections in O(1) per write.
|
|
482
|
-
for (const [projName, projDef] of Object.entries(feature.projections)) {
|
|
487
|
+
for (const [projName, projDef] of Object.entries(feature.projections ?? {})) {
|
|
483
488
|
const qualified = qualify(feature.name, "projection", projName);
|
|
484
489
|
if (projectionMap.has(qualified)) {
|
|
485
490
|
throw new Error(`Duplicate projection: "${qualified}" (registered by multiple features)`);
|
|
@@ -498,7 +503,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
498
503
|
// event-dispatcher. Namespace is shared with single-stream projections —
|
|
499
504
|
// defineFeature already catches name collisions inside one feature, but
|
|
500
505
|
// we also guard the cross-feature case here.
|
|
501
|
-
for (const [mspName, mspDef] of Object.entries(feature.multiStreamProjections)) {
|
|
506
|
+
for (const [mspName, mspDef] of Object.entries(feature.multiStreamProjections ?? {})) {
|
|
502
507
|
const qualified = qualify(feature.name, "projection", mspName);
|
|
503
508
|
if (projectionMap.has(qualified) || multiStreamProjectionMap.has(qualified)) {
|
|
504
509
|
throw new Error(`Duplicate projection: "${qualified}" (registered by multiple features)`);
|
|
@@ -527,7 +532,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
527
532
|
// event-stream binding to disambiguate). Reject cross-feature
|
|
528
533
|
// duplicates at boot so the dev-server doesn't race two CREATE TABLE
|
|
529
534
|
// statements that target the same physical table name.
|
|
530
|
-
for (const [rawName, rawDef] of Object.entries(feature.rawTables)) {
|
|
535
|
+
for (const [rawName, rawDef] of Object.entries(feature.rawTables ?? {})) {
|
|
531
536
|
const existing = rawTableMap.get(rawName);
|
|
532
537
|
if (existing) {
|
|
533
538
|
throw new Error(
|
|
@@ -559,7 +564,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
559
564
|
// qualified name includes the feature-prefix. The separate featureMap
|
|
560
565
|
// entry lets the nav resolver pause screens owned by disabled features
|
|
561
566
|
// in O(1) without walking every screen.
|
|
562
|
-
for (const [screenId, screenDef] of Object.entries(feature.screens)) {
|
|
567
|
+
for (const [screenId, screenDef] of Object.entries(feature.screens ?? {})) {
|
|
563
568
|
const qualified = qualify(feature.name, "screen", screenId);
|
|
564
569
|
// Stored version overwrites `id` with the qualified name so callers
|
|
565
570
|
// never need a reverse index (NavDef → qn) during tree-walking.
|
|
@@ -587,7 +592,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
587
592
|
// loop because `parent` refers to a qualified name that doesn't need
|
|
588
593
|
// resolution — just string equality with whatever's in the target
|
|
589
594
|
// entry's QN.
|
|
590
|
-
for (const [navId, navDef] of Object.entries(feature.navs)) {
|
|
595
|
+
for (const [navId, navDef] of Object.entries(feature.navs ?? {})) {
|
|
591
596
|
const qualified = qualify(feature.name, "nav", navId);
|
|
592
597
|
// See screens above — stored version carries the qualified id so
|
|
593
598
|
// resolveNavigation can recurse via getNavsByParent(child.id) without
|
|
@@ -610,7 +615,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
610
615
|
// member list. Doing it in two passes keeps cross-feature workspace
|
|
611
616
|
// refs valid — a nav entry can self-assign to a workspace whose feature
|
|
612
617
|
// hasn't been ingested yet.
|
|
613
|
-
for (const [wsId, wsDef] of Object.entries(feature.workspaces)) {
|
|
618
|
+
for (const [wsId, wsDef] of Object.entries(feature.workspaces ?? {})) {
|
|
614
619
|
const qualified = qualify(feature.name, "workspace", wsId);
|
|
615
620
|
const stored = { ...wsDef, id: qualified };
|
|
616
621
|
workspaceMap.set(qualified, stored);
|
|
@@ -675,7 +680,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
675
680
|
// in defineFeature via the "entityName:verb" colon convention).
|
|
676
681
|
// Must happen before extension processing since extension preSave hooks need entity mappings.
|
|
677
682
|
for (const feature of features) {
|
|
678
|
-
for (const [handlerName, entityName] of Object.entries(feature.handlerEntityMappings)) {
|
|
683
|
+
for (const [handlerName, entityName] of Object.entries(feature.handlerEntityMappings ?? {})) {
|
|
679
684
|
const writeQn = qualify(feature.name, "write", handlerName);
|
|
680
685
|
const queryQn = qualify(feature.name, "query", handlerName);
|
|
681
686
|
if (writeHandlerMap.has(writeQn)) {
|
|
@@ -787,7 +792,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
787
792
|
// vermeidet Kollisionen wenn jemand z.B. eine Cross-Aggregate-Projection
|
|
788
793
|
// mit Entity-Name registriert.
|
|
789
794
|
for (const feature of features) {
|
|
790
|
-
for (const [entityName, entity] of Object.entries(feature.entities)) {
|
|
795
|
+
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
791
796
|
const def = buildImplicitProjection(feature.name, entityName, entity, qualify);
|
|
792
797
|
if (projectionMap.has(def.name)) {
|
|
793
798
|
throw new Error(
|
|
@@ -886,7 +891,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
886
891
|
// feature (same feature in practice, but the check stays lax for future
|
|
887
892
|
// cross-feature event packs).
|
|
888
893
|
for (const feature of features) {
|
|
889
|
-
for (const [shortName, migrations] of Object.entries(feature.eventMigrations)) {
|
|
894
|
+
for (const [shortName, migrations] of Object.entries(feature.eventMigrations ?? {})) {
|
|
890
895
|
const qualified = qualify(feature.name, "event", shortName);
|
|
891
896
|
const eventDef = eventMap.get(qualified);
|
|
892
897
|
if (!eventDef) {
|
|
@@ -916,7 +921,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
916
921
|
const chainMap = new Map<number, EventUpcastFn>();
|
|
917
922
|
// Locate the feature that owns this event (to pick up its migrations).
|
|
918
923
|
for (const feature of features) {
|
|
919
|
-
for (const [shortName, migs] of Object.entries(feature.eventMigrations)) {
|
|
924
|
+
for (const [shortName, migs] of Object.entries(feature.eventMigrations ?? {})) {
|
|
920
925
|
const candidateQn = qualify(feature.name, "event", shortName);
|
|
921
926
|
if (candidateQn !== qualified) continue;
|
|
922
927
|
for (const m of migs) chainMap.set(m.fromVersion, m.transform);
|