@plures/praxis 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +67 -6
  2. package/dist/browser/chunk-R45WXWKH.js +345 -0
  3. package/dist/browser/index.d.ts +171 -11
  4. package/dist/browser/index.js +279 -277
  5. package/dist/browser/integrations/svelte.d.ts +3 -1
  6. package/dist/browser/integrations/svelte.js +7 -0
  7. package/dist/browser/{engine-BjdqxeXG.d.ts → reactive-engine.svelte-C9OpcTHf.d.ts} +87 -1
  8. package/dist/node/chunk-R45WXWKH.js +345 -0
  9. package/dist/node/components/index.d.cts +2 -2
  10. package/dist/node/components/index.d.ts +2 -2
  11. package/dist/node/index.cjs +343 -8
  12. package/dist/node/index.d.cts +108 -15
  13. package/dist/node/index.d.ts +108 -15
  14. package/dist/node/index.js +279 -278
  15. package/dist/node/integrations/svelte.cjs +357 -2
  16. package/dist/node/integrations/svelte.d.cts +3 -1
  17. package/dist/node/integrations/svelte.d.ts +3 -1
  18. package/dist/node/integrations/svelte.js +6 -0
  19. package/dist/node/{engine-CVJobhHm.d.cts → reactive-engine.svelte-1M4m_C_v.d.cts} +87 -1
  20. package/dist/node/{engine-1iqLe6_P.d.ts → reactive-engine.svelte-ChNFn4Hj.d.ts} +87 -1
  21. package/dist/node/{terminal-adapter-XLtCjjb_.d.cts → terminal-adapter-CDzxoLKR.d.cts} +68 -1
  22. package/dist/node/{terminal-adapter-07HGftGQ.d.ts → terminal-adapter-CWka-yL8.d.ts} +68 -1
  23. package/package.json +3 -2
  24. package/src/__tests__/reactive-engine.test.ts +516 -0
  25. package/src/core/pluresdb/README.md +156 -0
  26. package/src/core/pluresdb/adapter.ts +165 -0
  27. package/src/core/pluresdb/index.ts +3 -3
  28. package/src/core/reactive-engine.svelte.ts +88 -19
  29. package/src/core/reactive-engine.ts +284 -22
  30. package/src/index.browser.ts +12 -0
  31. package/src/index.ts +12 -0
  32. package/src/integrations/pluresdb.ts +2 -2
  33. package/src/integrations/svelte.ts +8 -0
