@almadar/runtime 5.8.1 → 5.8.3

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