@cosmicdrift/kumiko-framework 0.12.1 → 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 +6 -0
- package/package.json +2 -2
- package/src/engine/registry.ts +24 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.12.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
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
|
@@ -311,7 +311,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
311
311
|
featureMap.set(feature.name, feature);
|
|
312
312
|
|
|
313
313
|
// Entities: NOT prefixed — entity names must be globally unique
|
|
314
|
-
for (const [name, entity] of Object.entries(feature.entities)) {
|
|
314
|
+
for (const [name, entity] of Object.entries(feature.entities ?? {})) {
|
|
315
315
|
if (entityMap.has(name)) {
|
|
316
316
|
throw new Error(`Duplicate entity: "${name}" (registered by multiple features)`);
|
|
317
317
|
}
|
|
@@ -319,7 +319,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
// Relations: entityName (not prefixed)
|
|
322
|
-
for (const [entityName, rels] of Object.entries(feature.relations)) {
|
|
322
|
+
for (const [entityName, rels] of Object.entries(feature.relations ?? {})) {
|
|
323
323
|
const existing = relationMap.get(entityName) ?? {};
|
|
324
324
|
for (const [relName, relDef] of Object.entries(rels)) {
|
|
325
325
|
if (existing[relName]) {
|
|
@@ -333,7 +333,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
// Write handlers: scope:write:name
|
|
336
|
-
for (const [name, handler] of Object.entries(feature.writeHandlers)) {
|
|
336
|
+
for (const [name, handler] of Object.entries(feature.writeHandlers ?? {})) {
|
|
337
337
|
const qualified = qualify(feature.name, "write", name);
|
|
338
338
|
if (writeHandlerMap.has(qualified)) {
|
|
339
339
|
throw new Error(
|
|
@@ -345,7 +345,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
// Query handlers: scope:query:name
|
|
348
|
-
for (const [name, handler] of Object.entries(feature.queryHandlers)) {
|
|
348
|
+
for (const [name, handler] of Object.entries(feature.queryHandlers ?? {})) {
|
|
349
349
|
const qualified = qualify(feature.name, "query", name);
|
|
350
350
|
if (queryHandlerMap.has(qualified)) {
|
|
351
351
|
throw new Error(
|
|
@@ -357,7 +357,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
// Config keys: scope:config:name
|
|
360
|
-
for (const [key, keyDef] of Object.entries(feature.configKeys)) {
|
|
360
|
+
for (const [key, keyDef] of Object.entries(feature.configKeys ?? {})) {
|
|
361
361
|
const qualifiedKey = qualify(feature.name, "config", key);
|
|
362
362
|
if (configKeyMap.has(qualifiedKey)) {
|
|
363
363
|
throw new Error(
|
|
@@ -368,7 +368,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
// Jobs: scope:job:name
|
|
371
|
-
for (const [name, jobDef] of Object.entries(feature.jobs)) {
|
|
371
|
+
for (const [name, jobDef] of Object.entries(feature.jobs ?? {})) {
|
|
372
372
|
const qualifiedName = qualify(feature.name, "job", name);
|
|
373
373
|
if (jobMap.has(qualifiedName)) {
|
|
374
374
|
throw new Error(`Duplicate job: "${qualifiedName}" (registered by multiple features)`);
|
|
@@ -390,7 +390,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
// Notifications: scope:notify:name
|
|
393
|
-
for (const [name, notifDef] of Object.entries(feature.notifications)) {
|
|
393
|
+
for (const [name, notifDef] of Object.entries(feature.notifications ?? {})) {
|
|
394
394
|
const qualifiedName = qualify(feature.name, "notify", name);
|
|
395
395
|
notificationMap.set(qualifiedName, {
|
|
396
396
|
...notifDef,
|
|
@@ -404,13 +404,13 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
404
404
|
// in the FeatureDefinition and get stitched into the eventUpcasterMap
|
|
405
405
|
// below (after ALL features are ingested) so cross-feature validation has
|
|
406
406
|
// the complete picture.
|
|
407
|
-
for (const [eventName, eventDef] of Object.entries(feature.events)) {
|
|
407
|
+
for (const [eventName, eventDef] of Object.entries(feature.events ?? {})) {
|
|
408
408
|
const qualified = qualify(feature.name, "event", eventName);
|
|
409
409
|
eventMap.set(qualified, { ...eventDef, name: qualified });
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
// Translations prefixed with featureName: (i18next namespace convention)
|
|
413
|
-
for (const [key, value] of Object.entries(feature.translations)) {
|
|
413
|
+
for (const [key, value] of Object.entries(feature.translations ?? {})) {
|
|
414
414
|
mergedTranslations[`${feature.name}:${key}`] = value;
|
|
415
415
|
}
|
|
416
416
|
|
|
@@ -431,14 +431,16 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
431
431
|
mergeHookList(entityPostQueryHooks, feature.entityHooks.postQuery);
|
|
432
432
|
|
|
433
433
|
// F3 search-payload-extensions: per-entity contributors merged additively
|
|
434
|
-
for (const [entityName, contributors] of Object.entries(
|
|
434
|
+
for (const [entityName, contributors] of Object.entries(
|
|
435
|
+
feature.searchPayloadExtensions ?? {},
|
|
436
|
+
)) {
|
|
435
437
|
const existing = searchPayloadExtensions.get(entityName) ?? [];
|
|
436
438
|
for (const c of contributors) existing.push(c);
|
|
437
439
|
searchPayloadExtensions.set(entityName, existing);
|
|
438
440
|
}
|
|
439
441
|
|
|
440
442
|
// Registrar extensions: collect definitions and usages
|
|
441
|
-
for (const [extName, extDef] of Object.entries(feature.registrarExtensions)) {
|
|
443
|
+
for (const [extName, extDef] of Object.entries(feature.registrarExtensions ?? {})) {
|
|
442
444
|
if (extensionMap.has(extName)) {
|
|
443
445
|
throw new Error(
|
|
444
446
|
`Duplicate registrar extension: "${extName}" (registered by multiple features)`,
|
|
@@ -455,7 +457,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
455
457
|
// different shapes (labels/type) because the resulting fully qualified
|
|
456
458
|
// names differ, but same short+feature combo would already fail in
|
|
457
459
|
// defineFeature. This loop catches cross-feature/extension edge cases.
|
|
458
|
-
for (const [shortName, def] of Object.entries(feature.metrics)) {
|
|
460
|
+
for (const [shortName, def] of Object.entries(feature.metrics ?? {})) {
|
|
459
461
|
const fullName = buildMetricName(feature.name, shortName);
|
|
460
462
|
validateMetricName(fullName, def.type);
|
|
461
463
|
if (metricMap.has(fullName)) {
|
|
@@ -482,7 +484,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
482
484
|
|
|
483
485
|
// Projections: qualified by feature name. Build the source-entity index so
|
|
484
486
|
// the event-store-executor can fetch matching projections in O(1) per write.
|
|
485
|
-
for (const [projName, projDef] of Object.entries(feature.projections)) {
|
|
487
|
+
for (const [projName, projDef] of Object.entries(feature.projections ?? {})) {
|
|
486
488
|
const qualified = qualify(feature.name, "projection", projName);
|
|
487
489
|
if (projectionMap.has(qualified)) {
|
|
488
490
|
throw new Error(`Duplicate projection: "${qualified}" (registered by multiple features)`);
|
|
@@ -501,7 +503,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
501
503
|
// event-dispatcher. Namespace is shared with single-stream projections —
|
|
502
504
|
// defineFeature already catches name collisions inside one feature, but
|
|
503
505
|
// we also guard the cross-feature case here.
|
|
504
|
-
for (const [mspName, mspDef] of Object.entries(feature.multiStreamProjections)) {
|
|
506
|
+
for (const [mspName, mspDef] of Object.entries(feature.multiStreamProjections ?? {})) {
|
|
505
507
|
const qualified = qualify(feature.name, "projection", mspName);
|
|
506
508
|
if (projectionMap.has(qualified) || multiStreamProjectionMap.has(qualified)) {
|
|
507
509
|
throw new Error(`Duplicate projection: "${qualified}" (registered by multiple features)`);
|
|
@@ -530,7 +532,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
530
532
|
// event-stream binding to disambiguate). Reject cross-feature
|
|
531
533
|
// duplicates at boot so the dev-server doesn't race two CREATE TABLE
|
|
532
534
|
// statements that target the same physical table name.
|
|
533
|
-
for (const [rawName, rawDef] of Object.entries(feature.rawTables)) {
|
|
535
|
+
for (const [rawName, rawDef] of Object.entries(feature.rawTables ?? {})) {
|
|
534
536
|
const existing = rawTableMap.get(rawName);
|
|
535
537
|
if (existing) {
|
|
536
538
|
throw new Error(
|
|
@@ -562,7 +564,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
562
564
|
// qualified name includes the feature-prefix. The separate featureMap
|
|
563
565
|
// entry lets the nav resolver pause screens owned by disabled features
|
|
564
566
|
// in O(1) without walking every screen.
|
|
565
|
-
for (const [screenId, screenDef] of Object.entries(feature.screens)) {
|
|
567
|
+
for (const [screenId, screenDef] of Object.entries(feature.screens ?? {})) {
|
|
566
568
|
const qualified = qualify(feature.name, "screen", screenId);
|
|
567
569
|
// Stored version overwrites `id` with the qualified name so callers
|
|
568
570
|
// never need a reverse index (NavDef → qn) during tree-walking.
|
|
@@ -590,7 +592,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
590
592
|
// loop because `parent` refers to a qualified name that doesn't need
|
|
591
593
|
// resolution — just string equality with whatever's in the target
|
|
592
594
|
// entry's QN.
|
|
593
|
-
for (const [navId, navDef] of Object.entries(feature.navs)) {
|
|
595
|
+
for (const [navId, navDef] of Object.entries(feature.navs ?? {})) {
|
|
594
596
|
const qualified = qualify(feature.name, "nav", navId);
|
|
595
597
|
// See screens above — stored version carries the qualified id so
|
|
596
598
|
// resolveNavigation can recurse via getNavsByParent(child.id) without
|
|
@@ -613,7 +615,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
613
615
|
// member list. Doing it in two passes keeps cross-feature workspace
|
|
614
616
|
// refs valid — a nav entry can self-assign to a workspace whose feature
|
|
615
617
|
// hasn't been ingested yet.
|
|
616
|
-
for (const [wsId, wsDef] of Object.entries(feature.workspaces)) {
|
|
618
|
+
for (const [wsId, wsDef] of Object.entries(feature.workspaces ?? {})) {
|
|
617
619
|
const qualified = qualify(feature.name, "workspace", wsId);
|
|
618
620
|
const stored = { ...wsDef, id: qualified };
|
|
619
621
|
workspaceMap.set(qualified, stored);
|
|
@@ -678,7 +680,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
678
680
|
// in defineFeature via the "entityName:verb" colon convention).
|
|
679
681
|
// Must happen before extension processing since extension preSave hooks need entity mappings.
|
|
680
682
|
for (const feature of features) {
|
|
681
|
-
for (const [handlerName, entityName] of Object.entries(feature.handlerEntityMappings)) {
|
|
683
|
+
for (const [handlerName, entityName] of Object.entries(feature.handlerEntityMappings ?? {})) {
|
|
682
684
|
const writeQn = qualify(feature.name, "write", handlerName);
|
|
683
685
|
const queryQn = qualify(feature.name, "query", handlerName);
|
|
684
686
|
if (writeHandlerMap.has(writeQn)) {
|
|
@@ -790,7 +792,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
790
792
|
// vermeidet Kollisionen wenn jemand z.B. eine Cross-Aggregate-Projection
|
|
791
793
|
// mit Entity-Name registriert.
|
|
792
794
|
for (const feature of features) {
|
|
793
|
-
for (const [entityName, entity] of Object.entries(feature.entities)) {
|
|
795
|
+
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
794
796
|
const def = buildImplicitProjection(feature.name, entityName, entity, qualify);
|
|
795
797
|
if (projectionMap.has(def.name)) {
|
|
796
798
|
throw new Error(
|
|
@@ -889,7 +891,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
889
891
|
// feature (same feature in practice, but the check stays lax for future
|
|
890
892
|
// cross-feature event packs).
|
|
891
893
|
for (const feature of features) {
|
|
892
|
-
for (const [shortName, migrations] of Object.entries(feature.eventMigrations)) {
|
|
894
|
+
for (const [shortName, migrations] of Object.entries(feature.eventMigrations ?? {})) {
|
|
893
895
|
const qualified = qualify(feature.name, "event", shortName);
|
|
894
896
|
const eventDef = eventMap.get(qualified);
|
|
895
897
|
if (!eventDef) {
|
|
@@ -919,7 +921,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
919
921
|
const chainMap = new Map<number, EventUpcastFn>();
|
|
920
922
|
// Locate the feature that owns this event (to pick up its migrations).
|
|
921
923
|
for (const feature of features) {
|
|
922
|
-
for (const [shortName, migs] of Object.entries(feature.eventMigrations)) {
|
|
924
|
+
for (const [shortName, migs] of Object.entries(feature.eventMigrations ?? {})) {
|
|
923
925
|
const candidateQn = qualify(feature.name, "event", shortName);
|
|
924
926
|
if (candidateQn !== qualified) continue;
|
|
925
927
|
for (const m of migs) chainMap.set(m.fromVersion, m.transform);
|