@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.
- package/README.md +260 -106
- package/SKILL.md +56 -0
- package/dist/cli/bin.d.ts +9 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +609 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/client-runtime.d.ts +25 -0
- package/dist/cli/client-runtime.d.ts.map +1 -0
- package/dist/cli/client-runtime.js +60 -0
- package/dist/cli/client-runtime.js.map +1 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +44 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/create-app.d.ts +66 -0
- package/dist/cli/create-app.d.ts.map +1 -0
- package/dist/cli/create-app.js +2948 -0
- package/dist/cli/create-app.js.map +1 -0
- package/dist/cli/http.d.ts +28 -0
- package/dist/cli/http.d.ts.map +1 -0
- package/dist/cli/http.js +113 -0
- package/dist/cli/http.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/server.d.ts +3 -0
- package/dist/cli/server.d.ts.map +1 -0
- package/dist/cli/server.js +440 -0
- package/dist/cli/server.js.map +1 -0
- package/dist/cli/types.d.ts +61 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/ui.d.ts +3 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +138 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/context.test-runner.js +3 -1
- package/dist/context.test-runner.js.map +1 -1
- package/dist/domain-doc.d.ts +2 -0
- package/dist/domain-doc.d.ts.map +1 -1
- package/dist/domain-doc.js +14 -0
- package/dist/domain-doc.js.map +1 -1
- package/dist/index.d.ts +228 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +397 -118
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +21 -21
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +213 -345
- package/dist/next.js.map +1 -1
- package/dist/polyfills/dom-events.d.ts +2 -0
- package/dist/polyfills/dom-events.d.ts.map +1 -0
- package/dist/polyfills/dom-events.js +92 -0
- package/dist/polyfills/dom-events.js.map +1 -0
- package/dist/runtime-handle.d.ts +45 -0
- package/dist/runtime-handle.d.ts.map +1 -0
- package/dist/runtime-handle.js +84 -0
- package/dist/runtime-handle.js.map +1 -0
- package/dist/runtime-step.d.ts.map +1 -1
- package/dist/runtime-step.js +2 -0
- package/dist/runtime-step.js.map +1 -1
- package/dist/runtime.d.ts +9 -8
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +80 -24
- package/dist/runtime.js.map +1 -1
- package/package.json +44 -7
package/dist/index.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { i } from "@instantdb/core";
|
|
2
|
-
import {
|
|
3
|
-
export {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return { doc:
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
608
|
-
|
|
819
|
+
const links = other.links;
|
|
820
|
+
if (links) {
|
|
821
|
+
finalLinks = { ...finalLinks, ...links };
|
|
609
822
|
}
|
|
610
823
|
}
|
|
611
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
}
|