Skip to content

Extract core-level in-memory fallbacks for cache/queue/job into @objectstack/core#673

Merged
hotlong merged 2 commits intomainfrom
copilot/extract-in-memory-fallbacks
Feb 14, 2026
Merged

Extract core-level in-memory fallbacks for cache/queue/job into @objectstack/core#673
hotlong merged 2 commits intomainfrom
copilot/extract-in-memory-fallbacks

Conversation

Copy link
Contributor

Copilot AI commented Feb 14, 2026

ObjectKernel.validateSystemRequirements() logs a warning for missing core-criticality services but provides no fallback — so ctx.getService('cache') crashes in production without Redis. Working Map-backed implementations already exist in DevPlugin but are inaccessible to the kernel.

Changes

  • New packages/core/src/fallbacks/ — extracted createMemoryCache, createMemoryQueue, createMemoryJob factories from plugin-dev, marked _fallback: true instead of _dev: true
  • CORE_FALLBACK_FACTORIES — exported map of { cache, queue, job } → factory functions
  • ObjectKernel.validateSystemRequirements() — auto-injects fallback when a core service is missing and a factory exists:
} else if (criticality === 'core') {
    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);
    }
}
  • plugin-dev — replaced local createCacheStub/createQueueStub/createJobStub with imports from @objectstack/core
  • Tests — 20 new tests covering all three fallback implementations + CORE_FALLBACK_FACTORIES shape

No new dependencies. No changes to @objectstack/spec.

Original prompt

Task: Extract core-level in-memory fallbacks for cache/queue/job into @objectstack/core

Context

ServiceRequirementDef in packages/spec/src/system/core-services.zod.ts defines cache, queue, job as 'core' criticality — meaning the kernel warns but continues if they're missing. Currently ObjectKernel.validateSystemRequirements() in packages/core/src/kernel.ts only logs a warning for missing core services but provides NO fallback. This means any code calling ctx.getService('cache') in production without Redis crashes.

Meanwhile, packages/plugins/plugin-dev/src/dev-plugin.ts lines 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/ directory

Create these 4 files:

packages/core/src/fallbacks/memory-cache.ts

  • Copy createCacheStub() from packages/plugins/plugin-dev/src/dev-plugin.ts lines 37-61
  • Rename to createMemoryCache()
  • Remove _dev: true marker, add _fallback: true instead
  • Keep the exact same Map-backed implementation (get/set/delete/has/clear/stats)
  • Export the factory function

packages/core/src/fallbacks/memory-queue.ts

  • Copy createQueueStub() from dev-plugin.ts lines 64-82
  • Rename to createMemoryQueue()
  • Same pattern: _fallback: true, export factory

packages/core/src/fallbacks/memory-job.ts

  • Copy createJobStub() from dev-plugin.ts lines 85-98
  • Rename to createMemoryJob()
  • Same pattern

packages/core/src/fallbacks/index.ts

export { createMemoryCache } from './memory-cache.js';
export { createMemoryQueue } from './memory-queue.js';
export { createMemoryJob } from './memory-job.js';

/**
 * Map of core-criticality service names to their in-memory fallback factories.
 * Used by ObjectKernel.validateSystemRequirements() to auto-inject fallbacks
 * when no real plugin provides the service.
 */
export const CORE_FALLBACK_FACTORIES: Record<string, () => Record<string, any>> = {
  cache: createMemoryCache,
  queue: createMemoryQueue,
  job:   createMemoryJob,
};

Step 2: Wire into ObjectKernel.validateSystemRequirements()

In packages/core/src/kernel.ts, modify the validateSystemRequirements() method.

Current code (around line ~280) does this for 'core' services:

} else if (criticality === 'core') {
    this.logger.warn(`CORE: Core service missing, functionality may be degraded: ${serviceName}`);
    missingCoreServices.push(serviceName);
}

Change to:

} else if (criticality === 'core') {
    // 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);
    }
}

Import at top of kernel.ts:

import { CORE_FALLBACK_FACTORIES } from './fallbacks/index.js';

Step 3: Export from core index

In packages/core/src/index.ts, add:

export * from './fallbacks/index.js';

Step 4: Add tests

Create packages/core/src/fallbacks/fallbacks.test.ts:

  • Test that createMemoryCache() implements get/set/delete/has/clear with TTL expiry
  • Test that createMemoryQueue() implements publish/subscribe with sync delivery
  • Test that createMemoryJob() implements schedule/cancel/trigger/listJobs
  • Test that CORE_FALLBACK_FACTORIES has exactly 3 entries: cache, queue, job

