@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
@@ -0,0 +1,156 @@
1
+ # PluresDB Integration
2
+
3
+ This module provides integration between Praxis and PluresDB for local-first, reactive data storage.
4
+
5
+ ## Overview
6
+
7
+ Praxis supports PluresDB through a simple adapter interface (`PraxisDB`) that can be backed by:
8
+
9
+ 1. **In-memory storage** (for development/testing) - `InMemoryPraxisDB`
10
+ 2. **Official PluresDB** (for production) - `PluresDBPraxisAdapter`
11
+
12
+ ## Using the Official PluresDB
13
+
14
+ The official PluresDB package from NPM provides a complete local-first database with P2P sync, CRDT conflict resolution, and more.
15
+
16
+ ### Installation
17
+
18
+ PluresDB is included as a dependency in Praxis:
19
+
20
+ ```bash
21
+ npm install @plures/praxis
22
+ # PluresDB is automatically installed as a dependency
23
+ ```
24
+
25
+ ### Basic Usage
26
+
27
+ ```typescript
28
+ import { PluresNode } from 'pluresdb';
29
+ import { createPluresDB, createPraxisDBStore } from '@plures/praxis';
30
+ import { PraxisRegistry } from '@plures/praxis';
31
+
32
+ // Initialize PluresDB
33
+ const pluresdb = new PluresNode({
34
+ config: {
35
+ port: 34567,
36
+ host: 'localhost',
37
+ dataDir: './data',
38
+ },
39
+ autoStart: true,
40
+ });
41
+
42
+ // Wrap it with the Praxis adapter
43
+ const db = createPluresDB(pluresdb);
44
+
45
+ // Use with Praxis store
46
+ const registry = new PraxisRegistry();
47
+ const store = createPraxisDBStore(db, registry);
48
+
49
+ // Now you can use the store with Praxis engine
50
+ ```
51
+
52
+ ### Using with In-Memory Storage (Development)
53
+
54
+ For testing and development, use the in-memory implementation:
55
+
56
+ ```typescript
57
+ import { createInMemoryDB, createPraxisDBStore } from '@plures/praxis';
58
+ import { PraxisRegistry } from '@plures/praxis';
59
+
60
+ // Create in-memory database
61
+ const db = createInMemoryDB();
62
+
63
+ // Use with Praxis store
64
+ const registry = new PraxisRegistry();
65
+ const store = createPraxisDBStore(db, registry);
66
+ ```
67
+
68
+ ## API Reference
69
+
70
+ ### PraxisDB Interface
71
+
72
+ All database adapters implement this interface:
73
+
74
+ ```typescript
75
+ interface PraxisDB {
76
+ // Get a value by key
77
+ get<T>(key: string): Promise<T | undefined>;
78
+
79
+ // Set a value by key
80
+ set<T>(key: string, value: T): Promise<void>;
81
+
82
+ // Watch a key for changes
83
+ watch<T>(key: string, callback: (val: T) => void): UnsubscribeFn;
84
+ }
85
+ ```
86
+
87
+ ### createPluresDB(db)
88
+
89
+ Creates a Praxis adapter for the official PluresDB package.
90
+
91
+ **Parameters:**
92
+ - `db`: PluresDB instance (PluresNode or SQLiteCompatibleAPI)
93
+
94
+ **Returns:** `PluresDBPraxisAdapter` instance
95
+
96
+ ### createInMemoryDB()
97
+
98
+ Creates an in-memory database for testing/development.
99
+
100
+ **Returns:** `InMemoryPraxisDB` instance
101
+
102
+ ## Features
103
+
104
+ ### Event Sourcing
105
+
106
+ Store facts and events in PluresDB:
107
+
108
+ ```typescript
109
+ import { createPraxisEngine, createPluresDBAdapter } from '@plures/praxis';
110
+
111
+ const adapter = createPluresDBAdapter({ db, registry });
112
+ const engine = createPraxisEngine({ initialContext: {}, registry });
113
+ adapter.attachEngine(engine);
114
+
115
+ // Events are now persisted to PluresDB
116
+ ```
117
+
118
+ ### Reactive Queries
119
+
120
+ Watch for changes in the database:
121
+
122
+ ```typescript
123
+ const unsubscribe = db.watch('user:123', (user) => {
124
+ console.log('User updated:', user);
125
+ });
126
+
127
+ // Later: unsubscribe
128
+ unsubscribe();
129
+ ```
130
+
131
+ ## PluresDB Features
132
+
133
+ The official PluresDB package provides:
134
+
135
+ - **P2P Graph Database**: Distributed, peer-to-peer data storage
136
+ - **SQLite Compatibility**: 95% SQLite API compatibility
137
+ - **CRDT Conflict Resolution**: Automatic conflict resolution
138
+ - **Vector Search**: Built-in vector embeddings and similarity search
139
+ - **Local-First**: Offline-first data storage with sync when online
140
+ - **Cross-Device Sync**: Automatic synchronization across devices
141
+
142
+ For more information, see the [PluresDB documentation](https://github.com/plures/pluresdb).
143
+
144
+ ## Migration
145
+
146
+ If you're using the in-memory implementation and want to migrate to PluresDB:
147
+
148
+ 1. Install PluresDB (already included as dependency)
149
+ 2. Replace `createInMemoryDB()` with `createPluresDB(pluresdb)`
150
+ 3. The API remains the same - no other code changes needed!
151
+
152
+ ## Notes
153
+
154
+ - The `PluresDBPraxisAdapter` uses polling to watch for changes (since PluresDB doesn't natively support reactive queries in the current API)
155
+ - For production use, consider the polling interval and adjust if needed
156
+ - The adapter automatically handles cleanup of polling intervals when watchers are removed
@@ -115,3 +115,168 @@ export class InMemoryPraxisDB implements PraxisDB {
115
115
  export function createInMemoryDB(): InMemoryPraxisDB {
116
116
  return new InMemoryPraxisDB();
117
117
  }
118
+
119
+ /**
120
+ * PluresDB instance type - represents either PluresNode or SQLiteCompatibleAPI
121
+ */
122
+ export type PluresDBInstance = {
123
+ get(key: string): Promise<any>;
124
+ put(key: string, value: any): Promise<void>;
125
+ };
126
+
127
+ /**
128
+ * Configuration options for PluresDBPraxisAdapter
129
+ */
130
+ export interface PluresDBAdapterConfig {
131
+ /** PluresDB instance */
132
+ db: PluresDBInstance;
133
+ /** Polling interval in milliseconds for watch functionality (default: 1000ms) */
134
+ pollInterval?: number;
135
+ }
136
+
137
+ /**
138
+ * PluresDB-backed implementation of PraxisDB
139
+ *
140
+ * Wraps the official PluresDB package from NPM to provide
141
+ * the PraxisDB interface for production use.
142
+ */
143
+ export class PluresDBPraxisAdapter implements PraxisDB {
144
+ private db: PluresDBInstance;
145
+ private watchers = new Map<string, Set<(val: unknown) => void>>();
146
+ private pollIntervals = new Map<string, NodeJS.Timeout>();
147
+ private lastValues = new Map<string, unknown>();
148
+ private pollInterval: number;
149
+
150
+ constructor(config: PluresDBAdapterConfig | PluresDBInstance) {
151
+ // Support both old API (direct db instance) and new config API
152
+ if ('get' in config && 'put' in config) {
153
+ this.db = config;
154
+ this.pollInterval = 1000;
155
+ } else {
156
+ this.db = config.db;
157
+ this.pollInterval = config.pollInterval ?? 1000;
158
+ }
159
+ }
160
+
161
+ async get<T>(key: string): Promise<T | undefined> {
162
+ try {
163
+ const value = await this.db.get(key);
164
+ return value as T | undefined;
165
+ } catch (error) {
166
+ // PluresDB returns undefined/null for missing keys
167
+ return undefined;
168
+ }
169
+ }
170
+
171
+ async set<T>(key: string, value: T): Promise<void> {
172
+ await this.db.put(key, value);
173
+
174
+ // Update last known value
175
+ this.lastValues.set(key, value);
176
+
177
+ // Notify watchers
178
+ const keyWatchers = this.watchers.get(key);
179
+ if (keyWatchers) {
180
+ for (const callback of keyWatchers) {
181
+ callback(value);
182
+ }
183
+ }
184
+ }
185
+
186
+ watch<T>(key: string, callback: (val: T) => void): UnsubscribeFn {
187
+ if (!this.watchers.has(key)) {
188
+ this.watchers.set(key, new Set());
189
+ }
190
+
191
+ const watchers = this.watchers.get(key)!;
192
+ const wrappedCallback = (val: unknown) => callback(val as T);
193
+ watchers.add(wrappedCallback);
194
+
195
+ // Set up polling for this key if not already set up
196
+ if (!this.pollIntervals.has(key)) {
197
+ const interval = setInterval(async () => {
198
+ try {
199
+ const value = await this.db.get(key);
200
+ const lastValue = this.lastValues.get(key);
201
+
202
+ // Only notify if value has actually changed
203
+ if (JSON.stringify(value) !== JSON.stringify(lastValue)) {
204
+ this.lastValues.set(key, value);
205
+ const currentWatchers = this.watchers.get(key);
206
+ if (currentWatchers) {
207
+ for (const cb of currentWatchers) {
208
+ cb(value);
209
+ }
210
+ }
211
+ }
212
+ } catch (error) {
213
+ // Ignore errors in polling
214
+ }
215
+ }, this.pollInterval);
216
+
217
+ this.pollIntervals.set(key, interval);
218
+ }
219
+
220
+ // Return unsubscribe function
221
+ return () => {
222
+ watchers.delete(wrappedCallback);
223
+ if (watchers.size === 0) {
224
+ this.watchers.delete(key);
225
+ // Clean up polling interval
226
+ const interval = this.pollIntervals.get(key);
227
+ if (interval) {
228
+ clearInterval(interval);
229
+ this.pollIntervals.delete(key);
230
+ }
231
+ // Clean up last value cache
232
+ this.lastValues.delete(key);
233
+ }
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Clean up all resources
239
+ */
240
+ dispose(): void {
241
+ // Clear all polling intervals
242
+ for (const interval of this.pollIntervals.values()) {
243
+ clearInterval(interval);
244
+ }
245
+ this.pollIntervals.clear();
246
+ this.watchers.clear();
247
+ this.lastValues.clear();
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Create a PluresDB-backed PraxisDB instance
253
+ *
254
+ * Wraps the official PluresDB package from NPM.
255
+ *
256
+ * @param config PluresDB instance or configuration object
257
+ * @returns PluresDBPraxisAdapter instance
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * import { PluresNode } from 'pluresdb';
262
+ * import { createPluresDB } from '@plures/praxis';
263
+ *
264
+ * const pluresdb = new PluresNode({ autoStart: true });
265
+ * const db = createPluresDB(pluresdb);
266
+ *
267
+ * await db.set('user:1', { name: 'Alice' });
268
+ * const user = await db.get('user:1');
269
+ * ```
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * // With custom polling interval
274
+ * const db = createPluresDB({
275
+ * db: pluresdb,
276
+ * pollInterval: 500, // Poll every 500ms
277
+ * });
278
+ * ```
279
+ */
280
+ export function createPluresDB(config: PluresDBAdapterConfig | PluresDBInstance): PluresDBPraxisAdapter {
281
+ return new PluresDBPraxisAdapter(config);
282
+ }
@@ -5,9 +5,9 @@
5
5
  * It provides the core adapter layer, store, and schema registry.
6
6
  */
7
7
 
8
- // Adapter - Core interface and in-memory implementation
9
- export type { PraxisDB, UnsubscribeFn } from './adapter.js';
10
- export { InMemoryPraxisDB, createInMemoryDB } from './adapter.js';
8
+ // Adapter - Core interface and implementations (in-memory + PluresDB)
9
+ export type { PraxisDB, UnsubscribeFn, PluresDBInstance, PluresDBAdapterConfig } from './adapter.js';
10
+ export { InMemoryPraxisDB, createInMemoryDB, PluresDBPraxisAdapter, createPluresDB } from './adapter.js';
11
11
 
12
12
  // Store - Manages facts, events, and reactive updates
13
13
  export type { EventStreamEntry, PraxisDBStoreOptions, RuleErrorHandler } from './store.js';
@@ -1,39 +1,60 @@
1
1
  /**
2
- * Praxis Reactive Logic Engine
2
+ * Praxis Reactive Logic Engine - Svelte 5 Implementation
3
3
  *
4
- * A Svelte 5 native implementation of the Praxis Logic Engine.
5
- * Uses Runes ($state, $derived, $effect) for fine-grained reactivity.
4
+ * This version uses Svelte 5 runes ($state) for built-in reactivity.
5
+ * The state object is automatically reactive when used in Svelte components.
6
6
  */
7
7
 
8
+ import { PraxisRegistry } from '../core/rules.js';
9
+ import type { PraxisEvent } from '../core/protocol.js';
10
+ import { LogicEngine, createPraxisEngine } from '../core/engine.js';
11
+
8
12
  export interface ReactiveEngineOptions<TContext> {
9
13
  initialContext: TContext;
10
14
  initialFacts?: any[];
11
15
  initialMeta?: Record<string, unknown>;
16
+ registry?: PraxisRegistry<TContext>;
12
17
  }
13
18
 
19
+ /**
20
+ * Reactive Logic Engine using Svelte 5 runes.
21
+ * Combines the standard LogicEngine with reactive state management.
22
+ */
14
23
  export class ReactiveLogicEngine<TContext extends object> {
15
- // The single source of truth, reactive by default
16
- // We use $state.raw for things that shouldn't be deeply reactive if needed,
17
- // but for context we usually want deep reactivity.
18
- state: { context: TContext; facts: any[]; meta: Record<string, unknown> } = $state<{
19
- context: TContext;
20
- facts: any[];
21
- meta: Record<string, unknown>;
22
- }>({
24
+ // Use Svelte's $state rune for automatic reactivity
25
+ state = $state({
23
26
  context: {} as TContext,
24
- facts: [],
25
- meta: {}
27
+ facts: [] as any[],
28
+ meta: {} as Record<string, unknown>
26
29
  });
27
30
 
31
+ // Internal engine for logic processing
32
+ private _engine: LogicEngine<TContext>;
33
+
28
34
  constructor(options: ReactiveEngineOptions<TContext>) {
35
+ // Initialize reactive state
29
36
  this.state.context = options.initialContext;
30
37
  this.state.facts = options.initialFacts ?? [];
31
38
  this.state.meta = options.initialMeta ?? {};
39
+
40
+ // Create internal engine if registry is provided
41
+ if (options.registry) {
42
+ this._engine = createPraxisEngine({
43
+ initialContext: options.initialContext,
44
+ registry: options.registry,
45
+ });
46
+ } else {
47
+ // Create a basic engine without registry
48
+ this._engine = createPraxisEngine({
49
+ initialContext: options.initialContext,
50
+ registry: new PraxisRegistry<TContext>(),
51
+ });
52
+ }
32
53
  }
33
54
 
34
55
  /**
35
- * Access the reactive context directly.
36
- * Consumers can use this in $derived() or $effect().
56
+ * Access the reactive context.
57
+ * In Svelte 5 components, changes to this object will automatically trigger updates.
37
58
  */
38
59
  get context(): TContext {
39
60
  return this.state.context;
@@ -46,9 +67,16 @@ export class ReactiveLogicEngine<TContext extends object> {
46
67
  return this.state.facts;
47
68
  }
48
69
 
70
+ /**
71
+ * Access the reactive metadata.
72
+ */
73
+ get meta(): Record<string, unknown> {
74
+ return this.state.meta;
75
+ }
76
+
49
77
  /**
50
78
  * Apply a mutation to the state.
51
- * This is the "Action" or "Rule" equivalent.
79
+ * Changes will automatically trigger Svelte reactivity.
52
80
  *
53
81
  * @param mutator A function that receives the state and modifies it.
54
82
  */
@@ -57,9 +85,50 @@ export class ReactiveLogicEngine<TContext extends object> {
57
85
  }
58
86
 
59
87
  /**
60
- * Access the reactive meta.
88
+ * Process events through the logic engine and update reactive state.
89
+ *
90
+ * @param events Events to process
61
91
  */
62
- get meta(): Record<string, unknown> {
63
- return this.state.meta;
92
+ step(events: PraxisEvent[]): void {
93
+ const result = this._engine.step(events);
94
+
95
+ // Update reactive state with engine results
96
+ this.state.context = result.state.context as TContext;
97
+ this.state.facts = result.state.facts;
98
+ this.state.meta = result.state.meta ?? {};
64
99
  }
65
100
  }
101
+
102
+ /**
103
+ * Create a reactive logic engine with Svelte 5 runes.
104
+ *
105
+ * @param options Configuration options
106
+ * @returns A reactive logic engine instance
107
+ *
108
+ * @example
109
+ * ```svelte
110
+ * <script lang="ts">
111
+ * import { createReactiveEngine } from '@plures/praxis/svelte';
112
+ *
113
+ * const engine = createReactiveEngine({
114
+ * initialContext: { count: 0 },
115
+ * registry
116
+ * });
117
+ *
118
+ * // Use $derived for computed values
119
+ * const count = $derived(engine.context.count);
120
+ * const doubled = $derived(engine.context.count * 2);
121
+ *
122
+ * function increment() {
123
+ * engine.step([Increment.create({ amount: 1 })]);
124
+ * }
125
+ * </script>
126
+ *
127
+ * <button on:click={increment}>Count: {count}, Doubled: {doubled}</button>
128
+ * ```
129
+ */
130
+ export function createReactiveEngine<TContext extends object>(
131
+ options: ReactiveEngineOptions<TContext>
132
+ ): ReactiveLogicEngine<TContext> {
133
+ return new ReactiveLogicEngine(options);
134
+ }