Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 8
feat: MemoryDriver persistence — file/localStorage adapters#816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
09cd38915da7d2833426fc23a9473da9227File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -6,6 +6,20 @@ import { Logger, createLogger } from '@objectstack/core'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Query, Aggregator } from 'mingo'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getValueByPath } from './memory-matcher.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Persistence adapter interface. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Matches the PersistenceAdapterSchema contract from @objectstack/spec. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface PersistenceAdapterInterface { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| load(): Promise<Record<string, any[]> | null>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| save(db: Record<string, any[]>): Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flush(): Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Optional: Start periodic auto-save (used by FileSystemPersistenceAdapter). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| startAutoSave?(): void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Optional: Stop auto-save timer and flush pending writes. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stopAutoSave?(): Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Configuration options for the InMemory driver. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Aligned with @objectstack/spec MemoryConfigSchema. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @@ -17,6 +31,21 @@ export interface InMemoryDriverConfig { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| strictMode?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Optional: Logger instance */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger?: Logger; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Optional persistence configuration. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - `'file'` — File-system persistence with defaults (Node.js only) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - `'local'` — localStorage persistence with defaults (Browser only) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - `{ type: 'file', path?: string, autoSaveInterval?: number }` — File-system with options | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - `{ type: 'local', key?: string }` — localStorage with options | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - `{ adapter: PersistenceAdapterInterface }` — Custom adapter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| persistence?: string | { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type?: 'file' | 'local'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| autoSaveInterval?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| adapter?: PersistenceAdapterInterface; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+42
to
+48
CopilotAI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| persistence?: string|{ | |
| type?: 'file'|'local'; | |
| path?: string; | |
| key?: string; | |
| autoSaveInterval?: number; | |
| adapter?: PersistenceAdapterInterface; | |
| }; | |
| persistence?: | |
| |'file' | |
| |'local' | |
| |{ | |
| /** File-system persistence adapter with options */ | |
| type: 'file'; | |
| path?: string; | |
| autoSaveInterval?: number; | |
| /** Not applicable for file persistence */ | |
| key?: never; | |
| /** Not applicable when using built-in adapters */ | |
| adapter?: never; | |
| } | |
| |{ | |
| /** localStorage persistence adapter with options */ | |
| type: 'local'; | |
| key?: string; | |
| /** Not applicable for local persistence */ | |
| path?: never; | |
| autoSaveInterval?: never; | |
| /** Not applicable when using built-in adapters */ | |
| adapter?: never; | |
| } | |
| |{ | |
| /** Custom persistence adapter implementation */ | |
| adapter: PersistenceAdapterInterface; | |
| /** Disallow built-in adapter discriminator on custom adapter configs */ | |
| type?: never; | |
| path?: never; | |
| key?: never; | |
| autoSaveInterval?: never; | |
| }; |
CopilotAIFeb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
markDirty() calls the async persistenceAdapter.save() without awaiting or handling rejections. If a custom adapter rejects (or JSON.stringify fails in the localStorage adapter), this can surface as an unhandled promise rejection. Consider firing-and-forgetting with explicit rejection handling (e.g., void save(...).catch(...)) or queueing/throttling saves with error logging via the driver logger.
| this.persistenceAdapter.save(this.db); | |
| voidthis.persistenceAdapter.save(this.db).catch((error)=>{ | |
| this.logger.error('MemoryDriver persistence save failed',{ error }); | |
| }); |
CopilotAIFeb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When persistence is a string, adapters are imported/initialized without checking whether the current runtime supports them. Add explicit environment checks before selecting/importing file vs local so persistence: 'local' in Node and persistence: 'file' in browsers fail with a clear, actionable error message (instead of silent no-ops or import errors).
CopilotAIFeb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the object-config branch, an unexpected persistence.type value currently results in no adapter being set (and no error). Add an explicit else that throws for unknown type values so misconfiguration doesn’t silently disable persistence.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,103 @@ | ||||||||||||||||||||||||||||||||
| // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. | ||||||||||||||||||||||||||||||||
| import * as fs from 'node:fs'; | ||||||||||||||||||||||||||||||||
| import * as path from 'node:path'; | ||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * FileSystemPersistenceAdapter | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * Persists the in-memory database to a JSON file on disk. | ||||||||||||||||||||||||||||||||
| * Supports atomic writes (write to temp file then rename) and auto-save with dirty tracking. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * Node.js only — will throw if used in non-Node.js environments. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| export class FileSystemPersistenceAdapter { | ||||||||||||||||||||||||||||||||
| private readonly filePath: string; | ||||||||||||||||||||||||||||||||
| private readonly autoSaveInterval: number; | ||||||||||||||||||||||||||||||||
| private dirty = false; | ||||||||||||||||||||||||||||||||
| private timer: ReturnType<typeof setInterval> | null = null; | ||||||||||||||||||||||||||||||||
| private currentDb: Record<string, any[]> | null = null; | ||||||||||||||||||||||||||||||||
| constructor(options?: { path?: string; autoSaveInterval?: number }) { | ||||||||||||||||||||||||||||||||
| this.filePath = options?.path || path.join('.objectstack', 'data', 'memory-driver.json'); | ||||||||||||||||||||||||||||||||
| this.autoSaveInterval = options?.autoSaveInterval ?? 2000; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Load persisted data from disk. | ||||||||||||||||||||||||||||||||
| * Returns null if no file exists. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| async load(): Promise<Record<string, any[]> | null> { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| if (!fs.existsSync(this.filePath)) { | ||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| const raw = fs.readFileSync(this.filePath, 'utf-8'); | ||||||||||||||||||||||||||||||||
| const data = JSON.parse(raw); | ||||||||||||||||||||||||||||||||
| return data as Record<string, any[]>; | ||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Save data to disk using atomic write (temp file + rename). | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| async save(db: Record<string, any[]>): Promise<void> { | ||||||||||||||||||||||||||||||||
| this.currentDb = db; | ||||||||||||||||||||||||||||||||
| this.dirty = true; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Flush pending writes to disk immediately. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| async flush(): Promise<void> { | ||||||||||||||||||||||||||||||||
| if (!this.dirty || !this.currentDb) return; | ||||||||||||||||||||||||||||||||
| await this.writeToDisk(this.currentDb); | ||||||||||||||||||||||||||||||||
| this.dirty = false; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Start the auto-save timer. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| startAutoSave(): void { | ||||||||||||||||||||||||||||||||
| if (this.timer) return; | ||||||||||||||||||||||||||||||||
| this.timer = setInterval(async () => { | ||||||||||||||||||||||||||||||||
| if (this.dirty && this.currentDb) { | ||||||||||||||||||||||||||||||||
| await this.writeToDisk(this.currentDb); | ||||||||||||||||||||||||||||||||
| this.dirty = false; | ||||||||||||||||||||||||||||||||
Comment on lines
+66
to
+68
CopilotAI | ||||||||||||||||||||||||||||||||
| if(this.dirty&&this.currentDb){ | |
| awaitthis.writeToDisk(this.currentDb); | |
| this.dirty=false; | |
| try{ | |
| if(this.dirty&&this.currentDb){ | |
| awaitthis.writeToDisk(this.currentDb); | |
| this.dirty=false; | |
| } | |
| }catch(error){ | |
| // Surface auto-save persistence failures without crashing the process | |
| console.error( | |
| '[FileSystemPersistenceAdapter] Auto-save failed for', | |
| this.filePath, | |
| error, | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. | ||
| export { FileSystemPersistenceAdapter } from './file-adapter.js'; | ||
| export { LocalStoragePersistenceAdapter } from './local-storage-adapter.js'; |
Uh oh!
There was an error while loading. Please reload this page.
CopilotAIFeb 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-exporting
FileSystemPersistenceAdapterfrom the package root forces consumers (including browser bundles) to resolve./persistence/file-adapter.js, which importsnode:fs/node:path. This undermines the dynamic-import strategy and will break browser/edge builds even when file persistence isn’t used. Consider removing these root exports and instead exposing adapters via environment-specific/conditional exports (or separate entrypoints) so browser imports don’t pull in Node-only modules.