@fjell/core 4.4.49 → 4.4.50

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,178 @@
1
+ import { ComKey, PriKey } from '../keys';
2
+ import { Item } from '../items';
3
+
4
+ /**
5
+ * Base event interface that all events extend.
6
+ * Provides core event properties with full type safety using the existing PriKey/ComKey system.
7
+ */
8
+ export interface BaseEvent<
9
+ S extends string,
10
+ L1 extends string = never,
11
+ L2 extends string = never,
12
+ L3 extends string = never,
13
+ L4 extends string = never,
14
+ L5 extends string = never
15
+ > {
16
+ /** Type of event - "create", "update", "delete", etc. */
17
+ eventType: string;
18
+
19
+ /** The key of the item that was affected - maintains full type safety */
20
+ key: PriKey<S> | ComKey<S, L1, L2, L3, L4, L5>;
21
+
22
+ /** Which storage backend(s) generated this event - enables filtering by implementation */
23
+ scopes: string[];
24
+
25
+ /** When the event occurred */
26
+ timestamp: Date;
27
+
28
+ /** Optional: the full item content - fully typed, no loss of type information */
29
+ item?: Item<S, L1, L2, L3, L4, L5>;
30
+ }
31
+
32
+ /**
33
+ * Event emitted when an item is created.
34
+ * The item property is required since we always have the created item data.
35
+ */
36
+ export interface CreateEvent<
37
+ S extends string,
38
+ L1 extends string = never,
39
+ L2 extends string = never,
40
+ L3 extends string = never,
41
+ L4 extends string = never,
42
+ L5 extends string = never
43
+ > extends BaseEvent<S, L1, L2, L3, L4, L5> {
44
+ eventType: 'create';
45
+ /** The created item - always available for create events */
46
+ item: Item<S, L1, L2, L3, L4, L5>;
47
+ }
48
+
49
+ /**
50
+ * Event emitted when an item is updated.
51
+ * Provides detailed change tracking with before/after states.
52
+ */
53
+ export interface UpdateEvent<
54
+ S extends string,
55
+ L1 extends string = never,
56
+ L2 extends string = never,
57
+ L3 extends string = never,
58
+ L4 extends string = never,
59
+ L5 extends string = never
60
+ > extends BaseEvent<S, L1, L2, L3, L4, L5> {
61
+ eventType: 'update';
62
+ /** List of field names that were changed */
63
+ changes: string[];
64
+ /** Optional: item state before the update */
65
+ before?: Item<S, L1, L2, L3, L4, L5>;
66
+ /** Optional: item state after the update */
67
+ after?: Item<S, L1, L2, L3, L4, L5>;
68
+ // Note: item property (from BaseEvent) contains current state if provided
69
+ }
70
+
71
+ /**
72
+ * Event emitted when an item is deleted.
73
+ * May include the deleted item data for cleanup/undo operations.
74
+ */
75
+ export interface DeleteEvent<
76
+ S extends string,
77
+ L1 extends string = never,
78
+ L2 extends string = never,
79
+ L3 extends string = never,
80
+ L4 extends string = never,
81
+ L5 extends string = never
82
+ > extends BaseEvent<S, L1, L2, L3, L4, L5> {
83
+ eventType: 'delete';
84
+ /** Optional: the deleted item content - useful for cleanup/undo */
85
+ item?: Item<S, L1, L2, L3, L4, L5>;
86
+ }
87
+
88
+ /**
89
+ * Event emitted when a custom action is performed on an item.
90
+ * Allows libraries to define custom event types beyond standard CRUD.
91
+ */
92
+ export interface ActionEvent<
93
+ S extends string,
94
+ L1 extends string = never,
95
+ L2 extends string = never,
96
+ L3 extends string = never,
97
+ L4 extends string = never,
98
+ L5 extends string = never
99
+ > extends BaseEvent<S, L1, L2, L3, L4, L5> {
100
+ eventType: 'action';
101
+ /** Name of the action that was performed */
102
+ actionName: string;
103
+ /** Optional: action-specific data */
104
+ actionData?: Record<string, unknown>;
105
+ }
106
+
107
+ /**
108
+ * Union type of all standard event types.
109
+ * Libraries can extend this with custom events if needed.
110
+ */
111
+ export type Event<
112
+ S extends string,
113
+ L1 extends string = never,
114
+ L2 extends string = never,
115
+ L3 extends string = never,
116
+ L4 extends string = never,
117
+ L5 extends string = never
118
+ > =
119
+ | CreateEvent<S, L1, L2, L3, L4, L5>
120
+ | UpdateEvent<S, L1, L2, L3, L4, L5>
121
+ | DeleteEvent<S, L1, L2, L3, L4, L5>
122
+ | ActionEvent<S, L1, L2, L3, L4, L5>;
123
+
124
+ /**
125
+ * Type guard to check if an event is a CreateEvent
126
+ */
127
+ export function isCreateEvent<
128
+ S extends string,
129
+ L1 extends string = never,
130
+ L2 extends string = never,
131
+ L3 extends string = never,
132
+ L4 extends string = never,
133
+ L5 extends string = never
134
+ >(event: BaseEvent<S, L1, L2, L3, L4, L5>): event is CreateEvent<S, L1, L2, L3, L4, L5> {
135
+ return event.eventType === 'create';
136
+ }
137
+
138
+ /**
139
+ * Type guard to check if an event is an UpdateEvent
140
+ */
141
+ export function isUpdateEvent<
142
+ S extends string,
143
+ L1 extends string = never,
144
+ L2 extends string = never,
145
+ L3 extends string = never,
146
+ L4 extends string = never,
147
+ L5 extends string = never
148
+ >(event: BaseEvent<S, L1, L2, L3, L4, L5>): event is UpdateEvent<S, L1, L2, L3, L4, L5> {
149
+ return event.eventType === 'update';
150
+ }
151
+
152
+ /**
153
+ * Type guard to check if an event is a DeleteEvent
154
+ */
155
+ export function isDeleteEvent<
156
+ S extends string,
157
+ L1 extends string = never,
158
+ L2 extends string = never,
159
+ L3 extends string = never,
160
+ L4 extends string = never,
161
+ L5 extends string = never
162
+ >(event: BaseEvent<S, L1, L2, L3, L4, L5>): event is DeleteEvent<S, L1, L2, L3, L4, L5> {
163
+ return event.eventType === 'delete';
164
+ }
165
+
166
+ /**
167
+ * Type guard to check if an event is an ActionEvent
168
+ */
169
+ export function isActionEvent<
170
+ S extends string,
171
+ L1 extends string = never,
172
+ L2 extends string = never,
173
+ L3 extends string = never,
174
+ L4 extends string = never,
175
+ L5 extends string = never
176
+ >(event: BaseEvent<S, L1, L2, L3, L4, L5>): event is ActionEvent<S, L1, L2, L3, L4, L5> {
177
+ return event.eventType === 'action';
178
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @fileoverview Event System Public API
3
+ *
4
+ * This module exports all the public interfaces and utilities for the Fjell event system.
5
+ * The event system provides type-safe, item-level change events with full PriKey/ComKey integration.
6
+ *
7
+ * Key Features:
8
+ * - Full type safety using existing PriKey/ComKey system
9
+ * - Storage-agnostic event interfaces
10
+ * - Item-specific and location-based subscriptions
11
+ * - Separate EventEmitters per item type for optimal type safety
12
+ * - Real-time awareness for application needs (not reliable business execution)
13
+ *
14
+ * Usage:
15
+ * - Libraries implement EventEmitter/EventSubscriber interfaces
16
+ * - Applications subscribe to events through library instances
17
+ * - Events are delivered through callback functions with full type safety
18
+ */
19
+
20
+ // Core Event Types
21
+ export {
22
+ BaseEvent,
23
+ CreateEvent,
24
+ UpdateEvent,
25
+ DeleteEvent,
26
+ ActionEvent,
27
+ Event,
28
+ isCreateEvent,
29
+ isUpdateEvent,
30
+ isDeleteEvent,
31
+ isActionEvent,
32
+ } from './events';
33
+
34
+ // Subscription Interfaces
35
+ export {
36
+ BaseSubscription,
37
+ ItemSubscription,
38
+ LocationSubscription,
39
+ Subscription,
40
+ SubscriptionOptions,
41
+ isItemSubscription,
42
+ isLocationSubscription,
43
+ generateSubscriptionId,
44
+ createItemSubscription,
45
+ createLocationSubscription,
46
+ } from './subscription';
47
+
48
+ // Event Emitter/Subscriber Interfaces
49
+ export {
50
+ EventEmitter,
51
+ ScopedEventEmitter,
52
+ EventSubscriber,
53
+ EventSystem,
54
+ EventSystemFactory,
55
+ // Type aliases for common patterns
56
+ UserEventEmitter,
57
+ UserEventSubscriber,
58
+ UserEventSystem,
59
+ MessageEventEmitter,
60
+ MessageEventSubscriber,
61
+ MessageEventSystem,
62
+ } from './emitter';
63
+
64
+ // Subscription Matching Logic
65
+ export {
66
+ doesEventMatchSubscription,
67
+ doesScopeMatch,
68
+ doesEventTypeMatch,
69
+ doesKeyMatch,
70
+ doesKeyMatchLocation,
71
+ doesLocationMatch,
72
+ findMatchingSubscriptions,
73
+ extractLocationValues,
74
+ compareLocationValues,
75
+ } from './matching';
76
+
77
+ // Shared Types and Utilities
78
+ export {
79
+ STANDARD_EVENT_TYPES,
80
+ StandardEventType,
81
+ STANDARD_SCOPES,
82
+ StandardScope,
83
+ SubscriptionStatus,
84
+ SubscriptionMetadata,
85
+ ManagedSubscription,
86
+ EventHandler,
87
+ SafeEventHandler,
88
+ EventBatch,
89
+ EventStats,
90
+ EventSystemConfig,
91
+ DEFAULT_EVENT_CONFIG,
92
+ EventSystemError,
93
+ SubscriptionError,
94
+ EventEmissionError,
95
+ EventMatchingError,
96
+ createEventSystemError,
97
+ isEventSystemError,
98
+ ExtractItemType,
99
+ ExtractEventTypes,
100
+ } from './types';
101
+
102
+ /**
103
+ * Version of the event system API.
104
+ * Used for compatibility checking and debugging.
105
+ */
106
+ export const EVENT_SYSTEM_VERSION = '1.0.0';
107
+
108
+ /**
109
+ * Supported event types for reference.
110
+ * Libraries should use these standard types for consistency.
111
+ */
112
+ export const SUPPORTED_EVENT_TYPES = [
113
+ 'create',
114
+ 'update',
115
+ 'delete',
116
+ 'action',
117
+ ] as const;
118
+
119
+ /**
120
+ * Supported storage scopes for reference.
121
+ * Libraries should use these standard scopes for consistency.
122
+ */
123
+ export const SUPPORTED_SCOPES = [
124
+ 'firestore',
125
+ 'sequelize',
126
+ 'postgresql',
127
+ 'mysql',
128
+ 'mongodb',
129
+ 'redis',
130
+ ] as const;
@@ -0,0 +1,264 @@
1
+ import { ComKey, ItemTypeArray, LocKeyArray, PriKey } from '../keys';
2
+ import { isComKey, isPriKey } from '../operations/Operations';
3
+ import { BaseEvent } from './events';
4
+ import { isItemSubscription, isLocationSubscription, Subscription } from './subscription';
5
+
6
+ /**
7
+ * Core subscription matching logic.
8
+ * Determines whether an event should be delivered to a specific subscription.
9
+ */
10
+ export function doesEventMatchSubscription<
11
+ S extends string,
12
+ L1 extends string = never,
13
+ L2 extends string = never,
14
+ L3 extends string = never,
15
+ L4 extends string = never,
16
+ L5 extends string = never
17
+ >(
18
+ event: BaseEvent<S, L1, L2, L3, L4, L5>,
19
+ subscription: Subscription<S, L1, L2, L3, L4, L5>
20
+ ): boolean {
21
+ // Check scope compatibility first (most efficient filter)
22
+ if (!doesScopeMatch(event.scopes, subscription.scopes)) {
23
+ return false;
24
+ }
25
+
26
+ // Check event type compatibility
27
+ if (!doesEventTypeMatch(event.eventType, subscription.eventTypes)) {
28
+ return false;
29
+ }
30
+
31
+ // Check key/location pattern matching
32
+ if (isItemSubscription(subscription)) {
33
+ return doesKeyMatch(event.key, subscription.key);
34
+ } else if (isLocationSubscription(subscription)) {
35
+ return doesKeyMatchLocation(event.key, subscription.kta, subscription.location);
36
+ }
37
+
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * Check if event scopes match subscription scope requirements.
43
+ *
44
+ * @param eventScopes - Scopes from the event (e.g., ["firestore"])
45
+ * @param subscriptionScopes - Optional scopes required by subscription
46
+ * @returns true if scopes are compatible
47
+ */
48
+ export function doesScopeMatch(
49
+ eventScopes: string[],
50
+ subscriptionScopes?: string[]
51
+ ): boolean {
52
+ // If subscription doesn't specify scopes, accept events from any scope
53
+ if (!subscriptionScopes || subscriptionScopes.length === 0) {
54
+ return true;
55
+ }
56
+
57
+ // Check if any of the event's scopes match any of the subscription's required scopes
58
+ return subscriptionScopes.some(requiredScope =>
59
+ eventScopes.includes(requiredScope)
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Check if event type matches subscription event type requirements.
65
+ *
66
+ * @param eventType - Type from the event (e.g., "create", "update")
67
+ * @param subscriptionEventTypes - Optional event types required by subscription
68
+ * @returns true if event type is compatible
69
+ */
70
+ export function doesEventTypeMatch(
71
+ eventType: string,
72
+ subscriptionEventTypes?: string[]
73
+ ): boolean {
74
+ // If subscription doesn't specify event types, accept all event types
75
+ if (!subscriptionEventTypes || subscriptionEventTypes.length === 0) {
76
+ return true;
77
+ }
78
+
79
+ // Check if the event type is in the subscription's allowed types
80
+ return subscriptionEventTypes.includes(eventType);
81
+ }
82
+
83
+ /**
84
+ * Check if two keys are exactly equal.
85
+ * Used for item-based subscriptions that want events for a specific key.
86
+ */
87
+ export function doesKeyMatch<
88
+ S extends string,
89
+ L1 extends string = never,
90
+ L2 extends string = never,
91
+ L3 extends string = never,
92
+ L4 extends string = never,
93
+ L5 extends string = never
94
+ >(
95
+ eventKey: PriKey<S> | ComKey<S, L1, L2, L3, L4, L5>,
96
+ subscriptionKey: PriKey<S> | ComKey<S, L1, L2, L3, L4, L5>
97
+ ): boolean {
98
+ // Both must be the same type (PriKey or ComKey)
99
+ if (isPriKey(eventKey) && isPriKey(subscriptionKey)) {
100
+ return eventKey.pk === subscriptionKey.pk && eventKey.kt === subscriptionKey.kt;
101
+ }
102
+
103
+ if (isComKey(eventKey) && isComKey(subscriptionKey)) {
104
+ const eventComKey = eventKey as ComKey<S, L1, L2, L3, L4, L5>;
105
+ const subscriptionComKey = subscriptionKey as ComKey<S, L1, L2, L3, L4, L5>;
106
+
107
+ // Compare primary key and key type
108
+ if (eventComKey.pk !== subscriptionComKey.pk || eventComKey.kt !== subscriptionComKey.kt) {
109
+ return false;
110
+ }
111
+
112
+ // Compare location arrays
113
+ if (eventComKey.loc.length !== subscriptionComKey.loc.length) {
114
+ return false;
115
+ }
116
+
117
+ // Check each location key
118
+ return eventComKey.loc.every((eventLocKey, index) => {
119
+ const subLocKey = subscriptionComKey.loc[index];
120
+ return eventLocKey.lk === subLocKey.lk && eventLocKey.kt === subLocKey.kt;
121
+ });
122
+ }
123
+
124
+ return false; // Different key types don't match
125
+ }
126
+
127
+ /**
128
+ * Check if an event key matches a location-based subscription.
129
+ * This is more complex as it needs to determine if the event key is "within" the subscription location.
130
+ */
131
+ export function doesKeyMatchLocation<
132
+ S extends string,
133
+ L1 extends string = never,
134
+ L2 extends string = never,
135
+ L3 extends string = never,
136
+ L4 extends string = never,
137
+ L5 extends string = never
138
+ >(
139
+ eventKey: PriKey<S> | ComKey<S, L1, L2, L3, L4, L5>,
140
+ subscriptionKta: ItemTypeArray<S, L1, L2, L3, L4, L5>,
141
+ subscriptionLocation: LocKeyArray<L1, L2, L3, L4, L5>
142
+ ): boolean {
143
+ // First, check if the key type matches the target type in the KTA
144
+ const targetItemType = subscriptionKta[subscriptionKta.length - 1];
145
+ if (eventKey.kt !== targetItemType) {
146
+ return false;
147
+ }
148
+
149
+ // For PriKey events
150
+ if (isPriKey(eventKey)) {
151
+ // PriKey can only match location subscriptions with empty locations (root level)
152
+ return subscriptionLocation.length === 0;
153
+ }
154
+
155
+ // For ComKey events
156
+ if (isComKey(eventKey)) {
157
+ const comKey = eventKey as ComKey<S, L1, L2, L3, L4, L5>;
158
+ // The event's location must match the subscription location exactly or be a sub-location
159
+ return doesLocationMatch(comKey.loc, subscriptionLocation, subscriptionKta);
160
+ }
161
+
162
+ return false;
163
+ }
164
+
165
+ /**
166
+ * Check if an event's location keys match a subscription's location requirements.
167
+ * This implements the hierarchical location matching logic.
168
+ */
169
+ export function doesLocationMatch<
170
+ L1 extends string = never,
171
+ L2 extends string = never,
172
+ L3 extends string = never,
173
+ L4 extends string = never,
174
+ L5 extends string = never
175
+ >(
176
+ eventLocation: LocKeyArray<L1, L2, L3, L4, L5>,
177
+ subscriptionLocation: LocKeyArray<L1, L2, L3, L4, L5>,
178
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
179
+ _subscriptionKta: ItemTypeArray<string, L1, L2, L3, L4, L5>
180
+ ): boolean {
181
+ // If subscription location is empty, it matches all locations (root level subscription)
182
+ if (subscriptionLocation.length === 0) {
183
+ return true;
184
+ }
185
+
186
+ // Event location must be at least as deep as subscription location
187
+ if (eventLocation.length < subscriptionLocation.length) {
188
+ return false;
189
+ }
190
+
191
+ // Check that all subscription location keys match the corresponding event location keys
192
+ for (let i = 0; i < subscriptionLocation.length; i++) {
193
+ const eventLocKey = eventLocation[i];
194
+ const subLocKey = subscriptionLocation[i];
195
+
196
+ if (!eventLocKey || !subLocKey) {
197
+ return false;
198
+ }
199
+
200
+ if (eventLocKey.lk !== subLocKey.lk || eventLocKey.kt !== subLocKey.kt) {
201
+ return false;
202
+ }
203
+ }
204
+
205
+ return true;
206
+ }
207
+
208
+ /**
209
+ * Find all subscriptions that match a given event.
210
+ * Used by EventSubscriber implementations to determine which subscriptions should receive an event.
211
+ */
212
+ export function findMatchingSubscriptions<
213
+ S extends string,
214
+ L1 extends string = never,
215
+ L2 extends string = never,
216
+ L3 extends string = never,
217
+ L4 extends string = never,
218
+ L5 extends string = never
219
+ >(
220
+ event: BaseEvent<S, L1, L2, L3, L4, L5>,
221
+ subscriptions: Subscription<S, L1, L2, L3, L4, L5>[]
222
+ ): Subscription<S, L1, L2, L3, L4, L5>[] {
223
+ return subscriptions.filter(subscription =>
224
+ doesEventMatchSubscription(event, subscription)
225
+ );
226
+ }
227
+
228
+ /**
229
+ * Utility function to extract the location from a ComKey for comparison purposes.
230
+ * Returns the location key values as strings for easier comparison.
231
+ */
232
+ export function extractLocationValues<
233
+ L1 extends string = never,
234
+ L2 extends string = never,
235
+ L3 extends string = never,
236
+ L4 extends string = never,
237
+ L5 extends string = never
238
+ >(location: LocKeyArray<L1, L2, L3, L4, L5>): string[] {
239
+ return location.map(locKey => String(locKey.lk));
240
+ }
241
+
242
+ /**
243
+ * Utility function to compare two location arrays by their values.
244
+ * Useful for debugging and testing location matching logic.
245
+ */
246
+ export function compareLocationValues<
247
+ L1 extends string = never,
248
+ L2 extends string = never,
249
+ L3 extends string = never,
250
+ L4 extends string = never,
251
+ L5 extends string = never
252
+ >(
253
+ location1: LocKeyArray<L1, L2, L3, L4, L5>,
254
+ location2: LocKeyArray<L1, L2, L3, L4, L5>
255
+ ): boolean {
256
+ if (location1.length !== location2.length) {
257
+ return false;
258
+ }
259
+
260
+ return location1.every((locKey1, index) => {
261
+ const locKey2 = location2[index];
262
+ return locKey1.lk === locKey2.lk && locKey1.kt === locKey2.kt;
263
+ });
264
+ }