@almadar/runtime 6.9.2 → 6.9.4

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