@ekairos/domain 1.22.36-beta.development.0 → 1.22.36

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.
Files changed (46) hide show
  1. package/README.md +177 -18
  2. package/SKILL.md +56 -0
  3. package/dist/cli/bin.d.ts +1 -1
  4. package/dist/cli/bin.d.ts.map +1 -1
  5. package/dist/cli/bin.js +141 -20
  6. package/dist/cli/bin.js.map +1 -1
  7. package/dist/cli/create-app.d.ts +34 -1
  8. package/dist/cli/create-app.d.ts.map +1 -1
  9. package/dist/cli/create-app.js +2138 -507
  10. package/dist/cli/create-app.js.map +1 -1
  11. package/dist/cli/http.d.ts.map +1 -1
  12. package/dist/cli/http.js +1 -5
  13. package/dist/cli/http.js.map +1 -1
  14. package/dist/cli/server.d.ts.map +1 -1
  15. package/dist/cli/server.js +5 -2
  16. package/dist/cli/server.js.map +1 -1
  17. package/dist/cli/types.d.ts +1 -0
  18. package/dist/cli/types.d.ts.map +1 -1
  19. package/dist/cli/ui.d.ts.map +1 -1
  20. package/dist/cli/ui.js +2 -0
  21. package/dist/cli/ui.js.map +1 -1
  22. package/dist/context.test-runner.js +3 -1
  23. package/dist/context.test-runner.js.map +1 -1
  24. package/dist/domain-doc.d.ts +2 -0
  25. package/dist/domain-doc.d.ts.map +1 -1
  26. package/dist/domain-doc.js +14 -0
  27. package/dist/domain-doc.js.map +1 -1
  28. package/dist/index.d.ts +194 -53
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +313 -137
  31. package/dist/index.js.map +1 -1
  32. package/dist/next.d.ts.map +1 -1
  33. package/dist/next.js +3 -2
  34. package/dist/next.js.map +1 -1
  35. package/dist/runtime-handle.d.ts +14 -3
  36. package/dist/runtime-handle.d.ts.map +1 -1
  37. package/dist/runtime-handle.js +2 -0
  38. package/dist/runtime-handle.js.map +1 -1
  39. package/dist/runtime-step.d.ts.map +1 -1
  40. package/dist/runtime-step.js +2 -0
  41. package/dist/runtime-step.js.map +1 -1
  42. package/dist/runtime.d.ts +7 -7
  43. package/dist/runtime.d.ts.map +1 -1
  44. package/dist/runtime.js +11 -8
  45. package/dist/runtime.js.map +1 -1
  46. package/package.json +15 -11
package/dist/index.js CHANGED
@@ -1,23 +1,25 @@
1
1
  import { i } from "@instantdb/core";
2
- import { filterDomainDoc, parseDomainDoc, renderDomainDoc, } from "./domain-doc.js";
3
- export { parseDomainDoc, renderDomainDoc, filterDomainDoc, } from "./domain-doc.js";
2
+ import { z } from "zod";
4
3
  export { EkairosRuntime, } from "./runtime-handle.js";
5
4
  let domainDocLoader = null;
5
+ let domainDocNormalizer = null;
6
6
  export function configureDomainDocLoader(loader) {
7
7
  domainDocLoader = loader ?? null;
8
8
  }
9
+ export function configureDomainDocNormalizer(normalizer) {
10
+ domainDocNormalizer = normalizer ?? null;
11
+ }
9
12
  const EKAIROS_META = Symbol.for("@ekairos/domain/meta");
10
13
  const EKAIROS_ACTIONS = Symbol.for("@ekairos/domain/actions");
11
14
  const EKAIROS_ACTION_MAP = Symbol.for("@ekairos/domain/action-map");
12
15
  const EKAIROS_ACTION_BINDING = Symbol.for("@ekairos/domain/action-binding");
13
- const EKAIROS_ACTION_STACK = Symbol.for("@ekairos/domain/action-stack");
14
16
  function getMeta(source) {
15
- if (!source || typeof source !== "object")
17
+ if (!isObjectLike(source))
16
18
  return null;
17
19
  return source[EKAIROS_META] ?? null;
18
20
  }
