@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 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.1",
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.1",
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",
@@ -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(feature.searchPayloadExtensions)) {
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);