Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… @objectstack/core - Create packages/core/src/fallbacks/ with memory-cache.ts, memory-queue.ts, memory-job.ts - Wire CORE_FALLBACK_FACTORIES into ObjectKernel.validateSystemRequirements() - Export fallbacks from core index.ts - Add comprehensive tests for all fallback implementations - Update plugin-dev to import from @objectstack/core instead of local stubs Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR extracts in-memory fallback implementations for cache, queue, and job services from plugin-dev into @objectstack/core, enabling the ObjectKernel to automatically inject fallbacks when these core-criticality services are missing in production environments. This prevents crashes when code calls ctx.getService('cache') without Redis or similar backing services.
Changes:
- Created new
packages/core/src/fallbacks/directory with Map-backed implementations for cache, queue, and job services - Modified
ObjectKernel.validateSystemRequirements()to auto-inject fallbacks for missing core services usingCORE_FALLBACK_FACTORIES - Updated
plugin-devto import and reuse these fallback factories instead of maintaining duplicate implementations
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/fallbacks/memory-cache.ts | In-memory Map-backed cache with TTL expiry support, extracted from plugin-dev |
| packages/core/src/fallbacks/memory-queue.ts | In-memory publish/subscribe queue with synchronous delivery, extracted from plugin-dev |
| packages/core/src/fallbacks/memory-job.ts | In-memory job scheduler with schedule/cancel/trigger operations, extracted from plugin-dev |
| packages/core/src/fallbacks/index.ts | Exports fallback factories and CORE_FALLBACK_FACTORIES map for kernel consumption |
| packages/core/src/fallbacks/fallbacks.test.ts | Comprehensive unit tests for all three fallback implementations (20 test cases) |
| packages/core/src/kernel.ts | Added auto-injection logic in validateSystemRequirements() to use fallbacks for missing core services |
| packages/core/src/index.ts | Exported fallbacks module for external consumption |
| packages/plugins/plugin-dev/src/dev-plugin.ts | Replaced local stub implementations with imports from @objectstack/core |
| 'cache': createMemoryCache, | ||
| 'queue': createMemoryQueue, | ||
| 'job': createMemoryJob, |
There was a problem hiding this comment.
The createMemoryCache, createMemoryQueue, and createMemoryJob factories now return objects with _fallback: true instead of _dev: true. However, the existing test in dev-plugin.test.ts at line 95 still expects cache._dev to be true. This will cause the test to fail.
When DevPlugin uses these fallback factories, they should retain the _dev marker to distinguish them as development stubs rather than production fallbacks. Consider one of these solutions:
- Modify the fallback factories to accept an optional marker parameter:
createMemoryCache({ marker: '_dev' }) - Wrap the fallback instances here to add the
_devmarker:{ ...createMemoryCache(), _dev: true } - Update the test file to accept either
_devor_fallbackmarkers
| 'cache': createMemoryCache, | |
| 'queue': createMemoryQueue, | |
| 'job': createMemoryJob, | |
| 'cache': () => ({ ...createMemoryCache(), _dev: true }), | |
| 'queue': () => ({ ...createMemoryQueue(), _dev: true }), | |
| 'job': () => ({ ...createMemoryJob(), _dev: true }), |
| import { createMemoryCache } from './memory-cache'; | ||
| import { createMemoryQueue } from './memory-queue'; | ||
| import { createMemoryJob } from './memory-job'; | ||
| import { CORE_FALLBACK_FACTORIES } from './index'; |
There was a problem hiding this comment.
The test file imports are missing .js file extensions. In ESM, all relative imports should include the file extension. The imports should be:
import { createMemoryCache } from './memory-cache.js';
import { createMemoryQueue } from './memory-queue.js';
import { createMemoryJob } from './memory-job.js';
import { CORE_FALLBACK_FACTORIES } from './index.js';This is inconsistent with the production code which correctly uses .js extensions (see index.ts and kernel.ts).
| import { createMemoryCache } from './memory-cache'; | |
| import { createMemoryQueue } from './memory-queue'; | |
| import { createMemoryJob } from './memory-job'; | |
| import { CORE_FALLBACK_FACTORIES } from './index'; | |
| import { createMemoryCache } from './memory-cache.js'; | |
| import { createMemoryQueue } from './memory-queue.js'; | |
| import { createMemoryJob } from './memory-job.js'; | |
| import { CORE_FALLBACK_FACTORIES } from './index.js'; |
| let msgId = 0; | ||
| return { | ||
| _fallback: true, _serviceName: 'queue', | ||
| async publish<T = unknown>(queue: string, data: T): Promise<string> { |
There was a problem hiding this comment.
The publish method should accept an optional options parameter to match the IQueueService contract. The interface specifies:
publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string>;But this implementation is missing the third parameter. It should be:
async publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string>Even if the fallback doesn't implement delay/priority/retries, it should accept the parameter for API compatibility.
| async publish<T = unknown>(queue: string, data: T): Promise<string> { | |
| async publish<T = unknown>(queue: string, data: T, _options?: unknown): Promise<string> { |
|
|
||
| it('should return queue size of 0', async () => { | ||
| const queue = createMemoryQueue(); | ||
| expect(await queue.getQueueSize()).toBe(0); |
There was a problem hiding this comment.
This test calls getQueueSize() without any parameters, but according to the IQueueService contract, it should accept a queue: string parameter. The test should be:
expect(await queue.getQueueSize('test-q')).toBe(0);This test failure would be caught if the CORE_FALLBACK_FACTORIES used proper typing (see comment on index.ts).
| expect(await queue.getQueueSize()).toBe(0); | |
| expect(await queue.getQueueSize('test-q')).toBe(0); |
| handlers.set(queue, [...(handlers.get(queue) ?? []), handler]); | ||
| }, | ||
| async unsubscribe(queue: string): Promise<void> { handlers.delete(queue); }, | ||
| async getQueueSize(): Promise<number> { return 0; }, |
There was a problem hiding this comment.
The getQueueSize method should accept a queue parameter to match the IQueueService contract defined in packages/spec/src/contracts/queue-service.ts. The interface specifies:
getQueueSize?(queue: string): Promise<number>;But this implementation has no parameters. It should be:
async getQueueSize(queue: string): Promise<number> { return 0; },Note: This is a pre-existing issue from the original dev-plugin implementation, but it should be fixed since we're extracting this as a production fallback.
| async getQueueSize(): Promise<number> { return 0; }, | |
| async getQueueSize(queue: string): Promise<number> { return 0; }, |
| // Auto-inject in-memory fallback if available | ||
| const factory = CORE_FALLBACK_FACTORIES[serviceName]; | ||
| if (factory) { | ||
| const fallback = factory(); | ||
| this.registerService(serviceName, fallback); | ||
| this.logger.warn(`Service '${serviceName}' not provided — using in-memory fallback`); | ||
| } else { | ||
| this.logger.warn(`CORE: Core service missing, functionality may be degraded: ${serviceName}`); | ||
| missingCoreServices.push(serviceName); | ||
| } |
There was a problem hiding this comment.
The new auto-injection behavior in ObjectKernel.validateSystemRequirements() lacks integration tests. There should be a test in packages/core/src/kernel.test.ts that verifies:
- When a core-criticality service (cache/queue/job) is missing, the kernel auto-injects the fallback
- The fallback service is properly registered and accessible via
getService() - The appropriate warning is logged
- The fallback has
_fallback: truemarker
Without these tests, regressions in this critical new feature could go undetected.
| export const CORE_FALLBACK_FACTORIES: Record<string, () => Record<string, any>> = { | ||
| cache: createMemoryCache, | ||
| queue: createMemoryQueue, | ||
| job: createMemoryJob, | ||
| }; |
There was a problem hiding this comment.
The type Record<string, () => Record<string, any>> is very loose and doesn't provide type safety. Consider importing the service interfaces and using a more precise type:
import type { ICacheService } from '@objectstack/spec/contracts/cache-service';
import type { IQueueService } from '@objectstack/spec/contracts/queue-service';
import type { IJobService } from '@objectstack/spec/contracts/job-service';
export const CORE_FALLBACK_FACTORIES: {
cache: () => ICacheService;
queue: () => IQueueService;
job: () => IJobService;
} = {
cache: createMemoryCache,
queue: createMemoryQueue,
job: createMemoryJob,
};This would provide better type checking and catch API mismatches at compile time (like the missing parameters in getQueueSize and publish).
| const jobs = new Map<string, any>(); | ||
| return { | ||
| _fallback: true, _serviceName: 'job', | ||
| async schedule(name: string, schedule: any, handler: any): Promise<void> { jobs.set(name, { schedule, handler }); }, |
There was a problem hiding this comment.
The schedule method parameters use any types instead of the proper interfaces defined in IJobService. According to the contract:
schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void>;The parameters should be properly typed. Consider importing the types:
import type { JobSchedule, JobHandler } from '@objectstack/spec/contracts/job-service';
async schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void>This would provide better type safety and catch errors at compile time. Note: This is a pre-existing issue from the original dev-plugin implementation.
| async publish<T = unknown>(queue: string, data: T): Promise<string> { | ||
| const id = `fallback-msg-${++msgId}`; | ||
| const fns = handlers.get(queue) ?? []; | ||
| for (const fn of fns) fn({ id, data, attempts: 1, timestamp: Date.now() }); |
There was a problem hiding this comment.
The handler functions are invoked without await, which means:
- Errors in handlers will result in unhandled promise rejections
- The publish operation completes immediately without waiting for handlers
Consider either:
- Awaiting handlers sequentially:
for (const fn of fns) await fn({ ... }) - Firing handlers in parallel and catching errors:
Promise.all(fns.map(fn => fn({ ... }).catch(err => console.error(err))))
The current synchronous delivery is documented in the JSDoc comment, so if this is intentional for the fallback's simplicity, at least wrap the handler call in a try-catch or .catch() to prevent unhandled rejections.
| for (const fn of fns) fn({ id, data, attempts: 1, timestamp: Date.now() }); | |
| for (const fn of fns) { | |
| try { | |
| const result = fn({ id, data, attempts: 1, timestamp: Date.now() }); | |
| if (result && typeof (result as any).catch === 'function') { | |
| (result as Promise<unknown>).catch((err: unknown) => { | |
| console.error('[memory-queue] handler error for queue "%s":', queue, err); | |
| }); | |
| } | |
| } catch (err) { | |
| console.error('[memory-queue] handler threw synchronously for queue "%s":', queue, err); | |
| } | |
| } |
ObjectKernel.validateSystemRequirements()logs a warning for missing core-criticality services but provides no fallback — soctx.getService('cache')crashes in production without Redis. Working Map-backed implementations already exist inDevPluginbut are inaccessible to the kernel.Changes
packages/core/src/fallbacks/— extractedcreateMemoryCache,createMemoryQueue,createMemoryJobfactories fromplugin-dev, marked_fallback: trueinstead of_dev: trueCORE_FALLBACK_FACTORIES— exported map of{ cache, queue, job }→ factory functionsObjectKernel.validateSystemRequirements()— auto-injects fallback when a core service is missing and a factory exists:plugin-dev— replaced localcreateCacheStub/createQueueStub/createJobStubwith imports from@objectstack/coreCORE_FALLBACK_FACTORIESshapeNo new dependencies. No changes to
@objectstack/spec.Original prompt
Task: Extract core-level in-memory fallbacks for cache/queue/job into @objectstack/core
Context
ServiceRequirementDefinpackages/spec/src/system/core-services.zod.tsdefines cache, queue, job as'core'criticality — meaning the kernel warns but continues if they're missing. CurrentlyObjectKernel.validateSystemRequirements()inpackages/core/src/kernel.tsonly logs a warning for missing core services but provides NO fallback. This means any code callingctx.getService('cache')in production without Redis crashes.Meanwhile,
packages/plugins/plugin-dev/src/dev-plugin.tslines 37-98 already has working in-memory implementations (createCacheStub,createQueueStub,createJobStub) that are trapped inside the DevPlugin.Goal
Move cache/queue/job in-memory fallback factories from plugin-dev into @objectstack/core so the kernel itself can auto-inject them when no real implementation is registered. This is NOT about DevPlugin — it's about making the kernel self-sufficient for core-criticality services.
Exact Steps
Step 1: Create
packages/core/src/fallbacks/directoryCreate these 4 files:
packages/core/src/fallbacks/memory-cache.tscreateCacheStub()frompackages/plugins/plugin-dev/src/dev-plugin.tslines 37-61createMemoryCache()_dev: truemarker, add_fallback: trueinsteadpackages/core/src/fallbacks/memory-queue.tscreateQueueStub()from dev-plugin.ts lines 64-82createMemoryQueue()_fallback: true, export factorypackages/core/src/fallbacks/memory-job.tscreateJobStub()from dev-plugin.ts lines 85-98createMemoryJob()packages/core/src/fallbacks/index.tsStep 2: Wire into ObjectKernel.validateSystemRequirements()
In
packages/core/src/kernel.ts, modify thevalidateSystemRequirements()method.Current code (around line ~280) does this for
'core'services:Change to:
Import at top of kernel.ts:
Step 3: Export from core index
In
packages/core/src/index.ts, add:Step 4: Add tests
Create
packages/core/src/fallbacks/fallbacks.test.ts:createMemoryCache()implements get/set/delete/has/clear with TTL expirycreateMemoryQueue()implements publish/subscribe with sync deliverycreateMemoryJob()implements schedule/cancel/trigger/listJobsCORE_FALLBACK_FACTORIEShas exactly 3 entries: cache, queue, jobStep 5: Update plugin-dev to import from core
In
packages/plugins/plugin-dev/src/dev-plugin.ts:createCacheStub,createQueueStub,createJobStubwith imports from@objectstack/core:DEV_STUB_FACTORIESmap to use the imported factories:Rules
packages/core/andpackages/plugins/plugin-dev/ServiceRequirementDefin spec✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.