@objectstack/core 3.0.3 → 3.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/core",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Microkernel Core for ObjectStack",
6
6
  "type": "module",
@@ -22,7 +22,7 @@
22
22
  "pino": "^10.3.0",
23
23
  "pino-pretty": "^13.1.3",
24
24
  "zod": "^4.3.6",
25
- "@objectstack/spec": "3.0.3"
25
+ "@objectstack/spec": "3.0.5"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "pino": "^8.0.0"
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createMemoryCache } from './memory-cache';
3
+ import { createMemoryQueue } from './memory-queue';
4
+ import { createMemoryJob } from './memory-job';
5
+ import { CORE_FALLBACK_FACTORIES } from './index';
6
+
7
+ describe('CORE_FALLBACK_FACTORIES', () => {
8
+ it('should have exactly 3 entries: cache, queue, job', () => {
9
+ expect(Object.keys(CORE_FALLBACK_FACTORIES)).toEqual(['cache', 'queue', 'job']);
10
+ });
11
+
12
+ it('should map to factory functions', () => {
13
+ for (const factory of Object.values(CORE_FALLBACK_FACTORIES)) {
14
+ expect(typeof factory).toBe('function');
15
+ }
16
+ });
17
+ });
18
+
19
+ describe('createMemoryCache', () => {
20
+ it('should return an object with _fallback: true', () => {
21
+ const cache = createMemoryCache();
22
+ expect(cache._fallback).toBe(true);
23
+ expect(cache._serviceName).toBe('cache');
24
+ });
25
+
26
+ it('should set and get a value', async () => {
27
+ const cache = createMemoryCache();
28
+ await cache.set('key1', 'value1');
29
+ expect(await cache.get('key1')).toBe('value1');
30
+ });
31
+
32
+ it('should return undefined for missing key', async () => {
33
+ const cache = createMemoryCache();
34
+ expect(await cache.get('nonexistent')).toBeUndefined();
35
+ });
36
+
37
+ it('should delete a key', async () => {
38
+ const cache = createMemoryCache();
39
+ await cache.set('key1', 'value1');
40
+ expect(await cache.delete('key1')).toBe(true);
41
+ expect(await cache.get('key1')).toBeUndefined();
42
+ });
43
+
44
+ it('should check if a key exists with has()', async () => {
45
+ const cache = createMemoryCache();
46
+ expect(await cache.has('key1')).toBe(false);
47
+ await cache.set('key1', 'value1');
48
+ expect(await cache.has('key1')).toBe(true);
49
+ });
50
+
51
+ it('should clear all entries', async () => {
52
+ const cache = createMemoryCache();
53
+ await cache.set('a', 1);
54
+ await cache.set('b', 2);
55
+ await cache.clear();
56
+ expect(await cache.has('a')).toBe(false);
57
+ expect(await cache.has('b')).toBe(false);
58
+ });
59
+
60
+ it('should expire entries based on TTL', async () => {
61
+ const cache = createMemoryCache();
62
+ // Set with very short TTL (0.001 seconds = 1ms)
63
+ await cache.set('temp', 'data', 0.001);
64
+ // Wait for expiry
65
+ await new Promise(r => setTimeout(r, 20));
66
+ expect(await cache.get('temp')).toBeUndefined();
67
+ });
68
+
69
+ it('should track hit/miss stats', async () => {
70
+ const cache = createMemoryCache();
71
+ await cache.set('key1', 'value1');
72
+ await cache.get('key1'); // hit
73
+ await cache.get('missing'); // miss
74
+ const stats = await cache.stats();
75
+ expect(stats.hits).toBe(1);
76
+ expect(stats.misses).toBe(1);
77
+ expect(stats.keyCount).toBe(1);
78
+ });
79
+ });
80
+
81
+ describe('createMemoryQueue', () => {
82
+ it('should return an object with _fallback: true', () => {
83
+ const queue = createMemoryQueue();
84
+ expect(queue._fallback).toBe(true);
85
+ expect(queue._serviceName).toBe('queue');
86
+ });
87
+
88
+ it('should publish and deliver to subscriber synchronously', async () => {
89
+ const queue = createMemoryQueue();
90
+ const received: any[] = [];
91
+ await queue.subscribe('test-q', async (msg: any) => { received.push(msg); });
92
+ const id = await queue.publish('test-q', { hello: 'world' });
93
+ expect(id).toMatch(/^fallback-msg-/);
94
+ expect(received).toHaveLength(1);
95
+ expect(received[0].data).toEqual({ hello: 'world' });
96
+ });
97
+
98
+ it('should not deliver to unsubscribed queue', async () => {
99
+ const queue = createMemoryQueue();
100
+ const received: any[] = [];
101
+ await queue.subscribe('q1', async (msg: any) => { received.push(msg); });
102
+ await queue.unsubscribe('q1');
103
+ await queue.publish('q1', 'data');
104
+ expect(received).toHaveLength(0);
105
+ });
106
+
107
+ it('should return queue size of 0', async () => {
108
+ const queue = createMemoryQueue();
109
+ expect(await queue.getQueueSize()).toBe(0);
110
+ });
111
+
112
+ it('should purge a queue', async () => {
113
+ const queue = createMemoryQueue();
114
+ const received: any[] = [];
115
+ await queue.subscribe('q1', async (msg: any) => { received.push(msg); });
116
+ await queue.purge('q1');
117
+ await queue.publish('q1', 'data');
118
+ expect(received).toHaveLength(0);
119
+ });
120
+ });
121
+
122
+ describe('createMemoryJob', () => {
123
+ it('should return an object with _fallback: true', () => {
124
+ const job = createMemoryJob();
125
+ expect(job._fallback).toBe(true);
126
+ expect(job._serviceName).toBe('job');
127
+ });
128
+
129
+ it('should schedule and list jobs', async () => {
130
+ const job = createMemoryJob();
131
+ await job.schedule('daily-report', '0 0 * * *', async () => {});
132
+ expect(await job.listJobs()).toEqual(['daily-report']);
133
+ });
134
+
135
+ it('should cancel a job', async () => {
136
+ const job = createMemoryJob();
137
+ await job.schedule('temp-job', '* * * * *', async () => {});
138
+ await job.cancel('temp-job');
139
+ expect(await job.listJobs()).toEqual([]);
140
+ });
141
+
142
+ it('should trigger a job handler', async () => {
143
+ const job = createMemoryJob();
144
+ let triggered = false;
145
+ await job.schedule('my-job', '* * * * *', async (ctx: any) => {
146
+ triggered = true;
147
+ expect(ctx.jobId).toBe('my-job');
148
+ expect(ctx.data).toEqual({ key: 'val' });
149
+ });
150
+ await job.trigger('my-job', { key: 'val' });
151
+ expect(triggered).toBe(true);
152
+ });
153
+
154
+ it('should return empty executions', async () => {
155
+ const job = createMemoryJob();
156
+ expect(await job.getExecutions()).toEqual([]);
157
+ });
158
+ });
@@ -0,0 +1,20 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { createMemoryCache } from './memory-cache.js';
4
+ import { createMemoryQueue } from './memory-queue.js';
5
+ import { createMemoryJob } from './memory-job.js';
6
+
7
+ export { createMemoryCache } from './memory-cache.js';
8
+ export { createMemoryQueue } from './memory-queue.js';
9
+ export { createMemoryJob } from './memory-job.js';
10
+
11
+ /**
12
+ * Map of core-criticality service names to their in-memory fallback factories.
13
+ * Used by ObjectKernel.validateSystemRequirements() to auto-inject fallbacks
14
+ * when no real plugin provides the service.
15
+ */
16
+ export const CORE_FALLBACK_FACTORIES: Record<string, () => Record<string, any>> = {
17
+ cache: createMemoryCache,
18
+ queue: createMemoryQueue,
19
+ job: createMemoryJob,
20
+ };
@@ -0,0 +1,34 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ /**
4
+ * In-memory Map-backed cache fallback.
5
+ *
6
+ * Implements the ICacheService contract with basic get/set/delete/has/clear
7
+ * and TTL expiry. Used by ObjectKernel as an automatic fallback when no
8
+ * real cache plugin (e.g. Redis) is registered.
9
+ */
10
+ export function createMemoryCache() {
11
+ const store = new Map<string, { value: unknown; expires?: number }>();
12
+ let hits = 0;
13
+ let misses = 0;
14
+ return {
15
+ _fallback: true, _serviceName: 'cache',
16
+ async get<T = unknown>(key: string): Promise<T | undefined> {
17
+ const entry = store.get(key);
18
+ if (!entry || (entry.expires && Date.now() > entry.expires)) {
19
+ store.delete(key);
20
+ misses++;
21
+ return undefined;
22
+ }
23
+ hits++;
24
+ return entry.value as T;
25
+ },
26
+ async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {
27
+ store.set(key, { value, expires: ttl ? Date.now() + ttl * 1000 : undefined });
28
+ },
29
+ async delete(key: string): Promise<boolean> { return store.delete(key); },
30
+ async has(key: string): Promise<boolean> { return store.has(key); },
31
+ async clear(): Promise<void> { store.clear(); },
32
+ async stats() { return { hits, misses, keyCount: store.size }; },
33
+ };
34
+ }
@@ -0,0 +1,23 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ /**
4
+ * In-memory job scheduler fallback.
5
+ *
6
+ * Implements the IJobService contract with basic schedule/cancel/trigger
7
+ * operations. Used by ObjectKernel as an automatic fallback when no real
8
+ * job plugin (e.g. Agenda / BullMQ) is registered.
9
+ */
10
+ export function createMemoryJob() {
11
+ const jobs = new Map<string, any>();
12
+ return {
13
+ _fallback: true, _serviceName: 'job',
14
+ async schedule(name: string, schedule: any, handler: any): Promise<void> { jobs.set(name, { schedule, handler }); },
15
+ async cancel(name: string): Promise<void> { jobs.delete(name); },
16
+ async trigger(name: string, data?: unknown): Promise<void> {
17
+ const job = jobs.get(name);
18
+ if (job?.handler) await job.handler({ jobId: name, data });
19
+ },
20
+ async getExecutions(): Promise<any[]> { return []; },
21
+ async listJobs(): Promise<string[]> { return [...jobs.keys()]; },
22
+ };
23
+ }
@@ -0,0 +1,28 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ /**
4
+ * In-memory publish/subscribe queue fallback.
5
+ *
6
+ * Implements the IQueueService contract with synchronous in-process delivery.
7
+ * Used by ObjectKernel as an automatic fallback when no real queue plugin
8
+ * (e.g. BullMQ / RabbitMQ) is registered.
9
+ */
10
+ export function createMemoryQueue() {
11
+ const handlers = new Map<string, Function[]>();
12
+ let msgId = 0;
13
+ return {
14
+ _fallback: true, _serviceName: 'queue',
15
+ async publish<T = unknown>(queue: string, data: T): Promise<string> {
16
+ const id = `fallback-msg-${++msgId}`;
17
+ const fns = handlers.get(queue) ?? [];
18
+ for (const fn of fns) fn({ id, data, attempts: 1, timestamp: Date.now() });
19
+ return id;
20
+ },
21
+ async subscribe(queue: string, handler: (msg: any) => Promise<void>): Promise<void> {
22
+ handlers.set(queue, [...(handlers.get(queue) ?? []), handler]);
23
+ },
24
+ async unsubscribe(queue: string): Promise<void> { handlers.delete(queue); },
25
+ async getQueueSize(): Promise<number> { return 0; },
26
+ async purge(queue: string): Promise<void> { handlers.delete(queue); },
27
+ };
28
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,9 @@ export * from './security/index.js';
23
23
  // Export environment utilities
24
24
  export * from './utils/env.js';
25
25
 
26
+ // Export in-memory fallbacks for core-criticality services
27
+ export * from './fallbacks/index.js';
28
+
26
29
  // Export Phase 2 components - Advanced lifecycle management
27
30
  export * from './health-monitor.js';
28
31
  export * from './hot-reload.js';
package/src/kernel.ts CHANGED
@@ -6,6 +6,7 @@ import type { LoggerConfig } from '@objectstack/spec/system';
6
6
  import { ServiceRequirementDef } from '@objectstack/spec/system';
7
7
  import { PluginLoader, PluginMetadata, ServiceLifecycle, ServiceFactory, PluginStartupResult } from './plugin-loader.js';
8
8
  import { isNode, safeExit } from './utils/env.js';
9
+ import { CORE_FALLBACK_FACTORIES } from './fallbacks/index.js';
9
10
 
10
11
  /**
11
12
  * Enhanced Kernel Configuration
@@ -234,8 +235,16 @@ export class ObjectKernel {
234
235
  this.logger.error(`CRITICAL: Required service missing: ${serviceName}`);
235
236
  missingServices.push(serviceName);
236
237
  } else if (criticality === 'core') {
237
- this.logger.warn(`CORE: Core service missing, functionality may be degraded: ${serviceName}`);
238
- missingCoreServices.push(serviceName);
238
+ // Auto-inject in-memory fallback if available
239
+ const factory = CORE_FALLBACK_FACTORIES[serviceName];
240
+ if (factory) {
241
+ const fallback = factory();
242
+ this.registerService(serviceName, fallback);
243
+ this.logger.warn(`Service '${serviceName}' not provided — using in-memory fallback`);
244
+ } else {
245
+ this.logger.warn(`CORE: Core service missing, functionality may be degraded: ${serviceName}`);
246
+ missingCoreServices.push(serviceName);
247
+ }
239
248
  } else {
240
249
  this.logger.info(`Info: Optional service not present: ${serviceName}`);
241
250
  }