@@ -71,6 +71,73 @@ declare class InMemoryPraxisDB implements PraxisDB {
71
71
  * ```
72
72
  */
73
73
  declare function createInMemoryDB(): InMemoryPraxisDB;
74
+ /**
75
+ * PluresDB instance type - represents either PluresNode or SQLiteCompatibleAPI
76
+ */
77
+ type PluresDBInstance = {
78
+ get(key: string): Promise<any>;
79
+ put(key: string, value: any): Promise<void>;
80
+ };
81
+ /**
82
+ * Configuration options for PluresDBPraxisAdapter
83
+ */
84
+ interface PluresDBAdapterConfig {
85
+ /** PluresDB instance */
86
+ db: PluresDBInstance;
87
+ /** Polling interval in milliseconds for watch functionality (default: 1000ms) */
88
+ pollInterval?: number;
89
+ }
90
+ /**
91
+ * PluresDB-backed implementation of PraxisDB
92
+ *
93
+ * Wraps the official PluresDB package from NPM to provide
94
+ * the PraxisDB interface for production use.
95
+ */
96
+ declare class PluresDBPraxisAdapter implements PraxisDB {
97
+ private db;
98
+ private watchers;
99
+ private pollIntervals;
100
+ private lastValues;
101
+ private pollInterval;
102
+ constructor(config: PluresDBAdapterConfig | PluresDBInstance);
103
+ get<T>(key: string): Promise<T | undefined>;
104
+ set<T>(key: string, value: T): Promise<void>;
105
+ watch<T>(key: string, callback: (val: T) => void): UnsubscribeFn;
106
+ /**
107
+ * Clean up all resources
108
+ */
109
+ dispose(): void;
110
+ }
111
+ /**
112
+ * Create a PluresDB-backed PraxisDB instance
113
+ *
114
+ * Wraps the official PluresDB package from NPM.
115
+ *
116
+ * @param config PluresDB instance or configuration object
117
+ * @returns PluresDBPraxisAdapter instance
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * import { PluresNode } from 'pluresdb';
122
+ * import { createPluresDB } from '@plures/praxis';
123
+ *
124
+ * const pluresdb = new PluresNode({ autoStart: true });
125
+ * const db = createPluresDB(pluresdb);
126
+ *
127
+ * await db.set('user:1', { name: 'Alice' });
128
+ * const user = await db.get('user:1');
129
+ * ```
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * // With custom polling interval
134
+ * const db = createPluresDB({
135
+ * db: pluresdb,
136
+ * pollInterval: 500, // Poll every 500ms
137
+ * });
138
+ * ```
139
+ */
140
+ declare function createPluresDB(config: PluresDBAdapterConfig | PluresDBInstance): PluresDBPraxisAdapter;
74
141
 
75
142
  /**
76
143
  * Terminal Node Runtime Adapter
@@ -228,4 +295,4 @@ declare function createMockExecutor(responses: Record<string, {
228
295
  error?: string;
229
296
  }>): CommandExecutor;
230
297
 
231
- export { type CommandExecutor as C, InMemoryPraxisDB as I, type PraxisDB as P, TerminalAdapter as T, type UnsubscribeFn as U, type TerminalExecutionResult as a, type TerminalNodeState as b, createTerminalAdapter as c, type TerminalAdapterOptions as d, createMockExecutor as e, createInMemoryDB as f, runTerminalCommand as r };
298
+ export { type CommandExecutor as C, InMemoryPraxisDB as I, type PraxisDB as P, TerminalAdapter as T, type UnsubscribeFn as U, type TerminalExecutionResult as a, type TerminalNodeState as b, createTerminalAdapter as c, type TerminalAdapterOptions as d, createMockExecutor as e, type PluresDBInstance as f, type PluresDBAdapterConfig as g, createInMemoryDB as h, PluresDBPraxisAdapter as i, createPluresDB as j, runTerminalCommand as r };
@@ -71,6 +71,73 @@ declare class InMemoryPraxisDB implements PraxisDB {
71
71
  * ```
72
72
  */
73
73
  declare function createInMemoryDB(): InMemoryPraxisDB;
74
+ /**
75
+ * PluresDB instance type - represents either PluresNode or SQLiteCompatibleAPI
76
+ */
77
+ type PluresDBInstance = {
78
+ get(key: string): Promise<any>;
79
+ put(key: string, value: any): Promise<void>;
80
+ };
81
+ /**
82
+ * Configuration options for PluresDBPraxisAdapter
83
+ */
84
+ interface PluresDBAdapterConfig {
85
+ /** PluresDB instance */
86
+ db: PluresDBInstance;
87
+ /** Polling interval in milliseconds for watch functionality (default: 1000ms) */
88
+ pollInterval?: number;
89
+ }
90
+ /**
91
+ * PluresDB-backed implementation of PraxisDB
92
+ *
93
+ * Wraps the official PluresDB package from NPM to provide
94
+ * the PraxisDB interface for production use.
95
+ */
96
+ declare class PluresDBPraxisAdapter implements PraxisDB {
97
+ private db;
98
+ private watchers;
99
+ private pollIntervals;
100
+ private lastValues;
101
+ private pollInterval;
102
+ constructor(config: PluresDBAdapterConfig | PluresDBInstance);
103
+ get<T>(key: string): Promise<T | undefined>;
104
+ set<T>(key: string, value: T): Promise<void>;
105
+ watch<T>(key: string, callback: (val: T) => void): UnsubscribeFn;
106
+ /**
107
+ * Clean up all resources
108
+ */
109
+ dispose(): void;
110
+ }
111
+ /**
112
+ * Create a PluresDB-backed PraxisDB instance
113
+ *
114
+ * Wraps the official PluresDB package from NPM.
115
+ *
116
+ * @param config PluresDB instance or configuration object
117
+ * @returns PluresDBPraxisAdapter instance
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * import { PluresNode } from 'pluresdb';
122
+ * import { createPluresDB } from '@plures/praxis';
123
+ *
124
+ * const pluresdb = new PluresNode({ autoStart: true });
125
+ * const db = createPluresDB(pluresdb);
126
+ *
127
+ * await db.set('user:1', { name: 'Alice' });
128
+ * const user = await db.get('user:1');
129
+ * ```
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * // With custom polling interval
134
+ * const db = createPluresDB({
135
+ * db: pluresdb,
136
+ * pollInterval: 500, // Poll every 500ms
137
+ * });
138
+ * ```
139
+ */
140
+ declare function createPluresDB(config: PluresDBAdapterConfig | PluresDBInstance): PluresDBPraxisAdapter;
74
141
 
75
142
  /**
76
143
  * Terminal Node Runtime Adapter
@@ -228,4 +295,4 @@ declare function createMockExecutor(responses: Record<string, {
228
295
  error?: string;
229
296
  }>): CommandExecutor;
230
297
 
231
- export { type CommandExecutor as C, InMemoryPraxisDB as I, type PraxisDB as P, TerminalAdapter as T, type UnsubscribeFn as U, type TerminalExecutionResult as a, type TerminalNodeState as b, createTerminalAdapter as c, type TerminalAdapterOptions as d, createMockExecutor as e, createInMemoryDB as f, runTerminalCommand as r };
298
+ export { type CommandExecutor as C, InMemoryPraxisDB as I, type PraxisDB as P, TerminalAdapter as T, type UnsubscribeFn as U, type TerminalExecutionResult as a, type TerminalNodeState as b, createTerminalAdapter as c, type TerminalAdapterOptions as d, createMockExecutor as e, type PluresDBInstance as f, type PluresDBAdapterConfig as g, createInMemoryDB as h, PluresDBPraxisAdapter as i, createPluresDB as j, runTerminalCommand as r };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plures/praxis",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "The Full Plures Application Framework - declarative schemas, logic engine, component generation, and local-first data",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -116,7 +116,8 @@
116
116
  },
117
117
  "dependencies": {
118
118
  "commander": "^14.0.2",
119
- "js-yaml": "^4.1.1"
119
+ "js-yaml": "^4.1.1",
120
+ "pluresdb": "^1.0.1"
120
121
  },
121
122
  "peerDependencies": {
122
123
  "svelte": "^5.46.0"
@@ -0,0 +1,516 @@
1
+ /**
2
+ * Tests for Framework-Agnostic Reactive Engine
3
+ *
4
+ * Tests the reactive engine implementation using JavaScript Proxies
5
+ * for automatic state tracking and change notifications.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
9
+ import {
10
+ ReactiveLogicEngine,
11
+ createReactiveEngine,
12
+ type StateChangeCallback,
13
+ } from '../core/reactive-engine.js';
14
+
15
+ interface TestContext {
16
+ count: number;
17
+ name: string;
18
+ nested: {
19
+ value: number;
20
+ };
21
+ }
22
+
23
+ describe('Framework-Agnostic Reactive Engine', () => {
24
+ describe('ReactiveLogicEngine', () => {
25
+ it('should create an engine with initial state', () => {
26
+ const engine = new ReactiveLogicEngine<TestContext>({
27
+ initialContext: {
28
+ count: 0,
29
+ name: 'test',
30
+ nested: { value: 10 },
31
+ },
32
+ initialFacts: [],
33
+ initialMeta: {},
34
+ });
35
+
36
+ expect(engine.context.count).toBe(0);
37
+ expect(engine.context.name).toBe('test');
38
+ expect(engine.context.nested.value).toBe(10);
39
+ expect(engine.facts).toEqual([]);
40
+ expect(engine.meta).toEqual({});
41
+ });
42
+
43
+ it('should provide access to state via getters', () => {
44
+ const engine = createReactiveEngine<TestContext>({
45
+ initialContext: { count: 5, name: 'hello', nested: { value: 20 } },
46
+ });
47
+
48
+ const state = engine.state;
49
+ expect(state.context.count).toBe(5);
50
+ expect(state.context.name).toBe('hello');
51
+ expect(state.facts).toEqual([]);
52
+ expect(state.meta).toEqual({});
53
+ });
54
+ });
55
+
56
+ describe('Reactivity', () => {
57
+ it('should notify subscribers when context changes', () => {
58
+ const engine = createReactiveEngine<TestContext>({
59
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
60
+ });
61
+
62
+ const states: any[] = [];
63
+ engine.subscribe((state) => {
64
+ states.push({ ...state.context });
65
+ });
66
+
67
+ // Initial state
68
+ expect(states.length).toBe(1);
69
+ expect(states[0].count).toBe(0);
70
+
71
+ // Mutate context
72
+ engine.apply((state) => {
73
+ state.context.count = 5;
74
+ });
75
+
76
+ // Should have notified
77
+ expect(states.length).toBe(2);
78
+ expect(states[1].count).toBe(5);
79
+ });
80
+
81
+ it('should notify subscribers when facts change', () => {
82
+ const engine = createReactiveEngine({
83
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
84
+ });
85
+
86
+ const factArrays: any[][] = [];
87
+ engine.subscribe((state) => {
88
+ factArrays.push([...state.facts]);
89
+ });
90
+
91
+ expect(factArrays.length).toBe(1);
92
+ expect(factArrays[0]).toEqual([]);
93
+
94
+ // Add a fact
95
+ engine.apply((state) => {
96
+ state.facts.push({ tag: 'TestFact', payload: { value: 1 } });
97
+ });
98
+
99
+ expect(factArrays.length).toBe(2);
100
+ expect(factArrays[1]).toEqual([{ tag: 'TestFact', payload: { value: 1 } }]);
101
+ });
102
+
103
+ it('should notify subscribers when meta changes', () => {
104
+ const engine = createReactiveEngine({
105
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
106
+ });
107
+
108
+ const metas: any[] = [];
109
+ engine.subscribe((state) => {
110
+ metas.push({ ...state.meta });
111
+ });
112
+
113
+ expect(metas.length).toBe(1);
114
+ expect(metas[0]).toEqual({});
115
+
116
+ // Set meta value
117
+ engine.apply((state) => {
118
+ state.meta.timestamp = Date.now();
119
+ });
120
+
121
+ expect(metas.length).toBe(2);
122
+ expect(metas[1].timestamp).toBeDefined();
123
+ });
124
+
125
+ it('should only notify when value actually changes', () => {
126
+ const engine = createReactiveEngine({
127
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
128
+ });
129
+
130
+ const callback = vi.fn();
131
+ engine.subscribe(callback);
132
+
133
+ expect(callback).toHaveBeenCalledTimes(1); // Initial call
134
+
135
+ // Set to same value
136
+ engine.apply((state) => {
137
+ state.context.count = 0;
138
+ });
139
+
140
+ // Should not notify because value didn't change
141
+ expect(callback).toHaveBeenCalledTimes(1);
142
+
143
+ // Set to different value
144
+ engine.apply((state) => {
145
+ state.context.count = 1;
146
+ });
147
+
148
+ // Should notify
149
+ expect(callback).toHaveBeenCalledTimes(2);
150
+ });
151
+
152
+ it('should support nested object mutations', () => {
153
+ const engine = createReactiveEngine({
154
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
155
+ });
156
+
157
+ const callback = vi.fn();
158
+ engine.subscribe(callback);
159
+
160
+ expect(callback).toHaveBeenCalledTimes(1);
161
+
162
+ // Mutate nested value
163
+ engine.apply((state) => {
164
+ state.context.nested.value = 20;
165
+ });
166
+
167
+ expect(callback).toHaveBeenCalledTimes(2);
168
+ expect(engine.context.nested.value).toBe(20);
169
+ });
170
+ });
171
+
172
+ describe('Subscription Management', () => {
173
+ it('should return unsubscribe function', () => {
174
+ const engine = createReactiveEngine({
175
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
176
+ });
177
+
178
+ const callback = vi.fn();
179
+ const unsubscribe = engine.subscribe(callback);
180
+
181
+ expect(callback).toHaveBeenCalledTimes(1);
182
+
183
+ engine.apply((state) => {
184
+ state.context.count = 1;
185
+ });
186
+
187
+ expect(callback).toHaveBeenCalledTimes(2);
188
+
189
+ // Unsubscribe
190
+ unsubscribe();
191
+
192
+ // Should not notify after unsubscribe
193
+ engine.apply((state) => {
194
+ state.context.count = 2;
195
+ });
196
+
197
+ expect(callback).toHaveBeenCalledTimes(2);
198
+ });
199
+
200
+ it('should support multiple subscribers', () => {
201
+ const engine = createReactiveEngine({
202
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
203
+ });
204
+
205
+ const callback1 = vi.fn();
206
+ const callback2 = vi.fn();
207
+ const callback3 = vi.fn();
208
+
209
+ engine.subscribe(callback1);
210
+ engine.subscribe(callback2);
211
+ engine.subscribe(callback3);
212
+
213
+ expect(callback1).toHaveBeenCalledTimes(1);
214
+ expect(callback2).toHaveBeenCalledTimes(1);
215
+ expect(callback3).toHaveBeenCalledTimes(1);
216
+
217
+ engine.apply((state) => {
218
+ state.context.count = 5;
219
+ });
220
+
221
+ expect(callback1).toHaveBeenCalledTimes(2);
222
+ expect(callback2).toHaveBeenCalledTimes(2);
223
+ expect(callback3).toHaveBeenCalledTimes(2);
224
+ });
225
+
226
+ it('should handle errors in subscriber callbacks gracefully', () => {
227
+ const engine = createReactiveEngine({
228
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
229
+ });
230
+
231
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
232
+
233
+ const callback1 = vi.fn(() => {
234
+ throw new Error('Test error');
235
+ });
236
+ const callback2 = vi.fn();
237
+
238
+ engine.subscribe(callback1);
239
+ engine.subscribe(callback2);
240
+
241
+ expect(callback1).toHaveBeenCalledTimes(1);
242
+ expect(callback2).toHaveBeenCalledTimes(1);
243
+
244
+ // Should not throw, should log error, and should still call callback2
245
+ engine.apply((state) => {
246
+ state.context.count = 1;
247
+ });
248
+
249
+ expect(consoleError).toHaveBeenCalled();
250
+ expect(callback1).toHaveBeenCalledTimes(2);
251
+ expect(callback2).toHaveBeenCalledTimes(2);
252
+
253
+ consoleError.mockRestore();
254
+ });
255
+ });
256
+
257
+ describe('Derived Values', () => {
258
+ it('should create derived values that update reactively', () => {
259
+ const engine = createReactiveEngine({
260
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
261
+ });
262
+
263
+ const doubled = engine.$derived((state) => state.context.count * 2);
264
+
265
+ const values: number[] = [];
266
+ doubled.subscribe((value) => {
267
+ values.push(value);
268
+ });
269
+
270
+ expect(values).toEqual([0]);
271
+
272
+ engine.apply((state) => {
273
+ state.context.count = 5;
274
+ });
275
+
276
+ expect(values).toEqual([0, 10]);
277
+
278
+ engine.apply((state) => {
279
+ state.context.count = 10;
280
+ });
281
+
282
+ expect(values).toEqual([0, 10, 20]);
283
+ });
284
+
285
+ it('should only notify derived subscribers when derived value changes', () => {
286
+ const engine = createReactiveEngine({
287
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
288
+ });
289
+
290
+ const isPositive = engine.$derived((state) => state.context.count > 0);
291
+
292
+ const values: boolean[] = [];
293
+ isPositive.subscribe((value) => {
294
+ values.push(value);
295
+ });
296
+
297
+ expect(values).toEqual([false]);
298
+
299
+ // Change count but isPositive stays false
300
+ engine.apply((state) => {
301
+ state.context.count = -5;
302
+ });
303
+
304
+ // Should not notify because derived value didn't change
305
+ expect(values).toEqual([false]);
306
+
307
+ // Change to positive
308
+ engine.apply((state) => {
309
+ state.context.count = 5;
310
+ });
311
+
312
+ // Should notify
313
+ expect(values).toEqual([false, true]);
314
+
315
+ // Change count but isPositive stays true
316
+ engine.apply((state) => {
317
+ state.context.count = 10;
318
+ });
319
+
320
+ // Should not notify
321
+ expect(values).toEqual([false, true]);
322
+ });
323
+
324
+ it('should support complex derived selectors', () => {
325
+ const engine = createReactiveEngine({
326
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
327
+ });
328
+
329
+ const complex = engine.$derived((state) => ({
330
+ total: state.context.count + state.context.nested.value,
331
+ label: `${state.context.name}: ${state.context.count}`,
332
+ }));
333
+
334
+ const values: any[] = [];
335
+ complex.subscribe((value) => {
336
+ values.push(value);
337
+ });
338
+
339
+ expect(values.length).toBe(1);
340
+ expect(values[0]).toEqual({ total: 10, label: 'test: 0' });
341
+
342
+ engine.apply((state) => {
343
+ state.context.count = 5;
344
+ });
345
+
346
+ expect(values.length).toBe(2);
347
+ expect(values[1]).toEqual({ total: 15, label: 'test: 5' });
348
+ });
349
+
350
+ it('should handle errors in derived subscriber callbacks', () => {
351
+ const engine = createReactiveEngine({
352
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
353
+ });
354
+
355
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
356
+
357
+ const doubled = engine.$derived((state) => state.context.count * 2);
358
+
359
+ const callback1 = vi.fn(() => {
360
+ throw new Error('Test error in derived');
361
+ });
362
+ const callback2 = vi.fn();
363
+
364
+ doubled.subscribe(callback1);
365
+ doubled.subscribe(callback2);
366
+
367
+ expect(callback1).toHaveBeenCalledTimes(1);
368
+ expect(callback2).toHaveBeenCalledTimes(1);
369
+
370
+ engine.apply((state) => {
371
+ state.context.count = 5;
372
+ });
373
+
374
+ expect(consoleError).toHaveBeenCalled();
375
+ expect(callback1).toHaveBeenCalledTimes(2);
376
+ expect(callback2).toHaveBeenCalledTimes(2);
377
+
378
+ consoleError.mockRestore();
379
+ });
380
+ });
381
+
382
+ describe('Apply Method', () => {
383
+ it('should allow direct mutation of state', () => {
384
+ const engine = createReactiveEngine({
385
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
386
+ });
387
+
388
+ engine.apply((state) => {
389
+ state.context.count = 100;
390
+ state.context.name = 'updated';
391
+ state.context.nested.value = 200;
392
+ });
393
+
394
+ expect(engine.context.count).toBe(100);
395
+ expect(engine.context.name).toBe('updated');
396
+ expect(engine.context.nested.value).toBe(200);
397
+ });
398
+
399
+ it('should work with array operations', () => {
400
+ const engine = createReactiveEngine({
401
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
402
+ });
403
+
404
+ engine.apply((state) => {
405
+ state.facts.push({ tag: 'Fact1', payload: {} });
406
+ state.facts.push({ tag: 'Fact2', payload: {} });
407
+ });
408
+
409
+ expect(engine.facts.length).toBe(2);
410
+ expect(engine.facts[0].tag).toBe('Fact1');
411
+ expect(engine.facts[1].tag).toBe('Fact2');
412
+ });
413
+
414
+ it('should work with object property additions and deletions', () => {
415
+ const engine = createReactiveEngine({
416
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
417
+ });
418
+
419
+ const callback = vi.fn();
420
+ engine.subscribe(callback);
421
+
422
+ expect(callback).toHaveBeenCalledTimes(1);
423
+
424
+ engine.apply((state) => {
425
+ (state.meta as any).newProp = 'new value';
426
+ });
427
+
428
+ expect(engine.meta.newProp).toBe('new value');
429
+ expect(callback).toHaveBeenCalledTimes(2);
430
+
431
+ engine.apply((state) => {
432
+ delete state.meta.newProp;
433
+ });
434
+
435
+ expect(engine.meta.newProp).toBeUndefined();
436
+ expect(callback).toHaveBeenCalledTimes(3);
437
+ });
438
+ });
439
+
440
+ describe('createReactiveEngine Helper', () => {
441
+ it('should create an engine with minimal options', () => {
442
+ const engine = createReactiveEngine({
443
+ initialContext: { count: 0, name: 'test', nested: { value: 10 } },
444
+ });
445
+
446
+ expect(engine).toBeInstanceOf(ReactiveLogicEngine);
447
+ expect(engine.context.count).toBe(0);
448
+ expect(engine.facts).toEqual([]);
449
+ expect(engine.meta).toEqual({});
450
+ });
451
+
452
+ it('should create an engine with all options', () => {
453
+ const engine = createReactiveEngine({
454
+ initialContext: { count: 5, name: 'hello', nested: { value: 20 } },
455
+ initialFacts: [{ tag: 'InitialFact', payload: {} }],
456
+ initialMeta: { timestamp: 123 },
457
+ });
458
+
459
+ expect(engine.context.count).toBe(5);
460
+ expect(engine.facts).toEqual([{ tag: 'InitialFact', payload: {} }]);
461
+ expect(engine.meta).toEqual({ timestamp: 123 });
462
+ });
463
+ });
464
+
465
+ describe('Integration Example', () => {
466
+ it('should work in a realistic counter scenario', () => {
467
+ interface CounterContext {
468
+ count: number;
469
+ history: number[];
470
+ }
471
+
472
+ const engine = createReactiveEngine<CounterContext>({
473
+ initialContext: {
474
+ count: 0,
475
+ history: [0],
476
+ },
477
+ });
478
+
479
+ // Create derived value for count display
480
+ const displayValue = engine.$derived((state) =>
481
+ `Count: ${state.context.count} (History: ${state.context.history.join(', ')})`
482
+ );
483
+
484
+ const displays: string[] = [];
485
+ displayValue.subscribe((value) => {
486
+ displays.push(value);
487
+ });
488
+
489
+ expect(displays[0]).toBe('Count: 0 (History: 0)');
490
+
491
+ // Increment
492
+ engine.apply((state) => {
493
+ state.context.count += 1;
494
+ state.context.history.push(state.context.count);
495
+ });
496
+
497
+ expect(displays[1]).toBe('Count: 1 (History: 0, 1)');
498
+
499
+ // Increment again
500
+ engine.apply((state) => {
501
+ state.context.count += 5;
502
+ state.context.history.push(state.context.count);
503
+ });
504
+
505
+ expect(displays[2]).toBe('Count: 6 (History: 0, 1, 6)');
506
+
507
+ // Reset
508
+ engine.apply((state) => {
509
+ state.context.count = 0;
510
+ state.context.history = [0];
511
+ });
512
+
513
+ expect(displays[3]).toBe('Count: 0 (History: 0)');
514
+ });
515
+ });
516
+ });