@ekairos/domain 1.22.34-beta.development.0 → 1.22.35

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 (68) hide show
  1. package/README.md +260 -106
  2. package/SKILL.md +56 -0
  3. package/dist/cli/bin.d.ts +9 -0
  4. package/dist/cli/bin.d.ts.map +1 -0
  5. package/dist/cli/bin.js +609 -0
  6. package/dist/cli/bin.js.map +1 -0
  7. package/dist/cli/client-runtime.d.ts +25 -0
  8. package/dist/cli/client-runtime.d.ts.map +1 -0
  9. package/dist/cli/client-runtime.js +60 -0
  10. package/dist/cli/client-runtime.js.map +1 -0
  11. package/dist/cli/config.d.ts +5 -0
  12. package/dist/cli/config.d.ts.map +1 -0
  13. package/dist/cli/config.js +44 -0
  14. package/dist/cli/config.js.map +1 -0
  15. package/dist/cli/create-app.d.ts +66 -0
  16. package/dist/cli/create-app.d.ts.map +1 -0
  17. package/dist/cli/create-app.js +2948 -0
  18. package/dist/cli/create-app.js.map +1 -0
  19. package/dist/cli/http.d.ts +28 -0
  20. package/dist/cli/http.d.ts.map +1 -0
  21. package/dist/cli/http.js +113 -0
  22. package/dist/cli/http.js.map +1 -0
  23. package/dist/cli/index.d.ts +8 -0
  24. package/dist/cli/index.d.ts.map +1 -0
  25. package/dist/cli/index.js +7 -0
  26. package/dist/cli/index.js.map +1 -0
  27. package/dist/cli/server.d.ts +3 -0
  28. package/dist/cli/server.d.ts.map +1 -0
  29. package/dist/cli/server.js +440 -0
  30. package/dist/cli/server.js.map +1 -0
  31. package/dist/cli/types.d.ts +61 -0
  32. package/dist/cli/types.d.ts.map +1 -0
  33. package/dist/cli/types.js +2 -0
  34. package/dist/cli/types.js.map +1 -0
  35. package/dist/cli/ui.d.ts +3 -0
  36. package/dist/cli/ui.d.ts.map +1 -0
  37. package/dist/cli/ui.js +138 -0
  38. package/dist/cli/ui.js.map +1 -0
  39. package/dist/context.test-runner.js +3 -1
  40. package/dist/context.test-runner.js.map +1 -1
  41. package/dist/domain-doc.d.ts +2 -0
  42. package/dist/domain-doc.d.ts.map +1 -1
  43. package/dist/domain-doc.js +14 -0
  44. package/dist/domain-doc.js.map +1 -1
  45. package/dist/index.d.ts +228 -28
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +397 -118
  48. package/dist/index.js.map +1 -1
  49. package/dist/next.d.ts +21 -21
  50. package/dist/next.d.ts.map +1 -1
  51. package/dist/next.js +213 -345
  52. package/dist/next.js.map +1 -1
  53. package/dist/polyfills/dom-events.d.ts +2 -0
  54. package/dist/polyfills/dom-events.d.ts.map +1 -0
  55. package/dist/polyfills/dom-events.js +92 -0
  56. package/dist/polyfills/dom-events.js.map +1 -0
  57. package/dist/runtime-handle.d.ts +45 -0
  58. package/dist/runtime-handle.d.ts.map +1 -0
  59. package/dist/runtime-handle.js +84 -0
  60. package/dist/runtime-handle.js.map +1 -0
  61. package/dist/runtime-step.d.ts.map +1 -1
  62. package/dist/runtime-step.js +2 -0
  63. package/dist/runtime-step.js.map +1 -1
  64. package/dist/runtime.d.ts +9 -8
  65. package/dist/runtime.d.ts.map +1 -1
  66. package/dist/runtime.js +80 -24
  67. package/dist/runtime.js.map +1 -1
  68. package/package.json +44 -7
package/dist/index.js CHANGED
@@ -1,20 +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";
3
+ export { EkairosRuntime, } from "./runtime-handle.js";
4
4
  let domainDocLoader = null;
5
+ let domainDocNormalizer = null;
5
6
  export function configureDomainDocLoader(loader) {
6
7
  domainDocLoader = loader ?? null;
7
8
  }
