@almadar/runtime 4.10.2 → 4.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { B as BindingContext, E as EvaluationContextExtensions, P as PatternProps, a as EffectHandlers, b as EffectContext, c as ExecutionEnvironment, d as EffectResult, T as TraitDefinition } from './types-DwDhc9Jt.js';
2
2
  export { e as Effect, f as EventListener, H as HANDLER_MANIFEST, I as IEventBus, R as RuntimeConfig, g as RuntimeEvent, h as TraitState, i as TransitionObserver, j as TransitionResult, U as Unsubscribe } from './types-DwDhc9Jt.js';
3
- import { U as UnifiedLoaderOptions, S as SchemaLoader, I as ImportChainLike, L as LoadResult, a as LoadedSchema, b as LoadedOrbital } from './OrbitalServerRuntime-B5lfym6T.js';
4
- export { E as EntitySharingMap, c as EventBus, d as EventNamespaceMap, O as OrbitalEventRequest, e as OrbitalEventResponse, f as OrbitalServerRuntimeConfig, P as PersistenceAdapter, g as PreprocessOptions, h as PreprocessResult, i as PreprocessedSchema, j as ProcessEventOptions, R as RegisteredOrbital, k as RuntimeOrbital, l as RuntimeOrbitalSchema, m as RuntimeTrait, n as StateMachineManager, o as createInitialTraitState, p as findInitialState, q as findTransition, r as getIsolatedCollectionName, s as getNamespacedEvent, t as isBrowser, u as isElectron, v as isNamespacedEvent, w as isNode, x as normalizeEventKey, y as parseNamespacedEvent, z as preprocessSchema, A as processEvent } from './OrbitalServerRuntime-B5lfym6T.js';
3
+ import { U as UnifiedLoaderOptions, S as SchemaLoader, I as ImportChainLike, L as LoadResult, a as LoadedSchema, b as LoadedOrbital, P as PersistenceAdapter } from './OrbitalServerRuntime-D-b_dg8I.js';
4
+ export { E as EntitySharingMap, c as EventBus, d as EventNamespaceMap, e as InMemoryPersistence, O as OrbitalEventRequest, f as OrbitalEventResponse, g as OrbitalServerRuntimeConfig, h as PreprocessOptions, i as PreprocessResult, j as PreprocessedSchema, k as ProcessEventOptions, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, o as StateMachineManager, p as createInitialTraitState, q as findInitialState, r as findTransition, s as getIsolatedCollectionName, t as getNamespacedEvent, u as isBrowser, v as isElectron, w as isNamespacedEvent, x as isNode, y as normalizeEventKey, z as parseNamespacedEvent, A as preprocessSchema, B as processEvent } from './OrbitalServerRuntime-D-b_dg8I.js';
5
5
  import { EvaluationContext } from '@almadar/evaluator';
6
6
  export { EvaluationContext, createMinimalContext } from '@almadar/evaluator';
7
7
  import { EventPayload, EntityRow, OrbitalDefinition, OrbitalSchema } from '@almadar/core';
@@ -370,29 +370,6 @@ interface CreateClientEffectHandlersOptions {
370
370
  */
371
371
  declare function createClientEffectHandlers(options: CreateClientEffectHandlersOptions): EffectHandlers;
372
372
 
373
- /**
374
- * OS Trigger Handlers — Server-Side Only
375
- *
376
- * Provides Node.js implementations for all 8 os/* operators.
377
- * Used by OrbitalServerRuntime (interpreted path).
378
- *
379
- * NOT exported from the main index.ts because it imports Node.js-only modules.
380
- * Import directly: import { createOsHandlers } from '@almadar/runtime/createOsHandlers';
381
- *
382
- * @packageDocumentation
383
- */
384
-
385
- interface OsHandlerContext {
386
- /** Emit an event on the EventBus */
387
- emitEvent: (type: string, payload: EventPayload) => void;
388
- /** Working directory for file watching (defaults to process.cwd()) */
389
- cwd?: string;
390
- }
391
- interface OsHandlerResult {
392
- handlers: Partial<EffectHandlers>;
393
- cleanup: () => void;
394
- }
395
-
396
373
  /**
397
374
  * MockPersistenceAdapter - In-memory data store with faker-based mock generation
398
375
  *
@@ -425,6 +402,208 @@ interface MockPersistenceConfig {
425
402
  /** Enable debug logging */
426
403
  debug?: boolean;
427
404
  }
