@mantiq/core 0.0.1
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/README.md +19 -0
- package/package.json +65 -0
- package/src/application/Application.ts +241 -0
- package/src/cache/CacheManager.ts +180 -0
- package/src/cache/FileCacheStore.ts +113 -0
- package/src/cache/MemcachedCacheStore.ts +115 -0
- package/src/cache/MemoryCacheStore.ts +62 -0
- package/src/cache/NullCacheStore.ts +39 -0
- package/src/cache/RedisCacheStore.ts +125 -0
- package/src/cache/events.ts +52 -0
- package/src/config/ConfigRepository.ts +115 -0
- package/src/config/env.ts +26 -0
- package/src/container/Container.ts +198 -0
- package/src/container/ContextualBindingBuilder.ts +21 -0
- package/src/contracts/Cache.ts +49 -0
- package/src/contracts/Config.ts +24 -0
- package/src/contracts/Container.ts +68 -0
- package/src/contracts/DriverManager.ts +16 -0
- package/src/contracts/Encrypter.ts +32 -0
- package/src/contracts/EventDispatcher.ts +32 -0
- package/src/contracts/ExceptionHandler.ts +20 -0
- package/src/contracts/Hasher.ts +19 -0
- package/src/contracts/Middleware.ts +23 -0
- package/src/contracts/Request.ts +54 -0
- package/src/contracts/Response.ts +19 -0
- package/src/contracts/Router.ts +62 -0
- package/src/contracts/ServiceProvider.ts +31 -0
- package/src/contracts/Session.ts +47 -0
- package/src/encryption/Encrypter.ts +197 -0
- package/src/encryption/errors.ts +30 -0
- package/src/errors/ConfigKeyNotFoundError.ts +7 -0
- package/src/errors/ContainerResolutionError.ts +13 -0
- package/src/errors/ForbiddenError.ts +7 -0
- package/src/errors/HttpError.ts +16 -0
- package/src/errors/MantiqError.ts +16 -0
- package/src/errors/NotFoundError.ts +7 -0
- package/src/errors/TokenMismatchError.ts +10 -0
- package/src/errors/TooManyRequestsError.ts +10 -0
- package/src/errors/UnauthorizedError.ts +7 -0
- package/src/errors/ValidationError.ts +10 -0
- package/src/exceptions/DevErrorPage.ts +564 -0
- package/src/exceptions/Handler.ts +118 -0
- package/src/hashing/Argon2Hasher.ts +46 -0
- package/src/hashing/BcryptHasher.ts +36 -0
- package/src/hashing/HashManager.ts +80 -0
- package/src/helpers/abort.ts +46 -0
- package/src/helpers/app.ts +17 -0
- package/src/helpers/cache.ts +12 -0
- package/src/helpers/config.ts +15 -0
- package/src/helpers/encrypt.ts +22 -0
- package/src/helpers/env.ts +1 -0
- package/src/helpers/hash.ts +20 -0
- package/src/helpers/response.ts +69 -0
- package/src/helpers/route.ts +24 -0
- package/src/helpers/session.ts +11 -0
- package/src/http/Cookie.ts +26 -0
- package/src/http/Kernel.ts +252 -0
- package/src/http/Request.ts +249 -0
- package/src/http/Response.ts +112 -0
- package/src/http/UploadedFile.ts +56 -0
- package/src/index.ts +97 -0
- package/src/macroable/Macroable.ts +174 -0
- package/src/middleware/Cors.ts +91 -0
- package/src/middleware/EncryptCookies.ts +101 -0
- package/src/middleware/Pipeline.ts +66 -0
- package/src/middleware/StartSession.ts +90 -0
- package/src/middleware/TrimStrings.ts +32 -0
- package/src/middleware/VerifyCsrfToken.ts +130 -0
- package/src/providers/CoreServiceProvider.ts +97 -0
- package/src/routing/ResourceRegistrar.ts +64 -0
- package/src/routing/Route.ts +40 -0
- package/src/routing/RouteCollection.ts +50 -0
- package/src/routing/RouteMatcher.ts +92 -0
- package/src/routing/Router.ts +280 -0
- package/src/routing/events.ts +19 -0
- package/src/session/SessionManager.ts +75 -0
- package/src/session/Store.ts +192 -0
- package/src/session/handlers/CookieSessionHandler.ts +42 -0
- package/src/session/handlers/FileSessionHandler.ts +79 -0
- package/src/session/handlers/MemorySessionHandler.ts +35 -0
- package/src/websocket/WebSocketContext.ts +20 -0
- package/src/websocket/WebSocketKernel.ts +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @mantiq/core
|
|
2
|
+
|
|
3
|
+
The foundation of MantiqJS — service container, router, middleware, HTTP kernel, config, encryption, hashing, caching, and sessions.
|
|
4
|
+
|
|
5
|
+
Part of [MantiqJS](https://github.com/abdullahkhan/mantiq) — a batteries-included TypeScript web framework for Bun.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @mantiq/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
See the [MantiqJS repository](https://github.com/abdullahkhan/mantiq) for full documentation.
|
|
16
|
+
|
|
17
|
+
## License
|
|
18
|
+
|
|
19
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mantiq/core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Service container, router, middleware, HTTP kernel, config, and exception handler",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Abdullah Khan",
|
|
8
|
+
"homepage": "https://github.com/abdullahkhan/mantiq/tree/main/packages/core",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/abdullahkhan/mantiq.git",
|
|
12
|
+
"directory": "packages/core"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/abdullahkhan/mantiq/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mantiq",
|
|
19
|
+
"mantiqjs",
|
|
20
|
+
"bun",
|
|
21
|
+
"typescript",
|
|
22
|
+
"framework",
|
|
23
|
+
"core"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"bun": ">=1.1.0"
|
|
27
|
+
},
|
|
28
|
+
"main": "./src/index.ts",
|
|
29
|
+
"types": "./src/index.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"bun": "./src/index.ts",
|
|
33
|
+
"default": "./src/index.ts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src/",
|
|
38
|
+
"package.json",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
|
|
44
|
+
"test": "bun test",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"clean": "rm -rf dist"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"bun-types": "latest",
|
|
50
|
+
"typescript": "^5.7.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"bun": ">=1.1.0",
|
|
54
|
+
"ioredis": ">=5.0.0",
|
|
55
|
+
"memjs": ">=1.3.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"ioredis": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"memjs": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { ContainerImpl } from '../container/Container.ts'
|
|
2
|
+
import { ConfigRepository } from '../config/ConfigRepository.ts'
|
|
3
|
+
import { ContainerResolutionError } from '../errors/ContainerResolutionError.ts'
|
|
4
|
+
import type { ServiceProvider } from '../contracts/ServiceProvider.ts'
|
|
5
|
+
import type { Bindable, Constructor } from '../contracts/Container.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Application is the heart of MantiqJS.
|
|
9
|
+
*
|
|
10
|
+
* It extends the service container and owns:
|
|
11
|
+
* - Config loading (always happens before any provider runs)
|
|
12
|
+
* - Provider registration + boot lifecycle
|
|
13
|
+
* - Deferred provider resolution
|
|
14
|
+
* - The global singleton accessible via app()
|
|
15
|
+
*
|
|
16
|
+
* Boot sequence:
|
|
17
|
+
* const app = await Application.create('config/')
|
|
18
|
+
* await app.registerProviders([...])
|
|
19
|
+
* await app.bootProviders()
|
|
20
|
+
* await app.make(HttpKernel).start()
|
|
21
|
+
*/
|
|
22
|
+
export class Application extends ContainerImpl {
|
|
23
|
+
/** The global singleton instance — set once at bootstrap. */
|
|
24
|
+
private static _instance: Application | null = null
|
|
25
|
+
|
|
26
|
+
private providers: ServiceProvider[] = []
|
|
27
|
+
private deferredProviders = new Map<Bindable<any>, ServiceProvider>()
|
|
28
|
+
private booted = false
|
|
29
|
+
|
|
30
|
+
private constructor(private readonly basePath: string = process.cwd()) {
|
|
31
|
+
super()
|
|
32
|
+
// Register the application itself so it can be resolved from the container
|
|
33
|
+
this.instance(Application, this)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Singleton access ──────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create and configure the application.
|
|
40
|
+
* Loads config immediately so config() is available before any provider runs.
|
|
41
|
+
*
|
|
42
|
+
* @param basePath - Project root directory
|
|
43
|
+
* @param configPath - Path to config directory (relative to basePath)
|
|
44
|
+
*/
|
|
45
|
+
static async create(
|
|
46
|
+
basePath: string = process.cwd(),
|
|
47
|
+
configPath: string = 'config',
|
|
48
|
+
): Promise<Application> {
|
|
49
|
+
const app = new Application(basePath)
|
|
50
|
+
await app.loadConfig(configPath)
|
|
51
|
+
Application._instance = app
|
|
52
|
+
return app
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return the global Application instance.
|
|
57
|
+
* @throws Error if the application has not been created yet.
|
|
58
|
+
*/
|
|
59
|
+
static getInstance(): Application {
|
|
60
|
+
if (!Application._instance) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'Application has not been created. Call Application.create() first.',
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
return Application._instance
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set the global instance (useful for testing).
|
|
70
|
+
*/
|
|
71
|
+
static setInstance(app: Application): void {
|
|
72
|
+
Application._instance = app
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Destroy the global instance (useful for testing).
|
|
77
|
+
*/
|
|
78
|
+
static resetInstance(): void {
|
|
79
|
+
Application._instance = null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Config ────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load config files from the given directory.
|
|
86
|
+
* Config is available immediately after this call via config() and app.make(ConfigRepository).
|
|
87
|
+
*
|
|
88
|
+
* Falls back to a cached config file at bootstrap/cache/config.json if present.
|
|
89
|
+
*/
|
|
90
|
+
async loadConfig(configDir: string): Promise<void> {
|
|
91
|
+
const cachePath = `${this.basePath}/bootstrap/cache/config.json`
|
|
92
|
+
let config: ConfigRepository
|
|
93
|
+
|
|
94
|
+
// Check for cached config first (production optimization)
|
|
95
|
+
try {
|
|
96
|
+
const cacheFile = Bun.file(cachePath)
|
|
97
|
+
if (await cacheFile.exists()) {
|
|
98
|
+
const data = await cacheFile.json()
|
|
99
|
+
config = new ConfigRepository(data)
|
|
100
|
+
this.instance(ConfigRepository, config)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// No cache — load from files
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const fullConfigPath = configDir.startsWith('/')
|
|
108
|
+
? configDir
|
|
109
|
+
: `${this.basePath}/${configDir}`
|
|
110
|
+
|
|
111
|
+
config = await ConfigRepository.fromDirectory(fullConfigPath)
|
|
112
|
+
this.instance(ConfigRepository, config)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the config repository directly (no container lookup overhead).
|
|
117
|
+
*/
|
|
118
|
+
config(): ConfigRepository {
|
|
119
|
+
return this.make(ConfigRepository)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Base path helpers ─────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
basePath_(path: string = ''): string {
|
|
125
|
+
return path ? `${this.basePath}/${path}` : this.basePath
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
configPath(path: string = ''): string {
|
|
129
|
+
return this.basePath_(path ? `config/${path}` : 'config')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
storagePath(path: string = ''): string {
|
|
133
|
+
return this.basePath_(path ? `storage/${path}` : 'storage')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Provider lifecycle ────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Register all service providers.
|
|
140
|
+
* - Deferred providers are indexed but not registered until their bindings are first resolved.
|
|
141
|
+
* - All non-deferred register() calls complete before any boot() is called.
|
|
142
|
+
*/
|
|
143
|
+
async registerProviders(providerClasses: Constructor<ServiceProvider>[]): Promise<void> {
|
|
144
|
+
const nonDeferred: ServiceProvider[] = []
|
|
145
|
+
|
|
146
|
+
for (const ProviderClass of providerClasses) {
|
|
147
|
+
const provider = new ProviderClass(this)
|
|
148
|
+
|
|
149
|
+
if (provider.deferred) {
|
|
150
|
+
// Index by every binding this provider offers
|
|
151
|
+
for (const binding of provider.provides()) {
|
|
152
|
+
this.deferredProviders.set(binding, provider)
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
await provider.register()
|
|
156
|
+
nonDeferred.push(provider)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.providers = nonDeferred
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Boot all registered (non-deferred) providers.
|
|
165
|
+
* Called after ALL providers have been registered.
|
|
166
|
+
*/
|
|
167
|
+
async bootProviders(): Promise<void> {
|
|
168
|
+
for (const provider of this.providers) {
|
|
169
|
+
await provider.boot()
|
|
170
|
+
}
|
|
171
|
+
this.booted = true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Register a single provider immediately (after initial boot).
|
|
176
|
+
* Useful for testing and dynamic provider loading.
|
|
177
|
+
*/
|
|
178
|
+
async register(ProviderClass: Constructor<ServiceProvider>): Promise<void> {
|
|
179
|
+
const provider = new ProviderClass(this)
|
|
180
|
+
await provider.register()
|
|
181
|
+
if (this.booted) {
|
|
182
|
+
await provider.boot()
|
|
183
|
+
} else {
|
|
184
|
+
this.providers.push(provider)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Deferred provider resolution ──────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Override make() to handle deferred provider loading.
|
|
192
|
+
* If a binding isn't found in the container, check deferred providers.
|
|
193
|
+
*/
|
|
194
|
+
make<T>(abstract: Bindable<T>): T {
|
|
195
|
+
try {
|
|
196
|
+
return super.make(abstract)
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (
|
|
199
|
+
err instanceof ContainerResolutionError &&
|
|
200
|
+
err.reason === 'not_bound'
|
|
201
|
+
) {
|
|
202
|
+
const deferredProvider = this.deferredProviders.get(abstract)
|
|
203
|
+
if (deferredProvider) {
|
|
204
|
+
// Remove from deferred map so we don't loop
|
|
205
|
+
for (const [key, p] of this.deferredProviders.entries()) {
|
|
206
|
+
if (p === deferredProvider) this.deferredProviders.delete(key)
|
|
207
|
+
}
|
|
208
|
+
// Register + boot the deferred provider now
|
|
209
|
+
const boot = async () => {
|
|
210
|
+
await deferredProvider.register()
|
|
211
|
+
await deferredProvider.boot()
|
|
212
|
+
this.providers.push(deferredProvider)
|
|
213
|
+
}
|
|
214
|
+
// @internal: sync wrapper — deferred providers must not have async register/boot
|
|
215
|
+
// that relies on other async operations. In practice this is fine.
|
|
216
|
+
void boot()
|
|
217
|
+
return super.make(abstract)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw err
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Environment ───────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
environment(): string {
|
|
227
|
+
return process.env['APP_ENV'] ?? 'production'
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
isLocal(): boolean {
|
|
231
|
+
return this.environment() === 'local'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
isProduction(): boolean {
|
|
235
|
+
return this.environment() === 'production'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
isDebug(): boolean {
|
|
239
|
+
return process.env['APP_DEBUG'] === 'true'
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { CacheStore } from '../contracts/Cache.ts'
|
|
2
|
+
import type { DriverManager } from '../contracts/DriverManager.ts'
|
|
3
|
+
import type { EventDispatcher } from '../contracts/EventDispatcher.ts'
|
|
4
|
+
import { MemoryCacheStore } from './MemoryCacheStore.ts'
|
|
5
|
+
import { FileCacheStore } from './FileCacheStore.ts'
|
|
6
|
+
import { NullCacheStore } from './NullCacheStore.ts'
|
|
7
|
+
import { CacheHit, CacheMissed, KeyWritten, KeyForgotten } from './events.ts'
|
|
8
|
+
import type { RedisCacheConfig } from './RedisCacheStore.ts'
|
|
9
|
+
import type { MemcachedCacheConfig } from './MemcachedCacheStore.ts'
|
|
10
|
+
|
|
11
|
+
export interface CacheConfig {
|
|
12
|
+
default: string
|
|
13
|
+
prefix?: string
|
|
14
|
+
stores: {
|
|
15
|
+
memory?: Record<string, unknown>
|
|
16
|
+
file?: { path: string }
|
|
17
|
+
redis?: RedisCacheConfig
|
|
18
|
+
memcached?: MemcachedCacheConfig
|
|
19
|
+
null?: Record<string, unknown>
|
|
20
|
+
[name: string]: Record<string, unknown> | RedisCacheConfig | MemcachedCacheConfig | undefined
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Multi-driver cache manager (Laravel-style).
|
|
26
|
+
*
|
|
27
|
+
* Built-in drivers: memory, file, redis, memcached, null.
|
|
28
|
+
* Custom drivers can be added via `extend()`.
|
|
29
|
+
*/
|
|
30
|
+
export class CacheManager implements DriverManager<CacheStore>, CacheStore {
|
|
31
|
+
private readonly config: CacheConfig
|
|
32
|
+
private readonly stores = new Map<string, CacheStore>()
|
|
33
|
+
private readonly customCreators = new Map<string, () => CacheStore>()
|
|
34
|
+
|
|
35
|
+
/** Optional event dispatcher. Set by @mantiq/events when installed. */
|
|
36
|
+
static _dispatcher: EventDispatcher | null = null
|
|
37
|
+
|
|
38
|
+
constructor(config?: Partial<CacheConfig>) {
|
|
39
|
+
this.config = {
|
|
40
|
+
default: config?.default ?? 'memory',
|
|
41
|
+
stores: config?.stores ?? {},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── DriverManager ───────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
driver(name?: string): CacheStore {
|
|
48
|
+
const storeName = name ?? this.getDefaultDriver()
|
|
49
|
+
|
|
50
|
+
if (!this.stores.has(storeName)) {
|
|
51
|
+
this.stores.set(storeName, this.createDriver(storeName))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return this.stores.get(storeName)!
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Alias for `driver()` — Laravel compatibility. */
|
|
58
|
+
store(name?: string): CacheStore {
|
|
59
|
+
return this.driver(name)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
extend(name: string, factory: () => CacheStore): void {
|
|
63
|
+
this.customCreators.set(name, factory)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getDefaultDriver(): string {
|
|
67
|
+
return this.config.default
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── CacheStore (delegates to default store) ─────────────────────────────
|
|
71
|
+
|
|
72
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
73
|
+
const value = await this.driver().get<T>(key)
|
|
74
|
+
const storeName = this.getDefaultDriver()
|
|
75
|
+
if (value !== undefined) {
|
|
76
|
+
await CacheManager._dispatcher?.emit(new CacheHit(key, value, storeName))
|
|
77
|
+
} else {
|
|
78
|
+
await CacheManager._dispatcher?.emit(new CacheMissed(key, storeName))
|
|
79
|
+
}
|
|
80
|
+
return value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async put(key: string, value: unknown, ttl?: number): Promise<void> {
|
|
84
|
+
await this.driver().put(key, value, ttl)
|
|
85
|
+
await CacheManager._dispatcher?.emit(new KeyWritten(key, value, ttl, this.getDefaultDriver()))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async forget(key: string): Promise<boolean> {
|
|
89
|
+
const result = await this.driver().forget(key)
|
|
90
|
+
await CacheManager._dispatcher?.emit(new KeyForgotten(key, this.getDefaultDriver()))
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async has(key: string): Promise<boolean> {
|
|
95
|
+
return this.driver().has(key)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async flush(): Promise<void> {
|
|
99
|
+
return this.driver().flush()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async increment(key: string, value = 1): Promise<number> {
|
|
103
|
+
return this.driver().increment(key, value)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async decrement(key: string, value = 1): Promise<number> {
|
|
107
|
+
return this.driver().decrement(key, value)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async add(key: string, value: unknown, ttl?: number): Promise<boolean> {
|
|
111
|
+
return this.driver().add(key, value, ttl)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Convenience methods ───────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get an item from the cache, or execute the given callback and store the result.
|
|
118
|
+
*/
|
|
119
|
+
async remember<T = unknown>(key: string, ttl: number | undefined, callback: () => T | Promise<T>): Promise<T> {
|
|
120
|
+
const cached = await this.get<T>(key)
|
|
121
|
+
if (cached !== undefined) return cached
|
|
122
|
+
|
|
123
|
+
const value = await callback()
|
|
124
|
+
await this.put(key, value, ttl)
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get an item from the cache, or execute the given callback and store the result forever.
|
|
130
|
+
*/
|
|
131
|
+
async rememberForever<T = unknown>(key: string, callback: () => T | Promise<T>): Promise<T> {
|
|
132
|
+
return this.remember<T>(key, undefined, callback)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Retrieve an item from the cache and delete it.
|
|
137
|
+
*/
|
|
138
|
+
async pull<T = unknown>(key: string): Promise<T | undefined> {
|
|
139
|
+
const value = await this.get<T>(key)
|
|
140
|
+
if (value !== undefined) {
|
|
141
|
+
await this.forget(key)
|
|
142
|
+
}
|
|
143
|
+
return value
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Store an item in the cache indefinitely.
|
|
148
|
+
*/
|
|
149
|
+
async forever(key: string, value: unknown): Promise<void> {
|
|
150
|
+
return this.put(key, value)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
private createDriver(name: string): CacheStore {
|
|
156
|
+
const custom = this.customCreators.get(name)
|
|
157
|
+
if (custom) return custom()
|
|
158
|
+
|
|
159
|
+
const storeConfig = this.config.stores[name] ?? {}
|
|
160
|
+
|
|
161
|
+
switch (name) {
|
|
162
|
+
case 'memory':
|
|
163
|
+
return new MemoryCacheStore()
|
|
164
|
+
case 'file':
|
|
165
|
+
return new FileCacheStore((storeConfig as { path?: string }).path ?? '/tmp/mantiq-cache')
|
|
166
|
+
case 'redis': {
|
|
167
|
+
const { RedisCacheStore } = require('./RedisCacheStore.ts')
|
|
168
|
+
return new RedisCacheStore(storeConfig as RedisCacheConfig)
|
|
169
|
+
}
|
|
170
|
+
case 'memcached': {
|
|
171
|
+
const { MemcachedCacheStore } = require('./MemcachedCacheStore.ts')
|
|
172
|
+
return new MemcachedCacheStore(storeConfig as MemcachedCacheConfig)
|
|
173
|
+
}
|
|
174
|
+
case 'null':
|
|
175
|
+
return new NullCacheStore()
|
|
176
|
+
default:
|
|
177
|
+
throw new Error(`Unsupported cache driver: ${name}. Use extend() to register custom drivers.`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { CacheStore } from '../contracts/Cache.ts'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { mkdir, rm, readdir } from 'node:fs/promises'
|
|
4
|
+
|
|
5
|
+
interface FileCachePayload {
|
|
6
|
+
value: unknown
|
|
7
|
+
expiresAt: number | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* File-based cache store. Survives process restarts.
|
|
12
|
+
* Each key maps to a JSON file in the configured directory.
|
|
13
|
+
*/
|
|
14
|
+
export class FileCacheStore implements CacheStore {
|
|
15
|
+
private readonly directory: string
|
|
16
|
+
private initialized = false
|
|
17
|
+
|
|
18
|
+
constructor(directory: string) {
|
|
19
|
+
this.directory = directory
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
23
|
+
await this.ensureDirectory()
|
|
24
|
+
const file = Bun.file(this.path(key))
|
|
25
|
+
|
|
26
|
+
if (!(await file.exists())) return undefined
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const payload: FileCachePayload = await file.json()
|
|
30
|
+
|
|
31
|
+
if (payload.expiresAt !== null && Date.now() > payload.expiresAt) {
|
|
32
|
+
await this.forget(key)
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return payload.value as T
|
|
37
|
+
} catch {
|
|
38
|
+
// Corrupted file — remove it
|
|
39
|
+
await this.forget(key)
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async put(key: string, value: unknown, ttl?: number): Promise<void> {
|
|
45
|
+
await this.ensureDirectory()
|
|
46
|
+
const payload: FileCachePayload = {
|
|
47
|
+
value,
|
|
48
|
+
expiresAt: ttl != null ? Date.now() + ttl * 1000 : null,
|
|
49
|
+
}
|
|
50
|
+
await Bun.write(this.path(key), JSON.stringify(payload))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async forget(key: string): Promise<boolean> {
|
|
54
|
+
try {
|
|
55
|
+
const file = Bun.file(this.path(key))
|
|
56
|
+
if (await file.exists()) {
|
|
57
|
+
await rm(this.path(key))
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
} catch {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async has(key: string): Promise<boolean> {
|
|
67
|
+
return (await this.get(key)) !== undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async flush(): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
const files = await readdir(this.directory)
|
|
73
|
+
await Promise.all(
|
|
74
|
+
files
|
|
75
|
+
.filter((f) => f.endsWith('.cache'))
|
|
76
|
+
.map((f) => rm(join(this.directory, f))),
|
|
77
|
+
)
|
|
78
|
+
} catch {
|
|
79
|
+
// Directory might not exist yet — that's fine
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async increment(key: string, value = 1): Promise<number> {
|
|
84
|
+
const current = await this.get<number>(key)
|
|
85
|
+
const newValue = (current ?? 0) + value
|
|
86
|
+
await this.put(key, newValue)
|
|
87
|
+
return newValue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async decrement(key: string, value = 1): Promise<number> {
|
|
91
|
+
return this.increment(key, -value)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async add(key: string, value: unknown, ttl?: number): Promise<boolean> {
|
|
95
|
+
if (await this.has(key)) return false
|
|
96
|
+
await this.put(key, value, ttl)
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
private path(key: string): string {
|
|
103
|
+
// Hash the key to avoid filesystem issues with special characters
|
|
104
|
+
const safe = Buffer.from(key).toString('hex')
|
|
105
|
+
return join(this.directory, `${safe}.cache`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async ensureDirectory(): Promise<void> {
|
|
109
|
+
if (this.initialized) return
|
|
110
|
+
await mkdir(this.directory, { recursive: true })
|
|
111
|
+
this.initialized = true
|
|
112
|
+
}
|
|
113
|
+
}
|