@semiont/event-sourcing 0.5.2 → 0.5.4

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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Event Storage - Physical Storage Layer
3
+ *
4
+ * Handles file I/O operations for event storage:
5
+ * - JSONL file writing/reading
6
+ * - 4-hex sharding (65,536 shards)
7
+ * - File rotation
8
+ * - Event stream initialization
9
+ *
10
+ * @see docs/EVENT-STORE.md#eventstorage for architecture details
11
+ */
12
+ import type { StoredEvent, EventInput, ResourceId, Logger } from '@semiont/core';
13
+ import type { SemiontProject } from '@semiont/core/node';
14
+ export interface EventStorageConfig {
15
+ maxEventsPerFile?: number;
16
+ enableSharding?: boolean;
17
+ numShards?: number;
18
+ enableCompression?: boolean;
19
+ }
20
+ /**
21
+ * EventStorage handles physical storage of events
22
+ * Owns: file I/O, sharding, AND sequence/hash tracking
23
+ */
24
+ export declare class EventStorage {
25
+ private config;
26
+ private project;
27
+ private logger?;
28
+ private resourceSequences;
29
+ private currentFiles;
30
+ constructor(project: SemiontProject, config?: EventStorageConfig, logger?: Logger);
31
+ /**
32
+ * Calculate shard path for a resource ID
33
+ * Uses jump consistent hash for uniform distribution
34
+ * Special case: __system__ events bypass sharding
35
+ */
36
+ getShardPath(resourceId: ResourceId): string;
37
+ /**
38
+ * Get full path to resource's event directory
39
+ */
40
+ getResourcePath(resourceId: ResourceId): string;
41
+ /**
42
+ * Initialize directory structure for a resource's event stream
43
+ * Also loads sequence number and last hash if stream exists
44
+ */
45
+ initializeResourceStream(resourceId: ResourceId): Promise<void>;
46
+ /**
47
+ * Append an event - handles EVERYTHING for event creation
48
+ * Creates ID, timestamp, metadata, sequence tracking, and writes to disk.
49
+ *
50
+ * Integrity is provided by git at the commit level (when gitSync is enabled),
51
+ * not by per-event chaining metadata. Per-event signatures (the unused
52
+ * `EventSignature` field on StoredEvent) are the planned mechanism for
53
+ * cross-KB authorship binding when federation becomes a real requirement.
54
+ *
55
+ * @param options.correlationId - Optional id propagated from a command. Stored
56
+ * on the event's metadata so subscribers (notably the events-stream → frontend
57
+ * path) can match command-result events back to the POST that initiated them.
58
+ */
59
+ appendEvent(event: EventInput, resourceId: ResourceId, options?: {
60
+ correlationId?: string;
61
+ }): Promise<StoredEvent>;
62
+ /**
63
+ * Write an event to storage (append to JSONL)
64
+ * Internal method - use appendEvent() instead
65
+ *
66
+ * Uses currentFiles cache to avoid fs.readdir() + countEventsInFile() on every append.
67
+ * Cache is populated on first append (cold start) and updated on rotation.
68
+ */
69
+ private writeEvent;
70
+ /**
71
+ * Count events in a specific file
72
+ */
73
+ countEventsInFile(resourceId: ResourceId, filename: string): Promise<number>;
74
+ /**
75
+ * Read all events from a specific file
76
+ */
77
+ readEventsFromFile(resourceId: ResourceId, filename: string): Promise<StoredEvent[]>;
78
+ /**
79
+ * Get list of event files for a resource (sorted by sequence)
80
+ */
81
+ getEventFiles(resourceId: ResourceId): Promise<string[]>;
82
+ /**
83
+ * Create a new event file for rotation
84
+ */
85
+ createNewEventFile(resourceId: ResourceId): Promise<string>;
86
+ /**
87
+ * Get the last event from a specific file
88
+ */
89
+ getLastEvent(resourceId: ResourceId, filename: string): Promise<StoredEvent | null>;
90
+ /**
91
+ * Get all events for a resource across all files
92
+ */
93
+ getAllEvents(resourceId: ResourceId): Promise<StoredEvent[]>;
94
+ /**
95
+ * Get all resource IDs by scanning shard directories
96
+ */
97
+ getAllResourceIds(): Promise<ResourceId[]>;
98
+ /**
99
+ * Create filename for event file
100
+ */
101
+ private createEventFilename;
102
+ /**
103
+ * Get current sequence number for a resource
104
+ */
105
+ getSequenceNumber(resourceId: ResourceId): number;
106
+ /**
107
+ * Increment and return next sequence number for a resource
108
+ */
109
+ getNextSequenceNumber(resourceId: ResourceId): number;
110
+ }
111
+ //# sourceMappingURL=event-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-storage.d.ts","sourceRoot":"","sources":["../../src/storage/event-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAE,WAAW,EAAiC,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEhH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,MAAM,CAAC,CAAS;IAGxB,OAAO,CAAC,iBAAiB,CAAkC;IAE3D,OAAO,CAAC,YAAY,CAAgE;gBAExE,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,MAAM;IAWjF;;;;OAIG;IACH,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM;IAgB5C;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM;IAK/C;;;OAGG;IACG,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA+CrE;;;;;;;;;;;;OAYG;IACG,WAAW,CACf,KAAK,EAAE,UAAU,EACjB,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GACnC,OAAO,CAAC,WAAW,CAAC;IA+BvB;;;;;;OAMG;YACW,UAAU;IAoCxB;;OAEG;IACG,iBAAiB,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBlF;;OAEG;IACG,kBAAkB,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAyC1F;;OAEG;IACG,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAwB9D;;OAEG;IACG,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBjE;;OAEG;IACG,YAAY,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAMzF;;OAEG;IACG,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAYlE;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAkChD;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAQ3B;;OAEG;IACH,iBAAiB,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM;IAIjD;;OAEG;IACH,qBAAqB,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM;CAOtD"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Sharding Utilities
3
+ *
4
+ * Shared utilities for consistent sharding across all storage layers
5
+ * Uses Google's Jump Consistent Hash algorithm for even distribution
6
+ */
7
+ /**
8
+ * TEMPORARY: Simple modulo-based hash sharding
9
+ *
10
+ * ⚠️ TODO: Replace with proper Jump Consistent Hash implementation
11
+ *
12
+ * This is a TEMPORARY implementation using simple modulo. It works and provides
13
+ * good distribution, but does NOT provide the minimal reshuffling property of
14
+ * Jump Consistent Hash when changing bucket counts.
15
+ *
16
+ * The proper implementation should use Google's Jump Consistent Hash algorithm:
17
+ * Reference: "A Fast, Minimal Memory, Consistent Hash Algorithm" by Lamping & Veach (2014)
18
+ * https://arxiv.org/abs/1406.2294
19
+ *
20
+ * Working implementations exist in npm packages like:
21
+ * - jumphash (https://www.npmjs.com/package/jumphash)
22
+ * - jump-gouache (https://github.com/bhoudu/jump-gouache)
23
+ *
24
+ * The algorithm requires proper 64-bit integer handling with BigInt to avoid
25
+ * precision loss in JavaScript. The previous attempt failed due to incorrect
26
+ * BigInt arithmetic in the while loop condition.
27
+ *
28
+ * Until replaced, this modulo approach will cause ALL data to be reshuffled
29
+ * if bucket count changes, rather than the optimal O(n/k) reshuffling that
30
+ * Jump Consistent Hash provides.
31
+ *
32
+ * @param key - The key to hash (typically a resource ID)
33
+ * @param numBuckets - Number of shards/buckets (default: 65536 for 4-hex sharding)
34
+ * @returns Shard number (0 to numBuckets-1)
35
+ */
36
+ export declare function jumpConsistentHash(key: string, numBuckets?: number): number;
37
+ /**
38
+ * Convert shard number to 4-hex directory path (ab/cd)
39
+ *
40
+ * @param shardId - Shard number (0-65535)
41
+ * @returns Path segments like ['ab', 'cd']
42
+ */
43
+ export declare function shardIdToPath(shardId: number): [string, string];
44
+ /**
45
+ * Get 4-hex shard path for a key
46
+ *
47
+ * @param key - The key to hash (typically a resource ID)
48
+ * @param numBuckets - Number of shards (default: 65536)
49
+ * @returns Path segments like ['ab', 'cd']
50
+ */
51
+ export declare function getShardPath(key: string, numBuckets?: number): [string, string];
52
+ /**
53
+ * Calculate SHA-256 hash of data
54
+ */
55
+ export declare function sha256(data: string | object): string;
56
+ //# sourceMappingURL=shard-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shard-utils.d.ts","sourceRoot":"","sources":["../../src/storage/shard-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,GAAE,MAAc,GAAG,MAAM,CAGlF;AAcD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAU/D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,GAAE,MAAc,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAGtF;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAGpD"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Storage URI Index
3
+ *
4
+ * Projection that maps file:// URIs → resourceIds.
5
+ * Sharded at:
6
+ * {projectionsDir}/storage-uri/{ab}/{cd}/{uri-hash}.json
7
+ *
8
+ * where {ab}/{cd} comes from jumpConsistentHash(uri) and
9
+ * {uri-hash} is the SHA-256 of the URI string.
10
+ *
11
+ * Each file contains: { uri: string; resourceId: string }
12
+ *
13
+ * This index is maintained by ViewMaterializer (the single owner).
14
+ * It is never modified by Stower or other actors.
15
+ *
16
+ * Archive/unarchive do NOT remove entries from this index.
17
+ * Archived resources are marked in the ResourceView (archived: true)
18
+ * but remain findable by URI.
19
+ */
20
+ export interface StorageUriEntry {
21
+ uri: string;
22
+ resourceId: string;
23
+ }
24
+ /**
25
+ * Thrown when a URI is not found in the storage-uri index.
26
+ */
27
+ export declare class ResourceNotFoundError extends Error {
28
+ readonly uri: string;
29
+ constructor(uri: string);
30
+ }
31
+ /**
32
+ * Resolve a file:// URI to a resourceId using the storage-uri index.
33
+ *
34
+ * @param projectionsDir - Path to the projections directory
35
+ * @param uri - file:// URI (e.g. "file://docs/overview.md")
36
+ * @returns resourceId
37
+ * @throws ResourceNotFoundError if URI is not in the index
38
+ */
39
+ export declare function resolveStorageUri(projectionsDir: string, uri: string): Promise<string>;
40
+ /**
41
+ * Write a URI → resourceId mapping to the index.
42
+ *
43
+ * Called by ViewMaterializer when handling resource.created, resource.moved.
44
+ *
45
+ * @param projectionsDir - Path to the projections directory
46
+ * @param uri - file:// URI
47
+ * @param resourceId - resourceId to map to
48
+ */
49
+ export declare function writeStorageUriEntry(projectionsDir: string, uri: string, resourceId: string): Promise<void>;
50
+ /**
51
+ * Remove a URI entry from the index.
52
+ *
53
+ * Called by ViewMaterializer when handling resource.moved (old URI only).
54
+ * NOT called on resource.archived — archived resources retain their index entry.
55
+ *
56
+ * @param projectionsDir - Path to the projections directory
57
+ * @param uri - file:// URI to remove
58
+ */
59
+ export declare function removeStorageUriEntry(projectionsDir: string, uri: string): Promise<void>;
60
+ //# sourceMappingURL=storage-uri-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-uri-index.d.ts","sourceRoot":"","sources":["../../src/storage/storage-uri-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM;CAIjC;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * View Storage - Materialized Views
3
+ *
4
+ * Stores materialized views of resource state and annotations
5
+ * Built from event streams, can be rebuilt at any time
6
+ *
7
+ * Stores both ResourceDescriptor metadata and ResourceAnnotations, but keeps them logically separate
8
+ */
9
+ import type { SemiontProject } from '@semiont/core/node';
10
+ import type { ResourceAnnotations, ResourceDescriptor, ResourceId, Logger } from '@semiont/core';
11
+ export interface ResourceView {
12
+ resource: ResourceDescriptor;
13
+ annotations: ResourceAnnotations;
14
+ }
15
+ export interface ViewStorage {
16
+ save(resourceId: ResourceId, view: ResourceView): Promise<void>;
17
+ get(resourceId: ResourceId): Promise<ResourceView | null>;
18
+ delete(resourceId: ResourceId): Promise<void>;
19
+ exists(resourceId: ResourceId): Promise<boolean>;
20
+ getAll(): Promise<ResourceView[]>;
21
+ }
22
+ export declare class FilesystemViewStorage implements ViewStorage {
23
+ private basePath;
24
+ private logger?;
25
+ constructor(project: SemiontProject, logger?: Logger);
26
+ private getProjectionPath;
27
+ save(resourceId: ResourceId, projection: ResourceView): Promise<void>;
28
+ get(resourceId: ResourceId): Promise<ResourceView | null>;
29
+ delete(resourceId: ResourceId): Promise<void>;
30
+ exists(resourceId: ResourceId): Promise<boolean>;
31
+ getAll(): Promise<ResourceView[]>;
32
+ }
33
+ //# sourceMappingURL=view-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-storage.d.ts","sourceRoot":"","sources":["../../src/storage/view-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAGjG,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC1D,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CACnC;AAED,qBAAa,qBAAsB,YAAW,WAAW;IACvD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,MAAM;IAKpD,OAAO,CAAC,iBAAiB;IAMnB,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBrE,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA8BzD,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAa7C,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;IAWhD,MAAM,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;CAsCxC"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ViewManager - Materialized View Management Layer
3
+ *
4
+ * Single Responsibility: View updates only
5
+ * - Updates resource views from events
6
+ * - Updates system views (entity types)
7
+ * - Rebuilds views when needed
8
+ *
9
+ * Does NOT handle:
10
+ * - Event persistence (see EventLog)
11
+ * - Pub/sub notifications (see EventBus)
12
+ */
13
+ import { type ResourceId, type PersistedEvent, type StoredEvent, type Logger } from '@semiont/core';
14
+ import { ViewMaterializer, type RebuildEventSource } from './views/view-materializer';
15
+ import type { ViewStorage, ResourceView } from './storage/view-storage';
16
+ export interface ViewManagerConfig {
17
+ basePath: string;
18
+ }
19
+ /**
20
+ * ViewManager wraps ViewMaterializer with a clean API
21
+ * Handles both resource and system-level views
22
+ *
23
+ * ## Per-resource serialization
24
+ *
25
+ * `materializeResource` runs read-modify-write cycles on the view file for
26
+ * a given resource (load JSON → apply event → save JSON). When multiple
27
+ * events arrive for the same resource in rapid succession — the canonical
28
+ * example is the reference-detection worker emitting `mark:added` +
29
+ * `job:progress` + `job:completed` within a few milliseconds — concurrent
30
+ * RMW cycles will clobber each other, losing events and occasionally
31
+ * corrupting the view file entirely.
32
+ *
33
+ * ViewManager serializes these via `serializePerKey` from `@semiont/core`:
34
+ * each incoming `materializeResource` call chains onto the previous one
35
+ * for the same `resourceId`, so the work runs strictly sequentially per
36
+ * resource while still parallelizing across different resources. System
37
+ * events go through their own shared chain (keyed by a sentinel).
38
+ *
39
+ * Why this shape and not RxJS `groupBy + concatMap`:
40
+ * ViewManager is called **synchronously** by `EventStore.appendEvent` — it
41
+ * must block the caller until the view is written, so SSE subscribers
42
+ * that see the subsequently-published event get the up-to-date view (a
43
+ * read-your-writes guarantee). The RxJS stream-consumer pattern used by
44
+ * `Smelter`, `GraphDBConsumer`, and `Gatherer` can't provide that
45
+ * guarantee because it's fire-and-forget from the publisher's perspective.
46
+ * Both patterns solve "serialize work per resource" — see also
47
+ * `packages/core/src/serialize-per-key.ts` for the shared primitive.
48
+ */
49
+ export declare class ViewManager {
50
+ readonly materializer: ViewMaterializer;
51
+ private resourceChains;
52
+ private systemChains;
53
+ private static readonly SYSTEM_KEY;
54
+ constructor(viewStorage: ViewStorage, config: ViewManagerConfig, logger?: Logger);
55
+ /**
56
+ * Update resource view with a new event.
57
+ * Serialized per resource — see class doc.
58
+ *
59
+ * @param resourceId - Branded ResourceId (from @semiont/core)
60
+ * @param event - Resource event (from @semiont/core)
61
+ * @param getAllEvents - Function to retrieve all events for rebuild if needed
62
+ */
63
+ materializeResource(resourceId: ResourceId, event: PersistedEvent, getAllEvents: () => Promise<StoredEvent[]>): Promise<void>;
64
+ /**
65
+ * Update system-level view (entity types + tag schemas today).
66
+ * Serialized through a shared chain — see class doc.
67
+ */
68
+ materializeSystem(eventType: string, payload: any): Promise<void>;
69
+ /**
70
+ * Rebuild all materialized views from the event log on startup.
71
+ * Mirrors GraphDBConsumer.rebuildAll() — call this once during
72
+ * createKnowledgeBase before the HTTP server begins accepting requests.
73
+ * Idempotent: existing view files are overwritten.
74
+ */
75
+ rebuildAll(eventLog: RebuildEventSource): Promise<void>;
76
+ /**
77
+ * Get resource view (builds from events if needed)
78
+ * @param resourceId - Branded ResourceId (from @semiont/core)
79
+ * @param events - Stored events for the resource (from @semiont/core)
80
+ * @returns Resource view or null if no events
81
+ */
82
+ getOrMaterialize(resourceId: ResourceId, events: StoredEvent[]): Promise<ResourceView | null>;
83
+ }
84
+ //# sourceMappingURL=view-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-manager.d.ts","sourceRoot":"","sources":["../src/view-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,MAAM,EAAmB,MAAM,eAAe,CAAC;AACrH,OAAO,EAAE,gBAAgB,EAA+B,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAExE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,WAAW;IAEtB,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IAKxC,OAAO,CAAC,cAAc,CAAoC;IAK1D,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAoB;gBAGpD,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,MAAM;IAQjB;;;;;;;OAOG;IACG,mBAAmB,CACvB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,cAAc,EACrB,YAAY,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,GACzC,OAAO,CAAC,IAAI,CAAC;IAMhB;;;OAGG;IACG,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvE;;;;;OAKG;IACG,UAAU,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7D;;;;;OAKG;IACG,gBAAgB,CACpB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,WAAW,EAAE,GACpB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;CAGhC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Projection reducers — the pure core of the system-level projection
3
+ * materializers in `view-materializer.ts`.
4
+ *
5
+ * Each reducer takes the current state of a `__system__` projection and
6
+ * a single command payload, and returns the next state plus any
7
+ * side-effect signals (currently just optional warnings). The
8
+ * surrounding I/O — read JSON file → reduce → write JSON file — lives
9
+ * in the `ViewMaterializer` shell.
10
+ *
11
+ * Why this split: tests for the projection-update semantics shouldn't
12
+ * need the filesystem or an Apple-container event store to assert
13
+ * "registering identical content twice is a no-op." The shell is
14
+ * already covered by integration tests at `tag-schemas-reader.test.ts`
15
+ * (Stower → materializer → reader round-trip) and
16
+ * `local-transport.test.ts` (real client → bus → cache invalidation).
17
+ *
18
+ * The reducers also become the natural home for the deferred schema-
19
+ * evolution work in `.plans/EVOLVE-TAG-SCHEMA.md` — migration
20
+ * commands (rename/remove a category, version-bump a schema id) are
21
+ * additional pure functions on the same view shapes.
22
+ *
23
+ * Load-bearing properties (sortedness, uniqueness, idempotence,
24
+ * most-recent-wins, no-mutation) are pinned by axiom-style fast-check
25
+ * tests in `__tests__/views/projection-reducers.test.ts`. See
26
+ * `docs/system/PROJECTION-PATTERN.md` for the full axiom catalog and
27
+ * the architectural narrative.
28
+ */
29
+ import type { TagSchema } from '@semiont/core';
30
+ /**
31
+ * Apply a `frame:entity-type-added` event to the entity-types
32
+ * projection. Idempotent — dedup via a `Set` + locale-aware sort.
33
+ *
34
+ * Sort uses `localeCompare` to match {@link applyTagSchemaAdded}'s
35
+ * id-sort and to give a sensible ordering across mixed-case + symbol
36
+ * tags. (The pre-refactor materializer used `Array.sort()` with no
37
+ * comparator, which is codepoint order — `_x` would sort *after* `A`
38
+ * because `_` (0x5F) > `A` (0x41). Surfaced by an axiom test in
39
+ * `projection-reducers.test.ts`.)
40
+ */
41
+ export declare function applyEntityTypeAdded(current: readonly string[], add: string): string[];
42
+ /**
43
+ * Result of {@link applyTagSchemaAdded}.
44
+ *
45
+ * The reducer is pure — it doesn't log warnings itself. Instead it
46
+ * returns the would-be warning as data; the I/O shell decides whether
47
+ * to forward it to the logger. This keeps the function trivially
48
+ * testable and lets callers compose multiple reductions without
49
+ * threading a logger through.
50
+ */
51
+ export interface ApplyTagSchemaAddedResult {
52
+ /** The next state of the tagSchemas list, sorted by id. */
53
+ next: TagSchema[];
54
+ /**
55
+ * Set when an existing schema with the same id was overwritten with
56
+ * differing content (deep-not-equal). Identical re-registrations
57
+ * return `undefined` here — the projection silently no-ops.
58
+ */
59
+ warning?: {
60
+ schemaId: string;
61
+ message: string;
62
+ };
63
+ }
64
+ /**
65
+ * Apply a `frame:tag-schema-added` event to the tag-schemas projection.
66
+ *
67
+ * Most-recent-wins by `schema.id`:
68
+ * - new id → append + sort
69
+ * - existing id with identical content → no-op (warning undefined)
70
+ * - existing id with differing content → replace + warning emitted
71
+ */
72
+ export declare function applyTagSchemaAdded(current: readonly TagSchema[], add: TagSchema): ApplyTagSchemaAddedResult;
73
+ //# sourceMappingURL=projection-reducers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection-reducers.d.ts","sourceRoot":"","sources":["../../src/views/projection-reducers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI/C;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAIV;AAID;;;;;;;;GAQG;AACH,MAAM,WAAW,yBAAyB;IACxC,2DAA2D;IAC3D,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,SAAS,SAAS,EAAE,EAC7B,GAAG,EAAE,SAAS,GACb,yBAAyB,CAqB3B"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * View Materializer - Materialized View Management
3
+ *
4
+ * Materializes resource views from events:
5
+ * - Full view materialization from scratch
6
+ * - Incremental view updates
7
+ * - System-level views (entity types)
8
+ *
9
+ * @see docs/EVENT-STORE.md#viewmaterializer for architecture details
10
+ */
11
+ import type { PersistedEvent, StoredEvent, ResourceId, Logger } from '@semiont/core';
12
+ import type { ViewStorage, ResourceView } from '../storage/view-storage';
13
+ /**
14
+ * Minimal structural type for the event log dependency of `rebuildAll`.
15
+ * Avoids importing the concrete EventLog class from a sibling directory and
16
+ * keeps the materializer independent of the event-log implementation.
17
+ */
18
+ export interface RebuildEventSource {
19
+ getEvents(resourceId: ResourceId): Promise<StoredEvent[]>;
20
+ getAllResourceIds(): Promise<ResourceId[]>;
21
+ }
22
+ export interface ViewMaterializerConfig {
23
+ basePath: string;
24
+ }
25
+ /**
26
+ * ViewMaterializer builds and maintains materialized views from events
27
+ */
28
+ export declare class ViewMaterializer {
29
+ private viewStorage;
30
+ private config;
31
+ private logger?;
32
+ constructor(viewStorage: ViewStorage, config: ViewMaterializerConfig, logger?: Logger);
33
+ /**
34
+ * Materialize resource view from events
35
+ * Loads existing view if cached, otherwise rebuilds from events
36
+ */
37
+ materialize(events: StoredEvent[], resourceId: ResourceId): Promise<ResourceView | null>;
38
+ /**
39
+ * Materialize view incrementally with a single event
40
+ * Falls back to full rebuild if view doesn't exist
41
+ */
42
+ materializeIncremental(resourceId: ResourceId, event: PersistedEvent, getAllEvents: () => Promise<StoredEvent[]>): Promise<void>;
43
+ /**
44
+ * Update the storage-uri index in response to an event.
45
+ *
46
+ * Only yield:created (with storageUri), yield:moved, need index changes.
47
+ * resource.archived / resource.unarchived do NOT modify the index.
48
+ */
49
+ private materializeStorageUriIndex;
50
+ /**
51
+ * Materialize view from event list (full rebuild)
52
+ */
53
+ private materializeFromEvents;
54
+ /**
55
+ * Apply an event to ResourceDescriptor state (metadata only)
56
+ */
57
+ private applyEventToResource;
58
+ /**
59
+ * Apply an event to ResourceAnnotations (annotation collections only)
60
+ */
61
+ private applyEventToAnnotations;
62
+ /**
63
+ * Walk every event stream in the event log and materialize the corresponding
64
+ * view from scratch. Idempotent: existing view files are overwritten.
65
+ *
66
+ * Mirrors GraphDBConsumer.rebuildAll() and Smelter.rebuildAll() — this is the
67
+ * recovery path that makes the ephemeral stateDir safe to wipe. The live
68
+ * append path (EventStore.appendEvent → materializeIncremental /
69
+ * materializeEntityTypes / materializeTagSchemas) is unchanged and runs in
70
+ * addition.
71
+ */
72
+ rebuildAll(eventLog: RebuildEventSource): Promise<void>;
73
+ /**
74
+ * Materialize entity types view — System-level view.
75
+ *
76
+ * I/O shell around the pure {@link applyEntityTypeAdded} reducer:
77
+ * read JSON file → reduce → write JSON file. The reducer owns the
78
+ * dedup + sort semantics; the shell owns the disk I/O.
79
+ */
80
+ materializeEntityTypes(entityType: string): Promise<void>;
81
+ /**
82
+ * Materialize tag schemas view — System-level view.
83
+ *
84
+ * I/O shell around the pure {@link applyTagSchemaAdded} reducer.
85
+ * The reducer owns the most-recent-wins semantics + the
86
+ * differing-content-overwrite-warning; the shell forwards the
87
+ * warning (when present) to this materializer's logger and writes
88
+ * the resulting state to disk.
89
+ */
90
+ materializeTagSchemas(schema: import('@semiont/core').TagSchema): Promise<void>;
91
+ }
92
+ //# sourceMappingURL=view-materializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-materializer.d.ts","sourceRoot":"","sources":["../../src/views/view-materializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAYH,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EAEX,UAAU,EACV,MAAM,EACP,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGzE;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1D,iBAAiB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAIzB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,MAAM;IAJhB,OAAO,CAAC,MAAM,CAAC,CAAS;gBAGd,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,sBAAsB,EACtC,MAAM,CAAC,EAAE,MAAM;IAKjB;;;OAGG;IACG,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAkB9F;;;OAGG;IACG,sBAAsB,CAC1B,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,cAAc,EACrB,YAAY,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,GACzC,OAAO,CAAC,IAAI,CAAC;IA4BhB;;;;;OAKG;YACW,0BAA0B;IAYxC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAiC7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA0I5B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IA6E/B;;;;;;;;;OASG;IACG,UAAU,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqD7D;;;;;;OAMG;IACG,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB/D;;;;;;;;OAQG;IACG,qBAAqB,CAAC,MAAM,EAAE,OAAO,eAAe,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CA4BtF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/event-sourcing",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Event sourcing infrastructure for Semiont - EventLog, EventBus, and ViewManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "scripts": {
31
31
  "typecheck": "tsc --noEmit",
32
- "build": "npm run typecheck && tsup",
32
+ "build": "npm run typecheck && tsup && tsc -p tsconfig.build.json",
33
33
  "watch": "tsup --watch",
34
34
  "clean": "rm -rf dist *.tsbuildinfo",
35
35
  "test": "vitest run",
@@ -50,8 +50,9 @@
50
50
  "devDependencies": {
51
51
  "@types/uuid": "^10.0.0",
52
52
  "@vitest/coverage-v8": "^4.1.0",
53
+ "fast-check": "^4.3.0",
53
54
  "tsup": "^8.5.1",
54
- "typescript": "^5.6.3"
55
+ "typescript": "^6.0.2"
55
56
  },
56
57
  "dependencies": {
57
58
  "@semiont/api-client": "*",