@almadar/runtime 5.8.2 → 5.8.4
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/dist/{OrbitalServerRuntime-BP5sz5Bn.d.ts → OrbitalServerRuntime-Bv8y7nIu.d.ts} +46 -37
- package/dist/OrbitalServerRuntime.d.ts +2 -2
- package/dist/OrbitalServerRuntime.js +1 -1775
- package/dist/OrbitalServerRuntime.js.map +1 -1
- package/dist/{chunk-6XUD4OHN.js → chunk-C4YIYOJI.js} +2991 -1200
- package/dist/chunk-C4YIYOJI.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-6XUD4OHN.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { resolveBinding, evaluate, createMinimalContext, evaluateGuard } from '@almadar/evaluator';
|
|
2
2
|
export { createMinimalContext } from '@almadar/evaluator';
|
|
3
3
|
import { isKnownOperator } from '@almadar/std';
|
|
4
|
+
import * as nodeModule from 'module';
|
|
5
|
+
import { isInlineTrait, isEntityCall, OrbitalSchemaSchema, isEntityReference, parseEntityRef, parseImportedTraitRef, isPageReference, isPageReferenceString, isPageReferenceObject, parsePageRef } from '@almadar/core';
|
|
4
6
|
import { faker } from '@faker-js/faker';
|
|
5
|
-
import { OrbitalSchemaSchema, isEntityCall, isEntityReference, parseEntityRef, parseImportedTraitRef, isPageReference, isPageReferenceString, isPageReferenceObject, parsePageRef } from '@almadar/core';
|
|
6
7
|
|
|
7
8
|
// src/EventBus.ts
|
|
8
9
|
var EventBus = class {
|
|
@@ -175,6 +176,8 @@ function createLogger(namespace) {
|
|
|
175
176
|
error: (msg, data) => log("ERROR", msg, data)
|
|
176
177
|
};
|
|
177
178
|
}
|
|
179
|
+
|
|
180
|
+
// src/BindingResolver.ts
|
|
178
181
|
var bindLog = createLogger("almadar:runtime:bindings");
|
|
179
182
|
var renderLog = createLogger("almadar:runtime:render-ui");
|
|
180
183
|
var CLIENT_ONLY_BINDING_ROOTS = /* @__PURE__ */ new Set(["trait"]);
|
|
@@ -1815,6 +1818,16 @@ var MockPersistenceAdapter = class {
|
|
|
1815
1818
|
*/
|
|
1816
1819
|
generateFieldValue(entityName, field, index) {
|
|
1817
1820
|
const fieldTypeLc = field.type.toLowerCase();
|
|
1821
|
+
mockLog.debug("field:generate", {
|
|
1822
|
+
entityName,
|
|
1823
|
+
fieldName: field.name,
|
|
1824
|
+
fieldType: fieldTypeLc,
|
|
1825
|
+
hasValues: !!field.values?.length,
|
|
1826
|
+
valuesCount: field.values?.length ?? 0,
|
|
1827
|
+
values: field.values?.length ? field.values.join(",") : null,
|
|
1828
|
+
format: field.format ?? null,
|
|
1829
|
+
hasDefault: field.default !== void 0
|
|
1830
|
+
});
|
|
1818
1831
|
const isNumeric = fieldTypeLc === "number" || fieldTypeLc === "integer";
|
|
1819
1832
|
if (isNumeric && field.default !== void 0) {
|
|
1820
1833
|
return field.default;
|
|
@@ -1853,7 +1866,7 @@ var MockPersistenceAdapter = class {
|
|
|
1853
1866
|
* — the schema is the source of truth. If a caller needs a real email, they
|
|
1854
1867
|
* declare `format: "email"`; if they need an enum, they declare `values: [...]`.
|
|
1855
1868
|
*/
|
|
1856
|
-
generateStringValue(
|
|
1869
|
+
generateStringValue(entityName, field, _index) {
|
|
1857
1870
|
if (field.values && field.values.length > 0) {
|
|
1858
1871
|
return faker.helpers.arrayElement(field.values);
|
|
1859
1872
|
}
|
|
@@ -1870,8 +1883,17 @@ var MockPersistenceAdapter = class {
|
|
|
1870
1883
|
return faker.date.recent().toISOString().split("T")[0];
|
|
1871
1884
|
case "datetime":
|
|
1872
1885
|
return faker.date.recent().toISOString();
|
|
1873
|
-
default:
|
|
1874
|
-
|
|
1886
|
+
default: {
|
|
1887
|
+
const value = faker.lorem.words(2);
|
|
1888
|
+
mockLog.warn("field:fallback-lorem", {
|
|
1889
|
+
entityName,
|
|
1890
|
+
fieldName: field.name,
|
|
1891
|
+
hasValues: false,
|
|
1892
|
+
format: field.format ?? null,
|
|
1893
|
+
generated: value
|
|
1894
|
+
});
|
|
1895
|
+
return value;
|
|
1896
|
+
}
|
|
1875
1897
|
}
|
|
1876
1898
|
}
|
|
1877
1899
|
/**
|
|
@@ -1984,509 +2006,822 @@ var MockPersistenceAdapter = class {
|
|
|
1984
2006
|
function createMockPersistence(config) {
|
|
1985
2007
|
return new MockPersistenceAdapter(config);
|
|
1986
2008
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
function isBrowser() {
|
|
1993
|
-
return typeof window !== "undefined" && !isElectron();
|
|
1994
|
-
}
|
|
1995
|
-
function isNode() {
|
|
1996
|
-
return typeof process !== "undefined" && !isBrowser();
|
|
1997
|
-
}
|
|
1998
|
-
var HttpImportChain = class _HttpImportChain {
|
|
1999
|
-
chain = [];
|
|
2000
|
-
/**
|
|
2001
|
-
* Try to add a path to the chain.
|
|
2002
|
-
* @returns Error message if circular, null if OK
|
|
2003
|
-
*/
|
|
2004
|
-
push(absolutePath) {
|
|
2005
|
-
if (this.chain.includes(absolutePath)) {
|
|
2006
|
-
const cycle = [
|
|
2007
|
-
...this.chain.slice(this.chain.indexOf(absolutePath)),
|
|
2008
|
-
absolutePath
|
|
2009
|
-
];
|
|
2010
|
-
return `Circular import detected: ${cycle.join(" -> ")}`;
|
|
2011
|
-
}
|
|
2012
|
-
this.chain.push(absolutePath);
|
|
2013
|
-
return null;
|
|
2009
|
+
var refResolverLog = createLogger("almadar:runtime:ref-resolver");
|
|
2010
|
+
function renameEventsInRenderUiConfig(node, rename) {
|
|
2011
|
+
if (node === null || node === void 0) return node;
|
|
2012
|
+
if (Array.isArray(node)) {
|
|
2013
|
+
return node.map((item) => renameEventsInRenderUiConfig(item, rename));
|
|
2014
2014
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2015
|
+
if (typeof node !== "object") return node;
|
|
2016
|
+
const obj = node;
|
|
2017
|
+
const next = { ...obj };
|
|
2018
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2019
|
+
if (key === "action" && typeof value === "string" && !value.startsWith("@")) {
|
|
2020
|
+
next[key] = rename(value) ?? value;
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
if (/^on[A-Z]/.test(key) && typeof value === "string" && !value.startsWith("@")) {
|
|
2024
|
+
next[key] = rename(value) ?? value;
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
if (key.endsWith("Event") && typeof value === "string" && !value.startsWith("@")) {
|
|
2028
|
+
next[key] = rename(value) ?? value;
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
if ((key === "actions" || key === "itemActions") && Array.isArray(value)) {
|
|
2032
|
+
const rewrittenArray = value.map((entry) => {
|
|
2033
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
2034
|
+
const action = entry;
|
|
2035
|
+
if (typeof action.event === "string" && !action.event.startsWith("@")) {
|
|
2036
|
+
return { ...action, event: rename(action.event) ?? action.event };
|
|
2037
|
+
}
|
|
2038
|
+
return action;
|
|
2039
|
+
});
|
|
2040
|
+
next[key] = rewrittenArray;
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
next[key] = renameEventsInRenderUiConfig(value, rename);
|
|
2020
2044
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2045
|
+
return next;
|
|
2046
|
+
}
|
|
2047
|
+
function renameEventsInEffects(effects, rename) {
|
|
2048
|
+
return effects.map((effect) => {
|
|
2049
|
+
if (!Array.isArray(effect)) return effect;
|
|
2050
|
+
if (effect[0] === "render-ui" && effect.length >= 3) {
|
|
2051
|
+
const slot = effect[1];
|
|
2052
|
+
const config = effect[2];
|
|
2053
|
+
const nextConfig = renameEventsInRenderUiConfig(config, rename);
|
|
2054
|
+
return [effect[0], slot, nextConfig, ...effect.slice(3)];
|
|
2055
|
+
}
|
|
2056
|
+
return effect;
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
function renameEntityInRenderUiConfig(node, oldName, newName) {
|
|
2060
|
+
if (node === null || node === void 0) return node;
|
|
2061
|
+
if (Array.isArray(node)) {
|
|
2062
|
+
return node.map((item) => renameEntityInRenderUiConfig(item, oldName, newName));
|
|
2028
2063
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2064
|
+
if (typeof node !== "object") return node;
|
|
2065
|
+
const obj = node;
|
|
2066
|
+
const next = { ...obj };
|
|
2067
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2068
|
+
if (key === "entity" && value === oldName) {
|
|
2069
|
+
next[key] = newName;
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
next[key] = renameEntityInRenderUiConfig(value, oldName, newName);
|
|
2034
2073
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2074
|
+
return next;
|
|
2075
|
+
}
|
|
2076
|
+
function renameEntityInEffects(effects, oldName, newName) {
|
|
2077
|
+
return effects.map((effect) => renameEntityInEffect(effect, oldName, newName));
|
|
2078
|
+
}
|
|
2079
|
+
var ENTITY_AT_POS_1 = /* @__PURE__ */ new Set(["fetch", "ref", "deref", "spawn"]);
|
|
2080
|
+
var ALL_ARGS_ARE_EFFECTS = /* @__PURE__ */ new Set([
|
|
2081
|
+
"do",
|
|
2082
|
+
"atomic",
|
|
2083
|
+
"async/race",
|
|
2084
|
+
"async/all",
|
|
2085
|
+
"async/sequence"
|
|
2086
|
+
]);
|
|
2087
|
+
var ARGS_FROM_POS_2_ARE_EFFECTS = /* @__PURE__ */ new Set([
|
|
2088
|
+
"if",
|
|
2089
|
+
"when",
|
|
2090
|
+
"let",
|
|
2091
|
+
"async/delay",
|
|
2092
|
+
"async/debounce",
|
|
2093
|
+
"async/throttle",
|
|
2094
|
+
"async/interval"
|
|
2095
|
+
]);
|
|
2096
|
+
function renameEntityInEffect(effect, oldName, newName) {
|
|
2097
|
+
if (!Array.isArray(effect) || effect.length === 0) return effect;
|
|
2098
|
+
const op = effect[0];
|
|
2099
|
+
if (typeof op !== "string") return effect;
|
|
2100
|
+
if (op === "render-ui" && effect.length >= 3) {
|
|
2101
|
+
const [, slot, config, ...rest] = effect;
|
|
2102
|
+
const nextConfig = renameEntityInRenderUiConfig(config, oldName, newName);
|
|
2103
|
+
return [op, slot, nextConfig, ...rest];
|
|
2037
2104
|
}
|
|
2038
|
-
|
|
2039
|
-
return
|
|
2105
|
+
if (op === "persist" && effect.length >= 3 && effect[2] === oldName) {
|
|
2106
|
+
return [op, effect[1], newName, ...effect.slice(3)];
|
|
2040
2107
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2108
|
+
if (ENTITY_AT_POS_1.has(op) && effect[1] === oldName) {
|
|
2109
|
+
return [op, newName, ...effect.slice(2)];
|
|
2043
2110
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2111
|
+
const skipFirstNonEffectArg = ARGS_FROM_POS_2_ARE_EFFECTS.has(op);
|
|
2112
|
+
const recurseAll = ALL_ARGS_ARE_EFFECTS.has(op);
|
|
2113
|
+
if (recurseAll || skipFirstNonEffectArg) {
|
|
2114
|
+
const startIndex = skipFirstNonEffectArg ? 2 : 1;
|
|
2115
|
+
return effect.map((arg, i) => {
|
|
2116
|
+
if (i < startIndex) return arg;
|
|
2117
|
+
if (Array.isArray(arg)) {
|
|
2118
|
+
return renameEntityInEffect(arg, oldName, newName);
|
|
2119
|
+
}
|
|
2120
|
+
return arg;
|
|
2121
|
+
});
|
|
2046
2122
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2123
|
+
return effect;
|
|
2124
|
+
}
|
|
2125
|
+
function applyLinkedEntityRename(trait, linkedEntity) {
|
|
2126
|
+
const atomLinked = trait.linkedEntity;
|
|
2127
|
+
if (!linkedEntity || !atomLinked || linkedEntity === atomLinked) return trait;
|
|
2128
|
+
const sm = trait.stateMachine;
|
|
2129
|
+
if (!sm) return { ...trait, linkedEntity };
|
|
2130
|
+
const nextTransitions = (sm.transitions ?? []).map((t) => {
|
|
2131
|
+
const nextEffects = t.effects ? renameEntityInEffects(
|
|
2132
|
+
t.effects,
|
|
2133
|
+
atomLinked,
|
|
2134
|
+
linkedEntity
|
|
2135
|
+
) : t.effects;
|
|
2136
|
+
return { ...t, effects: nextEffects };
|
|
2137
|
+
});
|
|
2138
|
+
refResolverLog.info("linkedEntity:rename", {
|
|
2139
|
+
trait: trait.name,
|
|
2140
|
+
from: atomLinked,
|
|
2141
|
+
to: linkedEntity,
|
|
2142
|
+
transitionCount: nextTransitions.length
|
|
2143
|
+
});
|
|
2144
|
+
return {
|
|
2145
|
+
...trait,
|
|
2146
|
+
linkedEntity,
|
|
2147
|
+
stateMachine: { ...sm, transitions: nextTransitions }
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function applyEventRenames(trait, renames) {
|
|
2151
|
+
if (!renames || Object.keys(renames).length === 0) return trait;
|
|
2152
|
+
const rename = (k) => k !== void 0 && k in renames ? renames[k] : k;
|
|
2153
|
+
const sm = trait.stateMachine;
|
|
2154
|
+
if (!sm) return trait;
|
|
2155
|
+
const nextTransitions = (sm.transitions ?? []).map((t) => {
|
|
2156
|
+
const nextEvent = rename(t.event) ?? t.event;
|
|
2157
|
+
const nextEffects = t.effects ? renameEventsInEffects(t.effects, rename) : t.effects;
|
|
2158
|
+
return { ...t, event: nextEvent, effects: nextEffects };
|
|
2159
|
+
});
|
|
2160
|
+
const nextEvents = (sm.events ?? []).map((e) => {
|
|
2161
|
+
const newKey = rename(e.key);
|
|
2162
|
+
if (newKey === e.key) return e;
|
|
2163
|
+
return { ...e, key: newKey ?? e.key };
|
|
2164
|
+
});
|
|
2165
|
+
const nextEmits = (trait.emits ?? []).map((em) => {
|
|
2166
|
+
if (typeof em === "string") return rename(em) ?? em;
|
|
2167
|
+
const newEvent = rename(em.event);
|
|
2168
|
+
return newEvent === em.event ? em : { ...em, event: newEvent ?? em.event };
|
|
2169
|
+
});
|
|
2170
|
+
return {
|
|
2171
|
+
...trait,
|
|
2172
|
+
stateMachine: {
|
|
2173
|
+
...sm,
|
|
2174
|
+
transitions: nextTransitions,
|
|
2175
|
+
events: nextEvents
|
|
2176
|
+
},
|
|
2177
|
+
emits: nextEmits
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
var ReferenceResolver = class {
|
|
2181
|
+
loader;
|
|
2049
2182
|
options;
|
|
2050
|
-
|
|
2183
|
+
localTraits;
|
|
2184
|
+
loaderInitialized = false;
|
|
2051
2185
|
constructor(options) {
|
|
2052
|
-
this.options =
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2186
|
+
this.options = options;
|
|
2187
|
+
this.loader = options.loader;
|
|
2188
|
+
this.localTraits = options.localTraits ?? /* @__PURE__ */ new Map();
|
|
2189
|
+
}
|
|
2190
|
+
async ensureLoader() {
|
|
2191
|
+
if (this.loader || this.loaderInitialized) return;
|
|
2192
|
+
this.loaderInitialized = true;
|
|
2193
|
+
try {
|
|
2194
|
+
const { ExternalOrbitalLoader } = await import('./external-loader-UHZQPCKW.js');
|
|
2195
|
+
this.loader = new ExternalOrbitalLoader(this.options);
|
|
2196
|
+
} catch {
|
|
2197
|
+
}
|
|
2061
2198
|
}
|
|
2062
2199
|
/**
|
|
2063
|
-
*
|
|
2200
|
+
* Resolve all references in an orbital.
|
|
2064
2201
|
*/
|
|
2065
|
-
async
|
|
2066
|
-
const
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2202
|
+
async resolve(orbital, sourcePath, chain) {
|
|
2203
|
+
const errors = [];
|
|
2204
|
+
const warnings = [];
|
|
2205
|
+
const importChain = chain ?? { push: () => null, pop: () => {
|
|
2206
|
+
}, clone() {
|
|
2207
|
+
return this;
|
|
2208
|
+
} };
|
|
2209
|
+
const importsResult = await this.resolveImports(
|
|
2210
|
+
orbital.uses ?? [],
|
|
2211
|
+
sourcePath,
|
|
2212
|
+
importChain
|
|
2213
|
+
);
|
|
2214
|
+
if (!importsResult.success) {
|
|
2215
|
+
return { success: false, errors: importsResult.errors };
|
|
2070
2216
|
}
|
|
2071
|
-
const
|
|
2072
|
-
const
|
|
2073
|
-
if (
|
|
2074
|
-
|
|
2217
|
+
const imports = importsResult.data;
|
|
2218
|
+
const entityResult = this.resolveEntity(orbital.entity, imports);
|
|
2219
|
+
if (!entityResult.success) {
|
|
2220
|
+
errors.push(...entityResult.errors);
|
|
2075
2221
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
return { success: true, data: cached };
|
|
2080
|
-
}
|
|
2081
|
-
const loadResult = await this.fetchSchema(absoluteUrl);
|
|
2082
|
-
if (!loadResult.success) {
|
|
2083
|
-
return loadResult;
|
|
2084
|
-
}
|
|
2085
|
-
const loaded = {
|
|
2086
|
-
schema: loadResult.data,
|
|
2087
|
-
sourcePath: absoluteUrl,
|
|
2088
|
-
importPath
|
|
2089
|
-
};
|
|
2090
|
-
this.cache.set(absoluteUrl, loaded);
|
|
2091
|
-
return { success: true, data: loaded };
|
|
2092
|
-
} finally {
|
|
2093
|
-
importChain.pop();
|
|
2222
|
+
const traitsResult = this.resolveTraits(orbital.traits, imports);
|
|
2223
|
+
if (!traitsResult.success) {
|
|
2224
|
+
errors.push(...traitsResult.errors);
|
|
2094
2225
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
*/
|
|
2099
|
-
async loadOrbital(importPath, orbitalName, fromPath, chain) {
|
|
2100
|
-
const schemaResult = await this.load(importPath, fromPath, chain);
|
|
2101
|
-
if (!schemaResult.success) {
|
|
2102
|
-
return schemaResult;
|
|
2226
|
+
const pagesResult = this.resolvePages(orbital.pages, imports);
|
|
2227
|
+
if (!pagesResult.success) {
|
|
2228
|
+
errors.push(...pagesResult.errors);
|
|
2103
2229
|
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
);
|
|
2110
|
-
if (!found) {
|
|
2111
|
-
return {
|
|
2112
|
-
success: false,
|
|
2113
|
-
error: `Orbital "${orbitalName}" not found in ${importPath}. Available: ${schema.orbitals.map((o) => o.name).join(", ")}`
|
|
2114
|
-
};
|
|
2115
|
-
}
|
|
2116
|
-
orbital = found;
|
|
2117
|
-
} else {
|
|
2118
|
-
if (schema.orbitals.length === 0) {
|
|
2119
|
-
return {
|
|
2120
|
-
success: false,
|
|
2121
|
-
error: `No orbitals found in ${importPath}`
|
|
2122
|
-
};
|
|
2123
|
-
}
|
|
2124
|
-
orbital = schema.orbitals[0];
|
|
2230
|
+
if (errors.length > 0) {
|
|
2231
|
+
return { success: false, errors };
|
|
2232
|
+
}
|
|
2233
|
+
if (!entityResult.success || !traitsResult.success || !pagesResult.success) {
|
|
2234
|
+
return { success: false, errors: ["Internal error: unexpected failure state"] };
|
|
2125
2235
|
}
|
|
2126
2236
|
return {
|
|
2127
2237
|
success: true,
|
|
2128
2238
|
data: {
|
|
2129
|
-
orbital,
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2239
|
+
name: orbital.name,
|
|
2240
|
+
entity: entityResult.data.entity,
|
|
2241
|
+
entitySource: entityResult.data.source,
|
|
2242
|
+
traits: traitsResult.data,
|
|
2243
|
+
pages: pagesResult.data,
|
|
2244
|
+
imports,
|
|
2245
|
+
original: orbital
|
|
2246
|
+
},
|
|
2247
|
+
warnings
|
|
2133
2248
|
};
|
|
2134
2249
|
}
|
|
2135
2250
|
/**
|
|
2136
|
-
* Resolve
|
|
2251
|
+
* Resolve `uses` declarations to loaded orbitals.
|
|
2137
2252
|
*/
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2253
|
+
async resolveImports(uses, sourcePath, chain) {
|
|
2254
|
+
const errors = [];
|
|
2255
|
+
const orbitals = /* @__PURE__ */ new Map();
|
|
2256
|
+
if (this.options.skipExternalLoading) {
|
|
2257
|
+
return {
|
|
2258
|
+
success: true,
|
|
2259
|
+
data: { orbitals },
|
|
2260
|
+
warnings: ["External loading skipped"]
|
|
2261
|
+
};
|
|
2144
2262
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2263
|
+
for (const use of uses) {
|
|
2264
|
+
if (orbitals.has(use.as)) {
|
|
2265
|
+
errors.push(`Duplicate import alias: ${use.as}`);
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
await this.ensureLoader();
|
|
2269
|
+
if (!this.loader) {
|
|
2270
|
+
errors.push(`No loader available to resolve import: ${use.from}`);
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
const loadResult = await this.loader.loadOrbital(
|
|
2274
|
+
use.from,
|
|
2275
|
+
void 0,
|
|
2276
|
+
sourcePath,
|
|
2277
|
+
chain
|
|
2278
|
+
);
|
|
2279
|
+
if (!loadResult.success) {
|
|
2280
|
+
errors.push(`Failed to load "${use.from}" as "${use.as}": ${loadResult.error}`);
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
orbitals.set(use.as, {
|
|
2284
|
+
alias: use.as,
|
|
2285
|
+
from: use.from,
|
|
2286
|
+
orbital: loadResult.data.orbital,
|
|
2287
|
+
sourcePath: loadResult.data.sourcePath
|
|
2288
|
+
});
|
|
2147
2289
|
}
|
|
2148
|
-
if (
|
|
2149
|
-
return
|
|
2290
|
+
if (errors.length > 0) {
|
|
2291
|
+
return { success: false, errors };
|
|
2150
2292
|
}
|
|
2151
|
-
return
|
|
2293
|
+
return { success: true, data: { orbitals }, warnings: [] };
|
|
2152
2294
|
}
|
|
2153
2295
|
/**
|
|
2154
|
-
* Resolve
|
|
2296
|
+
* Resolve entity reference.
|
|
2155
2297
|
*/
|
|
2156
|
-
|
|
2157
|
-
if (
|
|
2298
|
+
resolveEntity(entityRef, imports) {
|
|
2299
|
+
if (isEntityCall(entityRef)) {
|
|
2300
|
+
const fallbackName = entityRef.name ?? entityRef.extends.replace(/\.entity$/, "");
|
|
2158
2301
|
return {
|
|
2159
|
-
success:
|
|
2160
|
-
|
|
2302
|
+
success: true,
|
|
2303
|
+
data: {
|
|
2304
|
+
entity: {
|
|
2305
|
+
name: fallbackName,
|
|
2306
|
+
fields: entityRef.fields ?? [],
|
|
2307
|
+
...entityRef.persistence ? { persistence: entityRef.persistence } : {},
|
|
2308
|
+
...entityRef.collection ? { collection: entityRef.collection } : {}
|
|
2309
|
+
}
|
|
2310
|
+
},
|
|
2311
|
+
warnings: []
|
|
2161
2312
|
};
|
|
2162
2313
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2314
|
+
if (!isEntityReference(entityRef)) {
|
|
2315
|
+
return {
|
|
2316
|
+
success: true,
|
|
2317
|
+
data: { entity: entityRef },
|
|
2318
|
+
warnings: []
|
|
2319
|
+
};
|
|
2167
2320
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
/**
|
|
2171
|
-
* Resolve a scoped package path.
|
|
2172
|
-
*/
|
|
2173
|
-
resolveScopedPath(importPath) {
|
|
2174
|
-
const match = importPath.match(/^(@[^/]+)/);
|
|
2175
|
-
if (!match) {
|
|
2321
|
+
const parsed = parseEntityRef(entityRef);
|
|
2322
|
+
if (!parsed) {
|
|
2176
2323
|
return {
|
|
2177
2324
|
success: false,
|
|
2178
|
-
|
|
2325
|
+
errors: [`Invalid entity reference format: ${entityRef}. Expected "Alias.entity"`]
|
|
2179
2326
|
};
|
|
2180
2327
|
}
|
|
2181
|
-
const
|
|
2182
|
-
|
|
2183
|
-
if (!scopeRoot) {
|
|
2328
|
+
const imported = imports.orbitals.get(parsed.alias);
|
|
2329
|
+
if (!imported) {
|
|
2184
2330
|
return {
|
|
2185
2331
|
success: false,
|
|
2186
|
-
|
|
2332
|
+
errors: [
|
|
2333
|
+
`Unknown import alias in entity reference: ${parsed.alias}. Available aliases: ${Array.from(imports.orbitals.keys()).join(", ") || "none"}`
|
|
2334
|
+
]
|
|
2187
2335
|
};
|
|
2188
2336
|
}
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2337
|
+
const importedEntity = this.getEntityFromOrbital(imported.orbital);
|
|
2338
|
+
if (!importedEntity) {
|
|
2339
|
+
return {
|
|
2340
|
+
success: false,
|
|
2341
|
+
errors: [
|
|
2342
|
+
`Imported orbital "${parsed.alias}" does not have an inline entity. Entity references cannot be chained.`
|
|
2343
|
+
]
|
|
2344
|
+
};
|
|
2193
2345
|
}
|
|
2194
|
-
|
|
2346
|
+
const persistence = importedEntity.persistence ?? "persistent";
|
|
2347
|
+
return {
|
|
2348
|
+
success: true,
|
|
2349
|
+
data: {
|
|
2350
|
+
entity: importedEntity,
|
|
2351
|
+
source: {
|
|
2352
|
+
alias: parsed.alias,
|
|
2353
|
+
persistence
|
|
2354
|
+
}
|
|
2355
|
+
},
|
|
2356
|
+
warnings: []
|
|
2357
|
+
};
|
|
2195
2358
|
}
|
|
2196
2359
|
/**
|
|
2197
|
-
*
|
|
2360
|
+
* Get the entity from an orbital (handling EntityRef).
|
|
2198
2361
|
*/
|
|
2199
|
-
|
|
2200
|
-
const
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
absoluteUrl += ".orb";
|
|
2362
|
+
getEntityFromOrbital(orbital) {
|
|
2363
|
+
const entityRef = orbital.entity;
|
|
2364
|
+
if (typeof entityRef === "string") {
|
|
2365
|
+
return null;
|
|
2204
2366
|
}
|
|
2205
|
-
|
|
2367
|
+
if (isEntityCall(entityRef)) {
|
|
2368
|
+
const fallbackName = entityRef.name ?? entityRef.extends.replace(/\.entity$/, "");
|
|
2369
|
+
return {
|
|
2370
|
+
name: fallbackName,
|
|
2371
|
+
fields: entityRef.fields ?? [],
|
|
2372
|
+
...entityRef.persistence ? { persistence: entityRef.persistence } : {},
|
|
2373
|
+
...entityRef.collection ? { collection: entityRef.collection } : {}
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
return entityRef;
|
|
2206
2377
|
}
|
|
2207
2378
|
/**
|
|
2208
|
-
*
|
|
2379
|
+
* Resolve trait references.
|
|
2209
2380
|
*/
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
...this.options.fetchOptions,
|
|
2220
|
-
credentials: this.options.credentials,
|
|
2221
|
-
signal: controller.signal,
|
|
2222
|
-
headers: {
|
|
2223
|
-
Accept: "application/json",
|
|
2224
|
-
...this.options.fetchOptions?.headers
|
|
2225
|
-
}
|
|
2226
|
-
});
|
|
2227
|
-
if (!response.ok) {
|
|
2228
|
-
return {
|
|
2229
|
-
success: false,
|
|
2230
|
-
error: `HTTP ${response.status}: ${response.statusText} for ${url}`
|
|
2231
|
-
};
|
|
2232
|
-
}
|
|
2233
|
-
const text = await response.text();
|
|
2234
|
-
let data;
|
|
2235
|
-
try {
|
|
2236
|
-
data = JSON.parse(text);
|
|
2237
|
-
} catch (e) {
|
|
2238
|
-
return {
|
|
2239
|
-
success: false,
|
|
2240
|
-
error: `Invalid JSON in ${url}: ${e instanceof Error ? e.message : String(e)}`
|
|
2241
|
-
};
|
|
2242
|
-
}
|
|
2243
|
-
const parseResult = OrbitalSchemaSchema.safeParse(data);
|
|
2244
|
-
if (!parseResult.success) {
|
|
2245
|
-
const errors = parseResult.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
2246
|
-
return {
|
|
2247
|
-
success: false,
|
|
2248
|
-
error: `Invalid schema in ${url}:
|
|
2249
|
-
${errors}`
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
return { success: true, data: parseResult.data };
|
|
2253
|
-
} finally {
|
|
2254
|
-
clearTimeout(timeoutId);
|
|
2381
|
+
resolveTraits(traitRefs, imports) {
|
|
2382
|
+
const errors = [];
|
|
2383
|
+
const resolved = [];
|
|
2384
|
+
for (const traitRef of traitRefs) {
|
|
2385
|
+
const result = this.resolveTraitRef(traitRef, imports);
|
|
2386
|
+
if (!result.success) {
|
|
2387
|
+
errors.push(...result.errors);
|
|
2388
|
+
} else {
|
|
2389
|
+
resolved.push(result.data);
|
|
2255
2390
|
}
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2391
|
+
}
|
|
2392
|
+
if (errors.length > 0) {
|
|
2393
|
+
return { success: false, errors };
|
|
2394
|
+
}
|
|
2395
|
+
return { success: true, data: resolved, warnings: [] };
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Resolve a single trait reference.
|
|
2399
|
+
*/
|
|
2400
|
+
resolveTraitRef(traitRef, imports) {
|
|
2401
|
+
if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
|
|
2402
|
+
return {
|
|
2403
|
+
success: true,
|
|
2404
|
+
data: {
|
|
2405
|
+
trait: traitRef,
|
|
2406
|
+
source: { type: "inline" }
|
|
2407
|
+
},
|
|
2408
|
+
warnings: []
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
if (typeof traitRef !== "string" && "ref" in traitRef) {
|
|
2412
|
+
const refObj = traitRef;
|
|
2413
|
+
return this.resolveTraitRefString(
|
|
2414
|
+
refObj.ref,
|
|
2415
|
+
imports,
|
|
2416
|
+
refObj.config,
|
|
2417
|
+
refObj.linkedEntity,
|
|
2418
|
+
refObj.name,
|
|
2419
|
+
refObj.events,
|
|
2420
|
+
refObj.listens
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
if (typeof traitRef === "string") {
|
|
2424
|
+
return this.resolveTraitRefString(traitRef, imports);
|
|
2425
|
+
}
|
|
2426
|
+
return {
|
|
2427
|
+
success: false,
|
|
2428
|
+
errors: [`Unknown trait reference format: ${JSON.stringify(traitRef)}`]
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Resolve a trait reference string.
|
|
2433
|
+
*/
|
|
2434
|
+
resolveTraitRefString(ref, imports, config, linkedEntity, overrideName, eventRenames, listensOverride) {
|
|
2435
|
+
const parsed = parseImportedTraitRef(ref);
|
|
2436
|
+
if (parsed) {
|
|
2437
|
+
const imported = imports.orbitals.get(parsed.alias);
|
|
2438
|
+
if (!imported) {
|
|
2258
2439
|
return {
|
|
2259
2440
|
success: false,
|
|
2260
|
-
|
|
2441
|
+
errors: [
|
|
2442
|
+
`Unknown import alias in trait reference: ${parsed.alias}. Available aliases: ${Array.from(imports.orbitals.keys()).join(", ") || "none"}`
|
|
2443
|
+
]
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
const trait = this.findTraitInOrbital(imported.orbital, parsed.traitName);
|
|
2447
|
+
if (!trait) {
|
|
2448
|
+
return {
|
|
2449
|
+
success: false,
|
|
2450
|
+
errors: [
|
|
2451
|
+
`Trait "${parsed.traitName}" not found in imported orbital "${parsed.alias}". Available traits: ${this.listTraitsInOrbital(imported.orbital).join(", ") || "none"}`
|
|
2452
|
+
]
|
|
2261
2453
|
};
|
|
2262
2454
|
}
|
|
2455
|
+
const baseTrait = overrideName ? { ...trait, name: overrideName } : trait;
|
|
2456
|
+
const reboundTrait = applyLinkedEntityRename(baseTrait, linkedEntity);
|
|
2457
|
+
const renamedTrait = applyEventRenames(reboundTrait, eventRenames);
|
|
2458
|
+
const finalTrait = listensOverride !== void 0 ? { ...renamedTrait, listens: listensOverride } : renamedTrait;
|
|
2459
|
+
if (listensOverride !== void 0) {
|
|
2460
|
+
refResolverLog.info("listens-override:imported", {
|
|
2461
|
+
trait: finalTrait.name,
|
|
2462
|
+
ref,
|
|
2463
|
+
atomListens: trait.listens?.length ?? 0,
|
|
2464
|
+
callSiteListens: listensOverride.length
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2263
2467
|
return {
|
|
2264
|
-
success:
|
|
2265
|
-
|
|
2468
|
+
success: true,
|
|
2469
|
+
data: {
|
|
2470
|
+
trait: finalTrait,
|
|
2471
|
+
source: { type: "imported", alias: parsed.alias, traitName: parsed.traitName },
|
|
2472
|
+
config,
|
|
2473
|
+
linkedEntity
|
|
2474
|
+
},
|
|
2475
|
+
warnings: []
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
const localTrait = this.localTraits.get(ref);
|
|
2479
|
+
if (localTrait) {
|
|
2480
|
+
const baseLocal = overrideName ? { ...localTrait, name: overrideName } : localTrait;
|
|
2481
|
+
const reboundLocal = applyLinkedEntityRename(baseLocal, linkedEntity);
|
|
2482
|
+
const renamedLocalTrait = applyEventRenames(reboundLocal, eventRenames);
|
|
2483
|
+
const finalLocalTrait = listensOverride !== void 0 ? { ...renamedLocalTrait, listens: listensOverride } : renamedLocalTrait;
|
|
2484
|
+
if (listensOverride !== void 0) {
|
|
2485
|
+
refResolverLog.info("listens-override:local", {
|
|
2486
|
+
trait: finalLocalTrait.name,
|
|
2487
|
+
ref,
|
|
2488
|
+
atomListens: localTrait.listens?.length ?? 0,
|
|
2489
|
+
callSiteListens: listensOverride.length
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
return {
|
|
2493
|
+
success: true,
|
|
2494
|
+
data: {
|
|
2495
|
+
trait: finalLocalTrait,
|
|
2496
|
+
source: { type: "local", name: ref },
|
|
2497
|
+
config,
|
|
2498
|
+
linkedEntity
|
|
2499
|
+
},
|
|
2500
|
+
warnings: []
|
|
2266
2501
|
};
|
|
2267
2502
|
}
|
|
2503
|
+
return {
|
|
2504
|
+
success: false,
|
|
2505
|
+
errors: [
|
|
2506
|
+
`Trait "${ref}" not found. For imported traits, use format "Alias.traits.TraitName". Local traits available: ${Array.from(this.localTraits.keys()).join(", ") || "none"}`
|
|
2507
|
+
]
|
|
2508
|
+
};
|
|
2268
2509
|
}
|
|
2269
2510
|
/**
|
|
2270
|
-
*
|
|
2511
|
+
* Find a trait in an orbital by name.
|
|
2271
2512
|
*/
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2513
|
+
findTraitInOrbital(orbital, traitName) {
|
|
2514
|
+
for (const traitRef of orbital.traits) {
|
|
2515
|
+
if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
|
|
2516
|
+
if (traitRef.name === traitName) {
|
|
2517
|
+
return traitRef;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
if (typeof traitRef !== "string" && "ref" in traitRef) {
|
|
2521
|
+
const refObj = traitRef;
|
|
2522
|
+
if (refObj.ref === traitName || refObj.name === traitName) ;
|
|
2281
2523
|
}
|
|
2282
2524
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2525
|
+
return null;
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* List trait names in an orbital.
|
|
2529
|
+
*/
|
|
2530
|
+
listTraitsInOrbital(orbital) {
|
|
2531
|
+
const names = [];
|
|
2532
|
+
for (const traitRef of orbital.traits) {
|
|
2533
|
+
if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
|
|
2534
|
+
names.push(traitRef.name);
|
|
2535
|
+
}
|
|
2292
2536
|
}
|
|
2293
|
-
return
|
|
2537
|
+
return names;
|
|
2294
2538
|
}
|
|
2295
2539
|
/**
|
|
2296
|
-
*
|
|
2540
|
+
* Resolve page references.
|
|
2297
2541
|
*/
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2542
|
+
resolvePages(pageRefs, imports) {
|
|
2543
|
+
const errors = [];
|
|
2544
|
+
const resolved = [];
|
|
2545
|
+
for (const pageRef of pageRefs) {
|
|
2546
|
+
const result = this.resolvePageRef(pageRef, imports);
|
|
2547
|
+
if (!result.success) {
|
|
2548
|
+
errors.push(...result.errors);
|
|
2549
|
+
} else {
|
|
2550
|
+
resolved.push(result.data);
|
|
2307
2551
|
}
|
|
2308
2552
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
return url.slice(0, lastSlash);
|
|
2553
|
+
if (errors.length > 0) {
|
|
2554
|
+
return { success: false, errors };
|
|
2312
2555
|
}
|
|
2313
|
-
return
|
|
2556
|
+
return { success: true, data: resolved, warnings: [] };
|
|
2314
2557
|
}
|
|
2315
2558
|
/**
|
|
2316
|
-
*
|
|
2559
|
+
* Resolve a single page reference.
|
|
2317
2560
|
*/
|
|
2318
|
-
|
|
2319
|
-
|
|
2561
|
+
resolvePageRef(pageRef, imports) {
|
|
2562
|
+
if (!isPageReference(pageRef)) {
|
|
2563
|
+
return {
|
|
2564
|
+
success: true,
|
|
2565
|
+
data: {
|
|
2566
|
+
page: pageRef,
|
|
2567
|
+
source: { type: "inline" },
|
|
2568
|
+
pathOverridden: false
|
|
2569
|
+
},
|
|
2570
|
+
warnings: []
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
if (isPageReferenceString(pageRef)) {
|
|
2574
|
+
return this.resolvePageRefString(pageRef, imports);
|
|
2575
|
+
}
|
|
2576
|
+
if (isPageReferenceObject(pageRef)) {
|
|
2577
|
+
return this.resolvePageRefObject(pageRef, imports);
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
success: false,
|
|
2581
|
+
errors: [`Unknown page reference format: ${JSON.stringify(pageRef)}`]
|
|
2582
|
+
};
|
|
2320
2583
|
}
|
|
2321
2584
|
/**
|
|
2322
|
-
*
|
|
2585
|
+
* Resolve a page reference string.
|
|
2323
2586
|
*/
|
|
2324
|
-
|
|
2325
|
-
|
|
2587
|
+
resolvePageRefString(ref, imports) {
|
|
2588
|
+
const parsed = parsePageRef(ref);
|
|
2589
|
+
if (!parsed) {
|
|
2590
|
+
return {
|
|
2591
|
+
success: false,
|
|
2592
|
+
errors: [`Invalid page reference format: ${ref}. Expected "Alias.pages.PageName"`]
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
const imported = imports.orbitals.get(parsed.alias);
|
|
2596
|
+
if (!imported) {
|
|
2597
|
+
return {
|
|
2598
|
+
success: false,
|
|
2599
|
+
errors: [
|
|
2600
|
+
`Unknown import alias in page reference: ${parsed.alias}. Available aliases: ${Array.from(imports.orbitals.keys()).join(", ") || "none"}`
|
|
2601
|
+
]
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
const page = this.findPageInOrbital(imported.orbital, parsed.pageName);
|
|
2605
|
+
if (!page) {
|
|
2606
|
+
return {
|
|
2607
|
+
success: false,
|
|
2608
|
+
errors: [
|
|
2609
|
+
`Page "${parsed.pageName}" not found in imported orbital "${parsed.alias}". Available pages: ${this.listPagesInOrbital(imported.orbital).join(", ") || "none"}`
|
|
2610
|
+
]
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
return {
|
|
2614
|
+
success: true,
|
|
2615
|
+
data: {
|
|
2616
|
+
page,
|
|
2617
|
+
source: { type: "imported", alias: parsed.alias, pageName: parsed.pageName },
|
|
2618
|
+
pathOverridden: false
|
|
2619
|
+
},
|
|
2620
|
+
warnings: []
|
|
2621
|
+
};
|
|
2326
2622
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2623
|
+
/**
|
|
2624
|
+
* Resolve a page reference object with optional path override.
|
|
2625
|
+
*/
|
|
2626
|
+
resolvePageRefObject(refObj, imports) {
|
|
2627
|
+
const baseResult = this.resolvePageRefString(refObj.ref, imports);
|
|
2628
|
+
if (!baseResult.success) {
|
|
2629
|
+
return baseResult;
|
|
2630
|
+
}
|
|
2631
|
+
const resolved = baseResult.data;
|
|
2632
|
+
if (refObj.path) {
|
|
2633
|
+
const originalPath = resolved.page.path;
|
|
2634
|
+
resolved.page = {
|
|
2635
|
+
...resolved.page,
|
|
2636
|
+
path: refObj.path
|
|
2637
|
+
};
|
|
2638
|
+
resolved.pathOverridden = true;
|
|
2639
|
+
resolved.originalPath = originalPath;
|
|
2640
|
+
}
|
|
2641
|
+
return {
|
|
2642
|
+
success: true,
|
|
2643
|
+
data: resolved,
|
|
2644
|
+
warnings: baseResult.warnings
|
|
2645
|
+
};
|
|
2334
2646
|
}
|
|
2335
|
-
|
|
2647
|
+
/**
|
|
2648
|
+
* Find a page in an orbital by name.
|
|
2649
|
+
*/
|
|
2650
|
+
findPageInOrbital(orbital, pageName) {
|
|
2651
|
+
const pages = orbital.pages;
|
|
2652
|
+
if (!pages) return null;
|
|
2653
|
+
for (const pageRef of pages) {
|
|
2654
|
+
if (typeof pageRef !== "string" && !("ref" in pageRef)) {
|
|
2655
|
+
const page = pageRef;
|
|
2656
|
+
if (page.name === pageName) {
|
|
2657
|
+
return { ...page };
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2336
2661
|
return null;
|
|
2337
2662
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2663
|
+
/**
|
|
2664
|
+
* List page names in an orbital.
|
|
2665
|
+
*/
|
|
2666
|
+
listPagesInOrbital(orbital) {
|
|
2667
|
+
const pages = orbital.pages;
|
|
2668
|
+
if (!pages) return [];
|
|
2669
|
+
const names = [];
|
|
2670
|
+
for (const pageRef of pages) {
|
|
2671
|
+
if (typeof pageRef !== "string" && !("ref" in pageRef)) {
|
|
2672
|
+
names.push(pageRef.name);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return names;
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Add local traits for resolution.
|
|
2679
|
+
*/
|
|
2680
|
+
addLocalTraits(traits) {
|
|
2681
|
+
for (const trait of traits) {
|
|
2682
|
+
this.localTraits.set(trait.name, trait);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Clear loader cache.
|
|
2687
|
+
*/
|
|
2688
|
+
clearCache() {
|
|
2689
|
+
this.loader?.clearCache();
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
async function resolveSchema(schema, options) {
|
|
2693
|
+
const resolver = new ReferenceResolver(options);
|
|
2694
|
+
const errors = [];
|
|
2695
|
+
const warnings = [];
|
|
2696
|
+
const resolved = [];
|
|
2697
|
+
for (const orbital of schema.orbitals) {
|
|
2698
|
+
const inlineTraits = orbital.traits.filter(
|
|
2699
|
+
(t) => typeof t !== "string" && "stateMachine" in t
|
|
2700
|
+
);
|
|
2701
|
+
resolver.addLocalTraits(inlineTraits);
|
|
2702
|
+
}
|
|
2703
|
+
for (const orbital of schema.orbitals) {
|
|
2704
|
+
const result = await resolver.resolve(orbital);
|
|
2705
|
+
if (!result.success) {
|
|
2706
|
+
errors.push(`Orbital "${orbital.name}": ${result.errors.join(", ")}`);
|
|
2707
|
+
} else {
|
|
2708
|
+
resolved.push(result.data);
|
|
2709
|
+
warnings.push(...result.warnings.map((w) => `Orbital "${orbital.name}": ${w}`));
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (errors.length > 0) {
|
|
2713
|
+
return { success: false, errors };
|
|
2343
2714
|
}
|
|
2715
|
+
return { success: true, data: resolved, warnings };
|
|
2344
2716
|
}
|
|
2345
|
-
|
|
2717
|
+
|
|
2718
|
+
// src/loader/schema-loader.ts
|
|
2719
|
+
function isElectron() {
|
|
2720
|
+
return typeof process !== "undefined" && !!process.versions?.electron;
|
|
2721
|
+
}
|
|
2722
|
+
function isBrowser() {
|
|
2723
|
+
return typeof window !== "undefined" && !isElectron();
|
|
2724
|
+
}
|
|
2725
|
+
function isNode() {
|
|
2726
|
+
return typeof process !== "undefined" && !isBrowser();
|
|
2727
|
+
}
|
|
2728
|
+
var HttpImportChain = class _HttpImportChain {
|
|
2346
2729
|
chain = [];
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2730
|
+
/**
|
|
2731
|
+
* Try to add a path to the chain.
|
|
2732
|
+
* @returns Error message if circular, null if OK
|
|
2733
|
+
*/
|
|
2734
|
+
push(absolutePath) {
|
|
2735
|
+
if (this.chain.includes(absolutePath)) {
|
|
2350
2736
|
const cycle = [
|
|
2351
|
-
...this.chain.slice(this.chain.indexOf(
|
|
2352
|
-
|
|
2737
|
+
...this.chain.slice(this.chain.indexOf(absolutePath)),
|
|
2738
|
+
absolutePath
|
|
2353
2739
|
];
|
|
2354
2740
|
return `Circular import detected: ${cycle.join(" -> ")}`;
|
|
2355
2741
|
}
|
|
2356
|
-
this.chain.push(
|
|
2742
|
+
this.chain.push(absolutePath);
|
|
2357
2743
|
return null;
|
|
2358
2744
|
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Remove the last path from the chain.
|
|
2747
|
+
*/
|
|
2359
2748
|
pop() {
|
|
2360
2749
|
this.chain.pop();
|
|
2361
2750
|
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Clone the chain for nested loading.
|
|
2753
|
+
*/
|
|
2362
2754
|
clone() {
|
|
2363
|
-
const newChain = new
|
|
2755
|
+
const newChain = new _HttpImportChain();
|
|
2364
2756
|
newChain.chain = [...this.chain];
|
|
2365
2757
|
return newChain;
|
|
2366
2758
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
return
|
|
2759
|
+
};
|
|
2760
|
+
var HttpLoaderCache = class {
|
|
2761
|
+
cache = /* @__PURE__ */ new Map();
|
|
2762
|
+
get(url) {
|
|
2763
|
+
return this.cache.get(url);
|
|
2764
|
+
}
|
|
2765
|
+
set(url, schema) {
|
|
2766
|
+
this.cache.set(url, schema);
|
|
2767
|
+
}
|
|
2768
|
+
has(url) {
|
|
2769
|
+
return this.cache.has(url);
|
|
2770
|
+
}
|
|
2771
|
+
clear() {
|
|
2772
|
+
this.cache.clear();
|
|
2773
|
+
}
|
|
2774
|
+
get size() {
|
|
2775
|
+
return this.cache.size;
|
|
2372
2776
|
}
|
|
2373
2777
|
};
|
|
2374
|
-
var
|
|
2778
|
+
var HttpLoader = class {
|
|
2375
2779
|
options;
|
|
2376
|
-
|
|
2377
|
-
fsLoader = null;
|
|
2378
|
-
fsLoaderInitialized = false;
|
|
2379
|
-
cache = /* @__PURE__ */ new Map();
|
|
2780
|
+
cache;
|
|
2380
2781
|
constructor(options) {
|
|
2381
|
-
this.options =
|
|
2382
|
-
this.httpLoader = new HttpLoader({
|
|
2782
|
+
this.options = {
|
|
2383
2783
|
basePath: options.basePath,
|
|
2384
|
-
stdLibPath: options.stdLibPath,
|
|
2385
|
-
scopedPaths: options.scopedPaths,
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
*/
|
|
2392
|
-
async initFsLoader() {
|
|
2393
|
-
if (this.fsLoaderInitialized) {
|
|
2394
|
-
return;
|
|
2395
|
-
}
|
|
2396
|
-
this.fsLoaderInitialized = true;
|
|
2397
|
-
if (this.options.forceLoader === "http") {
|
|
2398
|
-
return;
|
|
2399
|
-
}
|
|
2400
|
-
const module = await getExternalLoaderModule();
|
|
2401
|
-
if (module) {
|
|
2402
|
-
this.fsLoader = new module.ExternalOrbitalLoader({
|
|
2403
|
-
basePath: this.options.basePath,
|
|
2404
|
-
stdLibPath: this.options.stdLibPath,
|
|
2405
|
-
scopedPaths: this.options.scopedPaths,
|
|
2406
|
-
...this.options.fileSystem
|
|
2407
|
-
});
|
|
2408
|
-
}
|
|
2784
|
+
stdLibPath: options.stdLibPath ?? "",
|
|
2785
|
+
scopedPaths: options.scopedPaths ?? {},
|
|
2786
|
+
fetchOptions: options.fetchOptions,
|
|
2787
|
+
timeout: options.timeout ?? 3e4,
|
|
2788
|
+
credentials: options.credentials ?? "same-origin"
|
|
2789
|
+
};
|
|
2790
|
+
this.cache = new HttpLoaderCache();
|
|
2409
2791
|
}
|
|
2410
2792
|
/**
|
|
2411
|
-
*
|
|
2793
|
+
* Load a schema from an import path.
|
|
2412
2794
|
*/
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2795
|
+
async load(importPath, fromPath, chain) {
|
|
2796
|
+
const importChain = chain ?? new HttpImportChain();
|
|
2797
|
+
const resolveResult = this.resolvePath(importPath, fromPath);
|
|
2798
|
+
if (!resolveResult.success) {
|
|
2799
|
+
return resolveResult;
|
|
2416
2800
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2801
|
+
const absoluteUrl = resolveResult.data;
|
|
2802
|
+
const circularError = importChain.push(absoluteUrl);
|
|
2803
|
+
if (circularError) {
|
|
2804
|
+
return { success: false, error: circularError };
|
|
2419
2805
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
}
|
|
2425
|
-
if (importPath.startsWith("@") && this.options.scopedPaths) {
|
|
2426
|
-
const match = importPath.match(/^(@[^/]+)/);
|
|
2427
|
-
if (match) {
|
|
2428
|
-
const scopePath = this.options.scopedPaths[match[1]];
|
|
2429
|
-
if (scopePath && (scopePath.startsWith("http://") || scopePath.startsWith("https://"))) {
|
|
2430
|
-
return "http";
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
if (isBrowser()) {
|
|
2435
|
-
return "http";
|
|
2436
|
-
}
|
|
2437
|
-
return "filesystem";
|
|
2438
|
-
}
|
|
2439
|
-
/**
|
|
2440
|
-
* Load a schema from an import path.
|
|
2441
|
-
*
|
|
2442
|
-
* Note: We delegate chain management to the inner loader (HttpLoader or FsLoader).
|
|
2443
|
-
* The inner loader handles circular import detection, so we don't push/pop here.
|
|
2444
|
-
* We only use the unified cache to avoid duplicate loads across loaders.
|
|
2445
|
-
*/
|
|
2446
|
-
async load(importPath, fromPath, chain) {
|
|
2447
|
-
await this.initFsLoader();
|
|
2448
|
-
const importChain = chain ?? new UnifiedImportChain();
|
|
2449
|
-
const loaderType = this.getLoaderForPath(importPath);
|
|
2450
|
-
const resolveResult = this.resolvePath(importPath, fromPath);
|
|
2451
|
-
if (!resolveResult.success) {
|
|
2452
|
-
return resolveResult;
|
|
2453
|
-
}
|
|
2454
|
-
const absolutePath = resolveResult.data;
|
|
2455
|
-
const cached = this.cache.get(absolutePath);
|
|
2456
|
-
if (cached) {
|
|
2457
|
-
return { success: true, data: cached };
|
|
2458
|
-
}
|
|
2459
|
-
let result;
|
|
2460
|
-
if (loaderType === "http") {
|
|
2461
|
-
if (!this.httpLoader) {
|
|
2462
|
-
return {
|
|
2463
|
-
success: false,
|
|
2464
|
-
error: "HTTP loader not available"
|
|
2465
|
-
};
|
|
2806
|
+
try {
|
|
2807
|
+
const cached = this.cache.get(absoluteUrl);
|
|
2808
|
+
if (cached) {
|
|
2809
|
+
return { success: true, data: cached };
|
|
2466
2810
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
if (this.httpLoader) {
|
|
2471
|
-
result = await this.httpLoader.load(
|
|
2472
|
-
importPath,
|
|
2473
|
-
fromPath,
|
|
2474
|
-
importChain
|
|
2475
|
-
);
|
|
2476
|
-
} else {
|
|
2477
|
-
return {
|
|
2478
|
-
success: false,
|
|
2479
|
-
error: `Filesystem loader not available and import "${importPath}" cannot be loaded via HTTP. This typically happens when loading local files in a browser environment.`
|
|
2480
|
-
};
|
|
2481
|
-
}
|
|
2482
|
-
} else {
|
|
2483
|
-
result = await this.fsLoader.load(importPath, fromPath, importChain);
|
|
2811
|
+
const loadResult = await this.fetchSchema(absoluteUrl);
|
|
2812
|
+
if (!loadResult.success) {
|
|
2813
|
+
return loadResult;
|
|
2484
2814
|
}
|
|
2815
|
+
const loaded = {
|
|
2816
|
+
schema: loadResult.data,
|
|
2817
|
+
sourcePath: absoluteUrl,
|
|
2818
|
+
importPath
|
|
2819
|
+
};
|
|
2820
|
+
this.cache.set(absoluteUrl, loaded);
|
|
2821
|
+
return { success: true, data: loaded };
|
|
2822
|
+
} finally {
|
|
2823
|
+
importChain.pop();
|
|
2485
2824
|
}
|
|
2486
|
-
if (result.success) {
|
|
2487
|
-
this.cache.set(absolutePath, result.data);
|
|
2488
|
-
}
|
|
2489
|
-
return result;
|
|
2490
2825
|
}
|
|
2491
2826
|
/**
|
|
2492
2827
|
* Load a specific orbital from a schema by name.
|
|
@@ -2497,988 +2832,2444 @@ var UnifiedLoader = class {
|
|
|
2497
2832
|
return schemaResult;
|
|
2498
2833
|
}
|
|
2499
2834
|
const schema = schemaResult.data.schema;
|
|
2835
|
+
let orbital;
|
|
2500
2836
|
if (orbitalName) {
|
|
2501
|
-
const found = schema.orbitals.find(
|
|
2837
|
+
const found = schema.orbitals.find(
|
|
2838
|
+
(o) => o.name === orbitalName
|
|
2839
|
+
);
|
|
2502
2840
|
if (!found) {
|
|
2503
2841
|
return {
|
|
2504
2842
|
success: false,
|
|
2505
2843
|
error: `Orbital "${orbitalName}" not found in ${importPath}. Available: ${schema.orbitals.map((o) => o.name).join(", ")}`
|
|
2506
2844
|
};
|
|
2507
2845
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
importPath
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
if (schema.orbitals.length === 0) {
|
|
2518
|
-
return {
|
|
2519
|
-
success: false,
|
|
2520
|
-
error: `No orbitals found in ${importPath}`
|
|
2521
|
-
};
|
|
2846
|
+
orbital = found;
|
|
2847
|
+
} else {
|
|
2848
|
+
if (schema.orbitals.length === 0) {
|
|
2849
|
+
return {
|
|
2850
|
+
success: false,
|
|
2851
|
+
error: `No orbitals found in ${importPath}`
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
orbital = schema.orbitals[0];
|
|
2522
2855
|
}
|
|
2523
2856
|
return {
|
|
2524
2857
|
success: true,
|
|
2525
2858
|
data: {
|
|
2526
|
-
orbital
|
|
2859
|
+
orbital,
|
|
2527
2860
|
sourcePath: schemaResult.data.sourcePath,
|
|
2528
2861
|
importPath
|
|
2529
2862
|
}
|
|
2530
2863
|
};
|
|
2531
2864
|
}
|
|
2532
2865
|
/**
|
|
2533
|
-
* Resolve an import path to an absolute
|
|
2866
|
+
* Resolve an import path to an absolute URL.
|
|
2534
2867
|
*/
|
|
2535
2868
|
resolvePath(importPath, fromPath) {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
return this.httpLoader.resolvePath(importPath, fromPath);
|
|
2869
|
+
if (importPath.startsWith("http://") || importPath.startsWith("https://")) {
|
|
2870
|
+
return { success: true, data: importPath };
|
|
2539
2871
|
}
|
|
2540
|
-
if (
|
|
2541
|
-
return this.
|
|
2872
|
+
if (importPath.startsWith("std/")) {
|
|
2873
|
+
return this.resolveStdPath(importPath);
|
|
2542
2874
|
}
|
|
2543
|
-
if (
|
|
2544
|
-
return this.
|
|
2875
|
+
if (importPath.startsWith("@")) {
|
|
2876
|
+
return this.resolveScopedPath(importPath);
|
|
2545
2877
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
};
|
|
2878
|
+
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
2879
|
+
return this.resolveRelativePath(importPath, fromPath);
|
|
2880
|
+
}
|
|
2881
|
+
return this.resolveRelativePath(`./${importPath}`, fromPath);
|
|
2550
2882
|
}
|
|
2551
2883
|
/**
|
|
2552
|
-
*
|
|
2884
|
+
* Resolve a standard library path.
|
|
2553
2885
|
*/
|
|
2554
|
-
|
|
2555
|
-
this.
|
|
2556
|
-
|
|
2557
|
-
|
|
2886
|
+
resolveStdPath(importPath) {
|
|
2887
|
+
if (!this.options.stdLibPath) {
|
|
2888
|
+
return {
|
|
2889
|
+
success: false,
|
|
2890
|
+
error: `Standard library URL not configured. Cannot load: ${importPath}`
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
const relativePath = importPath.slice(4);
|
|
2894
|
+
let absoluteUrl = this.joinUrl(this.options.stdLibPath, relativePath);
|
|
2895
|
+
if (!absoluteUrl.endsWith(".orb")) {
|
|
2896
|
+
absoluteUrl += ".orb";
|
|
2897
|
+
}
|
|
2898
|
+
return { success: true, data: absoluteUrl };
|
|
2558
2899
|
}
|
|
2559
2900
|
/**
|
|
2560
|
-
*
|
|
2901
|
+
* Resolve a scoped package path.
|
|
2561
2902
|
*/
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
if (
|
|
2565
|
-
|
|
2903
|
+
resolveScopedPath(importPath) {
|
|
2904
|
+
const match = importPath.match(/^(@[^/]+)/);
|
|
2905
|
+
if (!match) {
|
|
2906
|
+
return {
|
|
2907
|
+
success: false,
|
|
2908
|
+
error: `Invalid scoped package path: ${importPath}`
|
|
2909
|
+
};
|
|
2566
2910
|
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2911
|
+
const scope = match[1];
|
|
2912
|
+
const scopeRoot = this.options.scopedPaths[scope];
|
|
2913
|
+
if (!scopeRoot) {
|
|
2914
|
+
return {
|
|
2915
|
+
success: false,
|
|
2916
|
+
error: `Scoped package "${scope}" not configured. Available: ${Object.keys(this.options.scopedPaths).join(", ") || "none"}`
|
|
2917
|
+
};
|
|
2569
2918
|
}
|
|
2570
|
-
|
|
2919
|
+
const relativePath = importPath.slice(scope.length + 1);
|
|
2920
|
+
let absoluteUrl = this.joinUrl(scopeRoot, relativePath);
|
|
2921
|
+
if (!absoluteUrl.endsWith(".orb")) {
|
|
2922
|
+
absoluteUrl += ".orb";
|
|
2923
|
+
}
|
|
2924
|
+
return { success: true, data: absoluteUrl };
|
|
2571
2925
|
}
|
|
2572
2926
|
/**
|
|
2573
|
-
*
|
|
2927
|
+
* Resolve a relative path.
|
|
2574
2928
|
*/
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2929
|
+
resolveRelativePath(importPath, fromPath) {
|
|
2930
|
+
const baseUrl = fromPath ? this.getParentUrl(fromPath) : this.options.basePath;
|
|
2931
|
+
let absoluteUrl = this.joinUrl(baseUrl, importPath);
|
|
2932
|
+
if (!absoluteUrl.endsWith(".orb")) {
|
|
2933
|
+
absoluteUrl += ".orb";
|
|
2934
|
+
}
|
|
2935
|
+
return { success: true, data: absoluteUrl };
|
|
2578
2936
|
}
|
|
2579
2937
|
/**
|
|
2580
|
-
*
|
|
2938
|
+
* Fetch and parse an OrbitalSchema from a URL.
|
|
2581
2939
|
*/
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2940
|
+
async fetchSchema(url) {
|
|
2941
|
+
try {
|
|
2942
|
+
const controller = new AbortController();
|
|
2943
|
+
const timeoutId = setTimeout(
|
|
2944
|
+
() => controller.abort(),
|
|
2945
|
+
this.options.timeout
|
|
2946
|
+
);
|
|
2947
|
+
try {
|
|
2948
|
+
const response = await fetch(url, {
|
|
2949
|
+
...this.options.fetchOptions,
|
|
2950
|
+
credentials: this.options.credentials,
|
|
2951
|
+
signal: controller.signal,
|
|
2952
|
+
headers: {
|
|
2953
|
+
Accept: "application/json",
|
|
2954
|
+
...this.options.fetchOptions?.headers
|
|
2955
|
+
}
|
|
2956
|
+
});
|
|
2957
|
+
if (!response.ok) {
|
|
2958
|
+
return {
|
|
2959
|
+
success: false,
|
|
2960
|
+
error: `HTTP ${response.status}: ${response.statusText} for ${url}`
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
const text = await response.text();
|
|
2964
|
+
let data;
|
|
2965
|
+
try {
|
|
2966
|
+
data = JSON.parse(text);
|
|
2967
|
+
} catch (e) {
|
|
2968
|
+
return {
|
|
2969
|
+
success: false,
|
|
2970
|
+
error: `Invalid JSON in ${url}: ${e instanceof Error ? e.message : String(e)}`
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
const parseResult = OrbitalSchemaSchema.safeParse(data);
|
|
2974
|
+
if (!parseResult.success) {
|
|
2975
|
+
const errors = parseResult.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
2976
|
+
return {
|
|
2977
|
+
success: false,
|
|
2978
|
+
error: `Invalid schema in ${url}:
|
|
2979
|
+
${errors}`
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
return { success: true, data: parseResult.data };
|
|
2983
|
+
} finally {
|
|
2984
|
+
clearTimeout(timeoutId);
|
|
2985
|
+
}
|
|
2986
|
+
} catch (e) {
|
|
2987
|
+
if (e instanceof Error && e.name === "AbortError") {
|
|
2988
|
+
return {
|
|
2989
|
+
success: false,
|
|
2990
|
+
error: `Request timeout for ${url} (${this.options.timeout}ms)`
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
return {
|
|
2994
|
+
success: false,
|
|
2995
|
+
error: `Failed to fetch ${url}: ${e instanceof Error ? e.message : String(e)}`
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Join URL parts, handling relative paths and trailing slashes.
|
|
3001
|
+
*/
|
|
3002
|
+
joinUrl(base, path) {
|
|
3003
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
3004
|
+
return path;
|
|
3005
|
+
}
|
|
3006
|
+
if (typeof URL !== "undefined") {
|
|
3007
|
+
try {
|
|
3008
|
+
const baseUrl = base.endsWith("/") ? base : base + "/";
|
|
3009
|
+
return new URL(path, baseUrl).href;
|
|
3010
|
+
} catch {
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
3014
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
3015
|
+
if (normalizedPath.startsWith("/./")) {
|
|
3016
|
+
return normalizedBase + normalizedPath.slice(2);
|
|
3017
|
+
}
|
|
3018
|
+
if (normalizedPath.startsWith("/../")) {
|
|
3019
|
+
const baseParts = normalizedBase.split("/");
|
|
3020
|
+
baseParts.pop();
|
|
3021
|
+
return baseParts.join("/") + normalizedPath.slice(3);
|
|
3022
|
+
}
|
|
3023
|
+
return normalizedBase + normalizedPath;
|
|
3024
|
+
}
|
|
3025
|
+
/**
|
|
3026
|
+
* Get parent URL (directory) from a URL.
|
|
3027
|
+
*/
|
|
3028
|
+
getParentUrl(url) {
|
|
3029
|
+
if (typeof URL !== "undefined") {
|
|
3030
|
+
try {
|
|
3031
|
+
const urlObj = new URL(url);
|
|
3032
|
+
const pathParts = urlObj.pathname.split("/");
|
|
3033
|
+
pathParts.pop();
|
|
3034
|
+
urlObj.pathname = pathParts.join("/");
|
|
3035
|
+
return urlObj.href;
|
|
3036
|
+
} catch {
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
const lastSlash = url.lastIndexOf("/");
|
|
3040
|
+
if (lastSlash !== -1) {
|
|
3041
|
+
return url.slice(0, lastSlash);
|
|
3042
|
+
}
|
|
3043
|
+
return url;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Clear the cache.
|
|
3047
|
+
*/
|
|
3048
|
+
clearCache() {
|
|
3049
|
+
this.cache.clear();
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Get cache statistics.
|
|
3053
|
+
*/
|
|
3054
|
+
getCacheStats() {
|
|
3055
|
+
return { size: this.cache.size };
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
|
|
3059
|
+
// src/loader/unified-loader.ts
|
|
3060
|
+
var externalLoaderModule = null;
|
|
3061
|
+
async function getExternalLoaderModule() {
|
|
3062
|
+
if (externalLoaderModule) {
|
|
3063
|
+
return externalLoaderModule;
|
|
3064
|
+
}
|
|
3065
|
+
if (isBrowser()) {
|
|
3066
|
+
return null;
|
|
3067
|
+
}
|
|
3068
|
+
try {
|
|
3069
|
+
externalLoaderModule = await import('./external-loader-UHZQPCKW.js');
|
|
3070
|
+
return externalLoaderModule;
|
|
3071
|
+
} catch {
|
|
3072
|
+
return null;
|
|
3073
|
+
}
|
|
2643
3074
|
}
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
3075
|
+
var UnifiedImportChain = class _UnifiedImportChain {
|
|
3076
|
+
chain = [];
|
|
3077
|
+
push(path) {
|
|
3078
|
+
const normalized = this.normalizePath(path);
|
|
3079
|
+
if (this.chain.includes(normalized)) {
|
|
3080
|
+
const cycle = [
|
|
3081
|
+
...this.chain.slice(this.chain.indexOf(normalized)),
|
|
3082
|
+
normalized
|
|
3083
|
+
];
|
|
3084
|
+
return `Circular import detected: ${cycle.join(" -> ")}`;
|
|
3085
|
+
}
|
|
3086
|
+
this.chain.push(normalized);
|
|
3087
|
+
return null;
|
|
2648
3088
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3089
|
+
pop() {
|
|
3090
|
+
this.chain.pop();
|
|
3091
|
+
}
|
|
3092
|
+
clone() {
|
|
3093
|
+
const newChain = new _UnifiedImportChain();
|
|
3094
|
+
newChain.chain = [...this.chain];
|
|
3095
|
+
return newChain;
|
|
3096
|
+
}
|
|
3097
|
+
normalizePath(path) {
|
|
3098
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
3099
|
+
return path;
|
|
3100
|
+
}
|
|
3101
|
+
return path.replace(/\\/g, "/");
|
|
3102
|
+
}
|
|
3103
|
+
};
|
|
3104
|
+
var UnifiedLoader = class {
|
|
3105
|
+
options;
|
|
3106
|
+
httpLoader = null;
|
|
3107
|
+
fsLoader = null;
|
|
3108
|
+
fsLoaderInitialized = false;
|
|
3109
|
+
cache = /* @__PURE__ */ new Map();
|
|
3110
|
+
constructor(options) {
|
|
3111
|
+
this.options = options;
|
|
3112
|
+
this.httpLoader = new HttpLoader({
|
|
3113
|
+
basePath: options.basePath,
|
|
3114
|
+
stdLibPath: options.stdLibPath,
|
|
3115
|
+
scopedPaths: options.scopedPaths,
|
|
3116
|
+
...options.http
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Initialize the filesystem loader if available.
|
|
3121
|
+
*/
|
|
3122
|
+
async initFsLoader() {
|
|
3123
|
+
if (this.fsLoaderInitialized) {
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
this.fsLoaderInitialized = true;
|
|
3127
|
+
if (this.options.forceLoader === "http") {
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
const module = await getExternalLoaderModule();
|
|
3131
|
+
if (module) {
|
|
3132
|
+
this.fsLoader = new module.ExternalOrbitalLoader({
|
|
3133
|
+
basePath: this.options.basePath,
|
|
3134
|
+
stdLibPath: this.options.stdLibPath,
|
|
3135
|
+
scopedPaths: this.options.scopedPaths,
|
|
3136
|
+
...this.options.fileSystem
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
/**
|
|
3141
|
+
* Determine which loader to use for an import path.
|
|
3142
|
+
*/
|
|
3143
|
+
getLoaderForPath(importPath) {
|
|
3144
|
+
if (this.options.forceLoader) {
|
|
3145
|
+
return this.options.forceLoader;
|
|
3146
|
+
}
|
|
3147
|
+
if (importPath.startsWith("http://") || importPath.startsWith("https://")) {
|
|
3148
|
+
return "http";
|
|
3149
|
+
}
|
|
3150
|
+
if (importPath.startsWith("std/") && this.options.stdLibPath) {
|
|
3151
|
+
if (this.options.stdLibPath.startsWith("http://") || this.options.stdLibPath.startsWith("https://")) {
|
|
3152
|
+
return "http";
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
if (importPath.startsWith("@") && this.options.scopedPaths) {
|
|
3156
|
+
const match = importPath.match(/^(@[^/]+)/);
|
|
3157
|
+
if (match) {
|
|
3158
|
+
const scopePath = this.options.scopedPaths[match[1]];
|
|
3159
|
+
if (scopePath && (scopePath.startsWith("http://") || scopePath.startsWith("https://"))) {
|
|
3160
|
+
return "http";
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (isBrowser()) {
|
|
3165
|
+
return "http";
|
|
3166
|
+
}
|
|
3167
|
+
return "filesystem";
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Load a schema from an import path.
|
|
3171
|
+
*
|
|
3172
|
+
* Note: We delegate chain management to the inner loader (HttpLoader or FsLoader).
|
|
3173
|
+
* The inner loader handles circular import detection, so we don't push/pop here.
|
|
3174
|
+
* We only use the unified cache to avoid duplicate loads across loaders.
|
|
3175
|
+
*/
|
|
3176
|
+
async load(importPath, fromPath, chain) {
|
|
3177
|
+
await this.initFsLoader();
|
|
3178
|
+
const importChain = chain ?? new UnifiedImportChain();
|
|
3179
|
+
const loaderType = this.getLoaderForPath(importPath);
|
|
3180
|
+
const resolveResult = this.resolvePath(importPath, fromPath);
|
|
3181
|
+
if (!resolveResult.success) {
|
|
3182
|
+
return resolveResult;
|
|
3183
|
+
}
|
|
3184
|
+
const absolutePath = resolveResult.data;
|
|
3185
|
+
const cached = this.cache.get(absolutePath);
|
|
3186
|
+
if (cached) {
|
|
3187
|
+
return { success: true, data: cached };
|
|
3188
|
+
}
|
|
3189
|
+
let result;
|
|
3190
|
+
if (loaderType === "http") {
|
|
3191
|
+
if (!this.httpLoader) {
|
|
3192
|
+
return {
|
|
3193
|
+
success: false,
|
|
3194
|
+
error: "HTTP loader not available"
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
result = await this.httpLoader.load(importPath, fromPath, importChain);
|
|
3198
|
+
} else {
|
|
3199
|
+
if (!this.fsLoader) {
|
|
3200
|
+
if (this.httpLoader) {
|
|
3201
|
+
result = await this.httpLoader.load(
|
|
3202
|
+
importPath,
|
|
3203
|
+
fromPath,
|
|
3204
|
+
importChain
|
|
3205
|
+
);
|
|
3206
|
+
} else {
|
|
3207
|
+
return {
|
|
3208
|
+
success: false,
|
|
3209
|
+
error: `Filesystem loader not available and import "${importPath}" cannot be loaded via HTTP. This typically happens when loading local files in a browser environment.`
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
} else {
|
|
3213
|
+
result = await this.fsLoader.load(importPath, fromPath, importChain);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
if (result.success) {
|
|
3217
|
+
this.cache.set(absolutePath, result.data);
|
|
3218
|
+
}
|
|
3219
|
+
return result;
|
|
3220
|
+
}
|
|
3221
|
+
/**
|
|
3222
|
+
* Load a specific orbital from a schema by name.
|
|
3223
|
+
*/
|
|
3224
|
+
async loadOrbital(importPath, orbitalName, fromPath, chain) {
|
|
3225
|
+
const schemaResult = await this.load(importPath, fromPath, chain);
|
|
3226
|
+
if (!schemaResult.success) {
|
|
3227
|
+
return schemaResult;
|
|
3228
|
+
}
|
|
3229
|
+
const schema = schemaResult.data.schema;
|
|
3230
|
+
if (orbitalName) {
|
|
3231
|
+
const found = schema.orbitals.find((o) => o.name === orbitalName);
|
|
3232
|
+
if (!found) {
|
|
3233
|
+
return {
|
|
3234
|
+
success: false,
|
|
3235
|
+
error: `Orbital "${orbitalName}" not found in ${importPath}. Available: ${schema.orbitals.map((o) => o.name).join(", ")}`
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
return {
|
|
3239
|
+
success: true,
|
|
3240
|
+
data: {
|
|
3241
|
+
orbital: found,
|
|
3242
|
+
sourcePath: schemaResult.data.sourcePath,
|
|
3243
|
+
importPath
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
if (schema.orbitals.length === 0) {
|
|
3248
|
+
return {
|
|
3249
|
+
success: false,
|
|
3250
|
+
error: `No orbitals found in ${importPath}`
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
return {
|
|
3254
|
+
success: true,
|
|
3255
|
+
data: {
|
|
3256
|
+
orbital: schema.orbitals[0],
|
|
3257
|
+
sourcePath: schemaResult.data.sourcePath,
|
|
3258
|
+
importPath
|
|
3259
|
+
}
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
/**
|
|
3263
|
+
* Resolve an import path to an absolute path/URL.
|
|
3264
|
+
*/
|
|
3265
|
+
resolvePath(importPath, fromPath) {
|
|
3266
|
+
const loaderType = this.getLoaderForPath(importPath);
|
|
3267
|
+
if (loaderType === "http" && this.httpLoader) {
|
|
3268
|
+
return this.httpLoader.resolvePath(importPath, fromPath);
|
|
3269
|
+
}
|
|
3270
|
+
if (this.fsLoader) {
|
|
3271
|
+
return this.fsLoader.resolvePath(importPath, fromPath);
|
|
3272
|
+
}
|
|
3273
|
+
if (this.httpLoader) {
|
|
3274
|
+
return this.httpLoader.resolvePath(importPath, fromPath);
|
|
3275
|
+
}
|
|
3276
|
+
return {
|
|
3277
|
+
success: false,
|
|
3278
|
+
error: "No loader available for path resolution"
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3282
|
+
* Clear all caches.
|
|
3283
|
+
*/
|
|
3284
|
+
clearCache() {
|
|
3285
|
+
this.cache.clear();
|
|
3286
|
+
this.httpLoader?.clearCache();
|
|
3287
|
+
this.fsLoader?.clearCache();
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Get combined cache statistics.
|
|
3291
|
+
*/
|
|
3292
|
+
getCacheStats() {
|
|
3293
|
+
let size = this.cache.size;
|
|
3294
|
+
if (this.httpLoader) {
|
|
3295
|
+
size += this.httpLoader.getCacheStats().size;
|
|
2656
3296
|
}
|
|
2657
|
-
|
|
3297
|
+
if (this.fsLoader) {
|
|
3298
|
+
size += this.fsLoader.getCacheStats().size;
|
|
3299
|
+
}
|
|
3300
|
+
return { size };
|
|
2658
3301
|
}
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
var ALL_ARGS_ARE_EFFECTS = /* @__PURE__ */ new Set([
|
|
2666
|
-
"do",
|
|
2667
|
-
"atomic",
|
|
2668
|
-
"async/race",
|
|
2669
|
-
"async/all",
|
|
2670
|
-
"async/sequence"
|
|
2671
|
-
]);
|
|
2672
|
-
var ARGS_FROM_POS_2_ARE_EFFECTS = /* @__PURE__ */ new Set([
|
|
2673
|
-
"if",
|
|
2674
|
-
"when",
|
|
2675
|
-
"let",
|
|
2676
|
-
"async/delay",
|
|
2677
|
-
"async/debounce",
|
|
2678
|
-
"async/throttle",
|
|
2679
|
-
"async/interval"
|
|
2680
|
-
]);
|
|
2681
|
-
function renameEntityInEffect(effect, oldName, newName) {
|
|
2682
|
-
if (!Array.isArray(effect) || effect.length === 0) return effect;
|
|
2683
|
-
const op = effect[0];
|
|
2684
|
-
if (typeof op !== "string") return effect;
|
|
2685
|
-
if (op === "render-ui" && effect.length >= 3) {
|
|
2686
|
-
const [, slot, config, ...rest] = effect;
|
|
2687
|
-
const nextConfig = renameEntityInRenderUiConfig(config, oldName, newName);
|
|
2688
|
-
return [op, slot, nextConfig, ...rest];
|
|
3302
|
+
/**
|
|
3303
|
+
* Check if filesystem loading is available.
|
|
3304
|
+
*/
|
|
3305
|
+
async hasFilesystemAccess() {
|
|
3306
|
+
await this.initFsLoader();
|
|
3307
|
+
return this.fsLoader !== null;
|
|
2689
3308
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
3309
|
+
/**
|
|
3310
|
+
* Get current environment info.
|
|
3311
|
+
*/
|
|
3312
|
+
getEnvironmentInfo() {
|
|
3313
|
+
return {
|
|
3314
|
+
isElectron: isElectron(),
|
|
3315
|
+
isBrowser: isBrowser(),
|
|
3316
|
+
isNode: isNode(),
|
|
3317
|
+
hasFilesystem: this.fsLoader !== null
|
|
3318
|
+
};
|
|
2692
3319
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
3320
|
+
};
|
|
3321
|
+
function createUnifiedLoader(options) {
|
|
3322
|
+
return new UnifiedLoader(options);
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
// src/UsesIntegration.ts
|
|
3326
|
+
async function preprocessSchema(schema, options) {
|
|
3327
|
+
const namespaceEvents = options.namespaceEvents ?? true;
|
|
3328
|
+
const resolveResult = await resolveSchema(schema, options);
|
|
3329
|
+
if (!resolveResult.success) {
|
|
3330
|
+
return { success: false, errors: resolveResult.errors };
|
|
2695
3331
|
}
|
|
2696
|
-
const
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3332
|
+
const resolved = resolveResult.data;
|
|
3333
|
+
const warnings = resolveResult.warnings;
|
|
3334
|
+
const preprocessedOrbitals = [];
|
|
3335
|
+
const entitySharing = {};
|
|
3336
|
+
const eventNamespaces = {};
|
|
3337
|
+
for (const resolvedOrbital of resolved) {
|
|
3338
|
+
const orbitalName = resolvedOrbital.name;
|
|
3339
|
+
const persistence = resolvedOrbital.entitySource?.persistence ?? resolvedOrbital.entity.persistence ?? "persistent";
|
|
3340
|
+
entitySharing[orbitalName] = {
|
|
3341
|
+
entityName: resolvedOrbital.entity.name,
|
|
3342
|
+
persistence,
|
|
3343
|
+
isShared: persistence !== "runtime",
|
|
3344
|
+
sourceAlias: resolvedOrbital.entitySource?.alias,
|
|
3345
|
+
collectionName: resolvedOrbital.entity.collection
|
|
3346
|
+
};
|
|
3347
|
+
eventNamespaces[orbitalName] = {};
|
|
3348
|
+
for (const resolvedTrait of resolvedOrbital.traits) {
|
|
3349
|
+
const traitName = resolvedTrait.trait.name;
|
|
3350
|
+
const namespace = {
|
|
3351
|
+
emits: {},
|
|
3352
|
+
listens: {}
|
|
3353
|
+
};
|
|
3354
|
+
if (namespaceEvents && resolvedTrait.source.type === "imported") {
|
|
3355
|
+
const emits = resolvedTrait.trait.emits ?? [];
|
|
3356
|
+
for (const emit of emits) {
|
|
3357
|
+
const eventName = typeof emit === "string" ? emit : emit.event;
|
|
3358
|
+
namespace.emits[eventName] = `${orbitalName}.${traitName}.${eventName}`;
|
|
3359
|
+
}
|
|
3360
|
+
const listens = resolvedTrait.trait.listens ?? [];
|
|
3361
|
+
for (const listen of listens) {
|
|
3362
|
+
namespace.listens[listen.event] = listen.event;
|
|
3363
|
+
}
|
|
2704
3364
|
}
|
|
2705
|
-
|
|
2706
|
-
}
|
|
3365
|
+
eventNamespaces[orbitalName][traitName] = namespace;
|
|
3366
|
+
}
|
|
3367
|
+
const preprocessedOrbital = {
|
|
3368
|
+
name: orbitalName,
|
|
3369
|
+
description: resolvedOrbital.original.description,
|
|
3370
|
+
visual_prompt: resolvedOrbital.original.visual_prompt,
|
|
3371
|
+
// Resolved entity (always inline now)
|
|
3372
|
+
entity: resolvedOrbital.entity,
|
|
3373
|
+
// Resolved traits (inline definitions)
|
|
3374
|
+
traits: (resolvedOrbital.traits || []).map((rt) => {
|
|
3375
|
+
if (rt.config || rt.linkedEntity) {
|
|
3376
|
+
return {
|
|
3377
|
+
ref: rt.trait.name,
|
|
3378
|
+
config: rt.config,
|
|
3379
|
+
linkedEntity: rt.linkedEntity,
|
|
3380
|
+
// Include the resolved trait definition for runtime
|
|
3381
|
+
_resolved: rt.trait
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
return rt.trait;
|
|
3385
|
+
}),
|
|
3386
|
+
// Resolved pages (inline definitions with path overrides applied)
|
|
3387
|
+
pages: resolvedOrbital.pages.map((rp) => rp.page),
|
|
3388
|
+
// Preserve other fields
|
|
3389
|
+
exposes: resolvedOrbital.original.exposes,
|
|
3390
|
+
domainContext: resolvedOrbital.original.domainContext,
|
|
3391
|
+
design: resolvedOrbital.original.design
|
|
3392
|
+
};
|
|
3393
|
+
preprocessedOrbitals.push(preprocessedOrbital);
|
|
2707
3394
|
}
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
const atomLinked = trait.linkedEntity;
|
|
2712
|
-
if (!linkedEntity || !atomLinked || linkedEntity === atomLinked) return trait;
|
|
2713
|
-
const sm = trait.stateMachine;
|
|
2714
|
-
if (!sm) return { ...trait, linkedEntity };
|
|
2715
|
-
const nextTransitions = (sm.transitions ?? []).map((t) => {
|
|
2716
|
-
const nextEffects = t.effects ? renameEntityInEffects(
|
|
2717
|
-
t.effects,
|
|
2718
|
-
atomLinked,
|
|
2719
|
-
linkedEntity
|
|
2720
|
-
) : t.effects;
|
|
2721
|
-
return { ...t, effects: nextEffects };
|
|
2722
|
-
});
|
|
2723
|
-
refResolverLog.info("linkedEntity:rename", {
|
|
2724
|
-
trait: trait.name,
|
|
2725
|
-
from: atomLinked,
|
|
2726
|
-
to: linkedEntity,
|
|
2727
|
-
transitionCount: nextTransitions.length
|
|
2728
|
-
});
|
|
2729
|
-
return {
|
|
2730
|
-
...trait,
|
|
2731
|
-
linkedEntity,
|
|
2732
|
-
stateMachine: { ...sm, transitions: nextTransitions }
|
|
3395
|
+
const preprocessedSchema = {
|
|
3396
|
+
...schema,
|
|
3397
|
+
orbitals: preprocessedOrbitals
|
|
2733
3398
|
};
|
|
2734
|
-
}
|
|
2735
|
-
function applyEventRenames(trait, renames) {
|
|
2736
|
-
if (!renames || Object.keys(renames).length === 0) return trait;
|
|
2737
|
-
const rename = (k) => k !== void 0 && k in renames ? renames[k] : k;
|
|
2738
|
-
const sm = trait.stateMachine;
|
|
2739
|
-
if (!sm) return trait;
|
|
2740
|
-
const nextTransitions = (sm.transitions ?? []).map((t) => {
|
|
2741
|
-
const nextEvent = rename(t.event) ?? t.event;
|
|
2742
|
-
const nextEffects = t.effects ? renameEventsInEffects(t.effects, rename) : t.effects;
|
|
2743
|
-
return { ...t, event: nextEvent, effects: nextEffects };
|
|
2744
|
-
});
|
|
2745
|
-
const nextEvents = (sm.events ?? []).map((e) => {
|
|
2746
|
-
const newKey = rename(e.key);
|
|
2747
|
-
if (newKey === e.key) return e;
|
|
2748
|
-
return { ...e, key: newKey ?? e.key };
|
|
2749
|
-
});
|
|
2750
|
-
const nextEmits = (trait.emits ?? []).map((em) => {
|
|
2751
|
-
if (typeof em === "string") return rename(em) ?? em;
|
|
2752
|
-
const newEvent = rename(em.event);
|
|
2753
|
-
return newEvent === em.event ? em : { ...em, event: newEvent ?? em.event };
|
|
2754
|
-
});
|
|
2755
3399
|
return {
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
3400
|
+
success: true,
|
|
3401
|
+
data: {
|
|
3402
|
+
schema: preprocessedSchema,
|
|
3403
|
+
entitySharing,
|
|
3404
|
+
eventNamespaces,
|
|
3405
|
+
warnings
|
|
3406
|
+
}
|
|
2763
3407
|
};
|
|
2764
3408
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
3409
|
+
function getIsolatedCollectionName(orbitalName, entitySharing) {
|
|
3410
|
+
const info = entitySharing[orbitalName];
|
|
3411
|
+
if (!info) {
|
|
3412
|
+
throw new Error(`Unknown orbital: ${orbitalName}`);
|
|
3413
|
+
}
|
|
3414
|
+
if (info.persistence === "runtime") {
|
|
3415
|
+
return `${orbitalName}_${info.entityName}`;
|
|
3416
|
+
}
|
|
3417
|
+
return info.collectionName || info.entityName.toLowerCase() + "s";
|
|
3418
|
+
}
|
|
3419
|
+
function getNamespacedEvent(orbitalName, traitName, eventName, eventNamespaces) {
|
|
3420
|
+
const orbitalNs = eventNamespaces[orbitalName];
|
|
3421
|
+
if (!orbitalNs) return eventName;
|
|
3422
|
+
const traitNs = orbitalNs[traitName];
|
|
3423
|
+
if (!traitNs) return eventName;
|
|
3424
|
+
return traitNs.emits[eventName] || eventName;
|
|
3425
|
+
}
|
|
3426
|
+
function isNamespacedEvent(eventName) {
|
|
3427
|
+
return eventName.includes(".");
|
|
3428
|
+
}
|
|
3429
|
+
function parseNamespacedEvent(eventName) {
|
|
3430
|
+
const parts = eventName.split(".");
|
|
3431
|
+
if (parts.length === 3) {
|
|
3432
|
+
return { orbital: parts[0], trait: parts[1], event: parts[2] };
|
|
3433
|
+
}
|
|
3434
|
+
if (parts.length === 2) {
|
|
3435
|
+
return { trait: parts[0], event: parts[1] };
|
|
3436
|
+
}
|
|
3437
|
+
return { event: eventName };
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
// src/PersistenceAdapter.ts
|
|
3441
|
+
var InMemoryPersistence = class {
|
|
3442
|
+
data = /* @__PURE__ */ new Map();
|
|
3443
|
+
idCounter = 0;
|
|
3444
|
+
/**
|
|
3445
|
+
* Seed the store with pre-existing rows.
|
|
3446
|
+
*
|
|
3447
|
+
* Accepts either a plain `Record<entityType, EntityRow[]>` or an iterable
|
|
3448
|
+
* of `[entityType, EntityRow[]]` entries. Rows without an `id` get one
|
|
3449
|
+
* generated at insert time; rows with an `id` keep it (so re-seeding
|
|
3450
|
+
* after a schema rebuild preserves identities used in render bindings).
|
|
3451
|
+
*/
|
|
3452
|
+
seed(seedData) {
|
|
3453
|
+
const entries = Symbol.iterator in Object(seedData) ? seedData : Object.entries(seedData);
|
|
3454
|
+
for (const [entityType, rows] of entries) {
|
|
3455
|
+
if (!this.data.has(entityType)) {
|
|
3456
|
+
this.data.set(entityType, /* @__PURE__ */ new Map());
|
|
3457
|
+
}
|
|
3458
|
+
const collection = this.data.get(entityType);
|
|
3459
|
+
for (const row of rows) {
|
|
3460
|
+
const id = row.id || `${entityType}-${++this.idCounter}`;
|
|
3461
|
+
collection.set(id, { ...row, id });
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
2774
3464
|
}
|
|
2775
|
-
async
|
|
2776
|
-
|
|
2777
|
-
this.
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
3465
|
+
async create(entityType, data) {
|
|
3466
|
+
const id = data.id || `${entityType}-${++this.idCounter}`;
|
|
3467
|
+
if (!this.data.has(entityType)) {
|
|
3468
|
+
this.data.set(entityType, /* @__PURE__ */ new Map());
|
|
3469
|
+
}
|
|
3470
|
+
this.data.get(entityType).set(id, { ...data, id });
|
|
3471
|
+
return { id };
|
|
3472
|
+
}
|
|
3473
|
+
async update(entityType, id, data) {
|
|
3474
|
+
const collection = this.data.get(entityType);
|
|
3475
|
+
if (collection?.has(id)) {
|
|
3476
|
+
const existing = collection.get(id);
|
|
3477
|
+
collection.set(id, { ...existing, ...data });
|
|
2782
3478
|
}
|
|
2783
3479
|
}
|
|
3480
|
+
async delete(entityType, id) {
|
|
3481
|
+
this.data.get(entityType)?.delete(id);
|
|
3482
|
+
}
|
|
3483
|
+
async getById(entityType, id) {
|
|
3484
|
+
return this.data.get(entityType)?.get(id) || null;
|
|
3485
|
+
}
|
|
3486
|
+
async list(entityType) {
|
|
3487
|
+
const collection = this.data.get(entityType);
|
|
3488
|
+
return collection ? Array.from(collection.values()) : [];
|
|
3489
|
+
}
|
|
2784
3490
|
/**
|
|
2785
|
-
*
|
|
3491
|
+
* Snapshot the entire store as a plain object (entityType → rows).
|
|
3492
|
+
* Useful for feeding a fresh render-time binding layer with the
|
|
3493
|
+
* current persistence view.
|
|
2786
3494
|
*/
|
|
2787
|
-
|
|
2788
|
-
const
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
}, clone() {
|
|
2792
|
-
return this;
|
|
2793
|
-
} };
|
|
2794
|
-
const importsResult = await this.resolveImports(
|
|
2795
|
-
orbital.uses ?? [],
|
|
2796
|
-
sourcePath,
|
|
2797
|
-
importChain
|
|
2798
|
-
);
|
|
2799
|
-
if (!importsResult.success) {
|
|
2800
|
-
return { success: false, errors: importsResult.errors };
|
|
3495
|
+
snapshot() {
|
|
3496
|
+
const out = {};
|
|
3497
|
+
for (const [entityType, collection] of this.data) {
|
|
3498
|
+
out[entityType] = Array.from(collection.values());
|
|
2801
3499
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
3500
|
+
return out;
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
|
|
3504
|
+
// src/OrbitalServerRuntime.ts
|
|
3505
|
+
var _resolvedNodeRequire = null;
|
|
3506
|
+
function nodeRequire(modulePath) {
|
|
3507
|
+
if (!_resolvedNodeRequire) {
|
|
3508
|
+
const evalRequire = (0, eval)('typeof require !== "undefined" ? require : null');
|
|
3509
|
+
if (evalRequire) {
|
|
3510
|
+
_resolvedNodeRequire = evalRequire;
|
|
3511
|
+
} else {
|
|
3512
|
+
const createReq = nodeModule.createRequire;
|
|
3513
|
+
if (typeof createReq !== "function") {
|
|
3514
|
+
throw new Error(
|
|
3515
|
+
"[OrbitalServerRuntime] No synchronous require available. This branch is Node-only \u2014 invoking it from a browser indicates an isNodeEnv() guard regression upstream."
|
|
3516
|
+
);
|
|
3517
|
+
}
|
|
3518
|
+
_resolvedNodeRequire = createReq(import.meta.url);
|
|
2806
3519
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
3520
|
+
}
|
|
3521
|
+
return _resolvedNodeRequire(modulePath);
|
|
3522
|
+
}
|
|
3523
|
+
var _nodeRequireExt = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
|
|
3524
|
+
var effectLog2 = createLogger("almadar:runtime:effects");
|
|
3525
|
+
var busLog = createLogger("almadar:runtime:bus");
|
|
3526
|
+
var renderLog2 = createLogger("almadar:runtime:render-ui");
|
|
3527
|
+
var xOrbitalLog = createLogger("almadar:runtime:cross-orbital");
|
|
3528
|
+
function isNodeEnv() {
|
|
3529
|
+
return typeof process !== "undefined" && Boolean(process.versions?.node);
|
|
3530
|
+
}
|
|
3531
|
+
function collectDeclaredConfigDefaults(trait) {
|
|
3532
|
+
if (!trait) return void 0;
|
|
3533
|
+
const schema = trait.config;
|
|
3534
|
+
if (!schema || typeof schema !== "object") return void 0;
|
|
3535
|
+
const defaults = {};
|
|
3536
|
+
let hasAny = false;
|
|
3537
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
3538
|
+
if (field && typeof field === "object" && !Array.isArray(field) && "default" in field) {
|
|
3539
|
+
const def = field.default;
|
|
3540
|
+
if (def !== void 0) {
|
|
3541
|
+
defaults[key] = def;
|
|
3542
|
+
hasAny = true;
|
|
3543
|
+
}
|
|
2810
3544
|
}
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
3545
|
+
}
|
|
3546
|
+
return hasAny ? defaults : void 0;
|
|
3547
|
+
}
|
|
3548
|
+
function needsPreprocessing(schema) {
|
|
3549
|
+
for (const orbital of schema.orbitals) {
|
|
3550
|
+
const uses = orbital.uses;
|
|
3551
|
+
if (Array.isArray(uses) && uses.length > 0) {
|
|
3552
|
+
return true;
|
|
3553
|
+
}
|
|
3554
|
+
const traits = orbital.traits ?? [];
|
|
3555
|
+
for (const t of traits) {
|
|
3556
|
+
if (!t || typeof t !== "object") continue;
|
|
3557
|
+
const obj = t;
|
|
3558
|
+
if (typeof obj.ref === "string" && obj.ref.includes(".") && !obj.stateMachine) {
|
|
3559
|
+
return true;
|
|
3560
|
+
}
|
|
2814
3561
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
3562
|
+
}
|
|
3563
|
+
return false;
|
|
3564
|
+
}
|
|
3565
|
+
var OrbitalServerRuntime = class {
|
|
3566
|
+
orbitals = /* @__PURE__ */ new Map();
|
|
3567
|
+
eventBus;
|
|
3568
|
+
config;
|
|
3569
|
+
persistence;
|
|
3570
|
+
listenerCleanups = [];
|
|
3571
|
+
tickBindings = [];
|
|
3572
|
+
loader = null;
|
|
3573
|
+
preprocessedCache = /* @__PURE__ */ new Map();
|
|
3574
|
+
entitySharingMap = {};
|
|
3575
|
+
eventNamespaceMap = {};
|
|
3576
|
+
osHandlers = null;
|
|
3577
|
+
localPersistence = null;
|
|
3578
|
+
resolvedSchema = null;
|
|
3579
|
+
constructor(config = {}) {
|
|
3580
|
+
this.config = {
|
|
3581
|
+
mode: "mock",
|
|
3582
|
+
// Default to mock mode for preview
|
|
3583
|
+
autoPreprocess: false,
|
|
3584
|
+
namespaceEvents: true,
|
|
3585
|
+
...config
|
|
3586
|
+
};
|
|
3587
|
+
this.eventBus = new EventBus();
|
|
3588
|
+
if (config.loaderConfig?.loader) {
|
|
3589
|
+
this.loader = config.loaderConfig.loader;
|
|
3590
|
+
} else if (config.loaderConfig?.stdLibPath) {
|
|
3591
|
+
this.loader = createUnifiedLoader({
|
|
3592
|
+
basePath: config.loaderConfig.basePath,
|
|
3593
|
+
stdLibPath: config.loaderConfig.stdLibPath,
|
|
3594
|
+
scopedPaths: config.loaderConfig.scopedPaths
|
|
3595
|
+
});
|
|
2817
3596
|
}
|
|
2818
|
-
if (
|
|
2819
|
-
|
|
3597
|
+
if (this.config.mode === "mock" && !config.persistence) {
|
|
3598
|
+
this.persistence = new MockPersistenceAdapter({
|
|
3599
|
+
seed: config.mockSeed,
|
|
3600
|
+
defaultSeedCount: config.mockSeedCount ?? 6,
|
|
3601
|
+
debug: config.debug
|
|
3602
|
+
});
|
|
3603
|
+
if (config.debug) {
|
|
3604
|
+
console.log("[OrbitalRuntime] Using mock persistence with faker data");
|
|
3605
|
+
}
|
|
3606
|
+
} else {
|
|
3607
|
+
this.persistence = config.persistence || new InMemoryPersistence();
|
|
2820
3608
|
}
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
},
|
|
2832
|
-
|
|
3609
|
+
if (config.localStorageRoot && isNodeEnv()) {
|
|
3610
|
+
const { LocalPersistenceAdapter } = nodeRequire(`./LocalPersistenceAdapter${_nodeRequireExt}`);
|
|
3611
|
+
this.localPersistence = new LocalPersistenceAdapter(config.localStorageRoot);
|
|
3612
|
+
}
|
|
3613
|
+
if (isNodeEnv()) {
|
|
3614
|
+
const { createOsHandlers } = nodeRequire(`./createOsHandlers${_nodeRequireExt}`);
|
|
3615
|
+
this.osHandlers = createOsHandlers({
|
|
3616
|
+
emitEvent: (type, payload) => this.eventBus.emit(type, payload)
|
|
3617
|
+
});
|
|
3618
|
+
} else {
|
|
3619
|
+
this.osHandlers = { handlers: {}, cleanup: () => {
|
|
3620
|
+
} };
|
|
3621
|
+
}
|
|
3622
|
+
this.config.effectHandlers = {
|
|
3623
|
+
...this.osHandlers.handlers,
|
|
3624
|
+
...this.config.effectHandlers
|
|
2833
3625
|
};
|
|
2834
3626
|
}
|
|
2835
3627
|
/**
|
|
2836
|
-
*
|
|
3628
|
+
* Lazily construct a default loader when the caller didn't provide one
|
|
3629
|
+
* but `register()` needs to preprocess. Looks for `@almadar/std` in the
|
|
3630
|
+
* nearest `node_modules` so cross-orbital `std/behaviors/<name>` imports
|
|
3631
|
+
* resolve to the tiered registry on disk.
|
|
3632
|
+
*
|
|
3633
|
+
* Node only — browsers should receive already-preprocessed schemas from
|
|
3634
|
+
* their server.
|
|
2837
3635
|
*/
|
|
2838
|
-
async
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
return {
|
|
2843
|
-
success: true,
|
|
2844
|
-
data: { orbitals },
|
|
2845
|
-
warnings: ["External loading skipped"]
|
|
2846
|
-
};
|
|
3636
|
+
async ensureLoader() {
|
|
3637
|
+
if (this.loader) return;
|
|
3638
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
3639
|
+
return;
|
|
2847
3640
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
3641
|
+
try {
|
|
3642
|
+
const [{ fileURLToPath }, path, fs] = await Promise.all([
|
|
3643
|
+
import('url'),
|
|
3644
|
+
import('path'),
|
|
3645
|
+
import('fs')
|
|
3646
|
+
]);
|
|
3647
|
+
const mainEntryUrl = import.meta.resolve("@almadar/std");
|
|
3648
|
+
const mainEntry = fileURLToPath(mainEntryUrl);
|
|
3649
|
+
let stdLibPath = path.dirname(mainEntry);
|
|
3650
|
+
while (stdLibPath !== path.dirname(stdLibPath)) {
|
|
3651
|
+
if (fs.existsSync(path.join(stdLibPath, "package.json"))) {
|
|
3652
|
+
const pkg = JSON.parse(
|
|
3653
|
+
fs.readFileSync(path.join(stdLibPath, "package.json"), "utf-8")
|
|
3654
|
+
);
|
|
3655
|
+
if (pkg.name === "@almadar/std") break;
|
|
3656
|
+
}
|
|
3657
|
+
stdLibPath = path.dirname(stdLibPath);
|
|
2852
3658
|
}
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
3659
|
+
const basePath = this.config.loaderConfig?.basePath ?? process.cwd();
|
|
3660
|
+
this.loader = createUnifiedLoader({
|
|
3661
|
+
basePath,
|
|
3662
|
+
stdLibPath,
|
|
3663
|
+
scopedPaths: this.config.loaderConfig?.scopedPaths
|
|
3664
|
+
});
|
|
3665
|
+
if (this.config.debug) {
|
|
3666
|
+
console.log(
|
|
3667
|
+
`[OrbitalRuntime] Default loader constructed: basePath=${basePath} stdLibPath=${stdLibPath}`
|
|
3668
|
+
);
|
|
2857
3669
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
);
|
|
2864
|
-
if (!loadResult.success) {
|
|
2865
|
-
errors.push(`Failed to load "${use.from}" as "${use.as}": ${loadResult.error}`);
|
|
2866
|
-
continue;
|
|
3670
|
+
} catch (err) {
|
|
3671
|
+
if (this.config.debug) {
|
|
3672
|
+
console.warn(
|
|
3673
|
+
`[OrbitalRuntime] Could not auto-construct loader: ${err instanceof Error ? err.message : String(err)}`
|
|
3674
|
+
);
|
|
2867
3675
|
}
|
|
2868
|
-
orbitals.set(use.as, {
|
|
2869
|
-
alias: use.as,
|
|
2870
|
-
from: use.from,
|
|
2871
|
-
orbital: loadResult.data.orbital,
|
|
2872
|
-
sourcePath: loadResult.data.sourcePath
|
|
2873
|
-
});
|
|
2874
3676
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
3677
|
+
}
|
|
3678
|
+
// ==========================================================================
|
|
3679
|
+
// Schema Registration
|
|
3680
|
+
// ==========================================================================
|
|
3681
|
+
/**
|
|
3682
|
+
* Register an OrbitalSchema for execution.
|
|
3683
|
+
*
|
|
3684
|
+
* Auto-preprocesses the schema when it contains `uses` declarations or
|
|
3685
|
+
* unresolved cross-orbital trait references (e.g. a trait with
|
|
3686
|
+
* `ref: "Modal.traits.ModalRecordModal"` and no inline `stateMachine`).
|
|
3687
|
+
* Without preprocessing, those refs arrive empty at the state machine and
|
|
3688
|
+
* button clicks silently do nothing — see Phase 9.5.H.
|
|
3689
|
+
*
|
|
3690
|
+
* Preprocessing needs a loader. If `loaderConfig` is set, that loader is
|
|
3691
|
+
* used. Otherwise, a default loader is constructed that points at
|
|
3692
|
+
* `<cwd>` (for `basePath`) and the nearest `node_modules/@almadar/std` (for
|
|
3693
|
+
* `stdLibPath`), which matches how every caller in this monorepo has the
|
|
3694
|
+
* std registry on disk.
|
|
3695
|
+
*/
|
|
3696
|
+
async register(schema) {
|
|
3697
|
+
if (this.config.debug) {
|
|
3698
|
+
console.log(`[OrbitalRuntime] Registering schema: ${schema.name}`);
|
|
2877
3699
|
}
|
|
2878
|
-
|
|
3700
|
+
if (needsPreprocessing(schema)) {
|
|
3701
|
+
await this.ensureLoader();
|
|
3702
|
+
if (this.loader) {
|
|
3703
|
+
if (this.config.debug) {
|
|
3704
|
+
console.log(`[OrbitalRuntime] Schema has uses/refs \u2014 auto-preprocessing`);
|
|
3705
|
+
}
|
|
3706
|
+
const result = await preprocessSchema(schema, {
|
|
3707
|
+
basePath: this.config.loaderConfig?.basePath || process.cwd(),
|
|
3708
|
+
stdLibPath: this.config.loaderConfig?.stdLibPath,
|
|
3709
|
+
scopedPaths: this.config.loaderConfig?.scopedPaths,
|
|
3710
|
+
loader: this.loader,
|
|
3711
|
+
namespaceEvents: this.config.namespaceEvents
|
|
3712
|
+
});
|
|
3713
|
+
if (!result.success) {
|
|
3714
|
+
throw new Error(
|
|
3715
|
+
`Schema preprocessing failed: ${result.errors.join("; ")}`
|
|
3716
|
+
);
|
|
3717
|
+
}
|
|
3718
|
+
schema = result.data.schema;
|
|
3719
|
+
this.entitySharingMap = {
|
|
3720
|
+
...this.entitySharingMap,
|
|
3721
|
+
...result.data.entitySharing
|
|
3722
|
+
};
|
|
3723
|
+
this.eventNamespaceMap = {
|
|
3724
|
+
...this.eventNamespaceMap,
|
|
3725
|
+
...result.data.eventNamespaces
|
|
3726
|
+
};
|
|
3727
|
+
} else if (this.config.debug) {
|
|
3728
|
+
console.warn(
|
|
3729
|
+
`[OrbitalRuntime] Schema has uses/refs but no loader available \u2014 proceeding without preprocessing. Cross-orbital trait refs will be empty.`
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
for (const orbital of schema.orbitals) {
|
|
3734
|
+
await this.registerOrbitalAsync(orbital);
|
|
3735
|
+
}
|
|
3736
|
+
this.setupEventListeners();
|
|
3737
|
+
this.setupTicks();
|
|
3738
|
+
this.resolvedSchema = schema;
|
|
2879
3739
|
}
|
|
2880
3740
|
/**
|
|
2881
|
-
*
|
|
3741
|
+
* Register an OrbitalSchema synchronously (for backward compatibility).
|
|
3742
|
+
* Note: This version doesn't wait for instance seeding to complete.
|
|
3743
|
+
* Use async register() for guaranteed instance seeding.
|
|
2882
3744
|
*/
|
|
2883
|
-
|
|
2884
|
-
if (
|
|
2885
|
-
|
|
2886
|
-
return {
|
|
2887
|
-
success: true,
|
|
2888
|
-
data: {
|
|
2889
|
-
entity: {
|
|
2890
|
-
name: fallbackName,
|
|
2891
|
-
fields: entityRef.fields ?? [],
|
|
2892
|
-
...entityRef.persistence ? { persistence: entityRef.persistence } : {},
|
|
2893
|
-
...entityRef.collection ? { collection: entityRef.collection } : {}
|
|
2894
|
-
}
|
|
2895
|
-
},
|
|
2896
|
-
warnings: []
|
|
2897
|
-
};
|
|
3745
|
+
registerSync(schema) {
|
|
3746
|
+
if (this.config.debug) {
|
|
3747
|
+
console.log(`[OrbitalRuntime] Registering schema (sync): ${schema.name}`);
|
|
2898
3748
|
}
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
success: true,
|
|
2902
|
-
data: { entity: entityRef },
|
|
2903
|
-
warnings: []
|
|
2904
|
-
};
|
|
3749
|
+
for (const orbital of schema.orbitals) {
|
|
3750
|
+
this.registerOrbital(orbital);
|
|
2905
3751
|
}
|
|
2906
|
-
|
|
2907
|
-
|
|
3752
|
+
this.setupEventListeners();
|
|
3753
|
+
this.setupTicks();
|
|
3754
|
+
this.resolvedSchema = schema;
|
|
3755
|
+
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Returns the schema that this runtime is currently executing, post-
|
|
3758
|
+
* preprocessing. Safe to expose from an HTTP `/api/schema` endpoint — every
|
|
3759
|
+
* cross-orbital trait ref will have an inline `stateMachine` already, which
|
|
3760
|
+
* is what the browser's `schema-to-ir` resolver needs to wire button clicks
|
|
3761
|
+
* back to state transitions.
|
|
3762
|
+
*
|
|
3763
|
+
* Returns `null` if `register()` hasn't run yet.
|
|
3764
|
+
*/
|
|
3765
|
+
getResolvedSchema() {
|
|
3766
|
+
return this.resolvedSchema;
|
|
3767
|
+
}
|
|
3768
|
+
/**
|
|
3769
|
+
* One-call entry point: read an `.orb` file from disk, parse it, preprocess
|
|
3770
|
+
* cross-orbital imports, and register the result. Callers never touch raw
|
|
3771
|
+
* `.orb` bytes — `register()` handles preprocessing internally.
|
|
3772
|
+
*
|
|
3773
|
+
* Node only. Browsers must receive already-resolved schemas from their
|
|
3774
|
+
* server (see `getResolvedSchema()`).
|
|
3775
|
+
*/
|
|
3776
|
+
async registerFromFile(path) {
|
|
3777
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
3778
|
+
throw new Error(
|
|
3779
|
+
"registerFromFile is Node-only. Browsers should receive resolved schemas from their server."
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3782
|
+
const { readFile } = await import('fs/promises');
|
|
3783
|
+
const raw = await readFile(path, "utf-8");
|
|
3784
|
+
let schema;
|
|
3785
|
+
try {
|
|
3786
|
+
schema = JSON.parse(raw);
|
|
3787
|
+
} catch (err) {
|
|
3788
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3789
|
+
throw new Error(`registerFromFile: ${path} is not valid JSON: ${msg}`);
|
|
3790
|
+
}
|
|
3791
|
+
await this.register(schema);
|
|
3792
|
+
}
|
|
3793
|
+
/**
|
|
3794
|
+
* Register an OrbitalSchema with preprocessing to resolve `uses` imports.
|
|
3795
|
+
*
|
|
3796
|
+
* This method:
|
|
3797
|
+
* 1. Loads all external orbitals referenced in `uses` declarations
|
|
3798
|
+
* 2. Expands entity/trait/page references to inline definitions
|
|
3799
|
+
* 3. Builds entity sharing and event namespace maps
|
|
3800
|
+
* 4. Caches the preprocessed result
|
|
3801
|
+
* 5. Registers the resolved schema
|
|
3802
|
+
*
|
|
3803
|
+
* @param schema - Schema with potential `uses` declarations
|
|
3804
|
+
* @param options - Optional preprocessing options
|
|
3805
|
+
* @returns Preprocessing result with entity sharing info
|
|
3806
|
+
*
|
|
3807
|
+
* @example
|
|
3808
|
+
* ```typescript
|
|
3809
|
+
* const runtime = new OrbitalServerRuntime({
|
|
3810
|
+
* loaderConfig: {
|
|
3811
|
+
* basePath: '/schemas',
|
|
3812
|
+
* stdLibPath: '/std',
|
|
3813
|
+
* },
|
|
3814
|
+
* });
|
|
3815
|
+
*
|
|
3816
|
+
* const result = await runtime.registerWithPreprocess(schema);
|
|
3817
|
+
* if (result.success) {
|
|
3818
|
+
* console.log('Registered with', Object.keys(result.entitySharing).length, 'orbitals');
|
|
3819
|
+
* }
|
|
3820
|
+
* ```
|
|
3821
|
+
*/
|
|
3822
|
+
async registerWithPreprocess(schema, options) {
|
|
3823
|
+
if (!this.loader && !this.config.loaderConfig) {
|
|
2908
3824
|
return {
|
|
2909
3825
|
success: false,
|
|
2910
|
-
errors: [
|
|
3826
|
+
errors: ["Loader not configured. Set loaderConfig in OrbitalServerRuntimeConfig."]
|
|
2911
3827
|
};
|
|
2912
3828
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
3829
|
+
if (!this.loader && this.config.loaderConfig) {
|
|
3830
|
+
this.loader = this.config.loaderConfig.loader ?? createUnifiedLoader({
|
|
3831
|
+
basePath: this.config.loaderConfig.basePath,
|
|
3832
|
+
stdLibPath: this.config.loaderConfig.stdLibPath,
|
|
3833
|
+
scopedPaths: this.config.loaderConfig.scopedPaths
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
const cacheKey = `${schema.name}:${schema.version || "1.0.0"}`;
|
|
3837
|
+
const cached = this.preprocessedCache.get(cacheKey);
|
|
3838
|
+
if (cached) {
|
|
3839
|
+
if (this.config.debug) {
|
|
3840
|
+
console.log(`[OrbitalRuntime] Using cached preprocessed schema: ${schema.name}`);
|
|
3841
|
+
}
|
|
3842
|
+
this.register(cached.schema);
|
|
3843
|
+
this.entitySharingMap = { ...this.entitySharingMap, ...cached.entitySharing };
|
|
3844
|
+
this.eventNamespaceMap = { ...this.eventNamespaceMap, ...cached.eventNamespaces };
|
|
2915
3845
|
return {
|
|
2916
|
-
success:
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
3846
|
+
success: true,
|
|
3847
|
+
entitySharing: cached.entitySharing,
|
|
3848
|
+
eventNamespaces: cached.eventNamespaces,
|
|
3849
|
+
warnings: cached.warnings
|
|
2920
3850
|
};
|
|
2921
3851
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
3852
|
+
if (this.config.debug) {
|
|
3853
|
+
console.log(`[OrbitalRuntime] Preprocessing schema: ${schema.name}`);
|
|
3854
|
+
}
|
|
3855
|
+
const result = await preprocessSchema(schema, {
|
|
3856
|
+
basePath: this.config.loaderConfig?.basePath || ".",
|
|
3857
|
+
stdLibPath: this.config.loaderConfig?.stdLibPath,
|
|
3858
|
+
scopedPaths: this.config.loaderConfig?.scopedPaths,
|
|
3859
|
+
loader: this.loader,
|
|
3860
|
+
namespaceEvents: this.config.namespaceEvents
|
|
3861
|
+
});
|
|
3862
|
+
if (!result.success) {
|
|
2924
3863
|
return {
|
|
2925
|
-
success: false,
|
|
2926
|
-
errors:
|
|
2927
|
-
`Imported orbital "${parsed.alias}" does not have an inline entity. Entity references cannot be chained.`
|
|
2928
|
-
]
|
|
3864
|
+
success: false,
|
|
3865
|
+
errors: result.errors
|
|
2929
3866
|
};
|
|
2930
3867
|
}
|
|
2931
|
-
|
|
3868
|
+
this.preprocessedCache.set(cacheKey, result.data);
|
|
3869
|
+
this.entitySharingMap = { ...this.entitySharingMap, ...result.data.entitySharing };
|
|
3870
|
+
this.eventNamespaceMap = { ...this.eventNamespaceMap, ...result.data.eventNamespaces };
|
|
3871
|
+
this.register(result.data.schema);
|
|
2932
3872
|
return {
|
|
2933
3873
|
success: true,
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
alias: parsed.alias,
|
|
2938
|
-
persistence
|
|
2939
|
-
}
|
|
2940
|
-
},
|
|
2941
|
-
warnings: []
|
|
3874
|
+
entitySharing: result.data.entitySharing,
|
|
3875
|
+
eventNamespaces: result.data.eventNamespaces,
|
|
3876
|
+
warnings: result.data.warnings
|
|
2942
3877
|
};
|
|
2943
3878
|
}
|
|
2944
3879
|
/**
|
|
2945
|
-
* Get
|
|
3880
|
+
* Get entity sharing information for registered orbitals.
|
|
3881
|
+
* Useful for determining entity isolation and collection names.
|
|
2946
3882
|
*/
|
|
2947
|
-
|
|
3883
|
+
getEntitySharing() {
|
|
3884
|
+
return { ...this.entitySharingMap };
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3887
|
+
* Get event namespace mapping for registered orbitals.
|
|
3888
|
+
* Useful for debugging cross-orbital event routing.
|
|
3889
|
+
*/
|
|
3890
|
+
getEventNamespaces() {
|
|
3891
|
+
return { ...this.eventNamespaceMap };
|
|
3892
|
+
}
|
|
3893
|
+
/**
|
|
3894
|
+
* Clear the preprocessing cache.
|
|
3895
|
+
*/
|
|
3896
|
+
clearPreprocessCache() {
|
|
3897
|
+
this.preprocessedCache.clear();
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Register a single orbital
|
|
3901
|
+
*/
|
|
3902
|
+
async registerOrbitalAsync(orbital) {
|
|
3903
|
+
const configByTrait = /* @__PURE__ */ new Map();
|
|
3904
|
+
const unwrapped = (orbital.traits || []).map((t) => {
|
|
3905
|
+
if (t && typeof t === "object" && "ref" in t && "_resolved" in t) {
|
|
3906
|
+
const wrapper = t;
|
|
3907
|
+
const inner = wrapper._resolved;
|
|
3908
|
+
if (wrapper.config && inner?.name) {
|
|
3909
|
+
configByTrait.set(inner.name, wrapper.config);
|
|
3910
|
+
}
|
|
3911
|
+
return inner;
|
|
3912
|
+
}
|
|
3913
|
+
return t;
|
|
3914
|
+
});
|
|
3915
|
+
const inlineTraits = unwrapped.filter(isInlineTrait);
|
|
3916
|
+
const traitDefs = inlineTraits.map((t) => {
|
|
3917
|
+
const sm = t.stateMachine;
|
|
3918
|
+
const states = sm?.states || [];
|
|
3919
|
+
const transitions = sm?.transitions || [];
|
|
3920
|
+
return {
|
|
3921
|
+
name: t.name,
|
|
3922
|
+
states,
|
|
3923
|
+
transitions,
|
|
3924
|
+
listens: t.listens
|
|
3925
|
+
};
|
|
3926
|
+
});
|
|
3927
|
+
const manager = new StateMachineManager(traitDefs, {
|
|
3928
|
+
contextExtensions: this.config.contextExtensions
|
|
3929
|
+
});
|
|
3930
|
+
for (const [traitName, traitConfig] of configByTrait) {
|
|
3931
|
+
manager.setTraitConfig(traitName, traitConfig);
|
|
3932
|
+
}
|
|
2948
3933
|
const entityRef = orbital.entity;
|
|
3934
|
+
let entity;
|
|
2949
3935
|
if (typeof entityRef === "string") {
|
|
2950
|
-
|
|
2951
|
-
}
|
|
2952
|
-
if (isEntityCall(entityRef)) {
|
|
3936
|
+
entity = { name: entityRef, fields: [] };
|
|
3937
|
+
} else if (isEntityCall(entityRef)) {
|
|
2953
3938
|
const fallbackName = entityRef.name ?? entityRef.extends.replace(/\.entity$/, "");
|
|
2954
|
-
|
|
3939
|
+
entity = {
|
|
2955
3940
|
name: fallbackName,
|
|
2956
3941
|
fields: entityRef.fields ?? [],
|
|
2957
3942
|
...entityRef.persistence ? { persistence: entityRef.persistence } : {},
|
|
2958
3943
|
...entityRef.collection ? { collection: entityRef.collection } : {}
|
|
2959
3944
|
};
|
|
3945
|
+
} else {
|
|
3946
|
+
entity = entityRef;
|
|
3947
|
+
}
|
|
3948
|
+
this.orbitals.set(orbital.name, {
|
|
3949
|
+
schema: orbital,
|
|
3950
|
+
entity,
|
|
3951
|
+
traits: inlineTraits,
|
|
3952
|
+
configByTrait,
|
|
3953
|
+
manager,
|
|
3954
|
+
entityData: /* @__PURE__ */ new Map()
|
|
3955
|
+
});
|
|
3956
|
+
if (entity?.name && entity.instances && Array.isArray(entity.instances)) {
|
|
3957
|
+
const instances = entity.instances;
|
|
3958
|
+
if (instances.length > 0) {
|
|
3959
|
+
console.log(`[OrbitalRuntime] Seeding ${instances.length} instances for ${entity.name} from schema`);
|
|
3960
|
+
const results = await Promise.all(
|
|
3961
|
+
instances.map(async (instance) => {
|
|
3962
|
+
try {
|
|
3963
|
+
const result = await this.persistence.create(entity.name, instance);
|
|
3964
|
+
console.log(`[OrbitalRuntime] Seeded instance: ${instance.id || "no-id"}`);
|
|
3965
|
+
return result;
|
|
3966
|
+
} catch (err) {
|
|
3967
|
+
console.error(`[OrbitalRuntime] Failed to seed instance ${instance.id}:`, err);
|
|
3968
|
+
return null;
|
|
3969
|
+
}
|
|
3970
|
+
})
|
|
3971
|
+
);
|
|
3972
|
+
const successCount = results.filter((r) => r !== null).length;
|
|
3973
|
+
console.log(`[OrbitalRuntime] Seeded ${successCount}/${instances.length} ${entity.name} instances from schema`);
|
|
3974
|
+
}
|
|
3975
|
+
} else if (this.config.mode === "mock" && this.persistence instanceof MockPersistenceAdapter) {
|
|
3976
|
+
if (this.config.debug) {
|
|
3977
|
+
console.log(`[OrbitalRuntime] No instances in schema, generating mock data for ${entity?.name}`);
|
|
3978
|
+
}
|
|
3979
|
+
if (entity?.name && entity.fields) {
|
|
3980
|
+
const fields = entity.fields.filter(
|
|
3981
|
+
(f) => typeof f.name === "string" && f.name.length > 0
|
|
3982
|
+
).map((f) => ({
|
|
3983
|
+
name: f.name,
|
|
3984
|
+
type: f.type,
|
|
3985
|
+
required: f.required,
|
|
3986
|
+
values: f.values,
|
|
3987
|
+
default: f.default
|
|
3988
|
+
}));
|
|
3989
|
+
this.persistence.registerEntity({ name: entity.name, fields });
|
|
3990
|
+
if (this.config.debug) {
|
|
3991
|
+
console.log(`[OrbitalRuntime] Seeded mock data for entity: ${entity.name}, count: ${this.persistence.count(entity.name)}`);
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
if (this.config.debug) {
|
|
3996
|
+
console.log(
|
|
3997
|
+
`[OrbitalRuntime] Registered orbital: ${orbital.name} with ${(orbital.traits || []).length} trait(s)`
|
|
3998
|
+
);
|
|
2960
3999
|
}
|
|
2961
|
-
return entityRef;
|
|
2962
4000
|
}
|
|
2963
4001
|
/**
|
|
2964
|
-
*
|
|
4002
|
+
* Register a single orbital (sync wrapper for backward compatibility)
|
|
2965
4003
|
*/
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
4004
|
+
registerOrbital(orbital) {
|
|
4005
|
+
this.registerOrbitalAsync(orbital).catch((err) => {
|
|
4006
|
+
console.error(`[OrbitalRuntime] Failed to register orbital:`, err);
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
/**
|
|
4010
|
+
* Set up event listeners for cross-orbital communication
|
|
4011
|
+
*/
|
|
4012
|
+
setupEventListeners() {
|
|
4013
|
+
for (const cleanup of this.listenerCleanups) {
|
|
4014
|
+
cleanup();
|
|
4015
|
+
}
|
|
4016
|
+
this.listenerCleanups = [];
|
|
4017
|
+
for (const [orbitalName, registered] of this.orbitals) {
|
|
4018
|
+
for (const trait of registered.traits) {
|
|
4019
|
+
if (!trait.listens) continue;
|
|
4020
|
+
for (const listener of trait.listens) {
|
|
4021
|
+
const { bareEvent, matcher } = parseListenSource(listener, orbitalName);
|
|
4022
|
+
const cleanup = this.eventBus.on(bareEvent, async (event) => {
|
|
4023
|
+
if (!matcher(event.source)) return;
|
|
4024
|
+
if (this.config.debug) {
|
|
4025
|
+
console.log(
|
|
4026
|
+
`[OrbitalRuntime] ${orbitalName}.${trait.name} received: ${listener.event} (from ${event.source?.orbital ?? "?"}.${event.source?.trait ?? "?"})`
|
|
4027
|
+
);
|
|
4028
|
+
}
|
|
4029
|
+
let mappedPayload = event.payload;
|
|
4030
|
+
if (listener.payloadMapping && event.payload) {
|
|
4031
|
+
mappedPayload = {};
|
|
4032
|
+
for (const [key, expr] of Object.entries(
|
|
4033
|
+
listener.payloadMapping
|
|
4034
|
+
)) {
|
|
4035
|
+
if (typeof expr === "string" && expr.startsWith("@payload.")) {
|
|
4036
|
+
const field = expr.slice("@payload.".length);
|
|
4037
|
+
mappedPayload[key] = event.payload[field];
|
|
4038
|
+
} else {
|
|
4039
|
+
mappedPayload[key] = expr;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
const raw = event.payload;
|
|
4044
|
+
const mapped = mappedPayload;
|
|
4045
|
+
const pickId = (field) => mapped?.[field] ?? raw?.[field];
|
|
4046
|
+
const forwardedEntityId = pickId("entityId") ?? pickId("orbitalName");
|
|
4047
|
+
await this.processOrbitalEvent(orbitalName, {
|
|
4048
|
+
event: listener.triggers,
|
|
4049
|
+
payload: mappedPayload,
|
|
4050
|
+
entityId: forwardedEntityId
|
|
4051
|
+
});
|
|
4052
|
+
});
|
|
4053
|
+
this.listenerCleanups.push(cleanup);
|
|
4054
|
+
}
|
|
2975
4055
|
}
|
|
2976
4056
|
}
|
|
2977
|
-
if (errors.length > 0) {
|
|
2978
|
-
return { success: false, errors };
|
|
2979
|
-
}
|
|
2980
|
-
return { success: true, data: resolved, warnings: [] };
|
|
2981
4057
|
}
|
|
2982
4058
|
/**
|
|
2983
|
-
*
|
|
4059
|
+
* Set up scheduled ticks for all traits
|
|
2984
4060
|
*/
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
};
|
|
4061
|
+
setupTicks() {
|
|
4062
|
+
this.cleanupTicks();
|
|
4063
|
+
for (const [orbitalName, registered] of this.orbitals) {
|
|
4064
|
+
for (const trait of registered.traits || []) {
|
|
4065
|
+
if (!trait.ticks || trait.ticks.length === 0) continue;
|
|
4066
|
+
for (const tick of trait.ticks) {
|
|
4067
|
+
this.registerTick(orbitalName, trait.name, tick, registered);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
2995
4070
|
}
|
|
2996
|
-
if (
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
refObj.ref,
|
|
3000
|
-
imports,
|
|
3001
|
-
refObj.config,
|
|
3002
|
-
refObj.linkedEntity,
|
|
3003
|
-
refObj.name,
|
|
3004
|
-
refObj.events,
|
|
3005
|
-
refObj.listens
|
|
4071
|
+
if (this.config.debug && this.tickBindings.length > 0) {
|
|
4072
|
+
console.log(
|
|
4073
|
+
`[OrbitalRuntime] Registered ${this.tickBindings.length} tick(s)`
|
|
3006
4074
|
);
|
|
3007
4075
|
}
|
|
3008
|
-
if (typeof traitRef === "string") {
|
|
3009
|
-
return this.resolveTraitRefString(traitRef, imports);
|
|
3010
|
-
}
|
|
3011
|
-
return {
|
|
3012
|
-
success: false,
|
|
3013
|
-
errors: [`Unknown trait reference format: ${JSON.stringify(traitRef)}`]
|
|
3014
|
-
};
|
|
3015
4076
|
}
|
|
3016
4077
|
/**
|
|
3017
|
-
*
|
|
4078
|
+
* Register a single tick
|
|
3018
4079
|
*/
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
if (
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
`Unknown import alias in trait reference: ${parsed.alias}. Available aliases: ${Array.from(imports.orbitals.keys()).join(", ") || "none"}`
|
|
3028
|
-
]
|
|
3029
|
-
};
|
|
3030
|
-
}
|
|
3031
|
-
const trait = this.findTraitInOrbital(imported.orbital, parsed.traitName);
|
|
3032
|
-
if (!trait) {
|
|
3033
|
-
return {
|
|
3034
|
-
success: false,
|
|
3035
|
-
errors: [
|
|
3036
|
-
`Trait "${parsed.traitName}" not found in imported orbital "${parsed.alias}". Available traits: ${this.listTraitsInOrbital(imported.orbital).join(", ") || "none"}`
|
|
3037
|
-
]
|
|
3038
|
-
};
|
|
3039
|
-
}
|
|
3040
|
-
const baseTrait = overrideName ? { ...trait, name: overrideName } : trait;
|
|
3041
|
-
const reboundTrait = applyLinkedEntityRename(baseTrait, linkedEntity);
|
|
3042
|
-
const renamedTrait = applyEventRenames(reboundTrait, eventRenames);
|
|
3043
|
-
const finalTrait = listensOverride !== void 0 ? { ...renamedTrait, listens: listensOverride } : renamedTrait;
|
|
3044
|
-
if (listensOverride !== void 0) {
|
|
3045
|
-
refResolverLog.info("listens-override:imported", {
|
|
3046
|
-
trait: finalTrait.name,
|
|
3047
|
-
ref,
|
|
3048
|
-
atomListens: trait.listens?.length ?? 0,
|
|
3049
|
-
callSiteListens: listensOverride.length
|
|
3050
|
-
});
|
|
3051
|
-
}
|
|
3052
|
-
return {
|
|
3053
|
-
success: true,
|
|
3054
|
-
data: {
|
|
3055
|
-
trait: finalTrait,
|
|
3056
|
-
source: { type: "imported", alias: parsed.alias, traitName: parsed.traitName },
|
|
3057
|
-
config,
|
|
3058
|
-
linkedEntity
|
|
3059
|
-
},
|
|
3060
|
-
warnings: []
|
|
3061
|
-
};
|
|
4080
|
+
registerTick(orbitalName, traitName, tick, registered) {
|
|
4081
|
+
let intervalMs;
|
|
4082
|
+
if (typeof tick.interval === "number") {
|
|
4083
|
+
intervalMs = tick.interval;
|
|
4084
|
+
} else if (typeof tick.interval === "string") {
|
|
4085
|
+
intervalMs = this.parseIntervalString(tick.interval);
|
|
4086
|
+
} else {
|
|
4087
|
+
intervalMs = 1e3;
|
|
3062
4088
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
const renamedLocalTrait = applyEventRenames(reboundLocal, eventRenames);
|
|
3068
|
-
const finalLocalTrait = listensOverride !== void 0 ? { ...renamedLocalTrait, listens: listensOverride } : renamedLocalTrait;
|
|
3069
|
-
if (listensOverride !== void 0) {
|
|
3070
|
-
refResolverLog.info("listens-override:local", {
|
|
3071
|
-
trait: finalLocalTrait.name,
|
|
3072
|
-
ref,
|
|
3073
|
-
atomListens: localTrait.listens?.length ?? 0,
|
|
3074
|
-
callSiteListens: listensOverride.length
|
|
3075
|
-
});
|
|
3076
|
-
}
|
|
3077
|
-
return {
|
|
3078
|
-
success: true,
|
|
3079
|
-
data: {
|
|
3080
|
-
trait: finalLocalTrait,
|
|
3081
|
-
source: { type: "local", name: ref },
|
|
3082
|
-
config,
|
|
3083
|
-
linkedEntity
|
|
3084
|
-
},
|
|
3085
|
-
warnings: []
|
|
3086
|
-
};
|
|
4089
|
+
if (this.config.debug) {
|
|
4090
|
+
console.log(
|
|
4091
|
+
`[OrbitalRuntime] Registering tick: ${orbitalName}.${traitName}.${tick.name} (${intervalMs}ms)`
|
|
4092
|
+
);
|
|
3087
4093
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
4094
|
+
const timerId = setInterval(async () => {
|
|
4095
|
+
await this.executeTick(orbitalName, traitName, tick, registered);
|
|
4096
|
+
}, intervalMs);
|
|
4097
|
+
this.tickBindings.push({
|
|
4098
|
+
orbitalName,
|
|
4099
|
+
traitName,
|
|
4100
|
+
tick,
|
|
4101
|
+
timerId
|
|
4102
|
+
});
|
|
3094
4103
|
}
|
|
3095
4104
|
/**
|
|
3096
|
-
*
|
|
4105
|
+
* Parse interval string to milliseconds
|
|
4106
|
+
* Supports: '5s', '1m', '1h', '30000' (ms)
|
|
3097
4107
|
*/
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
4108
|
+
parseIntervalString(interval) {
|
|
4109
|
+
const match = interval.match(/^(\d+)(ms|s|m|h)?$/);
|
|
4110
|
+
if (!match) {
|
|
4111
|
+
console.warn(
|
|
4112
|
+
`[OrbitalRuntime] Invalid interval format: ${interval}, defaulting to 1000ms`
|
|
4113
|
+
);
|
|
4114
|
+
return 1e3;
|
|
4115
|
+
}
|
|
4116
|
+
const value = parseInt(match[1], 10);
|
|
4117
|
+
const unit = match[2] || "ms";
|
|
4118
|
+
switch (unit) {
|
|
4119
|
+
case "ms":
|
|
4120
|
+
return value;
|
|
4121
|
+
case "s":
|
|
4122
|
+
return value * 1e3;
|
|
4123
|
+
case "m":
|
|
4124
|
+
return value * 60 * 1e3;
|
|
4125
|
+
case "h":
|
|
4126
|
+
return value * 60 * 60 * 1e3;
|
|
4127
|
+
default:
|
|
4128
|
+
return value;
|
|
3109
4129
|
}
|
|
3110
|
-
return null;
|
|
3111
4130
|
}
|
|
3112
4131
|
/**
|
|
3113
|
-
*
|
|
4132
|
+
* Execute a tick for all applicable entities
|
|
3114
4133
|
*/
|
|
3115
|
-
|
|
3116
|
-
const
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
4134
|
+
async executeTick(orbitalName, traitName, tick, registered) {
|
|
4135
|
+
const entityType = registered.entity.name;
|
|
4136
|
+
const emittedEvents = [];
|
|
4137
|
+
try {
|
|
4138
|
+
let entities = await this.persistence.list(entityType);
|
|
4139
|
+
if (tick.appliesTo && tick.appliesTo.length > 0) {
|
|
4140
|
+
const appliesToSet = new Set(tick.appliesTo);
|
|
4141
|
+
entities = entities.filter((e) => appliesToSet.has(e.id));
|
|
4142
|
+
}
|
|
4143
|
+
if (this.config.debug && entities.length > 0) {
|
|
4144
|
+
console.log(
|
|
4145
|
+
`[OrbitalRuntime] Tick ${orbitalName}.${traitName}.${tick.name}: processing ${entities.length} entities`
|
|
4146
|
+
);
|
|
4147
|
+
}
|
|
4148
|
+
for (const entity of entities) {
|
|
4149
|
+
if (tick.guard) {
|
|
4150
|
+
try {
|
|
4151
|
+
const ctx = createContextFromBindings({
|
|
4152
|
+
entity,
|
|
4153
|
+
payload: {},
|
|
4154
|
+
state: registered.manager.getState(traitName)?.currentState || "unknown"
|
|
4155
|
+
}, false, this.config.contextExtensions);
|
|
4156
|
+
const guardPasses = evaluateGuard(
|
|
4157
|
+
tick.guard,
|
|
4158
|
+
ctx
|
|
4159
|
+
);
|
|
4160
|
+
if (!guardPasses) {
|
|
4161
|
+
if (this.config.debug) {
|
|
4162
|
+
console.log(
|
|
4163
|
+
`[OrbitalRuntime] Tick ${tick.name}: guard failed for entity ${entity.id}`
|
|
4164
|
+
);
|
|
4165
|
+
}
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
} catch (error) {
|
|
4169
|
+
console.error(
|
|
4170
|
+
`[OrbitalRuntime] Tick ${tick.name}: guard evaluation error for entity ${entity.id}:`,
|
|
4171
|
+
error
|
|
4172
|
+
);
|
|
4173
|
+
continue;
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
if (tick.effects && tick.effects.length > 0) {
|
|
4177
|
+
const fetchedData = {};
|
|
4178
|
+
const clientEffects = [];
|
|
4179
|
+
const tickEffectResults = [];
|
|
4180
|
+
await this.executeEffects(
|
|
4181
|
+
registered,
|
|
4182
|
+
traitName,
|
|
4183
|
+
tick.effects,
|
|
4184
|
+
{},
|
|
4185
|
+
// No payload for ticks
|
|
4186
|
+
entity,
|
|
4187
|
+
entity.id,
|
|
4188
|
+
emittedEvents,
|
|
4189
|
+
fetchedData,
|
|
4190
|
+
clientEffects,
|
|
4191
|
+
tickEffectResults
|
|
4192
|
+
);
|
|
4193
|
+
if (this.config.debug) {
|
|
4194
|
+
console.log(
|
|
4195
|
+
`[OrbitalRuntime] Tick ${tick.name}: executed effects for entity ${entity.id}`
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
3120
4199
|
}
|
|
4200
|
+
} catch (error) {
|
|
4201
|
+
console.error(
|
|
4202
|
+
`[OrbitalRuntime] Tick ${tick.name} execution error:`,
|
|
4203
|
+
error
|
|
4204
|
+
);
|
|
3121
4205
|
}
|
|
3122
|
-
return names;
|
|
3123
4206
|
}
|
|
3124
4207
|
/**
|
|
3125
|
-
*
|
|
4208
|
+
* Clean up all active ticks
|
|
3126
4209
|
*/
|
|
3127
|
-
|
|
3128
|
-
const
|
|
3129
|
-
|
|
3130
|
-
for (const pageRef of pageRefs) {
|
|
3131
|
-
const result = this.resolvePageRef(pageRef, imports);
|
|
3132
|
-
if (!result.success) {
|
|
3133
|
-
errors.push(...result.errors);
|
|
3134
|
-
} else {
|
|
3135
|
-
resolved.push(result.data);
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3138
|
-
if (errors.length > 0) {
|
|
3139
|
-
return { success: false, errors };
|
|
4210
|
+
cleanupTicks() {
|
|
4211
|
+
for (const binding of this.tickBindings) {
|
|
4212
|
+
clearInterval(binding.timerId);
|
|
3140
4213
|
}
|
|
3141
|
-
|
|
4214
|
+
this.tickBindings = [];
|
|
3142
4215
|
}
|
|
3143
4216
|
/**
|
|
3144
|
-
*
|
|
4217
|
+
* Unregister all orbitals and clean up
|
|
3145
4218
|
*/
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
data: {
|
|
3151
|
-
page: pageRef,
|
|
3152
|
-
source: { type: "inline" },
|
|
3153
|
-
pathOverridden: false
|
|
3154
|
-
},
|
|
3155
|
-
warnings: []
|
|
3156
|
-
};
|
|
4219
|
+
unregisterAll() {
|
|
4220
|
+
this.cleanupTicks();
|
|
4221
|
+
for (const cleanup of this.listenerCleanups) {
|
|
4222
|
+
cleanup();
|
|
3157
4223
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
4224
|
+
this.listenerCleanups = [];
|
|
4225
|
+
this.orbitals.clear();
|
|
4226
|
+
this.eventBus.clear();
|
|
4227
|
+
if (this.persistence instanceof MockPersistenceAdapter) {
|
|
4228
|
+
this.persistence.clearAll();
|
|
3160
4229
|
}
|
|
3161
|
-
if (
|
|
3162
|
-
|
|
4230
|
+
if (this.osHandlers) {
|
|
4231
|
+
this.osHandlers.cleanup();
|
|
4232
|
+
this.osHandlers = null;
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
/**
|
|
4236
|
+
* Reset the mock persistence store to a clean-slate re-seed without
|
|
4237
|
+
* unregistering orbitals. Exposed for verifier tools that want to
|
|
4238
|
+
* start each test with deterministic seeded rows, not the residue of
|
|
4239
|
+
* the previous walk's persist-creates. No-op when the persistence
|
|
4240
|
+
* layer is not MockPersistenceAdapter.
|
|
4241
|
+
*/
|
|
4242
|
+
resetMockPersistence() {
|
|
4243
|
+
if (!(this.persistence instanceof MockPersistenceAdapter)) return;
|
|
4244
|
+
busLog.debug("mock:reset:enter", {
|
|
4245
|
+
orbitalCount: this.orbitals.size,
|
|
4246
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4247
|
+
});
|
|
4248
|
+
this.persistence.clearAll();
|
|
4249
|
+
for (const registered of this.orbitals.values()) {
|
|
4250
|
+
const entity = registered.entity;
|
|
4251
|
+
if (entity?.name && entity.fields) {
|
|
4252
|
+
const fields = entity.fields.filter(
|
|
4253
|
+
(f) => typeof f.name === "string" && f.name.length > 0
|
|
4254
|
+
).map((f) => ({
|
|
4255
|
+
name: f.name,
|
|
4256
|
+
type: f.type,
|
|
4257
|
+
required: f.required,
|
|
4258
|
+
values: f.values,
|
|
4259
|
+
default: f.default
|
|
4260
|
+
}));
|
|
4261
|
+
this.persistence.registerEntity({ name: entity.name, fields });
|
|
4262
|
+
}
|
|
3163
4263
|
}
|
|
3164
|
-
return {
|
|
3165
|
-
success: false,
|
|
3166
|
-
errors: [`Unknown page reference format: ${JSON.stringify(pageRef)}`]
|
|
3167
|
-
};
|
|
3168
4264
|
}
|
|
4265
|
+
// ==========================================================================
|
|
4266
|
+
// Event Processing
|
|
4267
|
+
// ==========================================================================
|
|
3169
4268
|
/**
|
|
3170
|
-
*
|
|
4269
|
+
* Process an event for an orbital
|
|
3171
4270
|
*/
|
|
3172
|
-
|
|
3173
|
-
const
|
|
3174
|
-
if (!
|
|
4271
|
+
async processOrbitalEvent(orbitalName, request) {
|
|
4272
|
+
const registered = this.orbitals.get(orbitalName);
|
|
4273
|
+
if (!registered) {
|
|
3175
4274
|
return {
|
|
3176
4275
|
success: false,
|
|
3177
|
-
|
|
4276
|
+
transitioned: false,
|
|
4277
|
+
states: {},
|
|
4278
|
+
emittedEvents: [],
|
|
4279
|
+
error: `Orbital not found: ${orbitalName}`
|
|
3178
4280
|
};
|
|
3179
4281
|
}
|
|
3180
|
-
const
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
4282
|
+
const payloadRow = request.payload?.["row"];
|
|
4283
|
+
const payloadRowAsPayload = payloadRow !== null && typeof payloadRow === "object" && !Array.isArray(payloadRow) ? payloadRow : void 0;
|
|
4284
|
+
const payloadRowId = payloadRowAsPayload?.["id"];
|
|
4285
|
+
renderLog2.debug("processOrbitalEvent:enter", {
|
|
4286
|
+
orbital: orbitalName,
|
|
4287
|
+
event: request.event,
|
|
4288
|
+
hasPayloadRow: payloadRowAsPayload !== void 0,
|
|
4289
|
+
payloadRowId: typeof payloadRowId === "string" || typeof payloadRowId === "number" ? payloadRowId : void 0,
|
|
4290
|
+
entityId: request.entityId
|
|
4291
|
+
});
|
|
4292
|
+
busLog.debug("bus:incoming", {
|
|
4293
|
+
orbital: orbitalName,
|
|
4294
|
+
event: request.event,
|
|
4295
|
+
payload: JSON.stringify(request.payload ?? null),
|
|
4296
|
+
entityId: request.entityId,
|
|
4297
|
+
traitStates: JSON.stringify(
|
|
4298
|
+
Array.from(registered.manager.getAllStates().entries()).map(([traitName, state]) => ({
|
|
4299
|
+
traitName,
|
|
4300
|
+
currentState: state.currentState
|
|
4301
|
+
}))
|
|
4302
|
+
)
|
|
4303
|
+
});
|
|
4304
|
+
xOrbitalLog.info("processOrbitalEvent:enter", {
|
|
4305
|
+
orbital: orbitalName,
|
|
4306
|
+
event: request.event,
|
|
4307
|
+
traitsInOrbital: registered.traits.map((t) => t.name).join(","),
|
|
4308
|
+
payloadActiveTraits: JSON.stringify(
|
|
4309
|
+
request.payload?.["_activeTraits"] ?? null
|
|
4310
|
+
)
|
|
4311
|
+
});
|
|
4312
|
+
const { event, payload, entityId, user } = request;
|
|
4313
|
+
const validationFailures = [];
|
|
4314
|
+
for (const trait of registered.traits) {
|
|
4315
|
+
const eventSchema = trait.stateMachine?.events?.find((e) => e.key === event);
|
|
4316
|
+
if (eventSchema?.payloadSchema && eventSchema.payloadSchema.length > 0) {
|
|
4317
|
+
validationFailures.push(
|
|
4318
|
+
...validateEventPayload(event, payload, eventSchema.payloadSchema)
|
|
4319
|
+
);
|
|
4320
|
+
}
|
|
3188
4321
|
}
|
|
3189
|
-
|
|
3190
|
-
if (!page) {
|
|
4322
|
+
if (validationFailures.length > 0) {
|
|
3191
4323
|
return {
|
|
3192
4324
|
success: false,
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
]
|
|
4325
|
+
transitioned: false,
|
|
4326
|
+
states: {},
|
|
4327
|
+
emittedEvents: [],
|
|
4328
|
+
error: formatPayloadValidationError(validationFailures)
|
|
3196
4329
|
};
|
|
3197
4330
|
}
|
|
3198
|
-
|
|
4331
|
+
const emittedEvents = [];
|
|
4332
|
+
const fetchedData = {};
|
|
4333
|
+
const clientEffects = [];
|
|
4334
|
+
const clientEffectsByTrait = [];
|
|
4335
|
+
const effectResults = [];
|
|
4336
|
+
const activeTraits = payload?._activeTraits;
|
|
4337
|
+
const cleanPayload = payload ? { ...payload } : void 0;
|
|
4338
|
+
if (cleanPayload) {
|
|
4339
|
+
delete cleanPayload._activeTraits;
|
|
4340
|
+
}
|
|
4341
|
+
let entityData = {};
|
|
4342
|
+
if (entityId) {
|
|
4343
|
+
const stored = await this.persistence.getById(
|
|
4344
|
+
registered.entity.name,
|
|
4345
|
+
entityId
|
|
4346
|
+
);
|
|
4347
|
+
if (stored) {
|
|
4348
|
+
entityData = stored;
|
|
4349
|
+
}
|
|
4350
|
+
} else if (registered.entity?.name) {
|
|
4351
|
+
try {
|
|
4352
|
+
const all = await this.persistence.list(registered.entity.name);
|
|
4353
|
+
if (Array.isArray(all) && all.length > 0 && all[0]) {
|
|
4354
|
+
entityData = all[0];
|
|
4355
|
+
}
|
|
4356
|
+
} catch {
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
const results = registered.manager.sendEvent(event, cleanPayload, entityData);
|
|
4360
|
+
const filteredResults = activeTraits && activeTraits.length > 0 ? results.filter(({ traitName }) => activeTraits.includes(traitName)) : results;
|
|
4361
|
+
if (this.config.debug && activeTraits) {
|
|
4362
|
+
console.log(`[OrbitalRuntime] Filtering traits: ${results.length} total, ${filteredResults.length} active (${activeTraits.join(", ")})`);
|
|
4363
|
+
}
|
|
4364
|
+
for (const { traitName, result } of filteredResults) {
|
|
4365
|
+
if (result.effects.length > 0) {
|
|
4366
|
+
await this.executeEffects(
|
|
4367
|
+
registered,
|
|
4368
|
+
traitName,
|
|
4369
|
+
result.effects,
|
|
4370
|
+
cleanPayload,
|
|
4371
|
+
entityData,
|
|
4372
|
+
entityId,
|
|
4373
|
+
emittedEvents,
|
|
4374
|
+
fetchedData,
|
|
4375
|
+
clientEffects,
|
|
4376
|
+
effectResults,
|
|
4377
|
+
user,
|
|
4378
|
+
clientEffectsByTrait
|
|
4379
|
+
);
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
const states = {};
|
|
4383
|
+
for (const [name, state] of registered.manager.getAllStates()) {
|
|
4384
|
+
states[name] = state.currentState;
|
|
4385
|
+
}
|
|
4386
|
+
const response = {
|
|
3199
4387
|
success: true,
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
4388
|
+
transitioned: results.length > 0,
|
|
4389
|
+
states,
|
|
4390
|
+
emittedEvents
|
|
4391
|
+
};
|
|
4392
|
+
if (clientEffects.length > 0) {
|
|
4393
|
+
response.clientEffects = clientEffects;
|
|
4394
|
+
}
|
|
4395
|
+
if (clientEffectsByTrait.length > 0) {
|
|
4396
|
+
response.clientEffectsByTrait = clientEffectsByTrait;
|
|
4397
|
+
}
|
|
4398
|
+
if (effectResults.length > 0) {
|
|
4399
|
+
response.effectResults = effectResults;
|
|
4400
|
+
}
|
|
4401
|
+
return response;
|
|
4402
|
+
}
|
|
4403
|
+
/**
|
|
4404
|
+
* Execute effects from a transition
|
|
4405
|
+
*/
|
|
4406
|
+
async executeEffects(registered, traitName, effects, payload, entityData, entityId, emittedEvents, fetchedData, clientEffects, effectResults, user, clientEffectsByTrait) {
|
|
4407
|
+
const entityType = registered.entity.name;
|
|
4408
|
+
const pushClientEffect = (effect) => {
|
|
4409
|
+
clientEffects.push(effect);
|
|
4410
|
+
clientEffectsByTrait?.push({ traitName, effect });
|
|
4411
|
+
};
|
|
4412
|
+
let bindingsRef = null;
|
|
4413
|
+
let contextRef = null;
|
|
4414
|
+
const handlers = {
|
|
4415
|
+
emit: (event, eventPayload, source) => {
|
|
4416
|
+
if (this.config.debug) {
|
|
4417
|
+
console.log(`[OrbitalRuntime] Emitting: ${event}`, eventPayload, source);
|
|
4418
|
+
}
|
|
4419
|
+
const stamp = source ?? {
|
|
4420
|
+
orbital: registered.schema.name,
|
|
4421
|
+
trait: traitName
|
|
4422
|
+
};
|
|
4423
|
+
this.eventBus.emit(event, eventPayload, stamp);
|
|
4424
|
+
emittedEvents.push({ event, payload: eventPayload, source: stamp });
|
|
4425
|
+
effectLog2.debug("emit:push", {
|
|
4426
|
+
event,
|
|
4427
|
+
cumulativeEmittedCount: emittedEvents.length,
|
|
4428
|
+
sourceTrait: stamp.trait,
|
|
4429
|
+
sourceOrbital: stamp.orbital
|
|
4430
|
+
});
|
|
4431
|
+
xOrbitalLog.info("emit:server", {
|
|
4432
|
+
event,
|
|
4433
|
+
sourceOrbital: stamp.orbital,
|
|
4434
|
+
sourceTrait: stamp.trait,
|
|
4435
|
+
dispatchOrbital: registered.schema.name
|
|
4436
|
+
});
|
|
3204
4437
|
},
|
|
3205
|
-
|
|
4438
|
+
set: async (targetId, field, value) => {
|
|
4439
|
+
const id = targetId || entityId;
|
|
4440
|
+
if (id) {
|
|
4441
|
+
try {
|
|
4442
|
+
await this.persistence.update(entityType, id, { [field]: value });
|
|
4443
|
+
effectResults.push({
|
|
4444
|
+
effect: "set",
|
|
4445
|
+
entityType,
|
|
4446
|
+
data: { id, field, value },
|
|
4447
|
+
success: true
|
|
4448
|
+
});
|
|
4449
|
+
} catch (err) {
|
|
4450
|
+
effectResults.push({
|
|
4451
|
+
effect: "set",
|
|
4452
|
+
entityType,
|
|
4453
|
+
data: { id, field, value },
|
|
4454
|
+
success: false,
|
|
4455
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
},
|
|
4460
|
+
persist: async (action, targetEntityType, data) => {
|
|
4461
|
+
if (action === "batch") {
|
|
4462
|
+
const operations = data?.operations;
|
|
4463
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
4464
|
+
effectResults.push({
|
|
4465
|
+
effect: "persist",
|
|
4466
|
+
action: "batch",
|
|
4467
|
+
success: false,
|
|
4468
|
+
error: "Batch requires a non-empty operations array"
|
|
4469
|
+
});
|
|
4470
|
+
return;
|
|
4471
|
+
}
|
|
4472
|
+
const batchResults = [];
|
|
4473
|
+
const completed = [];
|
|
4474
|
+
let batchFailed = false;
|
|
4475
|
+
let batchError = "";
|
|
4476
|
+
for (const op of operations) {
|
|
4477
|
+
if (!Array.isArray(op) || op.length < 2) {
|
|
4478
|
+
batchFailed = true;
|
|
4479
|
+
batchError = `Invalid batch operation format: ${JSON.stringify(op)}`;
|
|
4480
|
+
break;
|
|
4481
|
+
}
|
|
4482
|
+
const [opAction, opEntityType, ...opRest] = op;
|
|
4483
|
+
try {
|
|
4484
|
+
switch (opAction) {
|
|
4485
|
+
case "create": {
|
|
4486
|
+
const createData = opRest[0] || {};
|
|
4487
|
+
const { id: newId } = await this.persistence.create(opEntityType, createData);
|
|
4488
|
+
batchResults.push({ action: "create", entityType: opEntityType, id: newId, ...createData });
|
|
4489
|
+
completed.push({ action: "create", entityType: opEntityType, id: newId });
|
|
4490
|
+
break;
|
|
4491
|
+
}
|
|
4492
|
+
case "update": {
|
|
4493
|
+
const updateId = opRest[0];
|
|
4494
|
+
const updateData = opRest[1] || {};
|
|
4495
|
+
await this.persistence.update(opEntityType, updateId, updateData);
|
|
4496
|
+
const updated = await this.persistence.getById(opEntityType, updateId);
|
|
4497
|
+
batchResults.push({ action: "update", entityType: opEntityType, id: updateId, ...updated || updateData });
|
|
4498
|
+
completed.push({ action: "update", entityType: opEntityType, id: updateId });
|
|
4499
|
+
break;
|
|
4500
|
+
}
|
|
4501
|
+
case "delete": {
|
|
4502
|
+
const deleteId = opRest[0];
|
|
4503
|
+
await this.persistence.delete(opEntityType, deleteId);
|
|
4504
|
+
batchResults.push({ action: "delete", entityType: opEntityType, id: deleteId, deleted: true });
|
|
4505
|
+
completed.push({ action: "delete", entityType: opEntityType, id: deleteId });
|
|
4506
|
+
break;
|
|
4507
|
+
}
|
|
4508
|
+
default:
|
|
4509
|
+
batchFailed = true;
|
|
4510
|
+
batchError = `Unknown batch operation action: ${opAction}`;
|
|
4511
|
+
break;
|
|
4512
|
+
}
|
|
4513
|
+
} catch (err) {
|
|
4514
|
+
batchFailed = true;
|
|
4515
|
+
batchError = `Batch operation [${opAction}, ${opEntityType}] failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
4516
|
+
break;
|
|
4517
|
+
}
|
|
4518
|
+
if (batchFailed) break;
|
|
4519
|
+
}
|
|
4520
|
+
effectResults.push({
|
|
4521
|
+
effect: "persist",
|
|
4522
|
+
action: "batch",
|
|
4523
|
+
data: {
|
|
4524
|
+
operations: batchResults,
|
|
4525
|
+
completedCount: completed.length,
|
|
4526
|
+
totalCount: operations.length
|
|
4527
|
+
},
|
|
4528
|
+
success: !batchFailed,
|
|
4529
|
+
...batchFailed ? { error: batchError } : {}
|
|
4530
|
+
});
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
const type = targetEntityType || entityType;
|
|
4534
|
+
let resultData;
|
|
4535
|
+
const sizeBefore = (await this.persistence.list(type)).length;
|
|
4536
|
+
try {
|
|
4537
|
+
if (action === "create" || action === "update") {
|
|
4538
|
+
this.validateRelationCardinality(type, data || {});
|
|
4539
|
+
}
|
|
4540
|
+
switch (action) {
|
|
4541
|
+
case "create": {
|
|
4542
|
+
const { id } = await this.persistence.create(type, data || {});
|
|
4543
|
+
resultData = { id, ...data || {} };
|
|
4544
|
+
break;
|
|
4545
|
+
}
|
|
4546
|
+
case "update":
|
|
4547
|
+
if (data?.id || entityId) {
|
|
4548
|
+
const updateId = data?.id || entityId;
|
|
4549
|
+
await this.persistence.update(type, updateId, data || {});
|
|
4550
|
+
const updated = await this.persistence.getById(type, updateId);
|
|
4551
|
+
resultData = updated || { id: updateId, ...data || {} };
|
|
4552
|
+
}
|
|
4553
|
+
break;
|
|
4554
|
+
case "delete": {
|
|
4555
|
+
const directId = typeof data === "string" ? data : void 0;
|
|
4556
|
+
const nestedId = typeof data === "object" && data !== null ? data.id : void 0;
|
|
4557
|
+
const deleteId = directId ?? nestedId ?? entityId;
|
|
4558
|
+
if (deleteId) {
|
|
4559
|
+
await this.enforceOnDeleteRules(type, deleteId);
|
|
4560
|
+
await this.persistence.delete(type, deleteId);
|
|
4561
|
+
resultData = { id: deleteId, deleted: true };
|
|
4562
|
+
}
|
|
4563
|
+
break;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
const sizeAfter = (await this.persistence.list(type)).length;
|
|
4567
|
+
effectLog2.debug("persist:store-mutate", {
|
|
4568
|
+
action,
|
|
4569
|
+
entityType: type,
|
|
4570
|
+
resultId: resultData?.id,
|
|
4571
|
+
sizeBefore,
|
|
4572
|
+
sizeAfter,
|
|
4573
|
+
delta: sizeAfter - sizeBefore
|
|
4574
|
+
});
|
|
4575
|
+
effectResults.push({
|
|
4576
|
+
effect: "persist",
|
|
4577
|
+
action,
|
|
4578
|
+
entityType: type,
|
|
4579
|
+
data: resultData,
|
|
4580
|
+
success: true
|
|
4581
|
+
});
|
|
4582
|
+
} catch (err) {
|
|
4583
|
+
effectLog2.error("persist:store-mutate-error", {
|
|
4584
|
+
action,
|
|
4585
|
+
entityType: type,
|
|
4586
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4587
|
+
});
|
|
4588
|
+
effectResults.push({
|
|
4589
|
+
effect: "persist",
|
|
4590
|
+
action,
|
|
4591
|
+
entityType: type,
|
|
4592
|
+
success: false,
|
|
4593
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
},
|
|
4597
|
+
callService: async (service, action, params) => {
|
|
4598
|
+
try {
|
|
4599
|
+
let result = null;
|
|
4600
|
+
if (this.config.effectHandlers?.callService) {
|
|
4601
|
+
result = await this.config.effectHandlers.callService(
|
|
4602
|
+
service,
|
|
4603
|
+
action,
|
|
4604
|
+
params
|
|
4605
|
+
);
|
|
4606
|
+
} else {
|
|
4607
|
+
console.warn(
|
|
4608
|
+
`[OrbitalRuntime] call-service not configured: ${service}.${action}`
|
|
4609
|
+
);
|
|
4610
|
+
}
|
|
4611
|
+
effectResults.push({
|
|
4612
|
+
effect: "call-service",
|
|
4613
|
+
action: `${service}.${action}`,
|
|
4614
|
+
data: result,
|
|
4615
|
+
success: true
|
|
4616
|
+
});
|
|
4617
|
+
return result;
|
|
4618
|
+
} catch (err) {
|
|
4619
|
+
effectResults.push({
|
|
4620
|
+
effect: "call-service",
|
|
4621
|
+
action: `${service}.${action}`,
|
|
4622
|
+
success: false,
|
|
4623
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4624
|
+
});
|
|
4625
|
+
return null;
|
|
4626
|
+
}
|
|
4627
|
+
},
|
|
4628
|
+
fetch: async (fetchEntityType, options) => {
|
|
4629
|
+
try {
|
|
4630
|
+
let result = null;
|
|
4631
|
+
if (options?.id) {
|
|
4632
|
+
const entity = await this.persistence.getById(fetchEntityType, options.id);
|
|
4633
|
+
if (entity) {
|
|
4634
|
+
if (options?.include && options.include.length > 0) {
|
|
4635
|
+
await this.populateRelations([entity], fetchEntityType, options.include);
|
|
4636
|
+
}
|
|
4637
|
+
fetchedData[fetchEntityType] = [entity];
|
|
4638
|
+
result = entity;
|
|
4639
|
+
}
|
|
4640
|
+
} else {
|
|
4641
|
+
let entities = await this.persistence.list(fetchEntityType);
|
|
4642
|
+
if (options?.offset && options.offset > 0) {
|
|
4643
|
+
entities = entities.slice(options.offset);
|
|
4644
|
+
}
|
|
4645
|
+
if (options?.limit && options.limit > 0) {
|
|
4646
|
+
entities = entities.slice(0, options.limit);
|
|
4647
|
+
}
|
|
4648
|
+
if (options?.include && options.include.length > 0) {
|
|
4649
|
+
await this.populateRelations(entities, fetchEntityType, options.include);
|
|
4650
|
+
}
|
|
4651
|
+
fetchedData[fetchEntityType] = entities;
|
|
4652
|
+
result = entities;
|
|
4653
|
+
}
|
|
4654
|
+
if (bindingsRef && result) {
|
|
4655
|
+
const records = Array.isArray(result) ? result : [result];
|
|
4656
|
+
if (records.length > 0) {
|
|
4657
|
+
const merged = Object.assign([...records], records[0]);
|
|
4658
|
+
bindingsRef[fetchEntityType] = merged;
|
|
4659
|
+
if (fetchEntityType === entityType) {
|
|
4660
|
+
bindingsRef.entity = merged;
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
return result;
|
|
4665
|
+
} catch (error) {
|
|
4666
|
+
console.error(`[OrbitalRuntime] Fetch error for ${fetchEntityType}:`, error);
|
|
4667
|
+
return null;
|
|
4668
|
+
}
|
|
4669
|
+
},
|
|
4670
|
+
// Resource operators: ref, deref, swap, watch, atomic
|
|
4671
|
+
ref: async (refEntityType, options) => {
|
|
4672
|
+
try {
|
|
4673
|
+
return await handlers.fetch(refEntityType, options);
|
|
4674
|
+
} catch (error) {
|
|
4675
|
+
console.error(`[OrbitalRuntime] ref error for ${refEntityType}:`, error);
|
|
4676
|
+
return null;
|
|
4677
|
+
}
|
|
4678
|
+
},
|
|
4679
|
+
deref: async (derefEntityType, options) => {
|
|
4680
|
+
try {
|
|
4681
|
+
let result = null;
|
|
4682
|
+
if (options?.id) {
|
|
4683
|
+
const entity = await this.persistence.getById(derefEntityType, options.id);
|
|
4684
|
+
if (entity) {
|
|
4685
|
+
fetchedData[derefEntityType] = [entity];
|
|
4686
|
+
result = entity;
|
|
4687
|
+
}
|
|
4688
|
+
} else {
|
|
4689
|
+
const entities = await this.persistence.list(derefEntityType);
|
|
4690
|
+
fetchedData[derefEntityType] = entities;
|
|
4691
|
+
result = entities;
|
|
4692
|
+
}
|
|
4693
|
+
if (bindingsRef && result) {
|
|
4694
|
+
const records = Array.isArray(result) ? result : [result];
|
|
4695
|
+
if (records.length > 0) {
|
|
4696
|
+
const merged = Object.assign([...records], records[0]);
|
|
4697
|
+
bindingsRef[derefEntityType] = merged;
|
|
4698
|
+
if (derefEntityType === entityType) {
|
|
4699
|
+
bindingsRef.entity = merged;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
effectResults.push({
|
|
4704
|
+
effect: "deref",
|
|
4705
|
+
entityType: derefEntityType,
|
|
4706
|
+
success: true
|
|
4707
|
+
});
|
|
4708
|
+
return result;
|
|
4709
|
+
} catch (error) {
|
|
4710
|
+
effectResults.push({
|
|
4711
|
+
effect: "deref",
|
|
4712
|
+
entityType: derefEntityType,
|
|
4713
|
+
success: false,
|
|
4714
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4715
|
+
});
|
|
4716
|
+
return null;
|
|
4717
|
+
}
|
|
4718
|
+
},
|
|
4719
|
+
swap: async (swapEntityType, swapEntityId, transform) => {
|
|
4720
|
+
try {
|
|
4721
|
+
const current = await this.persistence.getById(swapEntityType, swapEntityId);
|
|
4722
|
+
if (!current) {
|
|
4723
|
+
effectResults.push({
|
|
4724
|
+
effect: "swap",
|
|
4725
|
+
entityType: swapEntityType,
|
|
4726
|
+
success: false,
|
|
4727
|
+
error: `Entity ${swapEntityType}/${swapEntityId} not found`
|
|
4728
|
+
});
|
|
4729
|
+
return null;
|
|
4730
|
+
}
|
|
4731
|
+
const ctx = createContextFromBindings({
|
|
4732
|
+
current,
|
|
4733
|
+
entity: entityData,
|
|
4734
|
+
payload
|
|
4735
|
+
}, false, this.config.contextExtensions);
|
|
4736
|
+
let newData;
|
|
4737
|
+
if (Array.isArray(transform)) {
|
|
4738
|
+
const result = evaluate(
|
|
4739
|
+
transform,
|
|
4740
|
+
ctx
|
|
4741
|
+
);
|
|
4742
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
4743
|
+
newData = result;
|
|
4744
|
+
} else {
|
|
4745
|
+
newData = current;
|
|
4746
|
+
}
|
|
4747
|
+
} else if (typeof transform === "object" && transform !== null) {
|
|
4748
|
+
newData = { ...current, ...transform };
|
|
4749
|
+
} else {
|
|
4750
|
+
effectResults.push({
|
|
4751
|
+
effect: "swap",
|
|
4752
|
+
entityType: swapEntityType,
|
|
4753
|
+
success: false,
|
|
4754
|
+
error: "swap! transform must be an S-expression or object"
|
|
4755
|
+
});
|
|
4756
|
+
return null;
|
|
4757
|
+
}
|
|
4758
|
+
await this.persistence.update(swapEntityType, swapEntityId, newData);
|
|
4759
|
+
effectResults.push({
|
|
4760
|
+
effect: "swap",
|
|
4761
|
+
entityType: swapEntityType,
|
|
4762
|
+
data: { id: swapEntityId, ...newData },
|
|
4763
|
+
success: true
|
|
4764
|
+
});
|
|
4765
|
+
return newData;
|
|
4766
|
+
} catch (error) {
|
|
4767
|
+
effectResults.push({
|
|
4768
|
+
effect: "swap",
|
|
4769
|
+
entityType: swapEntityType,
|
|
4770
|
+
success: false,
|
|
4771
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4772
|
+
});
|
|
4773
|
+
return null;
|
|
4774
|
+
}
|
|
4775
|
+
},
|
|
4776
|
+
watch: (_watchEntityType, _watchOptions) => {
|
|
4777
|
+
if (this.config.debug) {
|
|
4778
|
+
console.log(`[OrbitalRuntime] watch is a no-op on server: ${_watchEntityType}`);
|
|
4779
|
+
}
|
|
4780
|
+
},
|
|
4781
|
+
atomic: async (atomicEffects) => {
|
|
4782
|
+
let atomicFailed = false;
|
|
4783
|
+
let atomicError = "";
|
|
4784
|
+
const atomicExecutor = new EffectExecutor({
|
|
4785
|
+
handlers,
|
|
4786
|
+
bindings: bindingsRef ?? {},
|
|
4787
|
+
context: contextRef ?? { traitName, orbitalName: registered.schema.name, state: "unknown", transition: "unknown" },
|
|
4788
|
+
debug: this.config.debug,
|
|
4789
|
+
contextExtensions: this.config.contextExtensions
|
|
4790
|
+
});
|
|
4791
|
+
for (const innerEffect of atomicEffects) {
|
|
4792
|
+
if (atomicFailed) break;
|
|
4793
|
+
try {
|
|
4794
|
+
await atomicExecutor.execute(innerEffect);
|
|
4795
|
+
} catch (err) {
|
|
4796
|
+
atomicFailed = true;
|
|
4797
|
+
atomicError = err instanceof Error ? err.message : String(err);
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
if (atomicFailed) {
|
|
4801
|
+
effectResults.push({
|
|
4802
|
+
effect: "atomic",
|
|
4803
|
+
success: false,
|
|
4804
|
+
error: `Atomic block failed: ${atomicError}`
|
|
4805
|
+
});
|
|
4806
|
+
} else {
|
|
4807
|
+
effectResults.push({
|
|
4808
|
+
effect: "atomic",
|
|
4809
|
+
success: true,
|
|
4810
|
+
data: { innerCount: atomicEffects.length }
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
},
|
|
4814
|
+
// Client-side effects - collect for forwarding to client
|
|
4815
|
+
renderUI: (slot, pattern, props, priority) => {
|
|
4816
|
+
const patternNode = pattern !== null && typeof pattern === "object" && !Array.isArray(pattern) ? pattern : null;
|
|
4817
|
+
const patternEntity = patternNode?.entity;
|
|
4818
|
+
const entityRow = patternEntity !== null && typeof patternEntity === "object" && !Array.isArray(patternEntity) ? patternEntity : null;
|
|
4819
|
+
const patternTypeRaw = patternNode?.["type"];
|
|
4820
|
+
renderLog2.debug("renderUI:push", {
|
|
4821
|
+
trait: traitName,
|
|
4822
|
+
slot,
|
|
4823
|
+
patternType: typeof patternTypeRaw === "string" ? patternTypeRaw : void 0,
|
|
4824
|
+
entityRowId: typeof entityRow?.id === "string" ? entityRow.id : void 0,
|
|
4825
|
+
entityIsObject: entityRow !== null
|
|
4826
|
+
});
|
|
4827
|
+
pushClientEffect(["render-ui", slot, pattern, props, priority]);
|
|
4828
|
+
},
|
|
4829
|
+
navigate: (path, params) => {
|
|
4830
|
+
pushClientEffect(["navigate", path, params]);
|
|
4831
|
+
},
|
|
4832
|
+
notify: (message, type) => {
|
|
4833
|
+
if (this.config.debug) {
|
|
4834
|
+
console.log(`[OrbitalRuntime] Notification (${type}): ${message}`);
|
|
4835
|
+
}
|
|
4836
|
+
pushClientEffect(["notify", message, { type }]);
|
|
4837
|
+
},
|
|
4838
|
+
log: (message, level) => {
|
|
4839
|
+
const logFn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
4840
|
+
logFn(`[OrbitalRuntime] ${message}`);
|
|
4841
|
+
},
|
|
4842
|
+
// Allow custom handlers to override
|
|
4843
|
+
...this.config.effectHandlers
|
|
4844
|
+
};
|
|
4845
|
+
const state = registered.manager.getState(traitName);
|
|
4846
|
+
const bindings = {
|
|
4847
|
+
entity: entityData,
|
|
4848
|
+
payload,
|
|
4849
|
+
state: state?.currentState || "unknown",
|
|
4850
|
+
user
|
|
4851
|
+
// @user bindings from Firebase auth
|
|
4852
|
+
};
|
|
4853
|
+
const declaredDefaults = collectDeclaredConfigDefaults(
|
|
4854
|
+
registered.traits.find((t) => t.name === traitName)
|
|
4855
|
+
);
|
|
4856
|
+
const callSiteOverride = registered.configByTrait.get(traitName);
|
|
4857
|
+
if (declaredDefaults || callSiteOverride) {
|
|
4858
|
+
bindings.config = { ...declaredDefaults ?? {}, ...callSiteOverride ?? {} };
|
|
4859
|
+
}
|
|
4860
|
+
if (entityType) {
|
|
4861
|
+
bindings[entityType] = entityData;
|
|
4862
|
+
}
|
|
4863
|
+
bindingsRef = bindings;
|
|
4864
|
+
const context = {
|
|
4865
|
+
traitName,
|
|
4866
|
+
orbitalName: registered.schema.name,
|
|
4867
|
+
state: state?.currentState || "unknown",
|
|
4868
|
+
transition: "unknown",
|
|
4869
|
+
entityId
|
|
3206
4870
|
};
|
|
4871
|
+
contextRef = context;
|
|
4872
|
+
const executor = new EffectExecutor({
|
|
4873
|
+
handlers,
|
|
4874
|
+
bindings,
|
|
4875
|
+
context,
|
|
4876
|
+
debug: this.config.debug,
|
|
4877
|
+
contextExtensions: this.config.contextExtensions
|
|
4878
|
+
});
|
|
4879
|
+
await executor.executeAll(effects);
|
|
3207
4880
|
}
|
|
4881
|
+
// ==========================================================================
|
|
4882
|
+
// Relation Population
|
|
4883
|
+
// ==========================================================================
|
|
3208
4884
|
/**
|
|
3209
|
-
*
|
|
4885
|
+
* Populate relation fields on entities
|
|
4886
|
+
*
|
|
4887
|
+
* For each field in `include`, find the relation field configuration and
|
|
4888
|
+
* fetch the related entity, attaching it to the parent entity.
|
|
4889
|
+
*
|
|
4890
|
+
* @param entities - Entities to populate
|
|
4891
|
+
* @param entityType - Entity type name
|
|
4892
|
+
* @param include - Relation field names to populate
|
|
3210
4893
|
*/
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
4894
|
+
/**
|
|
4895
|
+
* Validate that relation field values match their declared cardinality.
|
|
4896
|
+
* Called before create/update to ensure data integrity.
|
|
4897
|
+
*/
|
|
4898
|
+
validateRelationCardinality(entityType, data) {
|
|
4899
|
+
for (const [, registered] of this.orbitals) {
|
|
4900
|
+
if (registered.entity.name !== entityType) continue;
|
|
4901
|
+
for (const field of registered.entity.fields ?? []) {
|
|
4902
|
+
if (field.type !== "relation") continue;
|
|
4903
|
+
if (field.name === void 0) continue;
|
|
4904
|
+
const fieldName = field.name;
|
|
4905
|
+
const value = data[fieldName];
|
|
4906
|
+
if (value === void 0 || value === null) continue;
|
|
4907
|
+
const cardinality = field.relation?.cardinality || "one";
|
|
4908
|
+
if (cardinality === "one" || cardinality === "many-to-one") {
|
|
4909
|
+
if (Array.isArray(value)) {
|
|
4910
|
+
throw new Error(
|
|
4911
|
+
`Cardinality violation: ${entityType}.${fieldName} has cardinality '${cardinality}' but received an array. Expected a single string ID.`
|
|
4912
|
+
);
|
|
4913
|
+
}
|
|
4914
|
+
} else if (cardinality === "many" || cardinality === "many-to-many" || cardinality === "one-to-many") {
|
|
4915
|
+
if (typeof value === "string") {
|
|
4916
|
+
data[fieldName] = [value];
|
|
4917
|
+
} else if (Array.isArray(value)) {
|
|
4918
|
+
const nonStrings = value.filter((v) => typeof v !== "string");
|
|
4919
|
+
if (nonStrings.length > 0) {
|
|
4920
|
+
throw new Error(
|
|
4921
|
+
`Cardinality violation: ${entityType}.${fieldName} has cardinality '${cardinality}' but array contains non-string values.`
|
|
4922
|
+
);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
break;
|
|
3215
4928
|
}
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
4929
|
+
}
|
|
4930
|
+
/**
|
|
4931
|
+
* Enforce onDelete rules for relation fields pointing to the entity being deleted.
|
|
4932
|
+
* Scans all registered entities for relation fields targeting the given entity type,
|
|
4933
|
+
* finds records referencing the ID being deleted, and applies cascade/nullify/restrict.
|
|
4934
|
+
*/
|
|
4935
|
+
async enforceOnDeleteRules(entityType, deletedId) {
|
|
4936
|
+
for (const [, registered] of this.orbitals) {
|
|
4937
|
+
const entity = registered.entity;
|
|
4938
|
+
const fields = entity.fields ?? [];
|
|
4939
|
+
for (const field of fields) {
|
|
4940
|
+
if (field.type !== "relation") continue;
|
|
4941
|
+
if (field.relation?.entity !== entityType) continue;
|
|
4942
|
+
if (field.name === void 0) continue;
|
|
4943
|
+
const fieldName = field.name;
|
|
4944
|
+
const onDelete = field.relation.onDelete || "restrict";
|
|
4945
|
+
const referringEntityType = entity.name;
|
|
4946
|
+
const allRecords = await this.persistence.list(referringEntityType);
|
|
4947
|
+
const affectedRecords = allRecords.filter((record) => {
|
|
4948
|
+
const fkValue = record[fieldName];
|
|
4949
|
+
if (typeof fkValue === "string") return fkValue === deletedId;
|
|
4950
|
+
if (Array.isArray(fkValue)) return fkValue.includes(deletedId);
|
|
4951
|
+
return false;
|
|
4952
|
+
});
|
|
4953
|
+
if (affectedRecords.length === 0) continue;
|
|
4954
|
+
switch (onDelete) {
|
|
4955
|
+
case "restrict":
|
|
4956
|
+
throw new Error(
|
|
4957
|
+
`Cannot delete ${entityType} ${deletedId}: ${affectedRecords.length} ${referringEntityType} record(s) reference it via ${field.name}. Rule: restrict.`
|
|
4958
|
+
);
|
|
4959
|
+
case "cascade":
|
|
4960
|
+
for (const record of affectedRecords) {
|
|
4961
|
+
const recordId = record.id;
|
|
4962
|
+
if (recordId) {
|
|
4963
|
+
await this.persistence.delete(referringEntityType, recordId);
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
if (this.config.debug) {
|
|
4967
|
+
console.log(`[OrbitalRuntime] Cascade deleted ${affectedRecords.length} ${referringEntityType} records`);
|
|
4968
|
+
}
|
|
4969
|
+
break;
|
|
4970
|
+
case "nullify":
|
|
4971
|
+
for (const record of affectedRecords) {
|
|
4972
|
+
const recordId = record.id;
|
|
4973
|
+
if (recordId && field.name !== void 0) {
|
|
4974
|
+
const fieldName2 = field.name;
|
|
4975
|
+
const update = {};
|
|
4976
|
+
const fkValue = record[fieldName2];
|
|
4977
|
+
if (Array.isArray(fkValue)) {
|
|
4978
|
+
update[fieldName2] = fkValue.filter((id) => id !== deletedId);
|
|
4979
|
+
} else {
|
|
4980
|
+
update[fieldName2] = null;
|
|
4981
|
+
}
|
|
4982
|
+
await this.persistence.update(referringEntityType, recordId, update);
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
if (this.config.debug) {
|
|
4986
|
+
console.log(`[OrbitalRuntime] Nullified ${field.name} on ${affectedRecords.length} ${referringEntityType} records`);
|
|
4987
|
+
}
|
|
4988
|
+
break;
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
async populateRelations(entities, entityType, include, depth = 0, visited = /* @__PURE__ */ new Set()) {
|
|
4994
|
+
const maxDepth = 2;
|
|
4995
|
+
if (depth >= maxDepth || visited.has(entityType)) {
|
|
4996
|
+
if (this.config.debug) {
|
|
4997
|
+
console.log(`[OrbitalRuntime] Skipping populateRelations for ${entityType}: depth=${depth}, visited=${visited.has(entityType)}`);
|
|
4998
|
+
}
|
|
4999
|
+
return;
|
|
5000
|
+
}
|
|
5001
|
+
visited.add(entityType);
|
|
5002
|
+
let entityFields;
|
|
5003
|
+
for (const [, registered] of this.orbitals) {
|
|
5004
|
+
if (registered.entity.name === entityType) {
|
|
5005
|
+
entityFields = registered.entity.fields.filter(
|
|
5006
|
+
(f) => typeof f.name === "string" && f.name.length > 0
|
|
5007
|
+
);
|
|
5008
|
+
break;
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
if (!entityFields) {
|
|
5012
|
+
if (this.config.debug) {
|
|
5013
|
+
console.warn(`[OrbitalRuntime] No entity definition found for ${entityType}`);
|
|
5014
|
+
}
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
for (const includeField of include) {
|
|
5018
|
+
const relationField = entityFields.find((f) => {
|
|
5019
|
+
if (f.type !== "relation") return false;
|
|
5020
|
+
return f.name === includeField || f.name === `${includeField}Id` || f.name.replace(/Id$/, "") === includeField;
|
|
5021
|
+
});
|
|
5022
|
+
if (!relationField?.relation?.entity) {
|
|
5023
|
+
if (this.config.debug) {
|
|
5024
|
+
console.warn(`[OrbitalRuntime] No relation field found for '${includeField}' on ${entityType}`);
|
|
5025
|
+
}
|
|
5026
|
+
continue;
|
|
5027
|
+
}
|
|
5028
|
+
const foreignKeyField = relationField.name;
|
|
5029
|
+
const relatedEntityType = relationField.relation.entity;
|
|
5030
|
+
const cardinality = relationField.relation.cardinality || "one";
|
|
5031
|
+
const foreignKeyIds = /* @__PURE__ */ new Set();
|
|
5032
|
+
for (const entity of entities) {
|
|
5033
|
+
const fkValue = entity[foreignKeyField];
|
|
5034
|
+
if (fkValue && typeof fkValue === "string") {
|
|
5035
|
+
foreignKeyIds.add(fkValue);
|
|
5036
|
+
} else if (Array.isArray(fkValue)) {
|
|
5037
|
+
for (const id of fkValue) {
|
|
5038
|
+
if (id && typeof id === "string") {
|
|
5039
|
+
foreignKeyIds.add(id);
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
if (foreignKeyIds.size === 0) continue;
|
|
5045
|
+
const relatedEntities = /* @__PURE__ */ new Map();
|
|
5046
|
+
for (const fkId of foreignKeyIds) {
|
|
5047
|
+
try {
|
|
5048
|
+
const related = await this.persistence.getById(relatedEntityType, fkId);
|
|
5049
|
+
if (related) {
|
|
5050
|
+
relatedEntities.set(fkId, related);
|
|
5051
|
+
}
|
|
5052
|
+
} catch (error) {
|
|
5053
|
+
if (this.config.debug) {
|
|
5054
|
+
console.error(`[OrbitalRuntime] Error fetching related ${relatedEntityType}:`, error);
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
const populatedFieldName = includeField.endsWith("Id") ? includeField.slice(0, -2) : includeField;
|
|
5059
|
+
for (const entity of entities) {
|
|
5060
|
+
const fkValue = entity[foreignKeyField];
|
|
5061
|
+
if (cardinality === "one" || cardinality === "many-to-one") {
|
|
5062
|
+
if (typeof fkValue === "string" && relatedEntities.has(fkValue)) {
|
|
5063
|
+
Object.defineProperty(entity, populatedFieldName, {
|
|
5064
|
+
value: relatedEntities.get(fkValue),
|
|
5065
|
+
writable: true,
|
|
5066
|
+
enumerable: true,
|
|
5067
|
+
configurable: true
|
|
5068
|
+
});
|
|
5069
|
+
}
|
|
5070
|
+
} else {
|
|
5071
|
+
if (Array.isArray(fkValue)) {
|
|
5072
|
+
const fkIds = fkValue.filter((id) => typeof id === "string");
|
|
5073
|
+
Object.defineProperty(entity, populatedFieldName, {
|
|
5074
|
+
value: fkIds.map((id) => relatedEntities.get(id)).filter(Boolean),
|
|
5075
|
+
writable: true,
|
|
5076
|
+
enumerable: true,
|
|
5077
|
+
configurable: true
|
|
5078
|
+
});
|
|
5079
|
+
} else if (typeof fkValue === "string" && relatedEntities.has(fkValue)) {
|
|
5080
|
+
Object.defineProperty(entity, populatedFieldName, {
|
|
5081
|
+
value: [relatedEntities.get(fkValue)],
|
|
5082
|
+
writable: true,
|
|
5083
|
+
enumerable: true,
|
|
5084
|
+
configurable: true
|
|
5085
|
+
});
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
if (this.config.debug) {
|
|
5090
|
+
console.log(`[OrbitalRuntime] Populated '${populatedFieldName}' on ${entities.length} ${entityType} entities`);
|
|
5091
|
+
}
|
|
3225
5092
|
}
|
|
3226
|
-
return {
|
|
3227
|
-
success: true,
|
|
3228
|
-
data: resolved,
|
|
3229
|
-
warnings: baseResult.warnings
|
|
3230
|
-
};
|
|
3231
5093
|
}
|
|
5094
|
+
// ==========================================================================
|
|
5095
|
+
// Express Router
|
|
5096
|
+
// ==========================================================================
|
|
3232
5097
|
/**
|
|
3233
|
-
*
|
|
5098
|
+
* Create Express router for orbital API endpoints
|
|
5099
|
+
*
|
|
5100
|
+
* All data access goes through trait events with guards.
|
|
5101
|
+
* No direct CRUD routes - use events with `fetch` effects.
|
|
5102
|
+
*
|
|
5103
|
+
* Routes:
|
|
5104
|
+
* - GET / - List registered orbitals
|
|
5105
|
+
* - GET /:orbital - Get orbital info and current states
|
|
5106
|
+
* - POST /:orbital/events - Send event to orbital (includes data from `fetch` effects)
|
|
3234
5107
|
*/
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
5108
|
+
router() {
|
|
5109
|
+
if (!isNodeEnv()) {
|
|
5110
|
+
throw new Error(
|
|
5111
|
+
"OrbitalServerRuntime.router() is Node-only (uses Express). For in-browser use, mount <BrowserPlayground> from @almadar/ui instead."
|
|
5112
|
+
);
|
|
5113
|
+
}
|
|
5114
|
+
const { Router } = nodeRequire("express");
|
|
5115
|
+
const router = Router();
|
|
5116
|
+
router.get("/", (_req, res) => {
|
|
5117
|
+
const orbitals = Array.from(this.orbitals.entries()).map(
|
|
5118
|
+
([name, reg]) => ({
|
|
5119
|
+
name,
|
|
5120
|
+
entity: reg.entity?.name,
|
|
5121
|
+
traits: (reg.traits || []).map((t) => t.name)
|
|
5122
|
+
})
|
|
5123
|
+
);
|
|
5124
|
+
res.json({ success: true, orbitals });
|
|
5125
|
+
});
|
|
5126
|
+
router.get("/:orbital", (req, res) => {
|
|
5127
|
+
const orbitalName = req.params.orbital;
|
|
5128
|
+
const registered = this.orbitals.get(orbitalName);
|
|
5129
|
+
if (!registered) {
|
|
5130
|
+
res.status(404).json({ success: false, error: "Orbital not found" });
|
|
5131
|
+
return;
|
|
5132
|
+
}
|
|
5133
|
+
const states = {};
|
|
5134
|
+
for (const [name, state] of registered.manager.getAllStates()) {
|
|
5135
|
+
states[name] = state.currentState;
|
|
5136
|
+
}
|
|
5137
|
+
res.json({
|
|
5138
|
+
success: true,
|
|
5139
|
+
orbital: {
|
|
5140
|
+
name: orbitalName,
|
|
5141
|
+
entity: registered.entity,
|
|
5142
|
+
traits: registered.traits.map((t) => ({
|
|
5143
|
+
name: t.name,
|
|
5144
|
+
currentState: states[t.name],
|
|
5145
|
+
states: (t.stateMachine?.states || []).map((s) => s.name),
|
|
5146
|
+
events: [...new Set((t.stateMachine?.transitions || []).map((tr) => tr.event))]
|
|
5147
|
+
}))
|
|
5148
|
+
}
|
|
5149
|
+
});
|
|
5150
|
+
});
|
|
5151
|
+
router.post(
|
|
5152
|
+
"/:orbital/events",
|
|
5153
|
+
async (req, res, next) => {
|
|
5154
|
+
try {
|
|
5155
|
+
const orbitalName = req.params.orbital;
|
|
5156
|
+
const firebaseUser = req.firebaseUser;
|
|
5157
|
+
const user = firebaseUser ? {
|
|
5158
|
+
...firebaseUser,
|
|
5159
|
+
displayName: firebaseUser.name ?? firebaseUser.displayName
|
|
5160
|
+
} : void 0;
|
|
5161
|
+
const result = await this.processOrbitalEvent(orbitalName, {
|
|
5162
|
+
...req.body,
|
|
5163
|
+
user
|
|
5164
|
+
});
|
|
5165
|
+
res.json(result);
|
|
5166
|
+
} catch (error) {
|
|
5167
|
+
next(error);
|
|
3243
5168
|
}
|
|
3244
5169
|
}
|
|
3245
|
-
|
|
3246
|
-
return
|
|
5170
|
+
);
|
|
5171
|
+
return router;
|
|
3247
5172
|
}
|
|
5173
|
+
// ==========================================================================
|
|
5174
|
+
// Direct API (for programmatic use)
|
|
5175
|
+
// ==========================================================================
|
|
3248
5176
|
/**
|
|
3249
|
-
*
|
|
5177
|
+
* Get the event bus for manual event emission
|
|
3250
5178
|
*/
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
if (!pages) return [];
|
|
3254
|
-
const names = [];
|
|
3255
|
-
for (const pageRef of pages) {
|
|
3256
|
-
if (typeof pageRef !== "string" && !("ref" in pageRef)) {
|
|
3257
|
-
names.push(pageRef.name);
|
|
3258
|
-
}
|
|
3259
|
-
}
|
|
3260
|
-
return names;
|
|
5179
|
+
getEventBus() {
|
|
5180
|
+
return this.eventBus;
|
|
3261
5181
|
}
|
|
3262
5182
|
/**
|
|
3263
|
-
*
|
|
5183
|
+
* Get state for a specific orbital/trait
|
|
3264
5184
|
*/
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
5185
|
+
getState(orbitalName, traitName) {
|
|
5186
|
+
const registered = this.orbitals.get(orbitalName);
|
|
5187
|
+
if (!registered) return void 0;
|
|
5188
|
+
if (traitName) {
|
|
5189
|
+
return registered.manager.getState(traitName);
|
|
5190
|
+
}
|
|
5191
|
+
const states = {};
|
|
5192
|
+
for (const [name, state] of registered.manager.getAllStates()) {
|
|
5193
|
+
states[name] = state;
|
|
3268
5194
|
}
|
|
5195
|
+
return states;
|
|
3269
5196
|
}
|
|
3270
5197
|
/**
|
|
3271
|
-
*
|
|
5198
|
+
* List registered orbitals
|
|
3272
5199
|
*/
|
|
3273
|
-
|
|
3274
|
-
this.
|
|
3275
|
-
}
|
|
3276
|
-
};
|
|
3277
|
-
async function resolveSchema(schema, options) {
|
|
3278
|
-
const resolver = new ReferenceResolver(options);
|
|
3279
|
-
const errors = [];
|
|
3280
|
-
const warnings = [];
|
|
3281
|
-
const resolved = [];
|
|
3282
|
-
for (const orbital of schema.orbitals) {
|
|
3283
|
-
const inlineTraits = orbital.traits.filter(
|
|
3284
|
-
(t) => typeof t !== "string" && "stateMachine" in t
|
|
3285
|
-
);
|
|
3286
|
-
resolver.addLocalTraits(inlineTraits);
|
|
5200
|
+
listOrbitals() {
|
|
5201
|
+
return Array.from(this.orbitals.keys());
|
|
3287
5202
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
resolved.push(result.data);
|
|
3294
|
-
warnings.push(...result.warnings.map((w) => `Orbital "${orbital.name}": ${w}`));
|
|
3295
|
-
}
|
|
5203
|
+
/**
|
|
5204
|
+
* Check if an orbital is registered
|
|
5205
|
+
*/
|
|
5206
|
+
hasOrbital(name) {
|
|
5207
|
+
return this.orbitals.has(name);
|
|
3296
5208
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
5209
|
+
/**
|
|
5210
|
+
* Get information about active ticks
|
|
5211
|
+
*/
|
|
5212
|
+
getActiveTicks() {
|
|
5213
|
+
return this.tickBindings.map((binding) => ({
|
|
5214
|
+
orbital: binding.orbitalName,
|
|
5215
|
+
trait: binding.traitName,
|
|
5216
|
+
tick: binding.tick.name,
|
|
5217
|
+
interval: binding.tick.interval,
|
|
5218
|
+
hasGuard: !!binding.tick.guard
|
|
5219
|
+
}));
|
|
3299
5220
|
}
|
|
3300
|
-
|
|
5221
|
+
};
|
|
5222
|
+
function createOrbitalServerRuntime(config) {
|
|
5223
|
+
return new OrbitalServerRuntime(config);
|
|
3301
5224
|
}
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
return { success: false, errors: resolveResult.errors };
|
|
3309
|
-
}
|
|
3310
|
-
const resolved = resolveResult.data;
|
|
3311
|
-
const warnings = resolveResult.warnings;
|
|
3312
|
-
const preprocessedOrbitals = [];
|
|
3313
|
-
const entitySharing = {};
|
|
3314
|
-
const eventNamespaces = {};
|
|
3315
|
-
for (const resolvedOrbital of resolved) {
|
|
3316
|
-
const orbitalName = resolvedOrbital.name;
|
|
3317
|
-
const persistence = resolvedOrbital.entitySource?.persistence ?? resolvedOrbital.entity.persistence ?? "persistent";
|
|
3318
|
-
entitySharing[orbitalName] = {
|
|
3319
|
-
entityName: resolvedOrbital.entity.name,
|
|
3320
|
-
persistence,
|
|
3321
|
-
isShared: persistence !== "runtime",
|
|
3322
|
-
sourceAlias: resolvedOrbital.entitySource?.alias,
|
|
3323
|
-
collectionName: resolvedOrbital.entity.collection
|
|
3324
|
-
};
|
|
3325
|
-
eventNamespaces[orbitalName] = {};
|
|
3326
|
-
for (const resolvedTrait of resolvedOrbital.traits) {
|
|
3327
|
-
const traitName = resolvedTrait.trait.name;
|
|
3328
|
-
const namespace = {
|
|
3329
|
-
emits: {},
|
|
3330
|
-
listens: {}
|
|
3331
|
-
};
|
|
3332
|
-
if (namespaceEvents && resolvedTrait.source.type === "imported") {
|
|
3333
|
-
const emits = resolvedTrait.trait.emits ?? [];
|
|
3334
|
-
for (const emit of emits) {
|
|
3335
|
-
const eventName = typeof emit === "string" ? emit : emit.event;
|
|
3336
|
-
namespace.emits[eventName] = `${orbitalName}.${traitName}.${eventName}`;
|
|
3337
|
-
}
|
|
3338
|
-
const listens = resolvedTrait.trait.listens ?? [];
|
|
3339
|
-
for (const listen of listens) {
|
|
3340
|
-
namespace.listens[listen.event] = listen.event;
|
|
3341
|
-
}
|
|
3342
|
-
}
|
|
3343
|
-
eventNamespaces[orbitalName][traitName] = namespace;
|
|
3344
|
-
}
|
|
3345
|
-
const preprocessedOrbital = {
|
|
3346
|
-
name: orbitalName,
|
|
3347
|
-
description: resolvedOrbital.original.description,
|
|
3348
|
-
visual_prompt: resolvedOrbital.original.visual_prompt,
|
|
3349
|
-
// Resolved entity (always inline now)
|
|
3350
|
-
entity: resolvedOrbital.entity,
|
|
3351
|
-
// Resolved traits (inline definitions)
|
|
3352
|
-
traits: (resolvedOrbital.traits || []).map((rt) => {
|
|
3353
|
-
if (rt.config || rt.linkedEntity) {
|
|
3354
|
-
return {
|
|
3355
|
-
ref: rt.trait.name,
|
|
3356
|
-
config: rt.config,
|
|
3357
|
-
linkedEntity: rt.linkedEntity,
|
|
3358
|
-
// Include the resolved trait definition for runtime
|
|
3359
|
-
_resolved: rt.trait
|
|
3360
|
-
};
|
|
3361
|
-
}
|
|
3362
|
-
return rt.trait;
|
|
3363
|
-
}),
|
|
3364
|
-
// Resolved pages (inline definitions with path overrides applied)
|
|
3365
|
-
pages: resolvedOrbital.pages.map((rp) => rp.page),
|
|
3366
|
-
// Preserve other fields
|
|
3367
|
-
exposes: resolvedOrbital.original.exposes,
|
|
3368
|
-
domainContext: resolvedOrbital.original.domainContext,
|
|
3369
|
-
design: resolvedOrbital.original.design
|
|
5225
|
+
function parseListenSource(listener, listenerOrbital) {
|
|
5226
|
+
const explicit = listener.source;
|
|
5227
|
+
if (explicit && typeof explicit === "object") {
|
|
5228
|
+
return {
|
|
5229
|
+
bareEvent: listener.event,
|
|
5230
|
+
matcher: buildMatcher(explicit, listenerOrbital)
|
|
3370
5231
|
};
|
|
3371
|
-
preprocessedOrbitals.push(preprocessedOrbital);
|
|
3372
|
-
}
|
|
3373
|
-
const preprocessedSchema = {
|
|
3374
|
-
...schema,
|
|
3375
|
-
orbitals: preprocessedOrbitals
|
|
3376
|
-
};
|
|
3377
|
-
return {
|
|
3378
|
-
success: true,
|
|
3379
|
-
data: {
|
|
3380
|
-
schema: preprocessedSchema,
|
|
3381
|
-
entitySharing,
|
|
3382
|
-
eventNamespaces,
|
|
3383
|
-
warnings
|
|
3384
|
-
}
|
|
3385
|
-
};
|
|
3386
|
-
}
|
|
3387
|
-
function getIsolatedCollectionName(orbitalName, entitySharing) {
|
|
3388
|
-
const info = entitySharing[orbitalName];
|
|
3389
|
-
if (!info) {
|
|
3390
|
-
throw new Error(`Unknown orbital: ${orbitalName}`);
|
|
3391
|
-
}
|
|
3392
|
-
if (info.persistence === "runtime") {
|
|
3393
|
-
return `${orbitalName}_${info.entityName}`;
|
|
3394
5232
|
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
if (!orbitalNs) return eventName;
|
|
3400
|
-
const traitNs = orbitalNs[traitName];
|
|
3401
|
-
if (!traitNs) return eventName;
|
|
3402
|
-
return traitNs.emits[eventName] || eventName;
|
|
3403
|
-
}
|
|
3404
|
-
function isNamespacedEvent(eventName) {
|
|
3405
|
-
return eventName.includes(".");
|
|
3406
|
-
}
|
|
3407
|
-
function parseNamespacedEvent(eventName) {
|
|
3408
|
-
const parts = eventName.split(".");
|
|
3409
|
-
if (parts.length === 3) {
|
|
3410
|
-
return { orbital: parts[0], trait: parts[1], event: parts[2] };
|
|
5233
|
+
const key = listener.event;
|
|
5234
|
+
const parts = key.split(".");
|
|
5235
|
+
if (parts.length === 1) {
|
|
5236
|
+
return { bareEvent: key, matcher: () => true };
|
|
3411
5237
|
}
|
|
3412
5238
|
if (parts.length === 2) {
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
|
-
// src/PersistenceAdapter.ts
|
|
3419
|
-
var InMemoryPersistence = class {
|
|
3420
|
-
data = /* @__PURE__ */ new Map();
|
|
3421
|
-
idCounter = 0;
|
|
3422
|
-
/**
|
|
3423
|
-
* Seed the store with pre-existing rows.
|
|
3424
|
-
*
|
|
3425
|
-
* Accepts either a plain `Record<entityType, EntityRow[]>` or an iterable
|
|
3426
|
-
* of `[entityType, EntityRow[]]` entries. Rows without an `id` get one
|
|
3427
|
-
* generated at insert time; rows with an `id` keep it (so re-seeding
|
|
3428
|
-
* after a schema rebuild preserves identities used in render bindings).
|
|
3429
|
-
*/
|
|
3430
|
-
seed(seedData) {
|
|
3431
|
-
const entries = Symbol.iterator in Object(seedData) ? seedData : Object.entries(seedData);
|
|
3432
|
-
for (const [entityType, rows] of entries) {
|
|
3433
|
-
if (!this.data.has(entityType)) {
|
|
3434
|
-
this.data.set(entityType, /* @__PURE__ */ new Map());
|
|
3435
|
-
}
|
|
3436
|
-
const collection = this.data.get(entityType);
|
|
3437
|
-
for (const row of rows) {
|
|
3438
|
-
const id = row.id || `${entityType}-${++this.idCounter}`;
|
|
3439
|
-
collection.set(id, { ...row, id });
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
async create(entityType, data) {
|
|
3444
|
-
const id = data.id || `${entityType}-${++this.idCounter}`;
|
|
3445
|
-
if (!this.data.has(entityType)) {
|
|
3446
|
-
this.data.set(entityType, /* @__PURE__ */ new Map());
|
|
3447
|
-
}
|
|
3448
|
-
this.data.get(entityType).set(id, { ...data, id });
|
|
3449
|
-
return { id };
|
|
3450
|
-
}
|
|
3451
|
-
async update(entityType, id, data) {
|
|
3452
|
-
const collection = this.data.get(entityType);
|
|
3453
|
-
if (collection?.has(id)) {
|
|
3454
|
-
const existing = collection.get(id);
|
|
3455
|
-
collection.set(id, { ...existing, ...data });
|
|
5239
|
+
const [sourceOrStar, eventName] = parts;
|
|
5240
|
+
if (sourceOrStar === "*") {
|
|
5241
|
+
return { bareEvent: eventName, matcher: () => true };
|
|
3456
5242
|
}
|
|
5243
|
+
return {
|
|
5244
|
+
bareEvent: eventName,
|
|
5245
|
+
matcher: buildMatcher(
|
|
5246
|
+
{ kind: "trait", trait: sourceOrStar },
|
|
5247
|
+
listenerOrbital
|
|
5248
|
+
)
|
|
5249
|
+
};
|
|
3457
5250
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
return
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
return collection ? Array.from(collection.values()) : [];
|
|
3467
|
-
}
|
|
3468
|
-
/**
|
|
3469
|
-
* Snapshot the entire store as a plain object (entityType → rows).
|
|
3470
|
-
* Useful for feeding a fresh render-time binding layer with the
|
|
3471
|
-
* current persistence view.
|
|
3472
|
-
*/
|
|
3473
|
-
snapshot() {
|
|
3474
|
-
const out = {};
|
|
3475
|
-
for (const [entityType, collection] of this.data) {
|
|
3476
|
-
out[entityType] = Array.from(collection.values());
|
|
3477
|
-
}
|
|
3478
|
-
return out;
|
|
5251
|
+
if (parts.length >= 3) {
|
|
5252
|
+
const eventName = parts[parts.length - 1];
|
|
5253
|
+
const trait = parts[parts.length - 2];
|
|
5254
|
+
const orbital = parts.slice(0, parts.length - 2).join(".");
|
|
5255
|
+
return {
|
|
5256
|
+
bareEvent: eventName,
|
|
5257
|
+
matcher: buildMatcher({ kind: "orbital", orbital, trait }, listenerOrbital)
|
|
5258
|
+
};
|
|
3479
5259
|
}
|
|
3480
|
-
};
|
|
5260
|
+
return { bareEvent: key, matcher: () => true };
|
|
5261
|
+
}
|
|
5262
|
+
function buildMatcher(src, listenerOrbital) {
|
|
5263
|
+
if (src.kind === "any") return () => true;
|
|
5264
|
+
if (src.kind === "trait") {
|
|
5265
|
+
const wantedTrait2 = src.trait;
|
|
5266
|
+
return (source) => !!source && source.orbital === listenerOrbital && source.trait === wantedTrait2;
|
|
5267
|
+
}
|
|
5268
|
+
const wantedOrbital = src.orbital;
|
|
5269
|
+
const wantedTrait = src.trait;
|
|
5270
|
+
return (source) => !!source && source.orbital === wantedOrbital && source.trait === wantedTrait;
|
|
5271
|
+
}
|
|
3481
5272
|
|
|
3482
|
-
export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, buildEmitsFromTraits, containsBindings, createContextFromBindings, createInitialTraitState, createLogger, createMockPersistence, createTestExecutor, createUnifiedLoader, extractBindings, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isBrowser, isElectron, isNamespacedEvent, isNode, normalizeEventKey, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes };
|
|
3483
|
-
//# sourceMappingURL=chunk-
|
|
3484
|
-
//# sourceMappingURL=chunk-
|
|
5273
|
+
export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, OrbitalServerRuntime, StateMachineManager, buildEmitsFromTraits, collectDeclaredConfigDefaults, containsBindings, createContextFromBindings, createInitialTraitState, createLogger, createMockPersistence, createOrbitalServerRuntime, createTestExecutor, createUnifiedLoader, extractBindings, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isBrowser, isElectron, isNamespacedEvent, isNode, normalizeEventKey, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes };
|
|
5274
|
+
//# sourceMappingURL=chunk-C4YIYOJI.js.map
|
|
5275
|
+
//# sourceMappingURL=chunk-C4YIYOJI.js.map
|