405
+ /**
406
+ * In-memory mock data store with CRUD operations and faker-based seeding.
407
+ */
408
+ declare class MockPersistenceAdapter implements PersistenceAdapter {
409
+ private stores;
410
+ private schemas;
411
+ private idCounters;
412
+ private config;
413
+ constructor(config?: MockPersistenceConfig);
414
+ private getStore;
415
+ private nextId;
416
+ /**
417
+ * Register an entity schema and seed mock data.
418
+ * If the schema has seedData, those instances are used directly.
419
+ * Otherwise, random mock data is generated with faker.
420
+ */
421
+ registerEntity(schema: EntitySchema, seedCount?: number): void;
422
+ /**
423
+ * Seed an entity with pre-authored instance data.
424
+ */
425
+ seedFromInstances(entityName: string, instances: EntityRow[]): void;
426
+ /**
427
+ * Seed an entity with mock data.
428
+ */
429
+ seed(entityName: string, fields: EntityField[], count: number): void;
430
+ /**
431
+ * Generate a single mock item based on field schemas.
432
+ */
433
+ private generateMockItem;
434
+ /**
435
+ * Generate a mock value for a field based on its schema.
436
+ */
437
+ private generateFieldValue;
438
+ /**
439
+ * Generate a string value based on the field's declared schema metadata.
440
+ * Reads `values` (enum) first, then `format` (email/url/phone/uuid/date/
441
+ * datetime), then falls back to faker.lorem.words. No field-name heuristics
442
+ * — the schema is the source of truth. If a caller needs a real email, they
443
+ * declare `format: "email"`; if they need an enum, they declare `values: [...]`.
444
+ */
445
+ private generateStringValue;
446
+ /**
447
+ * Generate a date value. Uses the field's `format` (date vs datetime) to
448
+ * decide ISO shape; otherwise returns a recent ISO-8601 datetime. No
449
+ * field-name heuristics.
450
+ */
451
+ private generateDateValue;
452
+ private capitalizeFirst;
453
+ create(entityType: string, data: EntityRow): Promise<{
454
+ id: string;
455
+ }>;
456
+ /**
457
+ * Fill in any entity-declared field defaults that the caller omitted.
458
+ * SAVE payloads coming from form-section only carry the fields the user
459
+ * edited; persisted rows should still honor `field.default` so downstream
460
+ * row-content probes (VG11f) see a row whose every declared-default field
461
+ * is non-empty. `@now` resolves to the current ISO timestamp.
462
+ */
463
+ private applyFieldDefaults;
464
+ update(entityType: string, id: string, data: EntityRow): Promise<void>;
465
+ delete(entityType: string, id: string): Promise<void>;
466
+ getById(entityType: string, id: string): Promise<EntityRow | null>;
467
+ list(entityType: string): Promise<Array<EntityRow>>;
468
+ /**
469
+ * Clear all data for an entity.
470
+ */
471
+ clear(entityName: string): void;
472
+ /**
473
+ * Clear all data.
474
+ */
475
+ clearAll(): void;
476
+ /**
477
+ * Get count of items for an entity.
478
+ */
479
+ count(entityName: string): number;
480
+ }
481
+ /**
482
+ * Create a MockPersistenceAdapter instance.
483
+ */
484
+ declare function createMockPersistence(config?: MockPersistenceConfig): MockPersistenceAdapter;
485
+
486
+ /**
487
+ * ServerEffectHandlers — reusable factory for the server-side effect layer.
488
+ *
489
+ * Mirrors the handlers built inline in
490
+ * `OrbitalServerRuntime.executeEffects` so both the real server runtime
491
+ * AND the in-browser mock runtime (`@almadar/ui` OrbPreview autoMock) can
492
+ * share the same `fetch` / `persist` / `set` / `ref` / `deref` / `swap!` /
493
+ * `atomic` / `callService` semantics against a `PersistenceAdapter`.
494
+ *
495
+ * Browser-safe: only imports core + this package's browser-safe modules.
496
+ * Does NOT import express/Node, so it is reachable from `@almadar/ui`.
497
+ *
498
+ * @packageDocumentation
499
+ */
500
+
501
+ /**
502
+ * Minimal event-bus contract the server handlers need. Narrower than the
503
+ * full `IEventBus` so clients with a React-context bus (only `emit` is
504
+ * relevant for effect dispatch) can hand it in without adapter code.
505
+ */
506
+ interface ServerEffectEventBus {
507
+ emit(event: string, payload?: EventPayload, source?: {
508
+ orbital?: string;
509
+ trait?: string;
510
+ }): void;
511
+ }
512
+ /**
513
+ * Result entry recorded for each effect invocation. Mirrors the server
514
+ * runtime's `EffectResult`. Callers who want telemetry pass an array that
515
+ * the factory appends to; otherwise it's unused.
516
+ */
517
+ interface ServerEffectResult {
518
+ effect: "set" | "persist" | "call-service" | "fetch" | "ref" | "deref" | "swap" | "atomic";
519
+ action?: string;
520
+ entityType?: string;
521
+ data?: unknown;
522
+ success: boolean;
523
+ error?: string;
524
+ }
525
+ interface CreateServerEffectHandlersOptions {
526
+ /** Persistent store backing `fetch` / `persist` / `ref` / `deref` / `swap`. */
527
+ persistence: PersistenceAdapter;
528
+ /** Event bus that `emit` delegates to. Only `.emit()` is required. */
529
+ eventBus: ServerEffectEventBus;
530
+ /** The trait's linked entity type (used as the default for persist/set). */
531
+ entityType: string;
532
+ /** Current entity row id (used by `set`, `persist update/delete` fallback). */
533
+ entityId?: string;
534
+ /**
535
+ * Binding object passed to inner `atomic` evaluator. When absent, atomic
536
+ * effects still run but have no access to `@entity` / `@payload` bindings.
537
+ */
538
+ bindings?: BindingContext;
539
+ /** Effect context passed to the inner `atomic` executor. */
540
+ context?: EffectContext;
541
+ /** Per-event result sink. Optional — telemetry only. */
542
+ effectResults?: ServerEffectResult[];
543
+ /**
544
+ * Per-event fetched-data cache. `fetch` writes here so downstream UI
545
+ * renderers can resolve `@entity` bindings from the latest load without
546
+ * a separate round-trip.
547
+ */
548
+ fetchedData?: Record<string, EntityRow[]>;
549
+ /** Per-event emit log. Optional — telemetry only. */
550
+ emittedEvents?: Array<{
551
+ event: string;
552
+ payload?: EventPayload;
553
+ }>;
554
+ /** Source stamp applied to all emits. */
555
+ source?: {
556
+ orbital?: string;
557
+ trait?: string;
558
+ };
559
+ /** Consumer-supplied `call-service` handler. When absent, calls warn and return null. */
560
+ callService?: (service: string, action: string, params: unknown) => Promise<unknown>;
561
+ /** Verbose logging. */
562
+ debug?: boolean;
563
+ }
564
+ /**
565
+ * Build the full server-side effect handler set bound to a persistence
566
+ * adapter. The returned object satisfies `EffectHandlers` and can be
567
+ * handed directly to `EffectExecutor`.
568
+ *
569
+ * Scope: one handler object per transition — capture `entityId`,
570
+ * `bindings`, `context`, and the sink arrays at build time. For a new
571
+ * transition, call this factory again.
572
+ *
573
+ * Intentionally does NOT implement:
574
+ * - `renderUI` / `notify` / `navigate` — these are client-side, provided
575
+ * by `createClientEffectHandlers`. A mock runtime merges both handler
576
+ * sets.
577
+ * - `os/watch-*` observers — these are a no-op outside the server.
578
+ * - Schema-aware relation cardinality / on-delete cascades — those live
579
+ * in `OrbitalServerRuntime` and depend on the full registered schema.
580
+ * Clients that need them can wrap the persist handler with their own
581
+ * validation.
582
+ */
583
+ declare function createServerEffectHandlers(opts: CreateServerEffectHandlersOptions): EffectHandlers;
584
+
585
+ /**
586
+ * OS Trigger Handlers — Server-Side Only
587
+ *
588
+ * Provides Node.js implementations for all 8 os/* operators.
589
+ * Used by OrbitalServerRuntime (interpreted path).
590
+ *
591
+ * NOT exported from the main index.ts because it imports Node.js-only modules.
592
+ * Import directly: import { createOsHandlers } from '@almadar/runtime/createOsHandlers';
593
+ *
594
+ * @packageDocumentation
595
+ */
596
+
597
+ interface OsHandlerContext {
598
+ /** Emit an event on the EventBus */
599
+ emitEvent: (type: string, payload: EventPayload) => void;
600
+ /** Working directory for file watching (defaults to process.cwd()) */
601
+ cwd?: string;
602
+ }
603
+ interface OsHandlerResult {
604
+ handlers: Partial<EffectHandlers>;
605
+ cleanup: () => void;
606
+ }
428
607
 
