@semiont/event-sourcing 0.2.28-build.40

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.js ADDED
@@ -0,0 +1,1170 @@
1
+ import { promises, createReadStream } from 'fs';
2
+ import * as path from 'path';
3
+ import * as readline from 'readline';
4
+ import crypto, { createHash } from 'crypto';
5
+ import { resourceId, isSystemEvent, isResourceEvent, didToAgent, findBodyItem } from '@semiont/core';
6
+ import { resourceUri, annotationUri } from '@semiont/api-client';
7
+
8
+ // src/storage/event-storage.ts
9
+ var rnds8Pool = new Uint8Array(256);
10
+ var poolPtr = rnds8Pool.length;
11
+ function rng() {
12
+ if (poolPtr > rnds8Pool.length - 16) {
13
+ crypto.randomFillSync(rnds8Pool);
14
+ poolPtr = 0;
15
+ }
16
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
17
+ }
18
+
19
+ // ../../node_modules/uuid/dist/esm-node/regex.js
20
+ var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
21
+
22
+ // ../../node_modules/uuid/dist/esm-node/validate.js
23
+ function validate(uuid) {
24
+ return typeof uuid === "string" && regex_default.test(uuid);
25
+ }
26
+ var validate_default = validate;
27
+
28
+ // ../../node_modules/uuid/dist/esm-node/stringify.js
29
+ var byteToHex = [];
30
+ for (let i = 0; i < 256; ++i) {
31
+ byteToHex.push((i + 256).toString(16).substr(1));
32
+ }
33
+ function stringify(arr, offset = 0) {
34
+ const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
35
+ if (!validate_default(uuid)) {
36
+ throw TypeError("Stringified UUID is invalid");
37
+ }
38
+ return uuid;
39
+ }
40
+ var stringify_default = stringify;
41
+
42
+ // ../../node_modules/uuid/dist/esm-node/v4.js
43
+ function v4(options, buf, offset) {
44
+ options = options || {};
45
+ const rnds = options.random || (options.rng || rng)();
46
+ rnds[6] = rnds[6] & 15 | 64;
47
+ rnds[8] = rnds[8] & 63 | 128;
48
+ if (buf) {
49
+ offset = offset || 0;
50
+ for (let i = 0; i < 16; ++i) {
51
+ buf[offset + i] = rnds[i];
52
+ }
53
+ return buf;
54
+ }
55
+ return stringify_default(rnds);
56
+ }
57
+ var v4_default = v4;
58
+ function jumpConsistentHash(key, numBuckets = 65536) {
59
+ const hash = hashToUint32(key);
60
+ return hash % numBuckets;
61
+ }
62
+ function hashToUint32(str) {
63
+ let hash = 0;
64
+ for (let i = 0; i < str.length; i++) {
65
+ hash = (hash << 5) - hash + str.charCodeAt(i);
66
+ hash = hash & 4294967295;
67
+ }
68
+ return Math.abs(hash);
69
+ }
70
+ function shardIdToPath(shardId) {
71
+ if (shardId < 0 || shardId >= 65536) {
72
+ throw new Error(`Invalid shard ID: ${shardId}. Must be 0-65535 for 4-hex sharding.`);
73
+ }
74
+ const shardHex = shardId.toString(16).padStart(4, "0");
75
+ const ab = shardHex.substring(0, 2);
76
+ const cd = shardHex.substring(2, 4);
77
+ return [ab, cd];
78
+ }
79
+ function getShardPath(key, numBuckets = 65536) {
80
+ const shardId = jumpConsistentHash(key, numBuckets);
81
+ return shardIdToPath(shardId);
82
+ }
83
+ function sha256(data) {
84
+ const content = typeof data === "string" ? data : JSON.stringify(data);
85
+ return createHash("sha256").update(content).digest("hex");
86
+ }
87
+
88
+ // src/storage/event-storage.ts
89
+ var EventStorage = class {
90
+ config;
91
+ // Per-resource sequence tracking: resourceId -> sequence number
92
+ resourceSequences = /* @__PURE__ */ new Map();
93
+ // Per-resource last event hash: resourceId -> hash
94
+ resourceLastHash = /* @__PURE__ */ new Map();
95
+ constructor(config) {
96
+ this.config = {
97
+ basePath: config.basePath,
98
+ dataDir: config.dataDir,
99
+ maxEventsPerFile: config.maxEventsPerFile || 1e4,
100
+ enableSharding: config.enableSharding ?? true,
101
+ numShards: config.numShards || 65536,
102
+ enableCompression: config.enableCompression ?? true
103
+ };
104
+ }
105
+ /**
106
+ * Calculate shard path for a resource ID
107
+ * Uses jump consistent hash for uniform distribution
108
+ * Special case: __system__ events bypass sharding
109
+ */
110
+ getShardPath(resourceId) {
111
+ if (resourceId === "__system__" || !this.config.enableSharding) {
112
+ return "";
113
+ }
114
+ const shardIndex = jumpConsistentHash(resourceId, this.config.numShards);
115
+ const hex = shardIndex.toString(16).padStart(4, "0");
116
+ const [ab, cd] = [hex.substring(0, 2), hex.substring(2, 4)];
117
+ return path.join(ab, cd);
118
+ }
119
+ /**
120
+ * Get full path to resource's event directory
121
+ */
122
+ getResourcePath(resourceId) {
123
+ const shardPath = this.getShardPath(resourceId);
124
+ return path.join(this.config.dataDir, "events", shardPath, resourceId);
125
+ }
126
+ /**
127
+ * Initialize directory structure for a resource's event stream
128
+ * Also loads sequence number and last hash if stream exists
129
+ */
130
+ async initializeResourceStream(resourceId) {
131
+ const docPath = this.getResourcePath(resourceId);
132
+ let exists = false;
133
+ try {
134
+ await promises.access(docPath);
135
+ exists = true;
136
+ } catch {
137
+ }
138
+ if (!exists) {
139
+ await promises.mkdir(docPath, { recursive: true });
140
+ const filename = this.createEventFilename(1);
141
+ const filePath = path.join(docPath, filename);
142
+ await promises.writeFile(filePath, "", "utf-8");
143
+ this.resourceSequences.set(resourceId, 0);
144
+ console.log(`[EventStorage] Initialized event stream for ${resourceId} at ${docPath}`);
145
+ } else {
146
+ const files = await this.getEventFiles(resourceId);
147
+ if (files.length > 0) {
148
+ const lastFile = files[files.length - 1];
149
+ if (lastFile) {
150
+ const lastEvent = await this.getLastEvent(resourceId, lastFile);
151
+ if (lastEvent) {
152
+ this.resourceSequences.set(resourceId, lastEvent.metadata.sequenceNumber);
153
+ if (lastEvent.metadata.checksum) {
154
+ this.resourceLastHash.set(resourceId, lastEvent.metadata.checksum);
155
+ }
156
+ }
157
+ }
158
+ } else {
159
+ this.resourceSequences.set(resourceId, 0);
160
+ }
161
+ }
162
+ }
163
+ /**
164
+ * Append an event - handles EVERYTHING for event creation
165
+ * Creates ID, timestamp, metadata, checksum, sequence tracking, and writes to disk
166
+ */
167
+ async appendEvent(event, resourceId) {
168
+ if (this.getSequenceNumber(resourceId) === 0) {
169
+ await this.initializeResourceStream(resourceId);
170
+ }
171
+ const completeEvent = {
172
+ ...event,
173
+ id: v4_default(),
174
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
175
+ };
176
+ const sequenceNumber = this.getNextSequenceNumber(resourceId);
177
+ const prevEventHash = this.getLastEventHash(resourceId);
178
+ const metadata = {
179
+ sequenceNumber,
180
+ streamPosition: 0,
181
+ // Will be set during write
182
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
183
+ prevEventHash: prevEventHash || void 0,
184
+ checksum: sha256(completeEvent)
185
+ };
186
+ const storedEvent = {
187
+ event: completeEvent,
188
+ metadata
189
+ };
190
+ await this.writeEvent(storedEvent, resourceId);
191
+ this.setLastEventHash(resourceId, metadata.checksum);
192
+ return storedEvent;
193
+ }
194
+ /**
195
+ * Write an event to storage (append to JSONL)
196
+ * Internal method - use appendEvent() instead
197
+ */
198
+ async writeEvent(event, resourceId) {
199
+ const docPath = this.getResourcePath(resourceId);
200
+ const files = await this.getEventFiles(resourceId);
201
+ let targetFile;
202
+ if (files.length === 0) {
203
+ targetFile = await this.createNewEventFile(resourceId);
204
+ } else {
205
+ const currentFile = files[files.length - 1];
206
+ if (!currentFile) {
207
+ targetFile = await this.createNewEventFile(resourceId);
208
+ } else {
209
+ const eventCount = await this.countEventsInFile(resourceId, currentFile);
210
+ if (eventCount >= this.config.maxEventsPerFile) {
211
+ targetFile = await this.createNewEventFile(resourceId);
212
+ } else {
213
+ targetFile = currentFile;
214
+ }
215
+ }
216
+ }
217
+ const targetPath = path.join(docPath, targetFile);
218
+ const eventLine = JSON.stringify(event) + "\n";
219
+ await promises.appendFile(targetPath, eventLine, "utf-8");
220
+ }
221
+ /**
222
+ * Count events in a specific file
223
+ */
224
+ async countEventsInFile(resourceId, filename) {
225
+ const docPath = this.getResourcePath(resourceId);
226
+ const filePath = path.join(docPath, filename);
227
+ try {
228
+ const content = await promises.readFile(filePath, "utf-8");
229
+ const lines = content.trim().split("\n").filter((line) => line.trim() !== "");
230
+ return lines.length;
231
+ } catch (error) {
232
+ if (error.code === "ENOENT") {
233
+ return 0;
234
+ }
235
+ throw error;
236
+ }
237
+ }
238
+ /**
239
+ * Read all events from a specific file
240
+ */
241
+ async readEventsFromFile(resourceId, filename) {
242
+ const docPath = this.getResourcePath(resourceId);
243
+ const filePath = path.join(docPath, filename);
244
+ const events = [];
245
+ try {
246
+ const fileStream = createReadStream(filePath, { encoding: "utf-8" });
247
+ const rl = readline.createInterface({
248
+ input: fileStream,
249
+ crlfDelay: Infinity
250
+ });
251
+ for await (const line of rl) {
252
+ const trimmed = line.trim();
253
+ if (trimmed === "") continue;
254
+ try {
255
+ const event = JSON.parse(trimmed);
256
+ events.push(event);
257
+ } catch (parseError) {
258
+ console.error(`[EventStorage] Failed to parse event in ${filePath}:`, parseError);
259
+ }
260
+ }
261
+ } catch (error) {
262
+ if (error.code === "ENOENT") {
263
+ return [];
264
+ }
265
+ throw error;
266
+ }
267
+ return events;
268
+ }
269
+ /**
270
+ * Get list of event files for a resource (sorted by sequence)
271
+ */
272
+ async getEventFiles(resourceId$1) {
273
+ const docPath = this.getResourcePath(resourceId(resourceId$1));
274
+ try {
275
+ const files = await promises.readdir(docPath);
276
+ const eventFiles = files.filter((f) => f.startsWith("events-") && f.endsWith(".jsonl")).sort((a, b) => {
277
+ const seqA = parseInt(a.match(/events-(\d+)\.jsonl/)?.[1] || "0");
278
+ const seqB = parseInt(b.match(/events-(\d+)\.jsonl/)?.[1] || "0");
279
+ return seqA - seqB;
280
+ });
281
+ return eventFiles;
282
+ } catch (error) {
283
+ if (error.code === "ENOENT") {
284
+ return [];
285
+ }
286
+ throw error;
287
+ }
288
+ }
289
+ /**
290
+ * Create a new event file for rotation
291
+ */
292
+ async createNewEventFile(resourceId) {
293
+ const files = await this.getEventFiles(resourceId);
294
+ const lastFile = files[files.length - 1];
295
+ const lastSeq = lastFile ? parseInt(lastFile.match(/events-(\d+)\.jsonl/)?.[1] || "1") : 1;
296
+ const newSeq = lastSeq + 1;
297
+ const filename = this.createEventFilename(newSeq);
298
+ const docPath = this.getResourcePath(resourceId);
299
+ const filePath = path.join(docPath, filename);
300
+ await promises.writeFile(filePath, "", "utf-8");
301
+ console.log(`[EventStorage] Created new event file: ${filename} for ${resourceId}`);
302
+ return filename;
303
+ }
304
+ /**
305
+ * Get the last event from a specific file
306
+ */
307
+ async getLastEvent(resourceId, filename) {
308
+ const events = await this.readEventsFromFile(resourceId, filename);
309
+ const lastEvent = events.length > 0 ? events[events.length - 1] : void 0;
310
+ return lastEvent ?? null;
311
+ }
312
+ /**
313
+ * Get all events for a resource across all files
314
+ */
315
+ async getAllEvents(resourceId) {
316
+ const files = await this.getEventFiles(resourceId);
317
+ const allEvents = [];
318
+ for (const file of files) {
319
+ const events = await this.readEventsFromFile(resourceId, file);
320
+ allEvents.push(...events);
321
+ }
322
+ return allEvents;
323
+ }
324
+ /**
325
+ * Get all resource IDs by scanning shard directories
326
+ */
327
+ async getAllResourceIds() {
328
+ const eventsDir = path.join(this.config.dataDir, "events");
329
+ const resourceIds = [];
330
+ try {
331
+ await promises.access(eventsDir);
332
+ } catch {
333
+ return [];
334
+ }
335
+ const scanDir = async (dir) => {
336
+ const entries = await promises.readdir(dir, { withFileTypes: true });
337
+ for (const entry of entries) {
338
+ const fullPath = path.join(dir, entry.name);
339
+ if (entry.isDirectory()) {
340
+ if (entry.name.length > 2) {
341
+ resourceIds.push(resourceId(entry.name));
342
+ } else {
343
+ await scanDir(fullPath);
344
+ }
345
+ }
346
+ }
347
+ };
348
+ await scanDir(eventsDir);
349
+ return resourceIds;
350
+ }
351
+ /**
352
+ * Create filename for event file
353
+ */
354
+ createEventFilename(sequenceNumber) {
355
+ return `events-${sequenceNumber.toString().padStart(6, "0")}.jsonl`;
356
+ }
357
+ // ============================================================
358
+ // Sequence/Hash Tracking
359
+ // ============================================================
360
+ /**
361
+ * Get current sequence number for a resource
362
+ */
363
+ getSequenceNumber(resourceId) {
364
+ return this.resourceSequences.get(resourceId) || 0;
365
+ }
366
+ /**
367
+ * Increment and return next sequence number for a resource
368
+ */
369
+ getNextSequenceNumber(resourceId) {
370
+ const current = this.getSequenceNumber(resourceId);
371
+ const next = current + 1;
372
+ this.resourceSequences.set(resourceId, next);
373
+ return next;
374
+ }
375
+ /**
376
+ * Get last event hash for a resource
377
+ */
378
+ getLastEventHash(resourceId) {
379
+ return this.resourceLastHash.get(resourceId) || null;
380
+ }
381
+ /**
382
+ * Set last event hash for a resource
383
+ */
384
+ setLastEventHash(resourceId, hash) {
385
+ this.resourceLastHash.set(resourceId, hash);
386
+ }
387
+ };
388
+
389
+ // src/event-log.ts
390
+ var EventLog = class {
391
+ // Expose storage for EventQuery (read operations)
392
+ storage;
393
+ constructor(config) {
394
+ this.storage = new EventStorage({
395
+ basePath: config.basePath,
396
+ dataDir: config.dataDir,
397
+ enableSharding: config.enableSharding ?? true,
398
+ maxEventsPerFile: config.maxEventsPerFile ?? 1e4
399
+ });
400
+ }
401
+ /**
402
+ * Append event to log
403
+ * @param event - Resource event (from @semiont/core)
404
+ * @param resourceId - Branded ResourceId (from @semiont/core)
405
+ * @returns Stored event with metadata (sequence number, timestamp, checksum)
406
+ */
407
+ async append(event, resourceId) {
408
+ return this.storage.appendEvent(event, resourceId);
409
+ }
410
+ /**
411
+ * Get all events for a resource
412
+ * @param resourceId - Branded ResourceId (from @semiont/core)
413
+ */
414
+ async getEvents(resourceId) {
415
+ return this.storage.getAllEvents(resourceId);
416
+ }
417
+ /**
418
+ * Get all resource IDs
419
+ * @returns Array of branded ResourceId types
420
+ */
421
+ async getAllResourceIds() {
422
+ return this.storage.getAllResourceIds();
423
+ }
424
+ /**
425
+ * Query events with filter
426
+ * @param resourceId - Branded ResourceId (from @semiont/core)
427
+ * @param filter - Optional event filter
428
+ */
429
+ async queryEvents(resourceId, filter) {
430
+ const events = await this.storage.getAllEvents(resourceId);
431
+ if (!filter) return events;
432
+ return events.filter((e) => {
433
+ if (filter.eventTypes && !filter.eventTypes.includes(e.event.type)) return false;
434
+ if (filter.fromSequence && e.metadata.sequenceNumber < filter.fromSequence) return false;
435
+ if (filter.fromTimestamp && e.event.timestamp < filter.fromTimestamp) return false;
436
+ if (filter.toTimestamp && e.event.timestamp > filter.toTimestamp) return false;
437
+ if (filter.userId && e.event.userId !== filter.userId) return false;
438
+ return true;
439
+ });
440
+ }
441
+ };
442
+ function toResourceUri(config, id) {
443
+ if (!config.baseUrl) {
444
+ throw new Error("baseUrl is required");
445
+ }
446
+ return resourceUri(`${config.baseUrl}/resources/${id}`);
447
+ }
448
+ function toAnnotationUri(config, id) {
449
+ if (!config.baseUrl) {
450
+ throw new Error("baseUrl is required");
451
+ }
452
+ return annotationUri(`${config.baseUrl}/annotations/${id}`);
453
+ }
454
+
455
+ // src/subscriptions/event-subscriptions.ts
456
+ var EventSubscriptions = class {
457
+ // Per-resource subscriptions: ResourceUri -> Set of callbacks
458
+ subscriptions = /* @__PURE__ */ new Map();
459
+ // Global subscriptions for system-level events (no resourceId)
460
+ globalSubscriptions = /* @__PURE__ */ new Set();
461
+ /**
462
+ * Subscribe to events for a specific resource using full URI
463
+ * Returns an EventSubscription with unsubscribe function
464
+ */
465
+ subscribe(resourceUri2, callback) {
466
+ if (!this.subscriptions.has(resourceUri2)) {
467
+ this.subscriptions.set(resourceUri2, /* @__PURE__ */ new Set());
468
+ }
469
+ const callbacks = this.subscriptions.get(resourceUri2);
470
+ callbacks.add(callback);
471
+ console.log(`[EventSubscriptions] Subscription added for resource ${resourceUri2} (total: ${callbacks.size} subscribers)`);
472
+ return {
473
+ resourceUri: resourceUri2,
474
+ callback,
475
+ unsubscribe: () => {
476
+ callbacks.delete(callback);
477
+ console.log(`[EventSubscriptions] Subscription removed for resource ${resourceUri2} (remaining: ${callbacks.size} subscribers)`);
478
+ if (callbacks.size === 0) {
479
+ this.subscriptions.delete(resourceUri2);
480
+ console.log(`[EventSubscriptions] No more subscribers for resource ${resourceUri2}, removed from subscriptions map`);
481
+ }
482
+ }
483
+ };
484
+ }
485
+ /**
486
+ * Subscribe to all system-level events (no resourceId)
487
+ * Returns an EventSubscription with unsubscribe function
488
+ *
489
+ * Use this for consumers that need to react to global events like:
490
+ * - entitytype.added (global entity type collection changes)
491
+ * - Future system-level events (user.created, workspace.created, etc.)
492
+ */
493
+ subscribeGlobal(callback) {
494
+ this.globalSubscriptions.add(callback);
495
+ console.log(`[EventSubscriptions] Global subscription added (total: ${this.globalSubscriptions.size} subscribers)`);
496
+ return {
497
+ resourceUri: "__global__",
498
+ // Special marker for global subscriptions
499
+ callback,
500
+ unsubscribe: () => {
501
+ this.globalSubscriptions.delete(callback);
502
+ console.log(`[EventSubscriptions] Global subscription removed (remaining: ${this.globalSubscriptions.size} subscribers)`);
503
+ }
504
+ };
505
+ }
506
+ /**
507
+ * Notify all subscribers for a resource when a new event is appended
508
+ * @param resourceUri - Full resource URI (e.g., http://localhost:4000/resources/abc123)
509
+ */
510
+ async notifySubscribers(resourceUri2, event) {
511
+ const callbacks = this.subscriptions.get(resourceUri2);
512
+ if (!callbacks || callbacks.size === 0) {
513
+ console.log(`[EventSubscriptions] Event ${event.event.type} for resource ${resourceUri2} - no subscribers to notify`);
514
+ return;
515
+ }
516
+ console.log(`[EventSubscriptions] Notifying ${callbacks.size} subscriber(s) of event ${event.event.type} for resource ${resourceUri2}`);
517
+ Array.from(callbacks).forEach((callback, index) => {
518
+ Promise.resolve(callback(event)).then(() => {
519
+ console.log(`[EventSubscriptions] Subscriber #${index + 1} successfully notified of ${event.event.type}`);
520
+ }).catch((error) => {
521
+ console.error(`[EventSubscriptions] Error in subscriber #${index + 1} for resource ${resourceUri2}, event ${event.event.type}:`, error);
522
+ });
523
+ });
524
+ }
525
+ /**
526
+ * Notify all global subscribers when a system-level event is appended
527
+ */
528
+ async notifyGlobalSubscribers(event) {
529
+ if (this.globalSubscriptions.size === 0) {
530
+ console.log(`[EventSubscriptions] System event ${event.event.type} - no global subscribers to notify`);
531
+ return;
532
+ }
533
+ console.log(`[EventSubscriptions] Notifying ${this.globalSubscriptions.size} global subscriber(s) of system event ${event.event.type}`);
534
+ Array.from(this.globalSubscriptions).forEach((callback, index) => {
535
+ Promise.resolve(callback(event)).then(() => {
536
+ console.log(`[EventSubscriptions] Global subscriber #${index + 1} successfully notified of ${event.event.type}`);
537
+ }).catch((error) => {
538
+ console.error(`[EventSubscriptions] Error in global subscriber #${index + 1} for system event ${event.event.type}:`, error);
539
+ });
540
+ });
541
+ }
542
+ /**
543
+ * Get subscription count for a resource (useful for debugging)
544
+ */
545
+ getSubscriptionCount(resourceUri2) {
546
+ return this.subscriptions.get(resourceUri2)?.size || 0;
547
+ }
548
+ /**
549
+ * Get total number of active subscriptions across all resources
550
+ */
551
+ getTotalSubscriptions() {
552
+ let total = 0;
553
+ for (const callbacks of this.subscriptions.values()) {
554
+ total += callbacks.size;
555
+ }
556
+ return total;
557
+ }
558
+ /**
559
+ * Get total number of global subscriptions
560
+ */
561
+ getGlobalSubscriptionCount() {
562
+ return this.globalSubscriptions.size;
563
+ }
564
+ };
565
+ var globalEventSubscriptions = null;
566
+ function getEventSubscriptions() {
567
+ if (!globalEventSubscriptions) {
568
+ globalEventSubscriptions = new EventSubscriptions();
569
+ console.log("[EventSubscriptions] Created global singleton instance");
570
+ }
571
+ return globalEventSubscriptions;
572
+ }
573
+
574
+ // src/event-bus.ts
575
+ var EventBus = class {
576
+ // Expose subscriptions for direct access (legacy compatibility)
577
+ subscriptions;
578
+ identifierConfig;
579
+ constructor(config) {
580
+ this.identifierConfig = config.identifierConfig;
581
+ this.subscriptions = getEventSubscriptions();
582
+ }
583
+ /**
584
+ * Publish event to subscribers
585
+ * - Resource events: notifies resource-scoped subscribers
586
+ * - System events: notifies global subscribers
587
+ * @param event - Stored event (from @semiont/core)
588
+ */
589
+ async publish(event) {
590
+ if (isSystemEvent(event.event)) {
591
+ await this.subscriptions.notifyGlobalSubscribers(event);
592
+ } else if (isResourceEvent(event.event)) {
593
+ const resourceId = event.event.resourceId;
594
+ const resourceUri2 = toResourceUri(this.identifierConfig, resourceId);
595
+ await this.subscriptions.notifySubscribers(resourceUri2, event);
596
+ } else {
597
+ console.warn("[EventBus] Event is neither resource nor system event:", event.event.type);
598
+ }
599
+ }
600
+ /**
601
+ * Subscribe to events for a specific resource
602
+ * @param resourceId - Branded ResourceId (from @semiont/core)
603
+ * @param callback - Event callback function
604
+ * @returns EventSubscription with unsubscribe function
605
+ */
606
+ subscribe(resourceId, callback) {
607
+ const resourceUri2 = toResourceUri(this.identifierConfig, resourceId);
608
+ return this.subscriptions.subscribe(resourceUri2, callback);
609
+ }
610
+ /**
611
+ * Subscribe to all system-level events
612
+ * @param callback - Event callback function
613
+ * @returns EventSubscription with unsubscribe function
614
+ */
615
+ subscribeGlobal(callback) {
616
+ return this.subscriptions.subscribeGlobal(callback);
617
+ }
618
+ /**
619
+ * Unsubscribe from resource events
620
+ * @param resourceId - Branded ResourceId (from @semiont/core)
621
+ * @param callback - Event callback function to remove
622
+ */
623
+ unsubscribe(resourceId, callback) {
624
+ const resourceUri2 = toResourceUri(this.identifierConfig, resourceId);
625
+ const callbacks = this.subscriptions.subscriptions.get(resourceUri2);
626
+ if (callbacks) {
627
+ callbacks.delete(callback);
628
+ if (callbacks.size === 0) {
629
+ this.subscriptions.subscriptions.delete(resourceUri2);
630
+ }
631
+ }
632
+ }
633
+ /**
634
+ * Unsubscribe from global events
635
+ * @param callback - Event callback function to remove
636
+ */
637
+ unsubscribeGlobal(callback) {
638
+ this.subscriptions.globalSubscriptions.delete(callback);
639
+ }
640
+ /**
641
+ * Get subscriber count for a resource
642
+ * @param resourceId - Branded ResourceId (from @semiont/core)
643
+ * @returns Number of active subscribers
644
+ */
645
+ getSubscriberCount(resourceId) {
646
+ const resourceUri2 = toResourceUri(this.identifierConfig, resourceId);
647
+ return this.subscriptions.getSubscriptionCount(resourceUri2);
648
+ }
649
+ /**
650
+ * Get total number of active subscriptions across all resources
651
+ */
652
+ getTotalSubscriptions() {
653
+ return this.subscriptions.getTotalSubscriptions();
654
+ }
655
+ /**
656
+ * Get total number of global subscriptions
657
+ */
658
+ getGlobalSubscriptionCount() {
659
+ return this.subscriptions.getGlobalSubscriptionCount();
660
+ }
661
+ };
662
+ var ViewMaterializer = class {
663
+ constructor(viewStorage, config) {
664
+ this.viewStorage = viewStorage;
665
+ this.config = config;
666
+ }
667
+ /**
668
+ * Materialize resource view from events
669
+ * Loads existing view if cached, otherwise rebuilds from events
670
+ */
671
+ async materialize(events, resourceId) {
672
+ const existing = await this.viewStorage.get(resourceId);
673
+ if (existing) {
674
+ return existing;
675
+ }
676
+ if (events.length === 0) return null;
677
+ const view = this.materializeFromEvents(events, resourceId);
678
+ await this.viewStorage.save(resourceId, view);
679
+ return view;
680
+ }
681
+ /**
682
+ * Materialize view incrementally with a single event
683
+ * Falls back to full rebuild if view doesn't exist
684
+ */
685
+ async materializeIncremental(resourceId, event, getAllEvents) {
686
+ console.log(`[ViewMaterializer] Updating view for ${resourceId} with event ${event.type}`);
687
+ let view = await this.viewStorage.get(resourceId);
688
+ if (!view) {
689
+ console.log(`[ViewMaterializer] No view found, rebuilding from scratch`);
690
+ const events = await getAllEvents();
691
+ view = this.materializeFromEvents(events, resourceId);
692
+ } else {
693
+ console.log(`[ViewMaterializer] Applying event incrementally to existing view (version ${view.annotations.version})`);
694
+ this.applyEventToResource(view.resource, event);
695
+ this.applyEventToAnnotations(view.annotations, event);
696
+ view.annotations.version++;
697
+ view.annotations.updatedAt = event.timestamp;
698
+ }
699
+ await this.viewStorage.save(resourceId, view);
700
+ console.log(`[ViewMaterializer] View saved (version ${view.annotations.version}, ${view.annotations.annotations.length} annotations)`);
701
+ }
702
+ /**
703
+ * Materialize view from event list (full rebuild)
704
+ */
705
+ materializeFromEvents(events, resourceId) {
706
+ const backendUrl = this.config.backendUrl;
707
+ const normalizedBase = backendUrl.endsWith("/") ? backendUrl.slice(0, -1) : backendUrl;
708
+ const resource = {
709
+ "@context": "https://schema.org/",
710
+ "@id": `${normalizedBase}/resources/${resourceId}`,
711
+ name: "",
712
+ representations: [],
713
+ archived: false,
714
+ entityTypes: [],
715
+ creationMethod: "api"
716
+ };
717
+ const annotations = {
718
+ resourceId,
719
+ annotations: [],
720
+ version: 0,
721
+ updatedAt: ""
722
+ };
723
+ events.sort((a, b) => a.metadata.sequenceNumber - b.metadata.sequenceNumber);
724
+ for (const storedEvent of events) {
725
+ this.applyEventToResource(resource, storedEvent.event);
726
+ this.applyEventToAnnotations(annotations, storedEvent.event);
727
+ annotations.version++;
728
+ annotations.updatedAt = storedEvent.event.timestamp;
729
+ }
730
+ return { resource, annotations };
731
+ }
732
+ /**
733
+ * Apply an event to ResourceDescriptor state (metadata only)
734
+ */
735
+ applyEventToResource(resource, event) {
736
+ switch (event.type) {
737
+ case "resource.created":
738
+ resource.name = event.payload.name;
739
+ resource.entityTypes = event.payload.entityTypes || [];
740
+ resource.dateCreated = event.timestamp;
741
+ resource.creationMethod = event.payload.creationMethod || "api";
742
+ resource.wasAttributedTo = didToAgent(event.userId);
743
+ if (!resource.representations) resource.representations = [];
744
+ const reps = Array.isArray(resource.representations) ? resource.representations : [resource.representations];
745
+ reps.push({
746
+ mediaType: event.payload.format,
747
+ checksum: event.payload.contentChecksum,
748
+ byteSize: event.payload.contentByteSize,
749
+ rel: "original",
750
+ language: event.payload.language
751
+ });
752
+ resource.representations = reps;
753
+ resource.isDraft = event.payload.isDraft;
754
+ resource.wasDerivedFrom = event.payload.generatedFrom;
755
+ break;
756
+ case "resource.cloned":
757
+ resource.name = event.payload.name;
758
+ resource.entityTypes = event.payload.entityTypes || [];
759
+ resource.dateCreated = event.timestamp;
760
+ resource.creationMethod = "clone";
761
+ resource.sourceResourceId = event.payload.parentResourceId;
762
+ resource.wasAttributedTo = didToAgent(event.userId);
763
+ if (!resource.representations) resource.representations = [];
764
+ const reps2 = Array.isArray(resource.representations) ? resource.representations : [resource.representations];
765
+ reps2.push({
766
+ mediaType: event.payload.format,
767
+ checksum: event.payload.contentChecksum,
768
+ byteSize: event.payload.contentByteSize,
769
+ rel: "original",
770
+ language: event.payload.language
771
+ });
772
+ resource.representations = reps2;
773
+ break;
774
+ case "resource.archived":
775
+ resource.archived = true;
776
+ break;
777
+ case "resource.unarchived":
778
+ resource.archived = false;
779
+ break;
780
+ case "entitytag.added":
781
+ if (!resource.entityTypes) resource.entityTypes = [];
782
+ if (!resource.entityTypes.includes(event.payload.entityType)) {
783
+ resource.entityTypes.push(event.payload.entityType);
784
+ }
785
+ break;
786
+ case "entitytag.removed":
787
+ if (resource.entityTypes) {
788
+ resource.entityTypes = resource.entityTypes.filter(
789
+ (t) => t !== event.payload.entityType
790
+ );
791
+ }
792
+ break;
793
+ }
794
+ }
795
+ /**
796
+ * Apply an event to ResourceAnnotations (annotation collections only)
797
+ */
798
+ applyEventToAnnotations(annotations, event) {
799
+ switch (event.type) {
800
+ case "annotation.added":
801
+ annotations.annotations.push({
802
+ ...event.payload.annotation,
803
+ creator: didToAgent(event.userId),
804
+ created: new Date(event.timestamp).toISOString()
805
+ });
806
+ break;
807
+ case "annotation.removed":
808
+ annotations.annotations = annotations.annotations.filter(
809
+ (a) => a.id !== event.payload.annotationId && !a.id.endsWith(`/annotations/${event.payload.annotationId}`)
810
+ );
811
+ break;
812
+ case "annotation.body.updated":
813
+ const annotation = annotations.annotations.find(
814
+ (a) => a.id === event.payload.annotationId || a.id.endsWith(`/annotations/${event.payload.annotationId}`)
815
+ );
816
+ if (annotation) {
817
+ if (!Array.isArray(annotation.body)) {
818
+ annotation.body = annotation.body ? [annotation.body] : [];
819
+ }
820
+ for (const op of event.payload.operations) {
821
+ if (op.op === "add") {
822
+ const exists = findBodyItem(annotation.body, op.item) !== -1;
823
+ if (!exists) {
824
+ annotation.body.push(op.item);
825
+ }
826
+ } else if (op.op === "remove") {
827
+ const index = findBodyItem(annotation.body, op.item);
828
+ if (index !== -1) {
829
+ annotation.body.splice(index, 1);
830
+ }
831
+ } else if (op.op === "replace") {
832
+ const index = findBodyItem(annotation.body, op.oldItem);
833
+ if (index !== -1) {
834
+ annotation.body[index] = op.newItem;
835
+ }
836
+ }
837
+ }
838
+ annotation.modified = new Date(event.timestamp).toISOString();
839
+ }
840
+ break;
841
+ }
842
+ }
843
+ /**
844
+ * Materialize entity types view - System-level view
845
+ */
846
+ async materializeEntityTypes(entityType) {
847
+ const entityTypesPath = path.join(
848
+ this.config.basePath,
849
+ "projections",
850
+ "entity-types",
851
+ "entity-types.json"
852
+ );
853
+ let view = { entityTypes: [] };
854
+ try {
855
+ const content = await promises.readFile(entityTypesPath, "utf-8");
856
+ view = JSON.parse(content);
857
+ } catch (error) {
858
+ if (error.code !== "ENOENT") throw error;
859
+ }
860
+ const entityTypeSet = new Set(view.entityTypes);
861
+ entityTypeSet.add(entityType);
862
+ view.entityTypes = Array.from(entityTypeSet).sort();
863
+ await promises.mkdir(path.dirname(entityTypesPath), { recursive: true });
864
+ await promises.writeFile(entityTypesPath, JSON.stringify(view, null, 2));
865
+ }
866
+ };
867
+
868
+ // src/view-manager.ts
869
+ var ViewManager = class {
870
+ // Expose materializer for direct access to view methods
871
+ materializer;
872
+ constructor(viewStorage, config) {
873
+ const materializerConfig = {
874
+ basePath: config.basePath,
875
+ backendUrl: config.backendUrl
876
+ };
877
+ this.materializer = new ViewMaterializer(viewStorage, materializerConfig);
878
+ }
879
+ /**
880
+ * Update resource view with a new event
881
+ * Falls back to full rebuild if view doesn't exist
882
+ * @param resourceId - Branded ResourceId (from @semiont/core)
883
+ * @param event - Resource event (from @semiont/core)
884
+ * @param getAllEvents - Function to retrieve all events for rebuild if needed
885
+ */
886
+ async materializeResource(resourceId, event, getAllEvents) {
887
+ await this.materializer.materializeIncremental(resourceId, event, getAllEvents);
888
+ }
889
+ /**
890
+ * Update system-level view (currently only entity types)
891
+ * @param eventType - Type of system event
892
+ * @param payload - Event payload
893
+ */
894
+ async materializeSystem(eventType, payload) {
895
+ if (eventType === "entitytype.added") {
896
+ await this.materializer.materializeEntityTypes(payload.entityType);
897
+ }
898
+ }
899
+ /**
900
+ * Get resource view (builds from events if needed)
901
+ * @param resourceId - Branded ResourceId (from @semiont/core)
902
+ * @param events - Stored events for the resource (from @semiont/core)
903
+ * @returns Resource view or null if no events
904
+ */
905
+ async getOrMaterialize(resourceId, events) {
906
+ return this.materializer.materialize(events, resourceId);
907
+ }
908
+ };
909
+
910
+ // src/event-store.ts
911
+ var EventStore = class {
912
+ // Focused components - each with single responsibility
913
+ log;
914
+ bus;
915
+ views;
916
+ constructor(config, viewStorage, identifierConfig) {
917
+ const logConfig = {
918
+ basePath: config.basePath,
919
+ dataDir: config.dataDir,
920
+ enableSharding: config.enableSharding,
921
+ maxEventsPerFile: config.maxEventsPerFile
922
+ };
923
+ this.log = new EventLog(logConfig);
924
+ const busConfig = {
925
+ identifierConfig
926
+ };
927
+ this.bus = new EventBus(busConfig);
928
+ const viewConfig = {
929
+ basePath: config.basePath,
930
+ backendUrl: identifierConfig.baseUrl
931
+ };
932
+ this.views = new ViewManager(viewStorage, viewConfig);
933
+ }
934
+ /**
935
+ * Append an event to the store
936
+ * Coordinates: persistence → view → notification
937
+ */
938
+ async appendEvent(event) {
939
+ const resourceId = event.resourceId || "__system__";
940
+ const storedEvent = await this.log.append(event, resourceId);
941
+ if (resourceId === "__system__") {
942
+ await this.views.materializeSystem(
943
+ storedEvent.event.type,
944
+ storedEvent.event.payload
945
+ );
946
+ } else {
947
+ await this.views.materializeResource(
948
+ resourceId,
949
+ storedEvent.event,
950
+ () => this.log.getEvents(resourceId)
951
+ );
952
+ }
953
+ await this.bus.publish(storedEvent);
954
+ return storedEvent;
955
+ }
956
+ };
957
+ var FilesystemViewStorage = class {
958
+ basePath;
959
+ constructor(basePath, projectRoot) {
960
+ if (path.isAbsolute(basePath)) {
961
+ this.basePath = basePath;
962
+ } else if (projectRoot) {
963
+ this.basePath = path.resolve(projectRoot, basePath);
964
+ } else {
965
+ this.basePath = path.resolve(basePath);
966
+ }
967
+ }
968
+ getProjectionPath(resourceId) {
969
+ const [ab, cd] = getShardPath(resourceId);
970
+ return path.join(this.basePath, "projections", "resources", ab, cd, `${resourceId}.json`);
971
+ }
972
+ async save(resourceId, projection) {
973
+ const projPath = this.getProjectionPath(resourceId);
974
+ const projDir = path.dirname(projPath);
975
+ await promises.mkdir(projDir, { recursive: true });
976
+ await promises.writeFile(projPath, JSON.stringify(projection, null, 2), "utf-8");
977
+ }
978
+ async get(resourceId) {
979
+ const projPath = this.getProjectionPath(resourceId);
980
+ try {
981
+ const content = await promises.readFile(projPath, "utf-8");
982
+ return JSON.parse(content);
983
+ } catch (error) {
984
+ if (error.code === "ENOENT") {
985
+ return null;
986
+ }
987
+ throw error;
988
+ }
989
+ }
990
+ async delete(resourceId) {
991
+ const projPath = this.getProjectionPath(resourceId);
992
+ try {
993
+ await promises.unlink(projPath);
994
+ } catch (error) {
995
+ if (error.code !== "ENOENT") {
996
+ throw error;
997
+ }
998
+ }
999
+ }
1000
+ async exists(resourceId) {
1001
+ const projPath = this.getProjectionPath(resourceId);
1002
+ try {
1003
+ await promises.access(projPath);
1004
+ return true;
1005
+ } catch {
1006
+ return false;
1007
+ }
1008
+ }
1009
+ async getAll() {
1010
+ const views = [];
1011
+ const annotationsPath = path.join(this.basePath, "projections", "resources");
1012
+ try {
1013
+ const walkDir = async (dir) => {
1014
+ const entries = await promises.readdir(dir, { withFileTypes: true });
1015
+ for (const entry of entries) {
1016
+ const fullPath = path.join(dir, entry.name);
1017
+ if (entry.isDirectory()) {
1018
+ await walkDir(fullPath);
1019
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
1020
+ try {
1021
+ const content = await promises.readFile(fullPath, "utf-8");
1022
+ const view = JSON.parse(content);
1023
+ views.push(view);
1024
+ } catch (error) {
1025
+ console.error(`[ViewStorage] Failed to read view ${fullPath}:`, error);
1026
+ }
1027
+ }
1028
+ }
1029
+ };
1030
+ await walkDir(annotationsPath);
1031
+ } catch (error) {
1032
+ if (error.code === "ENOENT") {
1033
+ return [];
1034
+ }
1035
+ throw error;
1036
+ }
1037
+ return views;
1038
+ }
1039
+ };
1040
+
1041
+ // src/query/event-query.ts
1042
+ var EventQuery = class {
1043
+ constructor(eventStorage) {
1044
+ this.eventStorage = eventStorage;
1045
+ }
1046
+ /**
1047
+ * Query events with filters
1048
+ * Supports filtering by: userId, eventTypes, timestamps, sequence number, limit
1049
+ */
1050
+ async queryEvents(query) {
1051
+ if (!query.resourceId) {
1052
+ throw new Error("resourceId is required for event queries");
1053
+ }
1054
+ const allEvents = await this.eventStorage.getAllEvents(query.resourceId);
1055
+ let results = allEvents;
1056
+ if (query.userId) {
1057
+ results = results.filter((e) => e.event.userId === query.userId);
1058
+ }
1059
+ if (query.eventTypes && query.eventTypes.length > 0) {
1060
+ results = results.filter((e) => query.eventTypes.includes(e.event.type));
1061
+ }
1062
+ if (query.fromTimestamp) {
1063
+ results = results.filter((e) => e.event.timestamp >= query.fromTimestamp);
1064
+ }
1065
+ if (query.toTimestamp) {
1066
+ results = results.filter((e) => e.event.timestamp <= query.toTimestamp);
1067
+ }
1068
+ if (query.fromSequence) {
1069
+ results = results.filter((e) => e.metadata.sequenceNumber >= query.fromSequence);
1070
+ }
1071
+ if (query.limit && query.limit > 0) {
1072
+ results = results.slice(0, query.limit);
1073
+ }
1074
+ return results;
1075
+ }
1076
+ /**
1077
+ * Get all events for a specific resource (no filters)
1078
+ */
1079
+ async getResourceEvents(resourceId) {
1080
+ return this.eventStorage.getAllEvents(resourceId);
1081
+ }
1082
+ /**
1083
+ * Get the last event from a specific file
1084
+ * Useful for initializing sequence numbers and last hashes
1085
+ */
1086
+ async getLastEvent(resourceId, filename) {
1087
+ return this.eventStorage.getLastEvent(resourceId, filename);
1088
+ }
1089
+ /**
1090
+ * Get the latest event for a resource across all files
1091
+ */
1092
+ async getLatestEvent(resourceId) {
1093
+ const files = await this.eventStorage.getEventFiles(resourceId);
1094
+ if (files.length === 0) return null;
1095
+ for (let i = files.length - 1; i >= 0; i--) {
1096
+ const file = files[i];
1097
+ if (!file) continue;
1098
+ const lastEvent = await this.eventStorage.getLastEvent(resourceId, file);
1099
+ if (lastEvent) return lastEvent;
1100
+ }
1101
+ return null;
1102
+ }
1103
+ /**
1104
+ * Get event count for a resource
1105
+ */
1106
+ async getEventCount(resourceId) {
1107
+ const events = await this.getResourceEvents(resourceId);
1108
+ return events.length;
1109
+ }
1110
+ /**
1111
+ * Check if a resource has any events
1112
+ */
1113
+ async hasEvents(resourceId) {
1114
+ const files = await this.eventStorage.getEventFiles(resourceId);
1115
+ return files.length > 0;
1116
+ }
1117
+ };
1118
+
1119
+ // src/validation/event-validator.ts
1120
+ var EventValidator = class {
1121
+ /**
1122
+ * Validate event chain integrity for a resource's events
1123
+ * Checks that each event properly links to the previous event
1124
+ */
1125
+ validateEventChain(events) {
1126
+ const errors = [];
1127
+ for (let i = 1; i < events.length; i++) {
1128
+ const prev = events[i - 1];
1129
+ const curr = events[i];
1130
+ if (!prev || !curr) continue;
1131
+ if (curr.metadata.prevEventHash !== prev.metadata.checksum) {
1132
+ errors.push(
1133
+ `Event chain broken at sequence ${curr.metadata.sequenceNumber}: prevEventHash=${curr.metadata.prevEventHash} but previous checksum=${prev.metadata.checksum}`
1134
+ );
1135
+ }
1136
+ const calculated = sha256(curr.event);
1137
+ if (calculated !== curr.metadata.checksum) {
1138
+ errors.push(
1139
+ `Checksum mismatch at sequence ${curr.metadata.sequenceNumber}: calculated=${calculated} but stored=${curr.metadata.checksum}`
1140
+ );
1141
+ }
1142
+ }
1143
+ return {
1144
+ valid: errors.length === 0,
1145
+ errors
1146
+ };
1147
+ }
1148
+ /**
1149
+ * Validate a single event's checksum
1150
+ * Useful for validating events before writing them
1151
+ */
1152
+ validateEventChecksum(event) {
1153
+ const calculated = sha256(event.event);
1154
+ return calculated === event.metadata.checksum;
1155
+ }
1156
+ /**
1157
+ * Validate that an event properly links to a previous event
1158
+ * Returns true if the link is valid or if this is the first event
1159
+ */
1160
+ validateEventLink(currentEvent, previousEvent) {
1161
+ if (!previousEvent) {
1162
+ return !currentEvent.metadata.prevEventHash;
1163
+ }
1164
+ return currentEvent.metadata.prevEventHash === previousEvent.metadata.checksum;
1165
+ }
1166
+ };
1167
+
1168
+ export { EventBus, EventLog, EventQuery, EventStorage, EventStore, EventSubscriptions, EventValidator, FilesystemViewStorage, ViewManager, ViewMaterializer, getEventSubscriptions, getShardPath, jumpConsistentHash, sha256, toAnnotationUri, toResourceUri };
1169
+ //# sourceMappingURL=index.js.map
1170
+ //# sourceMappingURL=index.js.map