19
21
  function getActionBinding(source) {
20
- if (!source || typeof source !== "object")
22
+ if (!isObjectLike(source))
21
23
  return null;
22
24
  const binding = source[EKAIROS_ACTION_BINDING];
23
25
  if (!binding || typeof binding !== "object")
@@ -50,7 +52,7 @@ function bindAction(action, params) {
50
52
  return registration;
51
53
  }
52
54
  function getStoredActions(source) {
53
- if (!source || typeof source !== "object")
55
+ if (!isObjectLike(source))
54
56
  return [];
55
57
  const raw = source[EKAIROS_ACTIONS];
56
58
  if (!Array.isArray(raw))
@@ -61,7 +63,7 @@ function getStoredActions(source) {
61
63
  typeof entry.execute === "function");
62
64
  }
63
65
  function getStoredActionMap(source) {
64
- if (!source || typeof source !== "object")
66
+ if (!isObjectLike(source))
65
67
  return {};
66
68
  const raw = source[EKAIROS_ACTION_MAP];
67
69
  if (!raw || typeof raw !== "object")
@@ -69,7 +71,7 @@ function getStoredActionMap(source) {
69
71
  return raw;
70
72
  }
71
73
  function setStoredActions(source, actions) {
72
- if (!source || typeof source !== "object")
74
+ if (!isObjectLike(source))
73
75
  return;
74
76
  const frozenActions = Object.freeze([...actions]);
75
77
  Object.defineProperty(source, EKAIROS_ACTIONS, {
@@ -80,7 +82,7 @@ function setStoredActions(source, actions) {
80
82
  });
81
83
  }
82
84
  function setStoredActionMap(source, actionMap) {
83
- if (!source || typeof source !== "object")
85
+ if (!isObjectLike(source))
84
86
  return;
85
87
  Object.defineProperty(source, EKAIROS_ACTION_MAP, {
86
88
  value: Object.freeze({ ...actionMap }),
@@ -89,29 +91,13 @@ function setStoredActionMap(source, actionMap) {
89
91
  writable: true,
90
92
  });
91
93
  }
92
- function readRuntimeActionStack(runtime) {
93
- if (!runtime || typeof runtime !== "object")
94
- return [];
95
- const stack = runtime[EKAIROS_ACTION_STACK];
96
- return Array.isArray(stack) ? [...stack] : [];
97
- }
98
- function cloneRuntimeWithActionStack(runtime, stack) {
99
- if (!runtime || typeof runtime !== "object")
100
- return runtime;
101
- const scoped = Object.assign(Object.create(Object.getPrototypeOf(runtime)), runtime);
102
- Object.defineProperty(scoped, EKAIROS_ACTION_STACK, {
103
- value: [...stack],
104
- enumerable: false,
105
- configurable: true,
106
- writable: true,
107
- });
108
- return scoped;
109
- }
110
94
  function normalizeActionLike(value, params) {
111
- const action = typeof value === "function"
112
- ? { execute: value }
113
- : value;
114
- if (!action || typeof action !== "object" || typeof action.execute !== "function") {
95
+ const action = value;
96
+ if (!action ||
97
+ typeof action !== "object" ||
98
+ typeof action.execute !== "function" ||
99
+ !action.input ||
100
+ !action.output) {
115
101
  throw new Error(`Invalid domain action definition: ${params.fallbackName}`);
116
102
  }
117
103
  const explicitName = typeof action.name === "string" ? action.name.trim() : "";
@@ -241,9 +227,22 @@ function listKeys(value) {
241
227
  return [];
242
228
  return Object.keys(value).filter((key) => !key.startsWith("$"));
243
229
  }
230
+ function isObjectLike(value) {
231
+ return !!value && (typeof value === "object" || typeof value === "function");
232
+ }
233
+ function isMaterializedDomainSource(value) {
234
+ if (!isObjectLike(value))
235
+ return false;
236
+ const source = value;
237
+ return (typeof source.instantSchema === "function" ||
238
+ typeof source.toInstantSchema === "function" ||
239
+ ("entities" in source && "links" in source && "rooms" in source));
240
+ }
244
241
  function resolveSchema(source) {
245
242
  if (!source)
246
243
  return null;
244
+ if (typeof source.instantSchema === "function")
245
+ return source.instantSchema();
247
246
  if (typeof source.toInstantSchema === "function")
248
247
  return source.toInstantSchema();
249
248
  if (typeof source.schema === "function")
@@ -280,6 +279,48 @@ function assertSchemaIncludes(fullSchema, requiredSchema) {
280
279
  throw new Error(`ConcreteDomain: schema is missing required keys (${parts.join(" | ")})`);
281
280
  }
282
281
  }
282
+ function collectTransitiveDomainNames(source, seen = new Set()) {
283
+ const names = new Set();
284
+ if (!isObjectLike(source))
285
+ return names;
286
+ if (seen.has(source))
287
+ return names;
288
+ seen.add(source);
289
+ const meta = getMeta(source);
290
+ if (!meta)
291
+ return names;
292
+ if (meta.name)
293
+ names.add(meta.name);
294
+ for (const getter of meta.includes ?? []) {
295
+ if (!getter)
296
+ continue;
297
+ let child = null;
298
+ try {
299
+ child = getter();
300
+ }
301
+ catch {
302
+ child = null;
303
+ }
304
+ for (const name of collectTransitiveDomainNames(child, seen)) {
305
+ names.add(name);
306
+ }
307
+ }
308
+ return names;
309
+ }
310
+ function assertDomainNamesInclude(rootDomain, requiredDomain) {
311
+ const rootMeta = getMeta(rootDomain);
312
+ const requiredMeta = getMeta(requiredDomain);
313
+ if (!rootMeta || !requiredMeta)
314
+ return;
315
+ const rootNames = collectTransitiveDomainNames(rootDomain);
316
+ const requiredNames = collectTransitiveDomainNames(requiredDomain);
317
+ if (rootNames.size === 0 || requiredNames.size === 0)
318
+ return;
319
+ const missing = Array.from(requiredNames).filter((name) => !rootNames.has(name));
320
+ if (missing.length > 0) {
321
+ throw new Error(`ConcreteDomain: domain is missing required names (${missing.join(", ")})`);
322
+ }
323
+ }
283
324
  function createConcreteDomain(domainInstance, db, fullSchema, bindings) {
284
325
  const baseSchema = fullSchema ?? resolveSchema(domainInstance);
285
326
  const actionMap = getStoredActionMap(domainInstance);
@@ -290,8 +331,16 @@ function createConcreteDomain(domainInstance, db, fullSchema, bindings) {
290
331
  context: (options) => domainInstance.context(options),
291
332
  contextString: (options) => domainInstance.contextString(options),
292
333
  };
293
- if (bindings?.env !== undefined && bindings?.runtime !== undefined) {
294
- const inheritedStack = readRuntimeActionStack(bindings.runtime);
334
+ if (bindings?.runtime !== undefined) {
335
+ const inheritedStack = [];
336
+ const createActionRuntime = (stack) => {
337
+ const runtime = {
338
+ ...concrete,
339
+ ...(bindings.env !== undefined ? { env: bindings.env } : {}),
340
+ };
341
+ runtime.actions = buildActions(stack);
342
+ return runtime;
343
+ };
295
344
  const buildActions = (stack) => Object.fromEntries(Object.entries(actionMap).map(([key, action]) => [
296
345
  key,
297
346
  async (input) => {
@@ -303,25 +352,51 @@ function createConcreteDomain(domainInstance, db, fullSchema, bindings) {
303
352
  throw new Error(`domain_action_cycle:${key}`);
304
353
  }
305
354
  const nextStack = [...stack, key];
306
- const scopedRuntime = cloneRuntimeWithActionStack(bindings.runtime, nextStack);
355
+ const scopedRuntime = createActionRuntime(nextStack);
356
+ const parsedInput = action.input.parse(input);
307
357
  const params = {
308
- env: bindings.env,
309
- input,
358
+ input: parsedInput,
310
359
  runtime: scopedRuntime,
311
360
  };
312
- return await execute(params);
361
+ const output = await execute(params);
362
+ return action.output.parse(output);
313
363
  },
314
364
  ]));
315
- ;
316
- concrete.env = bindings.env;
365
+ if (bindings.env !== undefined) {
366
+ ;
367
+ concrete.env = bindings.env;
368
+ }
317
369
  ;
318
370
  concrete.actions = buildActions(inheritedStack);
319
371
  }
320
372
  return concrete;
321
373
  }
374
+ function promoteRuntimeDomainScope(scoped) {
375
+ const promoted = { ...scoped };
376
+ const db = scoped?.db;
377
+ if (db && typeof db.query === "function") {
378
+ promoted.query = db.query.bind(db);
379
+ }
380
+ const actions = scoped?.actions;
381
+ if (actions && typeof actions === "object") {
382
+ for (const [key, action] of Object.entries(actions)) {
383
+ if (key in promoted)
384
+ continue;
385
+ promoted[key] = action;
386
+ }
387
+ }
388
+ return promoted;
389
+ }
390
+ async function callDomainRuntimeScope(domainInstance, runtime, options) {
391
+ if (!runtime || typeof runtime.use !== "function") {
392
+ throw new Error("domain(runtime) requires an Ekairos runtime with use(domain).");
393
+ }
394
+ return promoteRuntimeDomainScope(await runtime.use(domainInstance, options));
395
+ }
322
396
  export function materializeDomain(params) {
323
397
  const baseSchema = resolveSchema(params.rootDomain);
324
398
  const requiredSchema = resolveSchema(params.subdomain);
399
+ assertDomainNamesInclude(params.rootDomain, params.subdomain);
325
400
  assertSchemaIncludes(baseSchema, requiredSchema);
326
401
  return createConcreteDomain(params.subdomain, params.db, baseSchema, params.bindings);
327
402
  }
@@ -338,18 +413,18 @@ function loadDomainDoc(scope, meta) {
338
413
  function normalizeDoc(docInfo, options) {
339
414
  if (!docInfo?.doc)
340
415
  return { doc: null, docPath: docInfo?.docPath };
341
- const parsed = parseDomainDoc(docInfo.doc);
342
- if (!parsed)
343
- return { doc: docInfo.doc, docPath: docInfo.docPath };
344
- const filtered = filterDomainDoc(parsed.data, {
345
- subdomains: options.subdomains,
346
- entities: options.entities,
347
- });
348
- const rendered = renderDomainDoc(filtered, {
349
- titlePrefix: options.titlePrefix,
350
- includeSubdomains: options.includeSubdomains,
351
- });
352
- return { doc: rendered, docPath: docInfo.docPath };
416
+ if (domainDocNormalizer) {
417
+ try {
418
+ const normalized = domainDocNormalizer({ docInfo, options });
419
+ if (normalized)
420
+ return normalized;
421
+ }
422
+ catch {
423
+ // Fall through to raw docs. Domain context must remain usable without the
424
+ // optional markdown/YAML parser in workflow bundles.
425
+ }
426
+ }
427
+ return { doc: docInfo.doc, docPath: docInfo.docPath };
353
428
  }
354
429
  function buildRegistryEntries(meta, options) {
355
430
  if (!meta)
@@ -368,7 +443,7 @@ function buildRegistryEntries(meta, options) {
368
443
  catch {
369
444
  child = null;
370
445
  }
371
- if (!child || typeof child !== "object")
446
+ if (!isObjectLike(child))
372
447
  continue;
373
448
  if (seen.has(child))
374
449
  continue;
@@ -490,7 +565,7 @@ function resolveIncludeNames(meta) {
490
565
  catch {
491
566
  child = null;
492
567
  }
493
- if (!child || typeof child !== "object")
568
+ if (!isObjectLike(child))
494
569
  continue;
495
570
  const childMeta = getMeta(child);
496
571
  if (childMeta?.name)
@@ -498,6 +573,64 @@ function resolveIncludeNames(meta) {
498
573
  }
499
574
  return Array.from(names);
500
575
  }
576
+ function isRuntimeEntityDef(value) {
577
+ return Boolean(value &&
578
+ typeof value === "object" &&
579
+ "attrs" in value &&
580
+ value.attrs &&
581
+ typeof value.attrs === "object");
582
+ }
583
+ function stripRuntimeEntityLinks(entity) {
584
+ if (!isRuntimeEntityDef(entity))
585
+ return entity;
586
+ return i.entity({ ...entity.attrs });
587
+ }
588
+ function normalizeRuntimeAttrDef(value) {
589
+ if (!value || typeof value !== "object") {
590
+ return value;
591
+ }
592
+ const record = value;
593
+ const sorted = {};
594
+ for (const key of Object.keys(record).sort()) {
595
+ sorted[key] = normalizeRuntimeAttrDef(record[key]);
596
+ }
597
+ return sorted;
598
+ }
599
+ function stableRuntimeAttrDef(value) {
600
+ return JSON.stringify(normalizeRuntimeAttrDef(value));
601
+ }
602
+ function areRuntimeAttrDefsEquivalent(baseAttr, nextAttr) {
603
+ if (baseAttr === nextAttr)
604
+ return true;
605
+ return stableRuntimeAttrDef(baseAttr) === stableRuntimeAttrDef(nextAttr);
606
+ }
607
+ function mergeRuntimeEntityDefs(entityName, baseEntity, nextEntity) {
608
+ if (!isRuntimeEntityDef(baseEntity) || !isRuntimeEntityDef(nextEntity)) {
609
+ return stripRuntimeEntityLinks(nextEntity);
610
+ }
611
+ const conflictingAttrs = Object.keys(nextEntity.attrs).filter((attr) => Object.prototype.hasOwnProperty.call(baseEntity.attrs, attr) &&
612
+ !areRuntimeAttrDefsEquivalent(baseEntity.attrs[attr], nextEntity.attrs[attr]));
613
+ if (conflictingAttrs.length > 0) {
614
+ throw new Error(`domain_duplicate_entity_attr:${entityName}.${conflictingAttrs.join(",")}`);
615
+ }
616
+ return i.entity({
617
+ ...baseEntity.attrs,
618
+ ...nextEntity.attrs,
619
+ });
620
+ }
621
+ function mergeRuntimeEntities(baseEntities, nextEntities) {
622
+ const merged = {};
623
+ for (const [entityName, entity] of Object.entries(baseEntities)) {
624
+ merged[entityName] = stripRuntimeEntityLinks(entity);
625
+ }
626
+ for (const [entityName, entity] of Object.entries(nextEntities)) {
627
+ merged[entityName] =
628
+ entityName in merged
629
+ ? mergeRuntimeEntityDefs(entityName, merged[entityName], entity)
630
+ : stripRuntimeEntityLinks(entity);
631
+ }
632
+ return merged;
633
+ }
501
634
  function makeInstance(def, metaIncludes = []) {
502
635
  const meta = {
503
636
  name: def.name,
@@ -517,7 +650,7 @@ function makeInstance(def, metaIncludes = []) {
517
650
  const otherDef = "schema" in other
518
651
  ? { entities: other.entities, links: other.links, rooms: other.rooms }
519
652
  : other;
520
- const mergedEntities = { ...def.entities, ...otherDef.entities };
653
+ const mergedEntities = mergeRuntimeEntities(def.entities, otherDef.entities);
521
654
  const mergedLinks = { ...def.links, ...otherDef.links };
522
655
  const mergedRooms = { ...def.rooms, ...otherDef.rooms };
523
656
  const composed = makeInstance({
@@ -574,10 +707,10 @@ export function domain(arg) {
574
707
  // Support lazy includes for circular dependencies by storing references and resolving at schema()/toInstantSchema() time
575
708
  // AL preserves literal link keys from included domains
576
709
  function createBuilder(deps, linkDeps, lazyIncludes = [], meta) {
577
- return {
710
+ const builder = {
578
711
  includes(other) {
579
712
  // Support lazy includes via function for circular dependencies
580
- if (typeof other === 'function') {
713
+ if (typeof other === 'function' && !isMaterializedDomainSource(other)) {
581
714
  const lazyGetter = () => {
582
715
  try {
583
716
  return other();
@@ -611,7 +744,7 @@ export function domain(arg) {
611
744
  return createBuilder(deps, linkDeps, [...lazyIncludes, lazyGetter], nextMeta);
612
745
  }
613
746
  const links = other.links;
614
- const mergedEntities = { ...deps, ...entities };
747
+ const mergedEntities = mergeRuntimeEntities(deps, entities);
615
748
  // Preserve literal link keys by merging directly (not casting to LinksDef)
616
749
  const mergedLinks = (links ? { ...linkDeps, ...links } : { ...linkDeps });
617
750
  const includeRef = () => other;
@@ -626,10 +759,11 @@ export function domain(arg) {
626
759
  return createBuilder(deps, linkDeps, [...lazyIncludes, lazyGetter], nextMeta);
627
760
  }
628
761
  },
629
- schema(def) {
762
+ withSchema(def) {
630
763
  // Resolve lazy includes at schema() time (when all domains should be initialized)
631
764
  // This handles circular dependencies by deferring entity resolution
632
765
  let resolvedDeps = { ...deps };
766
+ const pendingLazyIncludes = [];
633
767
  // Preserve literal link keys from accumulated links
634
768
  let resolvedLinks = { ...linkDeps };
635
769
  for (const lazyGetter of lazyIncludes) {
@@ -638,7 +772,7 @@ export function domain(arg) {
638
772
  if (other) {
639
773
  const entities = other.entities;
640
774
  if (entities) {
641
- resolvedDeps = { ...resolvedDeps, ...entities };
775
+ resolvedDeps = mergeRuntimeEntities(resolvedDeps, entities);
642
776
  }
643
777
  const links = other.links;
644
778
  if (links) {
@@ -646,14 +780,18 @@ export function domain(arg) {
646
780
  resolvedLinks = { ...resolvedLinks, ...links };
647
781
  }
648
782
  }
783
+ else {
784
+ pendingLazyIncludes.push(lazyGetter);
785
+ }
649
786
  }
650
787
  catch (e) {
651
788
  // If lazy resolution fails, continue - entities might be available via string references
652
789
  // This is expected for circular dependencies that will be resolved when all domains are composed
790
+ pendingLazyIncludes.push(lazyGetter);
653
791
  }
654
792
  }
655
793
  // Runtime merge for output; compile-time validation handled by types above
656
- const allEntities = { ...resolvedDeps, ...def.entities };
794
+ const allEntities = mergeRuntimeEntities(resolvedDeps, def.entities);
657
795
  // allLinks contains merged links from included domains + current domain
658
796
  // Preserve literal link keys (owner, related, parent, etc.) by using MergeLinks
659
797
  const allLinks = { ...resolvedLinks, ...def.links };
@@ -662,92 +800,112 @@ export function domain(arg) {
662
800
  const capturedLinks = cloneLinksDef(allLinks);
663
801
  const capturedRooms = cloneRoomsDef(def.rooms);
664
802
  let cachedInstantSchema = null;
665
- const result = {
666
- entities: Object.freeze({ ...allEntities }),
667
- // Strip base phantom from public type so it's assignable to i.schema()
668
- links: Object.freeze(cloneLinksDef(allLinks)),
669
- rooms: Object.freeze(cloneRoomsDef(def.rooms)),
670
- // Add originalEntities for type-safe access to original entity definitions
671
- originalEntities: Object.freeze({ ...allEntities }),
672
- toInstantSchema: () => {
673
- if (cachedInstantSchema) {
674
- return cachedInstantSchema;
675
- }
676
- let finalEntities = { ...capturedEntities };
677
- let finalLinks = cloneLinksDef(capturedLinks);
678
- let hasUnresolvedIncludes = false;
679
- // Try to resolve lazy includes one more time (domains should be initialized by now)
680
- for (const lazyGetter of lazyIncludes) {
681
- try {
682
- const other = lazyGetter();
683
- if (other) {
684
- const entities = other.entities;
685
- if (entities) {
686
- finalEntities = { ...finalEntities, ...entities };
687
- }
688
- const links = other.links;
689
- if (links) {
690
- finalLinks = { ...finalLinks, ...links };
691
- }
803
+ const instantSchema = () => {
804
+ if (cachedInstantSchema) {
805
+ return cachedInstantSchema;
806
+ }
807
+ let finalEntities = { ...capturedEntities };
808
+ let finalLinks = cloneLinksDef(capturedLinks);
809
+ let hasUnresolvedIncludes = false;
810
+ // Try to resolve lazy includes one more time (domains should be initialized by now)
811
+ for (const lazyGetter of pendingLazyIncludes) {
812
+ try {
813
+ const other = lazyGetter();
814
+ if (other) {
815
+ const entities = other.entities;
816
+ if (entities) {
817
+ finalEntities = mergeRuntimeEntities(finalEntities, entities);
692
818
  }
693
- else {
694
- hasUnresolvedIncludes = true;
819
+ const links = other.links;
820
+ if (links) {
821
+ finalLinks = { ...finalLinks, ...links };
695
822
  }
696
823
  }
697
- catch {
698
- // If still can't resolve, entities should already be in allEntities from app domain composition
824
+ else {
699
825
  hasUnresolvedIncludes = true;
700
826
  }
701
827
  }
702
- assertNoDuplicateLinkAttributes(finalLinks);
703
- // Include base entities ($users, $files, $streams) that InstantDB manages
704
- // These need to be explicitly included since InstantDB doesn't auto-add them
705
- const baseEntities = {
706
- $users: i.entity({
707
- email: i.string().optional().indexed(),
708
- }),
709
- $files: i.entity({
710
- path: i.string(),
711
- url: i.string().optional(),
712
- contentType: i.string().optional(),
713
- size: i.number().optional(),
714
- }),
715
- $streams: i.entity({
716
- clientId: i.string().optional().indexed(),
717
- size: i.number().optional(),
718
- createdAt: i.date().optional().indexed(),
719
- updatedAt: i.date().optional().indexed(),
720
- }),
721
- };
722
- // Merge base entities with user entities, user entities take precedence
723
- const allEntitiesWithBase = {
724
- ...baseEntities,
725
- ...finalEntities,
726
- };
727
- const schemaResult = i.schema({
728
- entities: allEntitiesWithBase,
729
- links: cloneLinksDef(finalLinks),
730
- rooms: cloneRoomsDef(capturedRooms),
731
- });
732
- const frozenSchema = Object.freeze(schemaResult);
733
- if (!hasUnresolvedIncludes) {
734
- cachedInstantSchema = frozenSchema;
828
+ catch {
829
+ // If still can't resolve, entities should already be in allEntities from app domain composition
830
+ hasUnresolvedIncludes = true;
735
831
  }
736
- return frozenSchema;
737
- },
832
+ }
833
+ assertNoDuplicateLinkAttributes(finalLinks);
834
+ // Include base entities ($users, $files, $streams) that InstantDB manages
835
+ // These need to be explicitly included since InstantDB doesn't auto-add them
836
+ const baseEntities = {
837
+ $users: i.entity({
838
+ email: i.string().optional().indexed(),
839
+ }),
840
+ $files: i.entity({
841
+ path: i.string(),
842
+ url: i.string().optional(),
843
+ contentType: i.string().optional(),
844
+ size: i.number().optional(),
845
+ }),
846
+ $streams: i.entity({
847
+ clientId: i.string().optional().indexed(),
848
+ size: i.number().optional(),
849
+ createdAt: i.date().optional().indexed(),
850
+ updatedAt: i.date().optional().indexed(),
851
+ }),
852
+ };
853
+ // Merge base entities with user entities, user entities take precedence
854
+ const allEntitiesWithBase = {
855
+ ...baseEntities,
856
+ ...finalEntities,
857
+ };
858
+ const schemaResult = i.schema({
859
+ entities: allEntitiesWithBase,
860
+ links: cloneLinksDef(finalLinks),
861
+ rooms: cloneRoomsDef(capturedRooms),
862
+ });
863
+ const frozenSchema = Object.freeze(schemaResult);
864
+ if (!hasUnresolvedIncludes) {
865
+ cachedInstantSchema = frozenSchema;
866
+ }
867
+ return frozenSchema;
738
868
  };
869
+ let result;
870
+ const callableResult = (runtime, options) => callDomainRuntimeScope(result, runtime, options);
871
+ result = Object.assign(callableResult, {
872
+ entities: Object.freeze({ ...allEntities }),
873
+ // Strip base phantom from public type so it's assignable to i.schema()
874
+ links: Object.freeze(cloneLinksDef(allLinks)),
875
+ rooms: Object.freeze(cloneRoomsDef(def.rooms)),
876
+ // Add originalEntities for type-safe access to original entity definitions
877
+ originalEntities: Object.freeze({ ...allEntities }),
878
+ instantSchema,
879
+ toInstantSchema: instantSchema,
880
+ });
739
881
  attachMeta(result, freezeMeta(meta));
740
882
  result.context = (options) => buildContext(result, options);
741
883
  result.contextString = (options) => contextToString(buildContext(result, options));
742
884
  result.fromDB = (db, bindings) => createConcreteDomain(result, db, resolveSchema(result), bindings);
743
- const reboundActions = seedActions.map((action) => bindAction(action, {
744
- name: action.name,
745
- domain: result,
746
- key: getActionBinding(action)?.key,
885
+ const reboundByAction = new Map();
886
+ const reboundActionMap = Object.fromEntries(Object.entries(seedActionMap).map(([key, action]) => {
887
+ const rebound = bindAction(action, {
888
+ name: action.name,
889
+ domain: result,
890
+ key,
891
+ });
892
+ reboundByAction.set(action, rebound);
893
+ return [key, rebound];
747
894
  }));
895
+ const reboundActions = seedActions.map((action) => {
896
+ const rebound = reboundByAction.get(action);
897
+ if (rebound)
898
+ return rebound;
899
+ return bindAction(action, {
900
+ name: action.name,
901
+ domain: result,
902
+ key: getActionBinding(action)?.key,
903
+ });
904
+ });
748
905
  setStoredActions(result, [...reboundActions]);
749
- setStoredActionMap(result, { ...seedActionMap });
750
- result.actions = (actionsInput) => {
906
+ setStoredActionMap(result, reboundActionMap);
907
+ result.actions = getStoredActionMap(result);
908
+ result.withActions = (actionsInput) => {
751
909
  const current = getStoredActions(result);
752
910
  const currentMap = getStoredActionMap(result);
753
911
  const additions = normalizeActionCollection(result, actionsInput);
@@ -755,11 +913,16 @@ export function domain(arg) {
755
913
  };
756
914
  result.getActions = () => [...getStoredActions(result)];
757
915
  result.getActionMap = () => ({ ...getStoredActionMap(result) });
916
+ result.definition = () => result;
758
917
  return Object.freeze(result);
759
918
  };
760
919
  return createDomainResult([], {});
761
920
  },
921
+ schema(def) {
922
+ return this.withSchema(def);
923
+ },
762
924
  };
925
+ return builder;
763
926
  }
764
927
  if (typeof arg === "string" && !arg.trim()) {
765
928
  throw new Error("domain() requires a name");
@@ -772,21 +935,34 @@ export function composeDomain(name, includes = []) {
772
935
  for (const include of includes) {
773
936
  builder = builder.includes(include);
774
937
  }
775
- return builder.schema({ entities: {}, links: {}, rooms: {} });
938
+ return builder.withSchema({ entities: {}, links: {}, rooms: {} });
776
939
  }
777
940
  /**
778
941
  * Define a domain action without changing the public action contract.
779
942
  *
780
943
  * Convention for new actions:
781
944
  *
782
- * `async execute({ runtime, input }) { "use step"; const domain = await runtime.use(myDomain); ... }`
945
+ * `async execute({ runtime, input }) { await runtime.db.transact([...]); }`
783
946
  *
784
- * Actions remain callable directly, from nested `runtime.use(domain).actions.*`
785
- * composition, and from higher-level workflows that orchestrate them.
947
+ * Actions receive a runtime already scoped to the declaring domain. Nested
948
+ * action composition is available through `runtime.actions.*`.
786
949
  */
950
+ function toJsonSchema(schema) {
951
+ try {
952
+ return z.toJSONSchema(schema, { target: "draft-7" });
953
+ }
954
+ catch {
955
+ return undefined;
956
+ }
957
+ }
787
958
  export function defineDomainAction(action) {
788
- return action;
959
+ return Object.freeze({
960
+ ...action,
961
+ inputSchema: toJsonSchema(action.input),
962
+ outputSchema: toJsonSchema(action.output),
963
+ });
789
964
  }
965
+ export const defineAction = defineDomainAction;
790
966
  export function getDomainActions(source) {
791
967
  return getStoredActions(source);
792
968
  }