429
608
  /**
430
609
  * PayloadValidator - Cross-Trait Payload Shape Validation (RCG-10)
@@ -659,4 +838,4 @@ declare namespace index {
659
838
  export { type index_ComposeBehaviorsInput as ComposeBehaviorsInput, type index_ComposeBehaviorsResult as ComposeBehaviorsResult, type index_EventWiringEntry as EventWiringEntry, type index_LayoutStrategy as LayoutStrategy, type index_PipeStep as PipeStep, index_applyEventWiring as applyEventWiring, index_composeBehaviors as composeBehaviors, index_detectLayoutStrategy as detectLayoutStrategy, index_pipeBehaviors as pipeBehaviors };
660
839
  }
661
840
 
662
- export { BindingContext, type ClientEventBus, type ComposeBehaviorsInput, type ComposeBehaviorsResult, type CreateClientEffectHandlersOptions, EffectContext, EffectExecutor, type EffectExecutorOptions, EffectHandlers, EffectResult, type EntityField, type EntitySchema, type EventWiringEntry, ExecutionEnvironment, ImportChainLike, type LayoutStrategy, LoadResult, LoadedOrbital, LoadedSchema, type MockPersistenceConfig, type OsHandlerContext, type OsHandlerResult, type PayloadMismatch, type PipeStep, SchemaLoader, type SlotSetter, TraitDefinition, UnifiedLoaderOptions, applyEventWiring, buildEmitsFromTraits, composeBehaviors, index as composition, containsBindings, createClientEffectHandlers, createContextFromBindings, createTestExecutor, createUnifiedLoader, detectLayoutStrategy, extractBindings, interpolateProps, interpolateValue, pipeBehaviors, validatePayloadShapes };
841
+ export { BindingContext, type ClientEventBus, type ComposeBehaviorsInput, type ComposeBehaviorsResult, type CreateClientEffectHandlersOptions, type CreateServerEffectHandlersOptions, EffectContext, EffectExecutor, type EffectExecutorOptions, EffectHandlers, EffectResult, type EntityField, type EntitySchema, type EventWiringEntry, ExecutionEnvironment, ImportChainLike, type LayoutStrategy, LoadResult, LoadedOrbital, LoadedSchema, MockPersistenceAdapter, type MockPersistenceConfig, type OsHandlerContext, type OsHandlerResult, type PayloadMismatch, PersistenceAdapter, type PipeStep, SchemaLoader, type ServerEffectResult, type SlotSetter, TraitDefinition, UnifiedLoaderOptions, applyEventWiring, buildEmitsFromTraits, composeBehaviors, index as composition, containsBindings, createClientEffectHandlers, createContextFromBindings, createMockPersistence, createServerEffectHandlers, createTestExecutor, createUnifiedLoader, detectLayoutStrategy, extractBindings, interpolateProps, interpolateValue, pipeBehaviors, validatePayloadShapes };
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- export { EffectExecutor, EventBus, HANDLER_MANIFEST, StateMachineManager, containsBindings, createContextFromBindings, createInitialTraitState, createMinimalContext, createTestExecutor, createUnifiedLoader, extractBindings, findInitialState, findTransition, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isBrowser, isElectron, isNamespacedEvent, isNode, normalizeEventKey, parseNamespacedEvent, preprocessSchema, processEvent } from './chunk-K36736GF.js';
1
+ import { EffectExecutor, createContextFromBindings } from './chunk-RCWMQH7Y.js';
2
+ export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, containsBindings, createContextFromBindings, createInitialTraitState, createMinimalContext, createMockPersistence, createTestExecutor, createUnifiedLoader, extractBindings, findInitialState, findTransition, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isBrowser, isElectron, isNamespacedEvent, isNode, normalizeEventKey, parseNamespacedEvent, preprocessSchema, processEvent } from './chunk-RCWMQH7Y.js';
2
3
  import { __export } from './chunk-PZ5AY32C.js';
4
+ import { evaluate } from '@almadar/evaluator';
3
5
  import { isInlineTrait } from '@almadar/core';
4
6
 
5
7
  // src/ClientEffectHandlers.ts
@@ -35,6 +37,392 @@ function createClientEffectHandlers(options) {
35
37
  })
36
38
  };
37
39
  }
40
+ function createServerEffectHandlers(opts) {
41
+ const {
42
+ persistence,
43
+ eventBus,
44
+ entityType,
45
+ entityId,
46
+ bindings,
47
+ context,
48
+ effectResults,
49
+ fetchedData,
50
+ emittedEvents,
51
+ source,
52
+ callService: consumerCallService,
53
+ debug
54
+ } = opts;
55
+ const record = (entry) => {
56
+ effectResults?.push(entry);
57
+ };
58
+ const handlers = {
59
+ emit: (event, eventPayload, emitSource) => {
60
+ if (debug) {
61
+ console.log(`[ServerEffectHandlers] emit ${event}`, eventPayload);
62
+ }
63
+ const stamp = emitSource ?? source;
64
+ eventBus.emit(event, eventPayload, stamp);
65
+ emittedEvents?.push({ event, payload: eventPayload });
66
+ },
67
+ set: async (targetId, field, value) => {
68
+ const id = targetId || entityId;
69
+ if (!id) return;
70
+ try {
71
+ await persistence.update(entityType, id, { [field]: value });
72
+ record({
73
+ effect: "set",
74
+ entityType,
75
+ data: { id, field, value },
76
+ success: true
77
+ });
78
+ } catch (err) {
79
+ record({
80
+ effect: "set",
81
+ entityType,
82
+ data: { id, field, value },
83
+ success: false,
84
+ error: err instanceof Error ? err.message : String(err)
85
+ });
86
+ }
87
+ },
88
+ persist: async (action, targetEntityType, data) => {
89
+ if (action === "batch") {
90
+ const operations = data?.operations;
91
+ if (!Array.isArray(operations) || operations.length === 0) {
92
+ record({
93
+ effect: "persist",
94
+ action: "batch",
95
+ success: false,
96
+ error: "Batch requires a non-empty operations array"
97
+ });
98
+ return;
99
+ }
100
+ const batchResults = [];
101
+ const completed = [];
102
+ let batchFailed = false;
103
+ let batchError = "";
104
+ for (const op of operations) {
105
+ if (!Array.isArray(op) || op.length < 2) {
106
+ batchFailed = true;
107
+ batchError = `Invalid batch operation format: ${JSON.stringify(op)}`;
108
+ break;
109
+ }
110
+ const [opAction, opEntityType, ...opRest] = op;
111
+ try {
112
+ switch (opAction) {
113
+ case "create": {
114
+ const createData = opRest[0] || {};
115
+ const { id: newId } = await persistence.create(
116
+ opEntityType,
117
+ createData
118
+ );
119
+ batchResults.push({
120
+ action: "create",
121
+ entityType: opEntityType,
122
+ id: newId,
123
+ ...createData
124
+ });
125
+ completed.push({
126
+ action: "create",
127
+ entityType: opEntityType,
128
+ id: newId
129
+ });
130
+ break;
131
+ }
132
+ case "update": {
133
+ const updateId = opRest[0];
134
+ const updateData = opRest[1] || {};
135
+ await persistence.update(opEntityType, updateId, updateData);
136
+ const updated = await persistence.getById(opEntityType, updateId);
137
+ batchResults.push({
138
+ action: "update",
139
+ entityType: opEntityType,
140
+ id: updateId,
141
+ ...updated || updateData
142
+ });
143
+ completed.push({
144
+ action: "update",
145
+ entityType: opEntityType,
146
+ id: updateId
147
+ });
148
+ break;
149
+ }
150
+ case "delete": {
151
+ const deleteId = opRest[0];
152
+ await persistence.delete(opEntityType, deleteId);
153
+ batchResults.push({
154
+ action: "delete",
155
+ entityType: opEntityType,
156
+ id: deleteId,
157
+ deleted: true
158
+ });
159
+ completed.push({
160
+ action: "delete",
161
+ entityType: opEntityType,
162
+ id: deleteId
163
+ });
164
+ break;
165
+ }
166
+ default:
167
+ batchFailed = true;
168
+ batchError = `Unknown batch operation action: ${opAction}`;
169
+ break;
170
+ }
171
+ } catch (err) {
172
+ batchFailed = true;
173
+ batchError = `Batch operation [${opAction}, ${opEntityType}] failed: ${err instanceof Error ? err.message : String(err)}`;
174
+ break;
175
+ }
176
+ if (batchFailed) break;
177
+ }
178
+ record({
179
+ effect: "persist",
180
+ action: "batch",
181
+ data: {
182
+ operations: batchResults,
183
+ completedCount: completed.length,
184
+ totalCount: operations.length
185
+ },
186
+ success: !batchFailed,
187
+ ...batchFailed ? { error: batchError } : {}
188
+ });
189
+ return;
190
+ }
191
+ const type = targetEntityType || entityType;
192
+ let resultData;
193
+ try {
194
+ switch (action) {
195
+ case "create": {
196
+ const { id } = await persistence.create(type, data ?? {});
197
+ resultData = { id, ...data ?? {} };
198
+ break;
199
+ }
200
+ case "update": {
201
+ const row = data ?? {};
202
+ const idOrFallback = row.id ?? entityId;
203
+ if (idOrFallback) {
204
+ await persistence.update(type, idOrFallback, row);
205
+ const updated = await persistence.getById(type, idOrFallback);
206
+ resultData = updated ?? { id: idOrFallback, ...row };
207
+ }
208
+ break;
209
+ }
210
+ case "delete": {
211
+ const directId = typeof data === "string" ? data : void 0;
212
+ const nestedId = typeof data === "object" && data !== null ? data.id : void 0;
213
+ const deleteId = directId ?? nestedId ?? entityId;
214
+ if (deleteId) {
215
+ await persistence.delete(type, deleteId);
216
+ resultData = { id: deleteId, deleted: true };
217
+ }
218
+ break;
219
+ }
220
+ }
221
+ record({
222
+ effect: "persist",
223
+ action,
224
+ entityType: type,
225
+ data: resultData,
226
+ success: true
227
+ });
228
+ } catch (err) {
229
+ record({
230
+ effect: "persist",
231
+ action,
232
+ entityType: type,
233
+ success: false,
234
+ error: err instanceof Error ? err.message : String(err)
235
+ });
236
+ }
237
+ },
238
+ callService: async (service, action, params) => {
239
+ try {
240
+ let result = null;
241
+ if (consumerCallService) {
242
+ result = await consumerCallService(service, action, params);
243
+ } else if (debug) {
244
+ console.warn(
245
+ `[ServerEffectHandlers] call-service not configured: ${service}.${action}`
246
+ );
247
+ }
248
+ record({
249
+ effect: "call-service",
250
+ action: `${service}.${action}`,
251
+ data: result,
252
+ success: true
253
+ });
254
+ return result;
255
+ } catch (err) {
256
+ record({
257
+ effect: "call-service",
258
+ action: `${service}.${action}`,
259
+ success: false,
260
+ error: err instanceof Error ? err.message : String(err)
261
+ });
262
+ return null;
263
+ }
264
+ },
265
+ fetch: async (fetchEntityType, options) => {
266
+ try {
267
+ let result = null;
268
+ if (options?.id) {
269
+ const entity = await persistence.getById(fetchEntityType, options.id);
270
+ if (entity) {
271
+ if (fetchedData) fetchedData[fetchEntityType] = [entity];
272
+ result = entity;
273
+ }
274
+ } else {
275
+ let entities = await persistence.list(fetchEntityType);
276
+ if (options?.offset && options.offset > 0) {
277
+ entities = entities.slice(options.offset);
278
+ }
279
+ if (options?.limit && options.limit > 0) {
280
+ entities = entities.slice(0, options.limit);
281
+ }
282
+ if (fetchedData) fetchedData[fetchEntityType] = entities;
283
+ result = entities;
284
+ }
285
+ if (bindings && result) {
286
+ const records = Array.isArray(result) ? result : [result];
287
+ if (records.length > 0) {
288
+ const merged = Object.assign([...records], records[0]);
289
+ bindings[fetchEntityType] = merged;
290
+ if (fetchEntityType === entityType) {
291
+ bindings.entity = merged;
292
+ }
293
+ }
294
+ }
295
+ return result;
296
+ } catch (err) {
297
+ console.error(
298
+ `[ServerEffectHandlers] fetch error for ${fetchEntityType}:`,
299
+ err
300
+ );
301
+ return null;
302
+ }
303
+ },
304
+ ref: async (refEntityType, options) => {
305
+ return handlers.fetch(refEntityType, options);
306
+ },
307
+ deref: async (derefEntityType, options) => {
308
+ try {
309
+ let result = null;
310
+ if (options?.id) {
311
+ const entity = await persistence.getById(derefEntityType, options.id);
312
+ if (entity) {
313
+ if (fetchedData) fetchedData[derefEntityType] = [entity];
314
+ result = entity;
315
+ }
316
+ } else {
317
+ const entities = await persistence.list(derefEntityType);
318
+ if (fetchedData) fetchedData[derefEntityType] = entities;
319
+ result = entities;
320
+ }
321
+ if (bindings && result) {
322
+ const records = Array.isArray(result) ? result : [result];
323
+ if (records.length > 0) {
324
+ const merged = Object.assign([...records], records[0]);
325
+ bindings[derefEntityType] = merged;
326
+ if (derefEntityType === entityType) {
327
+ bindings.entity = merged;
328
+ }
329
+ }
330
+ }
331
+ record({
332
+ effect: "deref",
333
+ entityType: derefEntityType,
334
+ success: true
335
+ });
336
+ return result;
337
+ } catch (err) {
338
+ record({
339
+ effect: "deref",
340
+ entityType: derefEntityType,
341
+ success: false,
342
+ error: err instanceof Error ? err.message : String(err)
343
+ });
344
+ return null;
345
+ }
346
+ },
347
+ swap: async (swapEntityType, swapEntityId, transform) => {
348
+ try {
349
+ const current = await persistence.getById(swapEntityType, swapEntityId);
350
+ if (!current) {
351
+ record({
352
+ effect: "swap",
353
+ entityType: swapEntityType,
354
+ success: false,
355
+ error: `Entity ${swapEntityType}/${swapEntityId} not found`
356
+ });
357
+ return null;
358
+ }
359
+ const ctx = createContextFromBindings(
360
+ { current, entity: bindings?.entity, payload: bindings?.payload },
361
+ false
362
+ );
363
+ let newData;
364
+ if (Array.isArray(transform)) {
365
+ const evalResult = evaluate(transform, ctx);
366
+ if (evalResult && typeof evalResult === "object" && !Array.isArray(evalResult)) {
367
+ newData = evalResult;
368
+ } else {
369
+ newData = current;
370
+ }
371
+ } else if (typeof transform === "object" && transform !== null) {
372
+ newData = { ...current, ...transform };
373
+ } else {
374
+ record({
375
+ effect: "swap",
376
+ entityType: swapEntityType,
377
+ success: false,
378
+ error: "swap! transform must be an S-expression or object"
379
+ });
380
+ return null;
381
+ }
382
+ await persistence.update(swapEntityType, swapEntityId, newData);
383
+ record({
384
+ effect: "swap",
385
+ entityType: swapEntityType,
386
+ data: { id: swapEntityId, ...newData },
387
+ success: true
388
+ });
389
+ return newData;
390
+ } catch (err) {
391
+ record({
392
+ effect: "swap",
393
+ entityType: swapEntityType,
394
+ success: false,
395
+ error: err instanceof Error ? err.message : String(err)
396
+ });
397
+ return null;
398
+ }
399
+ },
400
+ watch: (_watchEntityType) => {
401
+ },
402
+ atomic: async (atomicEffects) => {
403
+ const atomicExecutor = new EffectExecutor({
404
+ handlers,
405
+ bindings: bindings ?? {},
406
+ context: context ?? {
407
+ traitName: "atomic",
408
+ state: "unknown",
409
+ transition: "unknown"
410
+ }
411
+ });
412
+ try {
413
+ await atomicExecutor.executeAll(atomicEffects);
414
+ record({ effect: "atomic", success: true });
415
+ } catch (err) {
416
+ record({
417
+ effect: "atomic",
418
+ success: false,
419
+ error: err instanceof Error ? err.message : String(err)
420
+ });
421
+ }
422
+ }
423
+ };
424
+ return handlers;
425
+ }
38
426
 
39
427
  // src/PayloadValidator.ts
40
428
  function validatePayloadShapes(traits, emits) {
@@ -327,6 +715,6 @@ function pipeBehaviors(seed, ...steps) {
327
715
  return current;
328
716
  }
329
717
 
330
- export { applyEventWiring, buildEmitsFromTraits, composeBehaviors, composition_exports as composition, createClientEffectHandlers, detectLayoutStrategy, pipeBehaviors, validatePayloadShapes };
718
+ export { applyEventWiring, buildEmitsFromTraits, composeBehaviors, composition_exports as composition, createClientEffectHandlers, createServerEffectHandlers, detectLayoutStrategy, pipeBehaviors, validatePayloadShapes };
331
719
  //# sourceMappingURL=index.js.map
332
720
  //# sourceMappingURL=index.js.map