@semiont/event-sourcing 0.4.13 → 0.4.15
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/README.md +141 -339
- package/dist/index.d.ts +62 -157
- package/dist/index.js +113 -261
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import path__default from 'path';
|
|
5
5
|
import * as readline from 'readline';
|
|
6
6
|
import { v4 } from 'uuid';
|
|
7
|
-
import { resourceId,
|
|
7
|
+
import { resourceId, didToAgent, findBodyItem } from '@semiont/core';
|
|
8
8
|
import { createHash } from 'crypto';
|
|
9
9
|
import { nanoid } from 'nanoid';
|
|
10
10
|
|
|
@@ -124,8 +124,12 @@ var EventStorage = class {
|
|
|
124
124
|
/**
|
|
125
125
|
* Append an event - handles EVERYTHING for event creation
|
|
126
126
|
* Creates ID, timestamp, metadata, checksum, sequence tracking, and writes to disk
|
|
127
|
+
*
|
|
128
|
+
* @param options.correlationId - Optional id propagated from a command. Stored
|
|
129
|
+
* on the event's metadata so subscribers (notably the events-stream → frontend
|
|
130
|
+
* path) can match command-result events back to the POST that initiated them.
|
|
127
131
|
*/
|
|
128
|
-
async appendEvent(event, resourceId) {
|
|
132
|
+
async appendEvent(event, resourceId, options) {
|
|
129
133
|
if (this.getSequenceNumber(resourceId) === 0) {
|
|
130
134
|
await this.initializeResourceStream(resourceId);
|
|
131
135
|
}
|
|
@@ -140,12 +144,12 @@ var EventStorage = class {
|
|
|
140
144
|
sequenceNumber,
|
|
141
145
|
streamPosition: 0,
|
|
142
146
|
// Will be set during write
|
|
143
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
144
147
|
prevEventHash: prevEventHash || void 0,
|
|
145
|
-
checksum: sha256(completeEvent)
|
|
148
|
+
checksum: sha256(completeEvent),
|
|
149
|
+
...options?.correlationId !== void 0 && { correlationId: options.correlationId }
|
|
146
150
|
};
|
|
147
151
|
const storedEvent = {
|
|
148
|
-
|
|
152
|
+
...completeEvent,
|
|
149
153
|
metadata
|
|
150
154
|
};
|
|
151
155
|
await this.writeEvent(storedEvent, resourceId);
|
|
@@ -221,8 +225,12 @@ var EventStorage = class {
|
|
|
221
225
|
const trimmed = line.trim();
|
|
222
226
|
if (trimmed === "") continue;
|
|
223
227
|
try {
|
|
224
|
-
const
|
|
225
|
-
|
|
228
|
+
const parsed = JSON.parse(trimmed);
|
|
229
|
+
if ("event" in parsed && "metadata" in parsed && !("type" in parsed)) {
|
|
230
|
+
events.push({ ...parsed.event, metadata: parsed.metadata, signature: parsed.signature });
|
|
231
|
+
} else {
|
|
232
|
+
events.push(parsed);
|
|
233
|
+
}
|
|
226
234
|
} catch (parseError) {
|
|
227
235
|
this.logger?.error("[EventStorage] Failed to parse event", { filePath, error: parseError });
|
|
228
236
|
}
|
|
@@ -369,10 +377,11 @@ var EventLog = class {
|
|
|
369
377
|
* Append event to log
|
|
370
378
|
* @param event - Resource event (from @semiont/core)
|
|
371
379
|
* @param resourceId - Branded ResourceId (from @semiont/core)
|
|
380
|
+
* @param options.correlationId - Optional command correlation id (stored on metadata)
|
|
372
381
|
* @returns Stored event with metadata (sequence number, timestamp, checksum)
|
|
373
382
|
*/
|
|
374
|
-
async append(event, resourceId) {
|
|
375
|
-
return this.storage.appendEvent(event, resourceId);
|
|
383
|
+
async append(event, resourceId, options) {
|
|
384
|
+
return this.storage.appendEvent(event, resourceId, options);
|
|
376
385
|
}
|
|
377
386
|
/**
|
|
378
387
|
* Get all events for a resource
|
|
@@ -397,224 +406,15 @@ var EventLog = class {
|
|
|
397
406
|
const events = await this.storage.getAllEvents(resourceId);
|
|
398
407
|
if (!filter) return events;
|
|
399
408
|
return events.filter((e) => {
|
|
400
|
-
if (filter.eventTypes && !filter.eventTypes.includes(e.
|
|
409
|
+
if (filter.eventTypes && !filter.eventTypes.includes(e.type)) return false;
|
|
401
410
|
if (filter.fromSequence && e.metadata.sequenceNumber < filter.fromSequence) return false;
|
|
402
|
-
if (filter.fromTimestamp && e.
|
|
403
|
-
if (filter.toTimestamp && e.
|
|
404
|
-
if (filter.userId && e.
|
|
411
|
+
if (filter.fromTimestamp && e.timestamp < filter.fromTimestamp) return false;
|
|
412
|
+
if (filter.toTimestamp && e.timestamp > filter.toTimestamp) return false;
|
|
413
|
+
if (filter.userId && e.userId !== filter.userId) return false;
|
|
405
414
|
return true;
|
|
406
415
|
});
|
|
407
416
|
}
|
|
408
417
|
};
|
|
409
|
-
|
|
410
|
-
// src/subscriptions/event-subscriptions.ts
|
|
411
|
-
var EventSubscriptions = class {
|
|
412
|
-
// Per-resource subscriptions: ResourceId -> Set of callbacks
|
|
413
|
-
subscriptions = /* @__PURE__ */ new Map();
|
|
414
|
-
// Global subscriptions for system-level events (no resourceId)
|
|
415
|
-
globalSubscriptions = /* @__PURE__ */ new Set();
|
|
416
|
-
logger;
|
|
417
|
-
constructor(logger) {
|
|
418
|
-
this.logger = logger;
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Subscribe to events for a specific resource
|
|
422
|
-
* Returns an EventSubscription with unsubscribe function
|
|
423
|
-
*/
|
|
424
|
-
subscribe(resourceId, callback) {
|
|
425
|
-
if (!this.subscriptions.has(resourceId)) {
|
|
426
|
-
this.subscriptions.set(resourceId, /* @__PURE__ */ new Set());
|
|
427
|
-
}
|
|
428
|
-
const callbacks = this.subscriptions.get(resourceId);
|
|
429
|
-
callbacks.add(callback);
|
|
430
|
-
this.logger?.info("[EventSubscriptions] Subscription added for resource", { resourceId, totalSubscribers: callbacks.size });
|
|
431
|
-
return {
|
|
432
|
-
resourceId,
|
|
433
|
-
callback,
|
|
434
|
-
unsubscribe: () => {
|
|
435
|
-
callbacks.delete(callback);
|
|
436
|
-
this.logger?.info("[EventSubscriptions] Subscription removed for resource", { resourceId, remainingSubscribers: callbacks.size });
|
|
437
|
-
if (callbacks.size === 0) {
|
|
438
|
-
this.subscriptions.delete(resourceId);
|
|
439
|
-
this.logger?.info("[EventSubscriptions] No more subscribers for resource, removed from subscriptions map", { resourceId });
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Subscribe to all system-level events (no resourceId)
|
|
446
|
-
* Returns an EventSubscription with unsubscribe function
|
|
447
|
-
*
|
|
448
|
-
* Use this for consumers that need to react to global events like:
|
|
449
|
-
* - entitytype.added (global entity type collection changes)
|
|
450
|
-
* - Future system-level events (user.created, workspace.created, etc.)
|
|
451
|
-
*/
|
|
452
|
-
subscribeGlobal(callback) {
|
|
453
|
-
this.globalSubscriptions.add(callback);
|
|
454
|
-
this.logger?.info("[EventSubscriptions] Global subscription added", { totalSubscribers: this.globalSubscriptions.size });
|
|
455
|
-
return {
|
|
456
|
-
resourceId: "__global__",
|
|
457
|
-
// Special marker for global subscriptions
|
|
458
|
-
callback,
|
|
459
|
-
unsubscribe: () => {
|
|
460
|
-
this.globalSubscriptions.delete(callback);
|
|
461
|
-
this.logger?.info("[EventSubscriptions] Global subscription removed", { remainingSubscribers: this.globalSubscriptions.size });
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Notify all subscribers for a resource when a new event is appended
|
|
467
|
-
* @param resourceId - Bare resource ID
|
|
468
|
-
*/
|
|
469
|
-
async notifySubscribers(resourceId, event) {
|
|
470
|
-
const callbacks = this.subscriptions.get(resourceId);
|
|
471
|
-
if (!callbacks || callbacks.size === 0) {
|
|
472
|
-
this.logger?.info("[EventSubscriptions] Event - no subscribers to notify", { eventType: event.event.type, resourceId });
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
this.logger?.info("[EventSubscriptions] Notifying subscribers of event", { subscriberCount: callbacks.size, eventType: event.event.type, resourceId });
|
|
476
|
-
Array.from(callbacks).forEach((callback, index) => {
|
|
477
|
-
Promise.resolve(callback(event)).then(() => {
|
|
478
|
-
this.logger?.info("[EventSubscriptions] Subscriber successfully notified", { subscriberIndex: index + 1, eventType: event.event.type });
|
|
479
|
-
}).catch((error) => {
|
|
480
|
-
this.logger?.error("[EventSubscriptions] Error in subscriber", { subscriberIndex: index + 1, resourceId, eventType: event.event.type, error });
|
|
481
|
-
});
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Notify all global subscribers when a system-level event is appended
|
|
486
|
-
*/
|
|
487
|
-
async notifyGlobalSubscribers(event) {
|
|
488
|
-
if (this.globalSubscriptions.size === 0) {
|
|
489
|
-
this.logger?.info("[EventSubscriptions] System event - no global subscribers to notify", { eventType: event.event.type });
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
this.logger?.info("[EventSubscriptions] Notifying global subscribers of system event", { subscriberCount: this.globalSubscriptions.size, eventType: event.event.type });
|
|
493
|
-
Array.from(this.globalSubscriptions).forEach((callback, index) => {
|
|
494
|
-
Promise.resolve(callback(event)).then(() => {
|
|
495
|
-
this.logger?.info("[EventSubscriptions] Global subscriber successfully notified", { subscriberIndex: index + 1, eventType: event.event.type });
|
|
496
|
-
}).catch((error) => {
|
|
497
|
-
this.logger?.error("[EventSubscriptions] Error in global subscriber", { subscriberIndex: index + 1, eventType: event.event.type, error });
|
|
498
|
-
});
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Get subscription count for a resource (useful for debugging)
|
|
503
|
-
*/
|
|
504
|
-
getSubscriptionCount(resourceId) {
|
|
505
|
-
return this.subscriptions.get(resourceId)?.size || 0;
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Get total number of active subscriptions across all resources
|
|
509
|
-
*/
|
|
510
|
-
getTotalSubscriptions() {
|
|
511
|
-
let total = 0;
|
|
512
|
-
for (const callbacks of this.subscriptions.values()) {
|
|
513
|
-
total += callbacks.size;
|
|
514
|
-
}
|
|
515
|
-
return total;
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Get total number of global subscriptions
|
|
519
|
-
*/
|
|
520
|
-
getGlobalSubscriptionCount() {
|
|
521
|
-
return this.globalSubscriptions.size;
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
var globalEventSubscriptions = null;
|
|
525
|
-
function getEventSubscriptions(logger) {
|
|
526
|
-
if (!globalEventSubscriptions) {
|
|
527
|
-
globalEventSubscriptions = new EventSubscriptions(logger);
|
|
528
|
-
logger?.info("[EventSubscriptions] Created global singleton instance");
|
|
529
|
-
}
|
|
530
|
-
return globalEventSubscriptions;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// src/event-bus.ts
|
|
534
|
-
var EventBus = class {
|
|
535
|
-
// Expose subscriptions for direct access
|
|
536
|
-
subscriptions;
|
|
537
|
-
logger;
|
|
538
|
-
constructor(logger) {
|
|
539
|
-
this.logger = logger;
|
|
540
|
-
this.subscriptions = getEventSubscriptions(logger?.child({ component: "EventSubscriptions" }));
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Publish event to subscribers
|
|
544
|
-
* - Resource events: notifies BOTH resource-scoped AND global subscribers
|
|
545
|
-
* - System events: notifies global subscribers only
|
|
546
|
-
* @param event - Stored event (from @semiont/core)
|
|
547
|
-
*/
|
|
548
|
-
async publish(event) {
|
|
549
|
-
if (isSystemEvent(event.event)) {
|
|
550
|
-
await this.subscriptions.notifyGlobalSubscribers(event);
|
|
551
|
-
} else if (isResourceEvent(event.event)) {
|
|
552
|
-
const rid = resourceId(event.event.resourceId);
|
|
553
|
-
await this.subscriptions.notifySubscribers(rid, event);
|
|
554
|
-
await this.subscriptions.notifyGlobalSubscribers(event);
|
|
555
|
-
} else {
|
|
556
|
-
this.logger?.warn("[EventBus] Event is neither resource nor system event", { eventType: event.event.type });
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Subscribe to events for a specific resource
|
|
561
|
-
* @param resourceId - Branded ResourceId (from @semiont/core)
|
|
562
|
-
* @param callback - Event callback function
|
|
563
|
-
* @returns EventSubscription with unsubscribe function
|
|
564
|
-
*/
|
|
565
|
-
subscribe(resourceId, callback) {
|
|
566
|
-
return this.subscriptions.subscribe(resourceId, callback);
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Subscribe to all system-level events
|
|
570
|
-
* @param callback - Event callback function
|
|
571
|
-
* @returns EventSubscription with unsubscribe function
|
|
572
|
-
*/
|
|
573
|
-
subscribeGlobal(callback) {
|
|
574
|
-
return this.subscriptions.subscribeGlobal(callback);
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Unsubscribe from resource events
|
|
578
|
-
* @param resourceId - Branded ResourceId (from @semiont/core)
|
|
579
|
-
* @param callback - Event callback function to remove
|
|
580
|
-
*/
|
|
581
|
-
unsubscribe(resourceId, callback) {
|
|
582
|
-
const callbacks = this.subscriptions.subscriptions.get(resourceId);
|
|
583
|
-
if (callbacks) {
|
|
584
|
-
callbacks.delete(callback);
|
|
585
|
-
if (callbacks.size === 0) {
|
|
586
|
-
this.subscriptions.subscriptions.delete(resourceId);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Unsubscribe from global events
|
|
592
|
-
* @param callback - Event callback function to remove
|
|
593
|
-
*/
|
|
594
|
-
unsubscribeGlobal(callback) {
|
|
595
|
-
this.subscriptions.globalSubscriptions.delete(callback);
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Get subscriber count for a resource
|
|
599
|
-
* @param resourceId - Branded ResourceId (from @semiont/core)
|
|
600
|
-
* @returns Number of active subscribers
|
|
601
|
-
*/
|
|
602
|
-
getSubscriberCount(resourceId) {
|
|
603
|
-
return this.subscriptions.getSubscriptionCount(resourceId);
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Get total number of active subscriptions across all resources
|
|
607
|
-
*/
|
|
608
|
-
getTotalSubscriptions() {
|
|
609
|
-
return this.subscriptions.getTotalSubscriptions();
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Get total number of global subscriptions
|
|
613
|
-
*/
|
|
614
|
-
getGlobalSubscriptionCount() {
|
|
615
|
-
return this.subscriptions.getGlobalSubscriptionCount();
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
418
|
var ResourceNotFoundError = class extends Error {
|
|
619
419
|
constructor(uri) {
|
|
620
420
|
super(`No resource found for URI: ${uri}`);
|
|
@@ -704,14 +504,14 @@ var ViewMaterializer = class {
|
|
|
704
504
|
/**
|
|
705
505
|
* Update the storage-uri index in response to an event.
|
|
706
506
|
*
|
|
707
|
-
* Only
|
|
507
|
+
* Only yield:created (with storageUri), yield:moved, need index changes.
|
|
708
508
|
* resource.archived / resource.unarchived do NOT modify the index.
|
|
709
509
|
*/
|
|
710
510
|
async materializeStorageUriIndex(resourceId, event) {
|
|
711
511
|
const projectionsDir = path.join(this.config.basePath, "projections");
|
|
712
|
-
if (event.type === "
|
|
512
|
+
if (event.type === "yield:created" && event.payload.storageUri) {
|
|
713
513
|
await writeStorageUriEntry(projectionsDir, event.payload.storageUri, resourceId);
|
|
714
|
-
} else if (event.type === "
|
|
514
|
+
} else if (event.type === "yield:moved") {
|
|
715
515
|
await removeStorageUriEntry(projectionsDir, event.payload.fromUri);
|
|
716
516
|
await writeStorageUriEntry(projectionsDir, event.payload.toUri, resourceId);
|
|
717
517
|
}
|
|
@@ -737,10 +537,10 @@ var ViewMaterializer = class {
|
|
|
737
537
|
};
|
|
738
538
|
events.sort((a, b) => a.metadata.sequenceNumber - b.metadata.sequenceNumber);
|
|
739
539
|
for (const storedEvent of events) {
|
|
740
|
-
this.applyEventToResource(resource, storedEvent
|
|
741
|
-
this.applyEventToAnnotations(annotations, storedEvent
|
|
540
|
+
this.applyEventToResource(resource, storedEvent);
|
|
541
|
+
this.applyEventToAnnotations(annotations, storedEvent);
|
|
742
542
|
annotations.version++;
|
|
743
|
-
annotations.updatedAt = storedEvent.
|
|
543
|
+
annotations.updatedAt = storedEvent.timestamp;
|
|
744
544
|
}
|
|
745
545
|
return { resource, annotations };
|
|
746
546
|
}
|
|
@@ -749,7 +549,7 @@ var ViewMaterializer = class {
|
|
|
749
549
|
*/
|
|
750
550
|
applyEventToResource(resource, event) {
|
|
751
551
|
switch (event.type) {
|
|
752
|
-
case "
|
|
552
|
+
case "yield:created":
|
|
753
553
|
resource.name = event.payload.name;
|
|
754
554
|
resource.entityTypes = event.payload.entityTypes || [];
|
|
755
555
|
resource.dateCreated = event.timestamp;
|
|
@@ -773,7 +573,7 @@ var ViewMaterializer = class {
|
|
|
773
573
|
}
|
|
774
574
|
resource.currentChecksum = event.payload.contentChecksum;
|
|
775
575
|
break;
|
|
776
|
-
case "
|
|
576
|
+
case "yield:cloned":
|
|
777
577
|
resource.name = event.payload.name;
|
|
778
578
|
resource.entityTypes = event.payload.entityTypes || [];
|
|
779
579
|
resource.dateCreated = event.timestamp;
|
|
@@ -791,21 +591,21 @@ var ViewMaterializer = class {
|
|
|
791
591
|
});
|
|
792
592
|
resource.representations = reps2;
|
|
793
593
|
break;
|
|
794
|
-
case "
|
|
594
|
+
case "yield:updated":
|
|
795
595
|
resource.currentChecksum = event.payload.contentChecksum;
|
|
796
596
|
resource.dateModified = event.timestamp;
|
|
797
597
|
break;
|
|
798
|
-
case "
|
|
598
|
+
case "yield:moved":
|
|
799
599
|
resource.storageUri = event.payload.toUri;
|
|
800
600
|
resource.dateModified = event.timestamp;
|
|
801
601
|
break;
|
|
802
|
-
case "
|
|
602
|
+
case "mark:archived":
|
|
803
603
|
resource.archived = true;
|
|
804
604
|
break;
|
|
805
|
-
case "
|
|
605
|
+
case "mark:unarchived":
|
|
806
606
|
resource.archived = false;
|
|
807
607
|
break;
|
|
808
|
-
case "representation
|
|
608
|
+
case "yield:representation-added": {
|
|
809
609
|
const { representation } = event.payload;
|
|
810
610
|
if (!resource.representations) {
|
|
811
611
|
resource.representations = [];
|
|
@@ -817,7 +617,7 @@ var ViewMaterializer = class {
|
|
|
817
617
|
}
|
|
818
618
|
break;
|
|
819
619
|
}
|
|
820
|
-
case "representation
|
|
620
|
+
case "yield:representation-removed": {
|
|
821
621
|
const { checksum } = event.payload;
|
|
822
622
|
if (resource.representations) {
|
|
823
623
|
const repsArray = Array.isArray(resource.representations) ? resource.representations : [resource.representations];
|
|
@@ -825,13 +625,13 @@ var ViewMaterializer = class {
|
|
|
825
625
|
}
|
|
826
626
|
break;
|
|
827
627
|
}
|
|
828
|
-
case "
|
|
628
|
+
case "mark:entity-tag-added":
|
|
829
629
|
if (!resource.entityTypes) resource.entityTypes = [];
|
|
830
630
|
if (!resource.entityTypes.includes(event.payload.entityType)) {
|
|
831
631
|
resource.entityTypes.push(event.payload.entityType);
|
|
832
632
|
}
|
|
833
633
|
break;
|
|
834
|
-
case "
|
|
634
|
+
case "mark:entity-tag-removed":
|
|
835
635
|
if (resource.entityTypes) {
|
|
836
636
|
resource.entityTypes = resource.entityTypes.filter(
|
|
837
637
|
(t) => t !== event.payload.entityType
|
|
@@ -845,15 +645,15 @@ var ViewMaterializer = class {
|
|
|
845
645
|
*/
|
|
846
646
|
applyEventToAnnotations(annotations, event) {
|
|
847
647
|
switch (event.type) {
|
|
848
|
-
case "
|
|
648
|
+
case "mark:added":
|
|
849
649
|
annotations.annotations.push(event.payload.annotation);
|
|
850
650
|
break;
|
|
851
|
-
case "
|
|
651
|
+
case "mark:removed":
|
|
852
652
|
annotations.annotations = annotations.annotations.filter(
|
|
853
653
|
(a) => a.id !== event.payload.annotationId
|
|
854
654
|
);
|
|
855
655
|
break;
|
|
856
|
-
case "
|
|
656
|
+
case "mark:body-updated":
|
|
857
657
|
const annotation = annotations.annotations.find(
|
|
858
658
|
(a) => a.id === event.payload.annotationId
|
|
859
659
|
);
|
|
@@ -884,6 +684,44 @@ var ViewMaterializer = class {
|
|
|
884
684
|
break;
|
|
885
685
|
}
|
|
886
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Walk every event stream in the event log and materialize the corresponding
|
|
689
|
+
* view from scratch. Idempotent: existing view files are overwritten.
|
|
690
|
+
*
|
|
691
|
+
* Mirrors GraphDBConsumer.rebuildAll() and Smelter.rebuildAll() — this is the
|
|
692
|
+
* recovery path that makes the ephemeral stateDir safe to wipe. The live
|
|
693
|
+
* append path (EventStore.appendEvent → materializeIncremental /
|
|
694
|
+
* materializeEntityTypes) is unchanged and runs in addition.
|
|
695
|
+
*/
|
|
696
|
+
async rebuildAll(eventLog) {
|
|
697
|
+
this.logger?.info("[ViewMaterializer] Rebuilding all materialized views from event log");
|
|
698
|
+
const SYSTEM_ID = "__system__";
|
|
699
|
+
const systemEvents = await eventLog.getEvents(SYSTEM_ID);
|
|
700
|
+
this.logger?.info("[ViewMaterializer] Replaying system events", { count: systemEvents.length });
|
|
701
|
+
for (const event of systemEvents) {
|
|
702
|
+
if (event.type === "mark:entity-type-added") {
|
|
703
|
+
await this.materializeEntityTypes(event.payload.entityType);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const allResourceIds = await eventLog.getAllResourceIds();
|
|
707
|
+
const resourceIds = allResourceIds.filter(
|
|
708
|
+
(rid) => rid !== "__system__"
|
|
709
|
+
);
|
|
710
|
+
this.logger?.info("[ViewMaterializer] Rebuilding resource views", { count: resourceIds.length });
|
|
711
|
+
for (const rid of resourceIds) {
|
|
712
|
+
const events = await eventLog.getEvents(rid);
|
|
713
|
+
if (events.length === 0) continue;
|
|
714
|
+
const view = this.materializeFromEvents(events, rid);
|
|
715
|
+
await this.viewStorage.save(rid, view);
|
|
716
|
+
for (const event of events) {
|
|
717
|
+
await this.materializeStorageUriIndex(rid, event);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
this.logger?.info("[ViewMaterializer] Rebuild complete", {
|
|
721
|
+
systemEvents: systemEvents.length,
|
|
722
|
+
resources: resourceIds.length
|
|
723
|
+
});
|
|
724
|
+
}
|
|
887
725
|
/**
|
|
888
726
|
* Materialize entity types view - System-level view
|
|
889
727
|
*/
|
|
@@ -935,10 +773,19 @@ var ViewManager = class {
|
|
|
935
773
|
* @param payload - Event payload
|
|
936
774
|
*/
|
|
937
775
|
async materializeSystem(eventType, payload) {
|
|
938
|
-
if (eventType === "
|
|
776
|
+
if (eventType === "mark:entity-type-added") {
|
|
939
777
|
await this.materializer.materializeEntityTypes(payload.entityType);
|
|
940
778
|
}
|
|
941
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Rebuild all materialized views from the event log on startup.
|
|
782
|
+
* Mirrors GraphDBConsumer.rebuildAll() — call this once during
|
|
783
|
+
* createKnowledgeBase before the HTTP server begins accepting requests.
|
|
784
|
+
* Idempotent: existing view files are overwritten.
|
|
785
|
+
*/
|
|
786
|
+
async rebuildAll(eventLog) {
|
|
787
|
+
return this.materializer.rebuildAll(eventLog);
|
|
788
|
+
}
|
|
942
789
|
/**
|
|
943
790
|
* Get resource view (builds from events if needed)
|
|
944
791
|
* @param resourceId - Branded ResourceId (from @semiont/core)
|
|
@@ -954,7 +801,6 @@ var ViewManager = class {
|
|
|
954
801
|
var EventStore = class {
|
|
955
802
|
// Focused components - each with single responsibility
|
|
956
803
|
log;
|
|
957
|
-
bus;
|
|
958
804
|
views;
|
|
959
805
|
viewStorage;
|
|
960
806
|
coreEventBus;
|
|
@@ -962,7 +808,6 @@ var EventStore = class {
|
|
|
962
808
|
this.viewStorage = viewStorage;
|
|
963
809
|
this.coreEventBus = coreEventBus;
|
|
964
810
|
this.log = new EventLog({ project }, logger?.child({ component: "EventLog" }));
|
|
965
|
-
this.bus = new EventBus(logger?.child({ component: "EventBus" }));
|
|
966
811
|
const viewConfig = {
|
|
967
812
|
basePath: stateDir
|
|
968
813
|
};
|
|
@@ -971,28 +816,31 @@ var EventStore = class {
|
|
|
971
816
|
/**
|
|
972
817
|
* Append an event to the store
|
|
973
818
|
* Coordinates: persistence → view → notification
|
|
819
|
+
*
|
|
820
|
+
* @param options.correlationId - Optional id propagated from a command. Stored
|
|
821
|
+
* on the event's metadata so that subscribers (notably the events-stream
|
|
822
|
+
* route) can match command-result events back to the POST that initiated
|
|
823
|
+
* them. Pass through from your route handler when handling commands.
|
|
974
824
|
*/
|
|
975
|
-
async appendEvent(event) {
|
|
825
|
+
async appendEvent(event, options) {
|
|
976
826
|
const resourceId = event.resourceId || "__system__";
|
|
977
|
-
const storedEvent = await this.log.append(event, resourceId);
|
|
827
|
+
const storedEvent = await this.log.append(event, resourceId, options);
|
|
978
828
|
if (resourceId === "__system__") {
|
|
979
829
|
await this.views.materializeSystem(
|
|
980
|
-
storedEvent.
|
|
981
|
-
storedEvent.
|
|
830
|
+
storedEvent.type,
|
|
831
|
+
storedEvent.payload
|
|
982
832
|
);
|
|
983
833
|
} else {
|
|
984
834
|
await this.views.materializeResource(
|
|
985
835
|
resourceId,
|
|
986
|
-
storedEvent
|
|
836
|
+
storedEvent,
|
|
987
837
|
() => this.log.getEvents(resourceId)
|
|
988
838
|
);
|
|
989
839
|
}
|
|
990
|
-
|
|
991
|
-
if (
|
|
840
|
+
this.coreEventBus.getDomainEvent(storedEvent.type).next(storedEvent);
|
|
841
|
+
if (resourceId !== "__system__") {
|
|
992
842
|
const scopedBus = this.coreEventBus.scope(resourceId);
|
|
993
|
-
|
|
994
|
-
scopedBus.get(eventChannel).next(storedEvent.event);
|
|
995
|
-
scopedBus.get("make-meaning:event").next(storedEvent.event);
|
|
843
|
+
scopedBus.getDomainEvent(storedEvent.type).next(storedEvent);
|
|
996
844
|
}
|
|
997
845
|
return storedEvent;
|
|
998
846
|
}
|
|
@@ -1115,16 +963,16 @@ var EventQuery = class {
|
|
|
1115
963
|
const allEvents = await this.eventStorage.getAllEvents(query.resourceId);
|
|
1116
964
|
let results = allEvents;
|
|
1117
965
|
if (query.userId) {
|
|
1118
|
-
results = results.filter((e) => e.
|
|
966
|
+
results = results.filter((e) => e.userId === query.userId);
|
|
1119
967
|
}
|
|
1120
968
|
if (query.eventTypes && query.eventTypes.length > 0) {
|
|
1121
|
-
results = results.filter((e) => query.eventTypes.includes(e.
|
|
969
|
+
results = results.filter((e) => query.eventTypes.includes(e.type));
|
|
1122
970
|
}
|
|
1123
971
|
if (query.fromTimestamp) {
|
|
1124
|
-
results = results.filter((e) => e.
|
|
972
|
+
results = results.filter((e) => e.timestamp >= query.fromTimestamp);
|
|
1125
973
|
}
|
|
1126
974
|
if (query.toTimestamp) {
|
|
1127
|
-
results = results.filter((e) => e.
|
|
975
|
+
results = results.filter((e) => e.timestamp <= query.toTimestamp);
|
|
1128
976
|
}
|
|
1129
977
|
if (query.fromSequence) {
|
|
1130
978
|
results = results.filter((e) => e.metadata.sequenceNumber >= query.fromSequence);
|
|
@@ -1178,6 +1026,10 @@ var EventQuery = class {
|
|
|
1178
1026
|
};
|
|
1179
1027
|
|
|
1180
1028
|
// src/validation/event-validator.ts
|
|
1029
|
+
function extractPersistedEvent(stored) {
|
|
1030
|
+
const { metadata, signature, ...event } = stored;
|
|
1031
|
+
return event;
|
|
1032
|
+
}
|
|
1181
1033
|
var EventValidator = class {
|
|
1182
1034
|
/**
|
|
1183
1035
|
* Validate event chain integrity for a resource's events
|
|
@@ -1194,7 +1046,7 @@ var EventValidator = class {
|
|
|
1194
1046
|
`Event chain broken at sequence ${curr.metadata.sequenceNumber}: prevEventHash=${curr.metadata.prevEventHash} but previous checksum=${prev.metadata.checksum}`
|
|
1195
1047
|
);
|
|
1196
1048
|
}
|
|
1197
|
-
const calculated = sha256(curr
|
|
1049
|
+
const calculated = sha256(extractPersistedEvent(curr));
|
|
1198
1050
|
if (calculated !== curr.metadata.checksum) {
|
|
1199
1051
|
errors.push(
|
|
1200
1052
|
`Checksum mismatch at sequence ${curr.metadata.sequenceNumber}: calculated=${calculated} but stored=${curr.metadata.checksum}`
|
|
@@ -1211,7 +1063,7 @@ var EventValidator = class {
|
|
|
1211
1063
|
* Useful for validating events before writing them
|
|
1212
1064
|
*/
|
|
1213
1065
|
validateEventChecksum(event) {
|
|
1214
|
-
const calculated = sha256(event
|
|
1066
|
+
const calculated = sha256(extractPersistedEvent(event));
|
|
1215
1067
|
return calculated === event.metadata.checksum;
|
|
1216
1068
|
}
|
|
1217
1069
|
/**
|
|
@@ -1229,6 +1081,6 @@ function generateAnnotationId() {
|
|
|
1229
1081
|
return nanoid(21);
|
|
1230
1082
|
}
|
|
1231
1083
|
|
|
1232
|
-
export {
|
|
1084
|
+
export { EventLog, EventQuery, EventStorage, EventStore, EventValidator, FilesystemViewStorage, ResourceNotFoundError, ViewManager, ViewMaterializer, createEventStore, generateAnnotationId, getShardPath, jumpConsistentHash, removeStorageUriEntry, resolveStorageUri, sha256, writeStorageUriEntry };
|
|
1233
1085
|
//# sourceMappingURL=index.js.map
|
|
1234
1086
|
//# sourceMappingURL=index.js.map
|