Step 5: Update plugin-dev to import from core

In packages/plugins/plugin-dev/src/dev-plugin.ts:

  • Replace createCacheStub, createQueueStub, createJobStub with imports from @objectstack/core:
import { createMemoryCache, createMemoryQueue, createMemoryJob } from '@objectstack/core';
  • Update DEV_STUB_FACTORIES map to use the imported factories:
'cache': createMemoryCache,
'queue': createMemoryQueue,
'job':   createMemoryJob,
  • Delete the original 3 stub functions (lines 37-98) from dev-plugin.ts

Rules

  • Do NOT touch any file outside of packages/core/ and packages/plugins/plugin-dev/
  • Do NOT change ServiceRequirementDef in spec
  • Do NOT add any new npm dependencies
  • Keep the fallback implementations simple — these are Map-backed, zero-dep, identical to the existing stubs
  • All existing tests must continue to pass

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link

vercel bot commented Feb 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Feb 14, 2026 1:46am
spec Ready Ready Preview, Comment Feb 14, 2026 1:46am

Request Review

… @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>
Copilot AI changed the title [WIP] Extract core-level in-memory fallbacks for cache, queue, and job Extract core-level in-memory fallbacks for cache/queue/job into @objectstack/core Feb 14, 2026
Copilot AI requested a review from hotlong February 14, 2026 01:44
@hotlong hotlong marked this pull request as ready for review February 14, 2026 02:32
Copilot AI review requested due to automatic review settings February 14, 2026 02:32
@hotlong hotlong merged commit 55d7400 into main Feb 14, 2026
5 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 using CORE_FALLBACK_FACTORIES
  • Updated plugin-dev to 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

Comment on lines +285 to +287
'cache': createMemoryCache,
'queue': createMemoryQueue,
'job': createMemoryJob,
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Modify the fallback factories to accept an optional marker parameter: createMemoryCache({ marker: '_dev' })
  2. Wrap the fallback instances here to add the _dev marker: { ...createMemoryCache(), _dev: true }
  3. Update the test file to accept either _dev or _fallback markers
Suggested change
'cache': createMemoryCache,
'queue': createMemoryQueue,
'job': createMemoryJob,
'cache': () => ({ ...createMemoryCache(), _dev: true }),
'queue': () => ({ ...createMemoryQueue(), _dev: true }),
'job': () => ({ ...createMemoryJob(), _dev: true }),

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +5
import { createMemoryCache } from './memory-cache';
import { createMemoryQueue } from './memory-queue';
import { createMemoryJob } from './memory-job';
import { CORE_FALLBACK_FACTORIES } from './index';
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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';

Copilot uses AI. Check for mistakes.
let msgId = 0;
return {
_fallback: true, _serviceName: 'queue',
async publish<T = unknown>(queue: string, data: T): Promise<string> {
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
async publish<T = unknown>(queue: string, data: T): Promise<string> {
async publish<T = unknown>(queue: string, data: T, _options?: unknown): Promise<string> {

Copilot uses AI. Check for mistakes.

it('should return queue size of 0', async () => {
const queue = createMemoryQueue();
expect(await queue.getQueueSize()).toBe(0);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
expect(await queue.getQueueSize()).toBe(0);
expect(await queue.getQueueSize('test-q')).toBe(0);

Copilot uses AI. Check for mistakes.
handlers.set(queue, [...(handlers.get(queue) ?? []), handler]);
},
async unsubscribe(queue: string): Promise<void> { handlers.delete(queue); },
async getQueueSize(): Promise<number> { return 0; },
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
async getQueueSize(): Promise<number> { return 0; },
async getQueueSize(queue: string): Promise<number> { return 0; },

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +247
// 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);
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. When a core-criticality service (cache/queue/job) is missing, the kernel auto-injects the fallback
  2. The fallback service is properly registered and accessible via getService()
  3. The appropriate warning is logged
  4. The fallback has _fallback: true marker

Without these tests, regressions in this critical new feature could go undetected.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +20
export const CORE_FALLBACK_FACTORIES: Record<string, () => Record<string, any>> = {
cache: createMemoryCache,
queue: createMemoryQueue,
job: createMemoryJob,
};
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
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 }); },
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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() });
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handler functions are invoked without await, which means:

  1. Errors in handlers will result in unhandled promise rejections
  2. The publish operation completes immediately without waiting for handlers

Consider either:

  1. Awaiting handlers sequentially: for (const fn of fns) await fn({ ... })
  2. 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.

Suggested change
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);
}
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants