boundlessdb 0.1.0

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 (60) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/better-sqlite3-shim.d.ts +12 -0
  5. package/dist/better-sqlite3-shim.d.ts.map +1 -0
  6. package/dist/better-sqlite3-shim.js +18 -0
  7. package/dist/better-sqlite3-shim.js.map +1 -0
  8. package/dist/browser.d.ts +14 -0
  9. package/dist/browser.d.ts.map +1 -0
  10. package/dist/browser.js +14 -0
  11. package/dist/browser.js.map +1 -0
  12. package/dist/config/extractor.d.ts +22 -0
  13. package/dist/config/extractor.d.ts.map +1 -0
  14. package/dist/config/extractor.js +132 -0
  15. package/dist/config/extractor.js.map +1 -0
  16. package/dist/config/validator.d.ts +14 -0
  17. package/dist/config/validator.d.ts.map +1 -0
  18. package/dist/config/validator.js +94 -0
  19. package/dist/config/validator.js.map +1 -0
  20. package/dist/event-store.browser.d.ts +61 -0
  21. package/dist/event-store.browser.d.ts.map +1 -0
  22. package/dist/event-store.browser.js +323 -0
  23. package/dist/event-store.browser.js.map +1 -0
  24. package/dist/event-store.d.ts +101 -0
  25. package/dist/event-store.d.ts.map +1 -0
  26. package/dist/event-store.js +249 -0
  27. package/dist/event-store.js.map +1 -0
  28. package/dist/index.d.ts +15 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +16 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/query-builder.d.ts +72 -0
  33. package/dist/query-builder.d.ts.map +1 -0
  34. package/dist/query-builder.js +84 -0
  35. package/dist/query-builder.js.map +1 -0
  36. package/dist/storage/interface.d.ts +48 -0
  37. package/dist/storage/interface.d.ts.map +1 -0
  38. package/dist/storage/interface.js +5 -0
  39. package/dist/storage/interface.js.map +1 -0
  40. package/dist/storage/memory.d.ts +27 -0
  41. package/dist/storage/memory.d.ts.map +1 -0
  42. package/dist/storage/memory.js +94 -0
  43. package/dist/storage/memory.js.map +1 -0
  44. package/dist/storage/postgres.d.ts +76 -0
  45. package/dist/storage/postgres.d.ts.map +1 -0
  46. package/dist/storage/postgres.js +346 -0
  47. package/dist/storage/postgres.js.map +1 -0
  48. package/dist/storage/sqlite.d.ts +47 -0
  49. package/dist/storage/sqlite.d.ts.map +1 -0
  50. package/dist/storage/sqlite.js +249 -0
  51. package/dist/storage/sqlite.js.map +1 -0
  52. package/dist/storage/sqljs.d.ts +60 -0
  53. package/dist/storage/sqljs.d.ts.map +1 -0
  54. package/dist/storage/sqljs.js +354 -0
  55. package/dist/storage/sqljs.js.map +1 -0
  56. package/dist/types.d.ts +172 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +52 -0
  59. package/dist/types.js.map +1 -0
  60. package/package.json +75 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,89 @@