9
+ export function configureDomainDocNormalizer(normalizer) {
10
+ domainDocNormalizer = normalizer ?? null;
11
+ }
8
12
  const EKAIROS_META = Symbol.for("@ekairos/domain/meta");
9
13
  const EKAIROS_ACTIONS = Symbol.for("@ekairos/domain/actions");
14
+ const EKAIROS_ACTION_MAP = Symbol.for("@ekairos/domain/action-map");
10
15
  const EKAIROS_ACTION_BINDING = Symbol.for("@ekairos/domain/action-binding");
11
16
  function getMeta(source) {
12
- if (!source || typeof source !== "object")
17
+ if (!isObjectLike(source))
13
18
  return null;
14
19
  return source[EKAIROS_META] ?? null;
15
20
  }
16
21
  function getActionBinding(source) {
17
- if (!source || typeof source !== "object")
22
+ if (!isObjectLike(source))
18
23
  return null;
19
24
  const binding = source[EKAIROS_ACTION_BINDING];
20
25
  if (!binding || typeof binding !== "object")
@@ -22,7 +27,12 @@ function getActionBinding(source) {
22
27
  const name = typeof binding.name === "string" ? binding.name.trim() : "";
23
28
  if (!name)
24
29
  return null;
25
- return { name, domain: binding.domain };
30
+ const key = typeof binding.key === "string" ? binding.key.trim() : "";
31
+ return {
32
+ name,
33
+ domain: binding.domain,
34
+ ...(key ? { key } : {}),
35
+ };
26
36
  }
27
37
  function bindAction(action, params) {
28
38
  const registration = {
@@ -33,6 +43,7 @@ function bindAction(action, params) {
33
43
  value: {
34
44
  name: params.name,
35
45
  domain: params.domain,
46
+ key: params.key,
36
47
  },
37
48
  enumerable: false,
38
49
  configurable: false,
@@ -41,7 +52,7 @@ function bindAction(action, params) {
41
52
  return registration;
42
53
  }
43
54
  function getStoredActions(source) {
44
- if (!source || typeof source !== "object")
55
+ if (!isObjectLike(source))
45
56
  return [];
46
57
  const raw = source[EKAIROS_ACTIONS];
47
58
  if (!Array.isArray(raw))
@@ -51,8 +62,16 @@ function getStoredActions(source) {
51
62
  typeof entry.name === "string" &&
52
63
  typeof entry.execute === "function");
53
64
  }
65
+ function getStoredActionMap(source) {
66
+ if (!isObjectLike(source))
67
+ return {};
68
+ const raw = source[EKAIROS_ACTION_MAP];
69
+ if (!raw || typeof raw !== "object")
70
+ return {};
71
+ return raw;
72
+ }
54
73
  function setStoredActions(source, actions) {
55
- if (!source || typeof source !== "object")
74
+ if (!isObjectLike(source))
56
75
  return;
57
76
  const frozenActions = Object.freeze([...actions]);
58
77
  Object.defineProperty(source, EKAIROS_ACTIONS, {
@@ -62,11 +81,23 @@ function setStoredActions(source, actions) {
62
81
  writable: true,
63
82
  });
64
83
  }
84
+ function setStoredActionMap(source, actionMap) {
85
+ if (!isObjectLike(source))
86
+ return;
87
+ Object.defineProperty(source, EKAIROS_ACTION_MAP, {
88
+ value: Object.freeze({ ...actionMap }),
89
+ enumerable: false,
90
+ configurable: true,
91
+ writable: true,
92
+ });
93
+ }
65
94
  function normalizeActionLike(value, params) {
66
- const action = typeof value === "function"
67
- ? { execute: value }
68
- : value;
69
- 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) {
70
101
  throw new Error(`Invalid domain action definition: ${params.fallbackName}`);
71
102
  }
72
103
  const explicitName = typeof action.name === "string" ? action.name.trim() : "";
@@ -76,16 +107,27 @@ function normalizeActionLike(value, params) {
76
107
  throw new Error(`Domain action is missing a name: ${params.fallbackName}`);
77
108
  }
78
109
  const domain = bound?.domain ?? params.domain;
79
- return bindAction(action, { name, domain });
110
+ return bindAction(action, { name, domain, key: params.key ?? params.fallbackName });
80
111
  }
81
112
  function normalizeActionCollection(source, input) {
82
113
  const current = getStoredActions(source);
114
+ const currentActionMap = getStoredActionMap(source);
83
115
  const byName = new Set(current.map((action) => action.name));
116
+ const byKey = new Set(Object.keys(currentActionMap));
84
117
  const normalized = [];
85
- const push = (candidate) => {
118
+ const actionMap = {};
119
+ const push = (candidate, key) => {
86
120
  if (byName.has(candidate.name)) {
87
121
  throw new Error(`Duplicate domain action name: ${candidate.name}`);
88
122
  }
123
+ const localKey = String(key ?? candidate?.name ?? "").trim();
124
+ if (localKey) {
125
+ if (byKey.has(localKey)) {
126
+ throw new Error(`Duplicate domain action key: ${localKey}`);
127
+ }
128
+ byKey.add(localKey);
129
+ actionMap[localKey] = candidate;
130
+ }
89
131
  byName.add(candidate.name);
90
132
  normalized.push(candidate);
91
133
  };
@@ -97,18 +139,19 @@ function normalizeActionCollection(source, input) {
97
139
  : "",
98
140
  domain: source,
99
141
  });
100
- push(normalizedEntry);
142
+ push(normalizedEntry, normalizedEntry?.name);
101
143
  }
102
- return normalized;
144
+ return { actions: normalized, actionMap };
103
145
  }
104
146
  for (const [key, value] of Object.entries(input ?? {})) {
105
147
  const normalizedEntry = normalizeActionLike(value, {
106
148
  fallbackName: key,
107
149
  domain: source,
150
+ key,
108
151
  });
109
- push(normalizedEntry);
152
+ push(normalizedEntry, key);
110
153
  }
111
- return normalized;
154
+ return { actions: normalized, actionMap };
112
155
  }
113
156
  function attachMeta(target, meta) {
114
157
  Object.defineProperty(target, EKAIROS_META, {
@@ -184,9 +227,22 @@ function listKeys(value) {
184
227
  return [];
185
228
  return Object.keys(value).filter((key) => !key.startsWith("$"));
186
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
+ }
187
241
  function resolveSchema(source) {
188
242
  if (!source)
189
243
  return null;
244
+ if (typeof source.instantSchema === "function")
245
+ return source.instantSchema();
190
246
  if (typeof source.toInstantSchema === "function")
191
247
  return source.toInstantSchema();
192
248
  if (typeof source.schema === "function")
@@ -223,22 +279,127 @@ function assertSchemaIncludes(fullSchema, requiredSchema) {
223
279
  throw new Error(`ConcreteDomain: schema is missing required keys (${parts.join(" | ")})`);
224
280
  }
225
281
  }
226
- function createConcreteDomain(domainInstance, db, fullSchema) {
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
+ }
324
+ function createConcreteDomain(domainInstance, db, fullSchema, bindings) {
227
325
  const baseSchema = fullSchema ?? resolveSchema(domainInstance);
326
+ const actionMap = getStoredActionMap(domainInstance);
228
327
  const concrete = {
229
328
  domain: domainInstance,
230
329
  db,
231
330
  schema: resolveSchema(domainInstance),
232
331
  context: (options) => domainInstance.context(options),
233
332
  contextString: (options) => domainInstance.contextString(options),
234
- fromDomain(subdomain) {
235
- const requiredSchema = resolveSchema(subdomain);
236
- assertSchemaIncludes(baseSchema, requiredSchema);
237
- return createConcreteDomain(subdomain, db, baseSchema);
238
- },
239
333
  };
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
+ };
344
+ const buildActions = (stack) => Object.fromEntries(Object.entries(actionMap).map(([key, action]) => [
345
+ key,
346
+ async (input) => {
347
+ const execute = action?.execute;
348
+ if (typeof execute !== "function") {
349
+ throw new Error(`domain_action_not_executable:${key}`);
350
+ }
351
+ if (stack.includes(key)) {
352
+ throw new Error(`domain_action_cycle:${key}`);
353
+ }
354
+ const nextStack = [...stack, key];
355
+ const scopedRuntime = createActionRuntime(nextStack);
356
+ const parsedInput = action.input.parse(input);
357
+ const params = {
358
+ input: parsedInput,
359
+ runtime: scopedRuntime,
360
+ };
361
+ const output = await execute(params);
362
+ return action.output.parse(output);
363
+ },
364
+ ]));
365
+ if (bindings.env !== undefined) {
366
+ ;
367
+ concrete.env = bindings.env;
368
+ }
369
+ ;
370
+ concrete.actions = buildActions(inheritedStack);
371
+ }
240
372
  return concrete;
241
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
+ }
396
+ export function materializeDomain(params) {
397
+ const baseSchema = resolveSchema(params.rootDomain);
398
+ const requiredSchema = resolveSchema(params.subdomain);
399
+ assertDomainNamesInclude(params.rootDomain, params.subdomain);
400
+ assertSchemaIncludes(baseSchema, requiredSchema);
401
+ return createConcreteDomain(params.subdomain, params.db, baseSchema, params.bindings);
402
+ }
242
403
  function loadDomainDoc(scope, meta) {
243
404
  if (!domainDocLoader)
244
405
  return null;
@@ -252,18 +413,18 @@ function loadDomainDoc(scope, meta) {
252
413
  function normalizeDoc(docInfo, options) {
253
414
  if (!docInfo?.doc)
254
415
  return { doc: null, docPath: docInfo?.docPath };
255
- const parsed = parseDomainDoc(docInfo.doc);
256
- if (!parsed)
257
- return { doc: docInfo.doc, docPath: docInfo.docPath };
258
- const filtered = filterDomainDoc(parsed.data, {
259
- subdomains: options.subdomains,
260
- entities: options.entities,
261
- });
262
- const rendered = renderDomainDoc(filtered, {
263
- titlePrefix: options.titlePrefix,
264
- includeSubdomains: options.includeSubdomains,
265
- });
266
- 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 };
267
428
  }
268
429
  function buildRegistryEntries(meta, options) {
269
430
  if (!meta)
@@ -282,7 +443,7 @@ function buildRegistryEntries(meta, options) {
282
443
  catch {
283
444
  child = null;
284
445
  }
285
- if (!child || typeof child !== "object")
446
+ if (!isObjectLike(child))
286
447
  continue;
287
448
  if (seen.has(child))
288
449
  continue;
@@ -404,7 +565,7 @@ function resolveIncludeNames(meta) {
404
565
  catch {
405
566
  child = null;
406
567
  }
407
- if (!child || typeof child !== "object")
568
+ if (!isObjectLike(child))
408
569
  continue;
409
570
  const childMeta = getMeta(child);
410
571
  if (childMeta?.name)
@@ -412,6 +573,64 @@ function resolveIncludeNames(meta) {
412
573
  }
413
574
  return Array.from(names);
414
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
+ }
415
634
  function makeInstance(def, metaIncludes = []) {
416
635
  const meta = {
417
636
  name: def.name,
@@ -431,7 +650,7 @@ function makeInstance(def, metaIncludes = []) {
431
650
  const otherDef = "schema" in other
432
651
  ? { entities: other.entities, links: other.links, rooms: other.rooms }
433
652
  : other;
434
- const mergedEntities = { ...def.entities, ...otherDef.entities };
653
+ const mergedEntities = mergeRuntimeEntities(def.entities, otherDef.entities);
435
654
  const mergedLinks = { ...def.links, ...otherDef.links };
436
655
  const mergedRooms = { ...def.rooms, ...otherDef.rooms };
437
656
  const composed = makeInstance({
@@ -488,10 +707,10 @@ export function domain(arg) {
488
707
  // Support lazy includes for circular dependencies by storing references and resolving at schema()/toInstantSchema() time
489
708
  // AL preserves literal link keys from included domains
490
709
  function createBuilder(deps, linkDeps, lazyIncludes = [], meta) {
491
- return {
710
+ const builder = {
492
711
  includes(other) {
493
712
  // Support lazy includes via function for circular dependencies
494
- if (typeof other === 'function') {
713
+ if (typeof other === 'function' && !isMaterializedDomainSource(other)) {
495
714
  const lazyGetter = () => {
496
715
  try {
497
716
  return other();
@@ -525,7 +744,7 @@ export function domain(arg) {
525
744
  return createBuilder(deps, linkDeps, [...lazyIncludes, lazyGetter], nextMeta);
526
745
  }
527
746
  const links = other.links;
528
- const mergedEntities = { ...deps, ...entities };
747
+ const mergedEntities = mergeRuntimeEntities(deps, entities);
529
748
  // Preserve literal link keys by merging directly (not casting to LinksDef)
530
749
  const mergedLinks = (links ? { ...linkDeps, ...links } : { ...linkDeps });
531
750
  const includeRef = () => other;
@@ -540,10 +759,11 @@ export function domain(arg) {
540
759
  return createBuilder(deps, linkDeps, [...lazyIncludes, lazyGetter], nextMeta);
541
760
  }
542
761
  },
543
- schema(def) {
762
+ withSchema(def) {
544
763
  // Resolve lazy includes at schema() time (when all domains should be initialized)
545
764
  // This handles circular dependencies by deferring entity resolution
546
765
  let resolvedDeps = { ...deps };
766
+ const pendingLazyIncludes = [];
547
767
  // Preserve literal link keys from accumulated links
548
768
  let resolvedLinks = { ...linkDeps };
549
769
  for (const lazyGetter of lazyIncludes) {
@@ -552,7 +772,7 @@ export function domain(arg) {
552
772
  if (other) {
553
773
  const entities = other.entities;
554
774
  if (entities) {
555
- resolvedDeps = { ...resolvedDeps, ...entities };
775
+ resolvedDeps = mergeRuntimeEntities(resolvedDeps, entities);
556
776
  }
557
777
  const links = other.links;
558
778
  if (links) {
@@ -560,113 +780,149 @@ export function domain(arg) {
560
780
  resolvedLinks = { ...resolvedLinks, ...links };
561
781
  }
562
782
  }
783
+ else {
784
+ pendingLazyIncludes.push(lazyGetter);
785
+ }
563
786
  }
564
787
  catch (e) {
565
788
  // If lazy resolution fails, continue - entities might be available via string references
566
789
  // This is expected for circular dependencies that will be resolved when all domains are composed
790
+ pendingLazyIncludes.push(lazyGetter);
567
791
  }
568
792
  }
569
793
  // Runtime merge for output; compile-time validation handled by types above
570
- const allEntities = { ...resolvedDeps, ...def.entities };
794
+ const allEntities = mergeRuntimeEntities(resolvedDeps, def.entities);
571
795
  // allLinks contains merged links from included domains + current domain
572
796
  // Preserve literal link keys (owner, related, parent, etc.) by using MergeLinks
573
797
  const allLinks = { ...resolvedLinks, ...def.links };
574
- const createDomainResult = (seedActions = []) => {
798
+ const createDomainResult = (seedActions = [], seedActionMap = {}) => {
575
799
  const capturedEntities = { ...allEntities };
576
800
  const capturedLinks = cloneLinksDef(allLinks);
577
801
  const capturedRooms = cloneRoomsDef(def.rooms);
578
802
  let cachedInstantSchema = null;
579
- const result = {
580
- entities: Object.freeze({ ...allEntities }),
581
- // Strip base phantom from public type so it's assignable to i.schema()
582
- links: Object.freeze(cloneLinksDef(allLinks)),
583
- rooms: Object.freeze(cloneRoomsDef(def.rooms)),
584
- // Add originalEntities for type-safe access to original entity definitions
585
- originalEntities: Object.freeze({ ...allEntities }),
586
- toInstantSchema: () => {
587
- if (cachedInstantSchema) {
588
- return cachedInstantSchema;
589
- }
590
- let finalEntities = { ...capturedEntities };
591
- let finalLinks = cloneLinksDef(capturedLinks);
592
- let hasUnresolvedIncludes = false;
593
- // Try to resolve lazy includes one more time (domains should be initialized by now)
594
- for (const lazyGetter of lazyIncludes) {
595
- try {
596
- const other = lazyGetter();
597
- if (other) {
598
- const entities = other.entities;
599
- if (entities) {
600
- finalEntities = { ...finalEntities, ...entities };
601
- }
602
- const links = other.links;
603
- if (links) {
604
- finalLinks = { ...finalLinks, ...links };
605
- }
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);
606
818
  }
607
- else {
608
- hasUnresolvedIncludes = true;
819
+ const links = other.links;
820
+ if (links) {
821
+ finalLinks = { ...finalLinks, ...links };
609
822
  }
610
823
  }
611
- catch {
612
- // If still can't resolve, entities should already be in allEntities from app domain composition
824
+ else {
613
825
  hasUnresolvedIncludes = true;
614
826
  }
615
827
  }
616
- assertNoDuplicateLinkAttributes(finalLinks);
617
- // Include base entities ($users, $files, $streams) that InstantDB manages
618
- // These need to be explicitly included since InstantDB doesn't auto-add them
619
- const baseEntities = {
620
- $users: i.entity({
621
- email: i.string().optional().indexed(),
622
- }),
623
- $files: i.entity({
624
- path: i.string(),
625
- url: i.string().optional(),
626
- contentType: i.string().optional(),
627
- size: i.number().optional(),
628
- }),
629
- $streams: i.entity({
630
- clientId: i.string().optional().indexed(),
631
- size: i.number().optional(),
632
- createdAt: i.date().optional().indexed(),
633
- updatedAt: i.date().optional().indexed(),
634
- }),
635
- };
636
- // Merge base entities with user entities, user entities take precedence
637
- const allEntitiesWithBase = {
638
- ...baseEntities,
639
- ...finalEntities,
640
- };
641
- const schemaResult = i.schema({
642
- entities: allEntitiesWithBase,
643
- links: cloneLinksDef(finalLinks),
644
- rooms: cloneRoomsDef(capturedRooms),
645
- });
646
- const frozenSchema = Object.freeze(schemaResult);
647
- if (!hasUnresolvedIncludes) {
648
- cachedInstantSchema = frozenSchema;
828
+ catch {
829
+ // If still can't resolve, entities should already be in allEntities from app domain composition
830
+ hasUnresolvedIncludes = true;
649
831
  }
650
- return frozenSchema;
651
- },
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;
652
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
+ });
653
881
  attachMeta(result, freezeMeta(meta));
654
882
  result.context = (options) => buildContext(result, options);
655
883
  result.contextString = (options) => contextToString(buildContext(result, options));
656
- result.fromDB = (db) => createConcreteDomain(result, db, resolveSchema(result));
657
- const reboundActions = seedActions.map((action) => bindAction(action, { name: action.name, domain: result }));
884
+ result.fromDB = (db, bindings) => createConcreteDomain(result, db, resolveSchema(result), bindings);
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];
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
+ });
658
905
  setStoredActions(result, [...reboundActions]);
659
- result.actions = (actionsInput) => {
906
+ setStoredActionMap(result, reboundActionMap);
907
+ result.actions = getStoredActionMap(result);
908
+ result.withActions = (actionsInput) => {
660
909
  const current = getStoredActions(result);
910
+ const currentMap = getStoredActionMap(result);
661
911
  const additions = normalizeActionCollection(result, actionsInput);
662
- return createDomainResult([...current, ...additions]);
912
+ return createDomainResult([...current, ...additions.actions], { ...currentMap, ...additions.actionMap });
663
913
  };
664
914
  result.getActions = () => [...getStoredActions(result)];
915
+ result.getActionMap = () => ({ ...getStoredActionMap(result) });
916
+ result.definition = () => result;
665
917
  return Object.freeze(result);
666
918
  };
667
- return createDomainResult();
919
+ return createDomainResult([], {});
920
+ },
921
+ schema(def) {
922
+ return this.withSchema(def);
668
923
  },
669
924
  };
925
+ return builder;
670
926
  }
671
927
  if (typeof arg === "string" && !arg.trim()) {
672
928
  throw new Error("domain() requires a name");
@@ -679,11 +935,34 @@ export function composeDomain(name, includes = []) {
679
935
  for (const include of includes) {
680
936
  builder = builder.includes(include);
681
937
  }
682
- return builder.schema({ entities: {}, links: {}, rooms: {} });
938
+ return builder.withSchema({ entities: {}, links: {}, rooms: {} });
939
+ }
940
+ /**
941
+ * Define a domain action without changing the public action contract.
942
+ *
943
+ * Convention for new actions:
944
+ *
945
+ * `async execute({ runtime, input }) { await runtime.db.transact([...]); }`
946
+ *
947
+ * Actions receive a runtime already scoped to the declaring domain. Nested
948
+ * action composition is available through `runtime.actions.*`.
949
+ */
950
+ function toJsonSchema(schema) {
951
+ try {
952
+ return z.toJSONSchema(schema, { target: "draft-7" });
953
+ }
954
+ catch {
955
+ return undefined;
956
+ }
683
957
  }
684
958
  export function defineDomainAction(action) {
685
- return action;
959
+ return Object.freeze({
960
+ ...action,
961
+ inputSchema: toJsonSchema(action.input),
962
+ outputSchema: toJsonSchema(action.output),
963
+ });
686
964
  }
965
+ export const defineAction = defineDomainAction;
687
966
  export function getDomainActions(source) {
688
967
  return getStoredActions(source);
689
968
  }