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