1
+ # Changelog
2
+
3
+ All notable changes to BoundlessDB will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ### Breaking Changes
8
+
9
+ #### Removed: Token/Cryptographic Signing
10
+ - Removed `token.ts` and `token.browser.ts`
11
+ - Removed `secret` option from `EventStoreOptions`
12
+ - **Migration:** Use `appendCondition` directly (see below)
13
+
14
+ #### Removed: Decider Pattern Helpers
15
+ - Removed `src/decider.ts` with `Decider` type, `evolve()` and `decide()` helpers
16
+ - **Migration:** Use plain functions with standard `reduce`:
17
+ ```typescript
18
+ // Before
19
+ const state = evolve(events, decider);
20
+ const newEvents = decide(command, state, decider);
21
+
22
+ // After
23
+ const state = events.reduce(evolve, initialState);
24
+ const newEvents = decide(command, state);
25
+ ```
26
+
27
+ #### Changed: Token → AppendCondition
28
+ - `read()` now returns `appendCondition` as a plain object (not encoded token)
29
+ - `append()` accepts `AppendCondition` directly
30
+ - **Migration:**
31
+ ```typescript
32
+ // Before
33
+ const { events, token } = await store.read({ conditions });
34
+ await store.append(newEvents, token);
35
+
36
+ // After
37
+ const { events, appendCondition } = await store.read({ conditions });
38
+ await store.append(newEvents, appendCondition);
39
+ ```
40
+
41
+ ### Added
42
+
43
+ #### QueryResult Class
44
+ - `read()` returns a `QueryResult` with helper methods:
45
+ - `isEmpty()`, `count`, `first()`, `last()`
46
+ - `position`, `conditions`, `appendCondition`
47
+
48
+ #### Typed Events
49
+ - `Event<Type, Payload>` marker type for type-safe events
50
+ - `read<E>()` and `append<E>()` support generics
51
+
52
+ #### Union Types for QueryCondition
53
+ - `UnconstrainedCondition`: `{ type: 'X' }` — match all events of type
54
+ - `ConstrainedCondition`: `{ type: 'X', key: 'a', value: 'b' }` — match specific key
55
+ - Partial conditions like `{ type: 'X', key: 'a' }` are now TypeScript errors
56
+
57
+ #### Type Guard
58
+ - `isConstrainedCondition()` exported for storage implementations
59
+
60
+ #### Fluent Query API
61
+ - Chainable query builder: `store.query<E>()`
62
+ - Methods: `matchType()`, `matchKey()`, `fromPosition()`, `limit()`, `read()`
63
+ ```typescript
64
+ const { events, appendCondition } = await store.query<CourseEvent>()
65
+ .matchType('CourseCreated')
66
+ .matchKey('StudentSubscribed', 'course', 'cs101')
67
+ .read();
68
+ ```
69
+
70
+ ### Changed
71
+
72
+ - Empty `conditions: []` now returns all events (was: error)
73
+ - `appendCondition` is a plain object: `{ position: bigint, conditions: QueryCondition[] }`
74
+
75
+ ### Documentation
76
+
77
+ - Landing page redesigned with tabbed code examples
78
+ - README updated to use `decide` pattern
79
+ - Removed npm install section (package not yet published)
80
+
81
+ ## [0.1.0] - 2026-02-20
82
+
83
+ ### Added
84
+
85
+ - Initial release
86
+ - DCB-inspired Event Store with config-based consistency keys
87
+ - Storage backends: SQLite, PostgreSQL, sql.js (browser), In-Memory
88
+ - Auto-reindex on config change
89
+ - Conflict detection with delta
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sebastian Bortz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,545 @@
1
+ # BoundlessDB
2
+
3
+ A **DCB-inspired** event store library for TypeScript.
4
+
5
+ > *BoundlessDB* — because consistency boundaries should be dynamic, not fixed.
6
+
7
+ ## 🎉 Try it Live!
8
+
9
+ **[Interactive Browser Demo](https://boundlessdb.dev/demo.html)** — No installation required!
10
+
11
+ The entire event store runs client-side in your browser using WebAssembly SQLite.
12
+
13
+ ## Features
14
+
15
+ - 🚀 **Works in Browser** — Full client-side event sourcing via sql.js (WASM)
16
+ - 🔑 **No Streams** — Events organized via configurable consistency keys
17
+ - ⚙️ **Config-based Key Extraction** — Events remain pure business data
18
+ - 🎟️ **AppendCondition** — Simple, transparent optimistic concurrency control
19
+ - ⚡ **Conflict Detection with Delta** — Get exactly what changed since your read
20
+ - 🔄 **Auto-Reindex** — Change your config, keys are automatically rebuilt
21
+ - 💾 **SQLite, PostgreSQL & In-Memory** — Multiple storage backends
22
+ - 📦 **Embedded Library** — No separate server, runs in your process
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { createEventStore, SqliteStorage } from 'boundlessdb';
28
+
29
+ const store = createEventStore({
30
+ storage: new SqliteStorage(':memory:'),
31
+ consistency: {
32
+ eventTypes: {
33
+ CourseCreated: {
34
+ keys: [{ name: 'course', path: 'data.courseId' }]
35
+ },
36
+ StudentSubscribed: {
37
+ keys: [
38
+ { name: 'course', path: 'data.courseId' },
39
+ { name: 'student', path: 'data.studentId' }
40
+ ]
41
+ }
42
+ }
43
+ }
44
+ });
45
+ ```
46
+
47
+ ## How It Works
48
+
49
+ ### 1️⃣ Event Appended
50
+ You append an event with business data:
51
+ ```typescript
52
+ await store.append([{
53
+ type: 'StudentSubscribed',
54
+ data: { courseId: 'cs101', studentId: 'alice' }
55
+ }], result.appendCondition);
56
+ ```
57
+
58
+ ### 2️⃣ Keys Extracted
59
+ Your config tells BoundlessDB which fields are consistency keys:
60
+ ```typescript
61
+ consistency: {
62
+ eventTypes: {
63
+ StudentSubscribed: {
64
+ keys: [
65
+ { name: 'course', path: 'data.courseId' },
66
+ { name: 'student', path: 'data.studentId' }
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ // → Extracts: course='cs101', student='alice'
72
+ ```
73
+
74
+ ### 3️⃣ Index Updated
75
+ Keys are stored in a separate index table, linked to the event position:
76
+ ```
77
+ event_keys: [pos:1, course, cs101], [pos:1, student, alice]
78
+ ```
79
+
80
+ ### 4️⃣ Query by Keys
81
+ Find all events matching any combination of key conditions:
82
+ ```typescript
83
+ const result = await store.read({
84
+ conditions: [
85
+ { type: 'StudentSubscribed', key: 'course', value: 'cs101' }
86
+ ]
87
+ });
88
+ // result.appendCondition captures: "I read all matching events up to position X"
89
+ ```
90
+
91
+ ## The DCB Pattern: Read → Decide → Write
92
+
93
+ ```typescript
94
+ // 1️⃣ READ — Query events and get an appendCondition
95
+ const { events, appendCondition } = await store.read({
96
+ conditions: [
97
+ { type: 'CourseCreated', key: 'course', value: 'cs101' },
98
+ { type: 'StudentSubscribed', key: 'course', value: 'cs101' },
99
+ ]
100
+ });
101
+
102
+ // 2️⃣ DECIDE — Build state, run business logic
103
+ const state = events.reduce(evolve, initialState);
104
+ const newEvents = decide(command, state);
105
+
106
+ // 3️⃣ WRITE — Append with optimistic concurrency
107
+ const result = await store.append(newEvents, appendCondition);
108
+ ```
109
+
110
+ ### Define Your Functions
111
+
112
+ ```typescript
113
+ const initialState = { enrolled: 0, capacity: 30 };
114
+
115
+ // evolve: (state, event) → new state
116
+ const evolve = (state, event) => {
117
+ switch (event.type) {
118
+ case 'StudentSubscribed':
119
+ return { ...state, enrolled: state.enrolled + 1 };
120
+ default:
121
+ return state;
122
+ }
123
+ };
124
+
125
+ // decide: (command, state) → events[]
126
+ const decide = (command, state) => {
127
+ if (state.enrolled >= state.capacity) {
128
+ throw new Error('Course is full!');
129
+ }
130
+ return [{ type: 'StudentSubscribed', data: command }];
131
+ };
132
+ ```
133
+
134
+ ### Handle Conflicts
135
+
136
+ ```typescript
137
+ if (result.conflict) {
138
+ // Someone else wrote while you were deciding
139
+ console.log('Events since your read:', result.conflictingEvents);
140
+ // Retry with result.appendCondition
141
+ } else {
142
+ console.log('Success at position', result.position);
143
+ }
144
+ ```
145
+
146
+ ## Fluent Query API
147
+
148
+ Build queries with a chainable API:
149
+
150
+ ```typescript
151
+ const { events, appendCondition } = await store.query<CourseEvent>()
152
+ .matchType('CourseCreated') // all events of type
153
+ .matchKey('StudentSubscribed', 'course', 'cs101') // where key = value
154
+ .fromPosition(100n) // start from position
155
+ .limit(50) // limit results
156
+ .read();
157
+ ```
158
+
159
+ ### Methods
160
+
161
+ | Method | Description |
162
+ |--------|-------------|
163
+ | `matchType(type)` | Match all events of type (unconstrained) |
164
+ | `matchKey(type, key, value)` | Match events where key equals value (constrained) |
165
+ | `fromPosition(bigint)` | Start reading from position |
166
+ | `limit(number)` | Limit number of results |
167
+ | `read()` | Execute query, returns `QueryResult` |
168
+
169
+ The fluent API is equivalent to calling `store.read()` with conditions — use whichever style you prefer.
170
+
171
+ ## AppendCondition
172
+
173
+ When you call `read()`, the result contains an `appendCondition` with:
174
+ - The **position** up to which events were read
175
+ - The **query conditions** you used
176
+
177
+ This is a simple, transparent object — no encoding, no magic:
178
+
179
+ ```typescript
180
+ // Option 1: Use appendCondition from read()
181
+ const result = await store.read({ conditions });
182
+ await store.append(newEvents, result.appendCondition);
183
+
184
+ // Option 2: Create conditions manually
185
+ await store.append(newEvents, {
186
+ position: 42n,
187
+ conditions: [{ type: 'UserCreated', key: 'username', value: 'alice' }]
188
+ });
189
+
190
+ // Option 3: Skip consistency check entirely
191
+ await store.append(newEvents, null);
192
+ ```
193
+
194
+ This flexibility lets you:
195
+ - **Create uniqueness checks without reading first** (e.g., "username must be unique")
196
+ - **Build custom retry logic** by constructing conditions manually
197
+ - **Optimize performance** by skipping unnecessary reads
198
+
199
+ ## Query Across Multiple Dimensions
200
+
201
+ Traditional streams give you ONE boundary. DCB lets you query ANY combination:
202
+
203
+ ```typescript
204
+ // "Has Alice already enrolled in CS101?"
205
+ const result = await store.read({
206
+ conditions: [
207
+ { type: 'StudentSubscribed', key: 'course', value: 'cs101' },
208
+ { type: 'StudentSubscribed', key: 'student', value: 'alice' },
209
+ ]
210
+ });
211
+ // Checks BOTH course AND student boundaries in one query!
212
+ ```
213
+
214
+ ## Config-based Key Extraction
215
+
216
+ Keys are extracted from event payloads via configuration — events stay pure:
217
+
218
+ ```typescript
219
+ const consistency = {
220
+ eventTypes: {
221
+ OrderPlaced: {
222
+ keys: [
223
+ { name: 'order', path: 'data.orderId' },
224
+ { name: 'customer', path: 'data.customer.id' },
225
+ { name: 'month', path: 'data.timestamp', transform: 'MONTH' }
226
+ ]
227
+ }
228
+ }
229
+ };
230
+ ```
231
+
232
+ ### Key Options
233
+
234
+ | Option | Description |
235
+ |--------|-------------|
236
+ | `name` | Key name for queries |
237
+ | `path` | Dot-notation path in event (e.g., `data.customer.id`) |
238
+ | `transform` | Transform the extracted value (see below) |
239
+ | `nullHandling` | `error` (default), `skip`, `default` |
240
+ | `defaultValue` | Value when `nullHandling: 'default'` |
241
+
242
+ ### Transforms
243
+
244
+ Transforms modify the extracted value before indexing:
245
+
246
+ | Transform | Input | Output | Use Case |
247
+ |-----------|-------|--------|----------|
248
+ | `LOWER` | `"Alice@Email.COM"` | `"alice@email.com"` | Case-insensitive matching |
249
+ | `UPPER` | `"alice"` | `"ALICE"` | Normalized codes |
250
+ | `MONTH` | `"2026-02-20T14:30:00Z"` | `"2026-02"` | Monthly partitioning |
251
+ | `YEAR` | `"2026-02-20T14:30:00Z"` | `"2026"` | Yearly aggregation |
252
+ | `DATE` | `"2026-02-20T14:30:00Z"` | `"2026-02-20"` | Daily partitioning |
253
+
254
+ **Example: Time-based partitioning**
255
+
256
+ ```typescript
257
+ const consistency = {
258
+ eventTypes: {
259
+ OrderPlaced: {
260
+ keys: [
261
+ { name: 'order', path: 'data.orderId' },
262
+ { name: 'month', path: 'data.placedAt', transform: 'MONTH' }
263
+ ]
264
+ }
265
+ }
266
+ };
267
+
268
+ // Event: { type: 'OrderPlaced', data: { orderId: 'ORD-123', placedAt: '2026-02-20T14:30:00Z' } }
269
+ // Extracted keys: order="ORD-123", month="2026-02"
270
+
271
+ // Query all orders from February 2026:
272
+ const { events } = await store.read({
273
+ conditions: [{ type: 'OrderPlaced', key: 'month', value: '2026-02' }]
274
+ });
275
+ ```
276
+
277
+ This is great for **Close the Books** patterns — query all events in a time period efficiently!
278
+
279
+ ## Auto-Reindex on Config Change
280
+
281
+ The config is hashed and stored in the database. On startup:
282
+
283
+ ```
284
+ stored_hash: "a1b2c3..." (from last run)
285
+ current_hash: "x9y8z7..." (from your config)
286
+
287
+ → Hash mismatch detected!
288
+ → Rebuilding key index...
289
+ → ✅ Reindex complete: 1523 events, 4211 keys (847ms)
290
+ ```
291
+
292
+ **Just change your config and restart.** No manual migration needed!
293
+
294
+ ## Browser Usage
295
+
296
+ BoundlessDB works **entirely in the browser** with no server required:
297
+
298
+ ```html
299
+ <script type="module">
300
+ import { createEventStore, SqlJsStorage } from './boundless.browser.js';
301
+
302
+ const store = createEventStore({
303
+ storage: new SqlJsStorage(),
304
+ consistency: {
305
+ eventTypes: {
306
+ TodoAdded: { keys: [{ name: 'list', path: 'data.listId' }] }
307
+ }
308
+ }
309
+ });
310
+
311
+ // Everything runs client-side!
312
+ </script>
313
+ ```
314
+
315
+ ### Build Browser Bundle
316
+
317
+ ```bash
318
+ npm run build:browser
319
+ # → ui/public/boundless.browser.js (~100KB)
320
+ ```
321
+
322
+ ## Storage Backends
323
+
324
+ | Backend | Environment | Persistence |
325
+ |---------|-------------|-------------|
326
+ | `SqliteStorage` | Node.js | File or `:memory:` |
327
+ | `SqlJsStorage` | Browser | In-memory (WASM) |
328
+ | `PostgresStorage` | Node.js | PostgreSQL database |
329
+ | `InMemoryStorage` | Any | None (testing) |
330
+
331
+ ### PostgreSQL Storage
332
+
333
+ For production deployments with PostgreSQL:
334
+
335
+ ```typescript
336
+ import { createEventStore, PostgresStorage } from 'boundlessdb';
337
+
338
+ const storage = new PostgresStorage('postgresql://user:pass@localhost/mydb');
339
+ await storage.init(); // Required: creates tables if they don't exist
340
+
341
+ const store = createEventStore({
342
+ storage,
343
+ consistency: { /* ... */ }
344
+ });
345
+ ```
346
+
347
+ **Note:** PostgreSQL support requires the `pg` package:
348
+
349
+ ```bash
350
+ npm install pg
351
+ ```
352
+
353
+ ## Typed Events
354
+
355
+ Define type-safe events using the `Event` marker type:
356
+
357
+ ```typescript
358
+ import { Event, EventStore } from 'boundlessdb';
359
+
360
+ // Define your events
361
+ type ProductItemAdded = Event<'ProductItemAdded', {
362
+ cartId: string;
363
+ productId: string;
364
+ quantity: number;
365
+ }>;
366
+
367
+ type ProductItemRemoved = Event<'ProductItemRemoved', {
368
+ cartId: string;
369
+ productId: string;
370
+ }>;
371
+
372
+ // Create a union type for all cart events
373
+ type CartEvents = ProductItemAdded | ProductItemRemoved;
374
+
375
+ // Read with type safety
376
+ const result = await store.read<CartEvents>({
377
+ conditions: [{ type: 'ProductItemAdded', key: 'cart', value: 'cart-123' }]
378
+ });
379
+
380
+ // TypeScript knows the event types!
381
+ for (const event of result.events) {
382
+ if (event.type === 'ProductItemAdded') {
383
+ console.log(event.data.quantity); // ✅ typed as number
384
+ }
385
+ }
386
+ ```
387
+
388
+ ## Query Conditions
389
+
390
+ Query conditions support both **constrained** (with key/value) and **unconstrained** (type-only) queries.
391
+
392
+ ### Constrained Query
393
+ Match events of a type where a specific key has a specific value:
394
+
395
+ ```typescript
396
+ // Get ProductItemAdded events where cart='cart-123'
397
+ const result = await store.read({
398
+ conditions: [
399
+ { type: 'ProductItemAdded', key: 'cart', value: 'cart-123' }
400
+ ]
401
+ });
402
+ ```
403
+
404
+ ### Unconstrained Query
405
+ Omit `key` and `value` to match **all events of a type**:
406
+
407
+ ```typescript
408
+ // Get ALL ProductItemAdded events (regardless of cart)
409
+ const result = await store.read({
410
+ conditions: [
411
+ { type: 'ProductItemAdded' } // no key/value = match all
412
+ ]
413
+ });
414
+ ```
415
+
416
+ ### Mixed Conditions
417
+ Combine constrained and unconstrained in one query (OR logic):
418
+
419
+ ```typescript
420
+ // "Give me the course definition + all enrollments for cs101"
421
+ const result = await store.read({
422
+ conditions: [
423
+ { type: 'CourseCreated', key: 'course', value: 'cs101' },
424
+ { type: 'StudentSubscribed', key: 'course', value: 'cs101' },
425
+ { type: 'StudentUnsubscribed', key: 'course', value: 'cs101' },
426
+ ]
427
+ });
428
+
429
+ // "All courses + only Alice's enrollments"
430
+ const result = await store.read({
431
+ conditions: [
432
+ { type: 'CourseCreated' }, // unconstrained: ALL courses
433
+ { type: 'StudentSubscribed', key: 'student', value: 'alice' } // constrained: only Alice
434
+ ]
435
+ });
436
+ ```
437
+
438
+ ### Same Type, Multiple Values
439
+ Query multiple values of the same key:
440
+
441
+ ```typescript
442
+ // Get ProductItemAdded for cart-1 OR cart-2
443
+ const result = await store.read({
444
+ conditions: [
445
+ { type: 'ProductItemAdded', key: 'cart', value: 'cart-1' },
446
+ { type: 'ProductItemAdded', key: 'cart', value: 'cart-2' }
447
+ ]
448
+ });
449
+ ```
450
+
451
+ ### Type Safety
452
+ With TypeScript, conditions are type-safe — you must provide either:
453
+ - **Only `type`** (unconstrained), or
454
+ - **`type` + `key` + `value`** (constrained)
455
+
456
+ ```typescript
457
+ // ✅ Valid
458
+ { type: 'ProductItemAdded' }
459
+ { type: 'ProductItemAdded', key: 'cart', value: 'cart-123' }
460
+
461
+ // ❌ TypeScript Error — key without value not allowed
462
+ { type: 'ProductItemAdded', key: 'cart' }
463
+ ```
464
+
465
+ ### Empty Conditions
466
+ Empty conditions returns **all events** in the store:
467
+
468
+ ```typescript
469
+ // Get ALL events (useful for admin/debug/export)
470
+ const result = await store.read({ conditions: [] });
471
+ ```
472
+
473
+ ## API Reference
474
+
475
+ ### `createEventStore(options)`
476
+
477
+ ```typescript
478
+ const store = createEventStore({
479
+ storage: SqliteStorage | SqlJsStorage | PostgresStorage | InMemoryStorage,
480
+ consistency: ConsistencyConfig, // Key extraction rules
481
+ });
482
+ ```
483
+
484
+ ### `store.read<E>(query)`
485
+
486
+ ```typescript
487
+ const result = await store.read<CartEvents>({
488
+ conditions: [{ type, key?, value? }],
489
+ fromPosition?: bigint,
490
+ limit?: number,
491
+ });
492
+
493
+ result.events // StoredEvent<E>[]
494
+ result.position // bigint
495
+ result.conditions // QueryCondition[]
496
+ result.appendCondition // AppendCondition (for store.append)
497
+ result.count // number
498
+ result.isEmpty() // boolean
499
+ result.first() // StoredEvent<E> | undefined
500
+ result.last() // StoredEvent<E> | undefined
501
+ ```
502
+
503
+ ### `store.append<E>(events, condition)`
504
+
505
+ ```typescript
506
+ // With appendCondition from read()
507
+ const readResult = await store.read<CartEvents>({ conditions });
508
+ const result = await store.append<CartEvents>([newEvent], readResult.appendCondition);
509
+
510
+ // With manual AppendCondition
511
+ const result = await store.append<CartEvents>([newEvent], {
512
+ position: 42n,
513
+ conditions: [{ type: 'UserCreated', key: 'username', value: 'alice' }]
514
+ });
515
+
516
+ // Without consistency check
517
+ const result = await store.append<CartEvents>([newEvent], null);
518
+
519
+ // Result handling
520
+ if (result.conflict) {
521
+ result.conflictingEvents; // StoredEvent<E>[] - what changed since your read
522
+ result.appendCondition; // Fresh condition for retry
523
+ } else {
524
+ result.position; // Position of last appended event
525
+ result.appendCondition; // Condition for next operation
526
+ }
527
+ ```
528
+
529
+ ## Development
530
+
531
+ ```bash
532
+ npm install
533
+ npm test
534
+ npm run build
535
+ npm run build:browser
536
+ ```
537
+
538
+ ## Related
539
+
540
+ - [dcb.events](https://dcb.events) — Dynamic Consistency Boundaries
541
+ - [Giraflow](https://giraflow.dev) — Event Modeling visualization
542
+
543
+ ---
544
+
545
+ Built with ❤️ for [Event Sourcing](https://www.eventstore.com/event-sourcing)
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Browser shim for better-sqlite3
3
+ *
4
+ * This module throws a helpful error if someone tries to use SqliteStorage in the browser.
5
+ * Use SqlJsStorage instead for browser environments.
6
+ */
7
+ declare class BrowserNotSupportedError extends Error {
8
+ constructor();
9
+ }
10
+ export default function Database(_path?: string): void;
11
+ export { BrowserNotSupportedError };
12
+ //# sourceMappingURL=better-sqlite3-shim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"better-sqlite3-shim.d.ts","sourceRoot":"","sources":["../src/better-sqlite3-shim.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAM,wBAAyB,SAAQ,KAAK;;CAQ3C;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,QAE9C;AAED,OAAO,EAAE,wBAAwB,EAAE,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser shim for better-sqlite3
3
+ *
4
+ * This module throws a helpful error if someone tries to use SqliteStorage in the browser.
5
+ * Use SqlJsStorage instead for browser environments.
6
+ */
7
+ class BrowserNotSupportedError extends Error {
8
+ constructor() {
9
+ super('better-sqlite3 is not available in browser environments. ' +
10
+ 'Use SqlJsStorage instead for browser-based event storage.');
11
+ this.name = 'BrowserNotSupportedError';
12
+ }
13
+ }
14
+ export default function Database(_path) {
15
+ throw new BrowserNotSupportedError();
16
+ }
17
+ export { BrowserNotSupportedError };
18
+ //# sourceMappingURL=better-sqlite3-shim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"better-sqlite3-shim.js","sourceRoot":"","sources":["../src/better-sqlite3-shim.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,wBAAyB,SAAQ,KAAK;IAC1C;QACE,KAAK,CACH,2DAA2D;YAC3D,2DAA2D,CAC5D,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,KAAc;IAC7C,MAAM,IAAI,wBAAwB,EAAE,CAAC;AACvC,CAAC;AAED,OAAO,EAAE,wBAAwB,EAAE,CAAC"}