@bakery-framework/core 1.0.0
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/LICENSE +19 -0
- package/README.md +89 -0
- package/package.json +69 -0
- package/src/cache/index.ts +8 -0
- package/src/cache/lru.ts +41 -0
- package/src/cache/shared-db.ts +51 -0
- package/src/cache/string.ts +150 -0
- package/src/cache/tiered.ts +493 -0
- package/src/client/globals.d.ts +74 -0
- package/src/client/livereload.ts +437 -0
- package/src/client/utils.ts +315 -0
- package/src/compiler/compiler.ts +263 -0
- package/src/compiler/dev-service.ts +660 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prompt-tracker.ts +36 -0
- package/src/compiler/tsconfig-sync.ts +71 -0
- package/src/core/bakery.ts +96 -0
- package/src/core/cache-version.ts +119 -0
- package/src/core/config.ts +296 -0
- package/src/core/context.ts +121 -0
- package/src/core/index.ts +61 -0
- package/src/core/init.ts +90 -0
- package/src/core/jsx.ts +152 -0
- package/src/core/paths.ts +24 -0
- package/src/core/plugins.ts +120 -0
- package/src/core/port.ts +73 -0
- package/src/global.d.ts +374 -0
- package/src/handlers/assets/google-font.ts +225 -0
- package/src/handlers/assets/image.ts +136 -0
- package/src/handlers/assets/nm.ts +73 -0
- package/src/handlers/assets/public.ts +17 -0
- package/src/handlers/assets/static.ts +86 -0
- package/src/handlers/assets/ts.ts +61 -0
- package/src/handlers/assets/tsx.ts +106 -0
- package/src/handlers/assets/virtual-asset.ts +104 -0
- package/src/handlers/core/$base.ts +256 -0
- package/src/handlers/core/$dynamic.ts +285 -0
- package/src/handlers/core/$error.ts +301 -0
- package/src/handlers/core/$middleware.ts +71 -0
- package/src/handlers/core/$mounts.ts +84 -0
- package/src/handlers/core/$registry.ts +153 -0
- package/src/handlers/core/$routing.ts +205 -0
- package/src/handlers/core/$static.ts +100 -0
- package/src/handlers/core/$websocket.ts +52 -0
- package/src/handlers/index.ts +21 -0
- package/src/handlers/routes/api.ts +95 -0
- package/src/handlers/routes/html.ts +95 -0
- package/src/handlers/routes/livereload.ts +54 -0
- package/src/handlers/routes/proxy.ts +74 -0
- package/src/logger/clients.ts +12 -0
- package/src/logger/index.ts +3 -0
- package/src/logger/logger.ts +375 -0
- package/src/logger/serve-log.ts +206 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/routes.ts +110 -0
- package/src/plugins/types.ts +19 -0
- package/src/router.ts +351 -0
- package/src/session.ts +556 -0
- package/src/shared.d.ts +63 -0
- package/src/startup.ts +154 -0
- package/src/types.d.ts +111 -0
- package/src/utils/common/case.ts +11 -0
- package/src/utils/common/index.ts +5 -0
- package/src/utils/common/json.ts +35 -0
- package/src/utils/common/match.ts +6 -0
- package/src/utils/common/misc.ts +53 -0
- package/src/utils/common/try.ts +6 -0
- package/src/utils/constants.ts +153 -0
- package/src/utils/fs.ts +621 -0
- package/src/utils/http/body.ts +65 -0
- package/src/utils/http/csrf.ts +111 -0
- package/src/utils/http/dom.ts +238 -0
- package/src/utils/http/escape.ts +8 -0
- package/src/utils/http/etag.ts +318 -0
- package/src/utils/http/html.ts +525 -0
- package/src/utils/http/index.ts +8 -0
- package/src/utils/http/ip.ts +32 -0
- package/src/utils/http/response.ts +129 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/isomorphic/case.ts +52 -0
- package/src/utils/isomorphic/escape.ts +43 -0
- package/src/utils/isomorphic/index.ts +15 -0
- package/src/utils/isomorphic/is.ts +36 -0
- package/src/utils/isomorphic/match.ts +50 -0
- package/src/utils/isomorphic/math.ts +11 -0
- package/src/utils/isomorphic/misc.ts +22 -0
- package/src/utils/isomorphic/stringify.ts +42 -0
- package/src/utils/isomorphic/try.ts +94 -0
- package/src/utils/jsonc.ts +10 -0
- package/src/utils/shared-pool.ts +193 -0
- package/tsconfig.app.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Kyle Cyrus Santos Obille
|
|
2
|
+
|
|
3
|
+
The Software is provided subject to the standard MIT License, as detailed below, with the addition of the Commons Clause v1.0.
|
|
4
|
+
|
|
5
|
+
The Commons Clause v1.0
|
|
6
|
+
|
|
7
|
+
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
|
8
|
+
|
|
9
|
+
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
|
|
10
|
+
|
|
11
|
+
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
|
|
12
|
+
|
|
13
|
+
Standard MIT License
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software (subject to the Commons Clause condition above), and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @bakery-framework/core
|
|
2
|
+
|
|
3
|
+
The core of [Bakery](https://github.com/obillekyle/bakery): request handling,
|
|
4
|
+
routing, sessions, caching and the server-side JSX runtime.
|
|
5
|
+
|
|
6
|
+
**Bun only.** This package ships TypeScript source with no build step, and the
|
|
7
|
+
runtime depends on Bun APIs throughout. It will not run on Node.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @bakery-framework/core @bakery-framework/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@bakery-framework/cli` owns the `bakery` binary that actually starts a server — core on
|
|
14
|
+
its own is the library it starts.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// server.config.ts
|
|
20
|
+
import { defineConfig } from '@bakery-framework/core'
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
root: 'src',
|
|
24
|
+
port: 3000,
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every file under `root` is a route. A `.tsx` file is a page, a `.ts` file under
|
|
29
|
+
`api/` is an endpoint:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// src/api/hello.ts
|
|
33
|
+
import { defineRoute, response } from '@bakery-framework/core'
|
|
34
|
+
|
|
35
|
+
export default defineRoute(async req => {
|
|
36
|
+
return response.json.success('ok', { method: req.method })
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## JSX setup
|
|
41
|
+
|
|
42
|
+
Pages are rendered server-side through Bakery's own `createElement`, so an app's
|
|
43
|
+
`tsconfig.json` must set the classic runtime **locally**:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"extends": "@bakery-framework/core/tsconfig.app.json",
|
|
48
|
+
"compilerOptions": {
|
|
49
|
+
"jsx": "react",
|
|
50
|
+
"jsxFactory": "createElement",
|
|
51
|
+
"jsxFragmentFactory": "Fragment"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`extends` alone is not enough. Bun's runtime does not follow `extends` into a
|
|
57
|
+
package specifier, so without the three options repeated here every `.tsx` route
|
|
58
|
+
fails with `Cannot find module 'react/jsx-dev-runtime'` — while `tsc` stays
|
|
59
|
+
perfectly happy, because it *does* follow the extends.
|
|
60
|
+
|
|
61
|
+
The fastest way to get a correct setup is to let the scaffolder write one:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bun create bakery my-app
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Notes
|
|
68
|
+
|
|
69
|
+
- No runtime dependencies.
|
|
70
|
+
- An unconfigured app gets an on-by-default per-IP rate limit (100 burst,
|
|
71
|
+
10 req/s). The startup banner says so; `rateLimit: false` turns it off.
|
|
72
|
+
- The export map is curated: only the subpaths it names can be imported, and
|
|
73
|
+
everything else is private. If you need something that is not exposed, open an
|
|
74
|
+
issue rather than reaching for a deep path — it will not resolve.
|
|
75
|
+
|
|
76
|
+
Full documentation lives in the
|
|
77
|
+
[repository](https://github.com/obillekyle/bakery).
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT with the Commons Clause v1.0 — see [LICENSE](./LICENSE). In short: use,
|
|
82
|
+
|
|
83
|
+
**Not an OSI-approved licence.** The Commons Clause removes the right to *sell*
|
|
84
|
+
the software — meaning to charge for a product or service whose value derives
|
|
85
|
+
substantially from it, hosting and support included. Everything else the MIT
|
|
86
|
+
licence grants is unchanged: use it, modify it, ship it inside your own product.
|
|
87
|
+
If your organisation only permits OSI-approved dependencies, this will not pass
|
|
88
|
+
that check.
|
|
89
|
+
modify and distribute freely; selling the software itself is not granted.
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bakery-framework/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bakery framework core: handlers, router, config, session, caches, logger, compiler.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bakery",
|
|
7
|
+
"bun",
|
|
8
|
+
"framework",
|
|
9
|
+
"web-framework",
|
|
10
|
+
"server",
|
|
11
|
+
"http",
|
|
12
|
+
"router",
|
|
13
|
+
"ssr",
|
|
14
|
+
"jsx"
|
|
15
|
+
],
|
|
16
|
+
"author": "obillekyle",
|
|
17
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/obillekyle/bakery.git",
|
|
21
|
+
"directory": "packages/core"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/obillekyle/bakery#readme",
|
|
24
|
+
"bugs": "https://github.com/obillekyle/bakery/issues",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "./src/core/index.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/core/index.ts",
|
|
32
|
+
"./compiler": "./src/compiler/index.ts",
|
|
33
|
+
"./core": "./src/core/index.ts",
|
|
34
|
+
"./handlers": "./src/handlers/index.ts",
|
|
35
|
+
"./logger": "./src/logger/index.ts",
|
|
36
|
+
"./utils": "./src/utils/index.ts",
|
|
37
|
+
"./utils/common": "./src/utils/common/index.ts",
|
|
38
|
+
"./utils/http": "./src/utils/http/index.ts",
|
|
39
|
+
"./utils/isomorphic": "./src/utils/isomorphic/index.ts",
|
|
40
|
+
"./types": "./src/types.d.ts",
|
|
41
|
+
"./package.json": "./package.json",
|
|
42
|
+
"./startup": "./src/startup.ts",
|
|
43
|
+
"./tsconfig.app.json": "./tsconfig.app.json",
|
|
44
|
+
"./session": "./src/session.ts",
|
|
45
|
+
"./jsx": "./src/core/jsx.ts",
|
|
46
|
+
"./plugins": "./src/plugins/index.ts",
|
|
47
|
+
"./cache/lru": "./src/cache/lru.ts",
|
|
48
|
+
"./cache/shared-db": "./src/cache/shared-db.ts",
|
|
49
|
+
"./cache/tiered": "./src/cache/tiered.ts",
|
|
50
|
+
"./compiler/tsconfig-sync": "./src/compiler/tsconfig-sync.ts",
|
|
51
|
+
"./core/plugins": "./src/core/plugins.ts",
|
|
52
|
+
"./core/config": "./src/core/config.ts",
|
|
53
|
+
"./core/init": "./src/core/init.ts",
|
|
54
|
+
"./core/port": "./src/core/port.ts",
|
|
55
|
+
"./router": "./src/router.ts",
|
|
56
|
+
"./utils/shared-pool": "./src/utils/shared-pool.ts",
|
|
57
|
+
"./global.d.ts": "./src/global.d.ts",
|
|
58
|
+
"./core/bakery": "./src/core/bakery.ts"
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"src",
|
|
62
|
+
"tsconfig.app.json",
|
|
63
|
+
"!src/**/*.test.ts",
|
|
64
|
+
"!src/tests"
|
|
65
|
+
],
|
|
66
|
+
"engines": {
|
|
67
|
+
"bun": ">=1.3.14"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/cache/lru.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class LRUCache<K, V> extends Map<K, V> {
|
|
2
|
+
/**
|
|
3
|
+
* Called with the entry dropped to make room. Needed when values own a
|
|
4
|
+
* resource that must be released — an evicted prepared statement, for
|
|
5
|
+
* instance, otherwise leaks until the process exits.
|
|
6
|
+
*/
|
|
7
|
+
private readonly onEvict?: (key: K, value: V) => void
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
public readonly maxSize: number,
|
|
11
|
+
onEvict?: (key: K, value: V) => void,
|
|
12
|
+
) {
|
|
13
|
+
super()
|
|
14
|
+
if (maxSize <= 0) throw new Error('LRUCache size must be greater than 0')
|
|
15
|
+
this.onEvict = onEvict
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(key: K): V | undefined {
|
|
19
|
+
const val = super.get(key)
|
|
20
|
+
// `has` only to disambiguate a stored `undefined` from a miss — on the
|
|
21
|
+
// common hit this is two map operations instead of four.
|
|
22
|
+
if (val === undefined && !super.has(key)) return undefined
|
|
23
|
+
super.delete(key)
|
|
24
|
+
super.set(key, val as V)
|
|
25
|
+
return val
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
set(key: K, value: V): this {
|
|
29
|
+
if (super.has(key)) super.delete(key)
|
|
30
|
+
super.set(key, value)
|
|
31
|
+
if (this.size > this.maxSize) {
|
|
32
|
+
const first = this.keys().next().value
|
|
33
|
+
if (first !== undefined) {
|
|
34
|
+
const evicted = super.get(first!)
|
|
35
|
+
super.delete(first!)
|
|
36
|
+
if (evicted !== undefined) this.onEvict?.(first!, evicted)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return this
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import { Bakery } from '../core/bakery'
|
|
4
|
+
import { checkCacheVersion } from '../core/cache-version'
|
|
5
|
+
import { fs } from '../utils'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The tiered cache's spill-to-disk store — sessions and LRU overflow.
|
|
9
|
+
*
|
|
10
|
+
* Under `Bakery.cacheDir`, not `dataDir`, because every row in it is
|
|
11
|
+
* rebuildable: a session is a cookie plus whatever the app chose to hang off
|
|
12
|
+
* it, and the LRU tier re-fills from source on the next miss. It used to sit
|
|
13
|
+
* beside `server.db` in the data directory, which put a disposable file behind
|
|
14
|
+
* the one durability guarantee the framework makes.
|
|
15
|
+
*
|
|
16
|
+
* **The consequence is that sessions do not survive a framework upgrade.**
|
|
17
|
+
* `checkCacheVersion` wipes the whole cache directory on every version bump and
|
|
18
|
+
* every dev<->prod switch, and this file goes with it — users are logged out.
|
|
19
|
+
* That is intended: the alternative is a cache format from an older framework
|
|
20
|
+
* version being read by a newer one, which is exactly the failure the version
|
|
21
|
+
* wipe exists to prevent. Anything that must outlive an upgrade belongs in the
|
|
22
|
+
* app's own tables under `Bakery.dataDir`.
|
|
23
|
+
*
|
|
24
|
+
* **That was aspirational until 2026-08-09, and the `await` below is what makes
|
|
25
|
+
* it true.** The check ran from `initConfig()`, while this module opens the
|
|
26
|
+
* database at *import* time — so the process already held a handle inside the
|
|
27
|
+
* directory the wipe was about to delete. On Windows the delete failed with
|
|
28
|
+
* `EBUSY`, node's recursive walk stopped at the locked entry, and whatever it
|
|
29
|
+
* had not reached yet survived: sessions *did* outlive upgrades, and so did
|
|
30
|
+
* `html/`, the compiled-page cache. Awaiting the check here orders the two by
|
|
31
|
+
* construction — the cache cannot be opened before it has been validated —
|
|
32
|
+
* rather than by hoping one import happens after another.
|
|
33
|
+
*/
|
|
34
|
+
await checkCacheVersion()
|
|
35
|
+
|
|
36
|
+
const dbFilePath = `${Bakery.cacheDir}/shared-cache.db`
|
|
37
|
+
if (!fs.exists(dbFilePath)) await fs.mkdir(dirname(dbFilePath))
|
|
38
|
+
|
|
39
|
+
export const cacheDb = new Database(dbFilePath, { create: true })
|
|
40
|
+
const journalMode = process.platform === 'win32' ? 'DELETE' : 'WAL'
|
|
41
|
+
cacheDb.run(`PRAGMA journal_mode = ${journalMode};`)
|
|
42
|
+
cacheDb.run('PRAGMA synchronous = NORMAL;')
|
|
43
|
+
cacheDb.run('PRAGMA temp_store = memory;')
|
|
44
|
+
// `core/init.ts` installs THREAD_WORKER as an accessor holding a **boolean**,
|
|
45
|
+
// so the string comparison this used to carry (`!== 'false'`) was always true
|
|
46
|
+
// and the flag worked by accident. Read it through `import.meta.env`, which is
|
|
47
|
+
// where every other mode flag is read from.
|
|
48
|
+
const isWorker = Boolean(import.meta.env.THREAD_WORKER)
|
|
49
|
+
cacheDb.run(`PRAGMA cache_size = ${isWorker ? -256 : -2000};`)
|
|
50
|
+
cacheDb.run('PRAGMA busy_timeout = 5000;')
|
|
51
|
+
cacheDb.run('PRAGMA mmap_size = 0;')
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Statement } from 'bun:sqlite'
|
|
2
|
+
import { db, type Milliseconds, registerCache } from './tiered'
|
|
3
|
+
|
|
4
|
+
type StringEntry = { value: string; accessedAt: number }
|
|
5
|
+
|
|
6
|
+
class StringCache {
|
|
7
|
+
private forwardMap = new Map<string, StringEntry>()
|
|
8
|
+
private reverseMap = new Map<string, string>()
|
|
9
|
+
private dirtyKeys = new Set<string>()
|
|
10
|
+
private flushTimer?: ReturnType<typeof setInterval>
|
|
11
|
+
private tableName: string
|
|
12
|
+
private stmt: Record<string, Statement>
|
|
13
|
+
|
|
14
|
+
constructor(tableId: string, flushIntervalMs?: Milliseconds) {
|
|
15
|
+
this.tableName = tableId.replace(/[^a-zA-Z0-9_]/g, '')
|
|
16
|
+
|
|
17
|
+
db.run(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
19
|
+
key TEXT PRIMARY KEY,
|
|
20
|
+
value TEXT UNIQUE,
|
|
21
|
+
accessedAt INTEGER
|
|
22
|
+
)
|
|
23
|
+
`)
|
|
24
|
+
db.run(
|
|
25
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_value ON ${this.tableName}(value)`,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// prettier-ignore
|
|
29
|
+
this.stmt = {
|
|
30
|
+
insert: db.prepare(
|
|
31
|
+
`INSERT OR REPLACE INTO ${this.tableName} (key, value, accessedAt) VALUES (?, ?, ?)`,
|
|
32
|
+
),
|
|
33
|
+
delete: db.prepare(`DELETE FROM ${this.tableName} WHERE key = ?`),
|
|
34
|
+
byKey: db.prepare(
|
|
35
|
+
`SELECT value, accessedAt FROM ${this.tableName} WHERE key = ?`,
|
|
36
|
+
),
|
|
37
|
+
byVal: db.prepare(
|
|
38
|
+
`SELECT key, accessedAt FROM ${this.tableName} WHERE value = ?`,
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (flushIntervalMs !== undefined) {
|
|
43
|
+
this.flushTimer = setInterval(() => this.flushToDisk(), flushIntervalMs)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
registerCache(this)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set(key: string, value: string): void {
|
|
50
|
+
const existingVal = this.forwardMap.get(key)?.value
|
|
51
|
+
const existingKey = this.reverseMap.get(value)
|
|
52
|
+
|
|
53
|
+
if (existingVal === value) {
|
|
54
|
+
// already mapped
|
|
55
|
+
} else if (existingKey !== undefined && existingKey !== key) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Collision! Value '${value}' is already mapped to key '${existingKey}'`,
|
|
58
|
+
)
|
|
59
|
+
} else if (existingVal !== undefined && existingVal !== value) {
|
|
60
|
+
this.reverseMap.delete(existingVal)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const now = Date.now()
|
|
64
|
+
this.forwardMap.set(key, { value, accessedAt: now })
|
|
65
|
+
this.reverseMap.set(value, key)
|
|
66
|
+
this.dirtyKeys.add(key)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getValue(key: string): string | undefined {
|
|
70
|
+
if (this.forwardMap.has(key)) {
|
|
71
|
+
const entry = this.forwardMap.get(key)!
|
|
72
|
+
entry.accessedAt = Date.now()
|
|
73
|
+
return entry.value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const row = this.stmt.byKey.get(key) as StringEntry | null
|
|
77
|
+
if (!row) return undefined
|
|
78
|
+
|
|
79
|
+
this.forwardMap.set(key, { value: row.value, accessedAt: Date.now() })
|
|
80
|
+
this.reverseMap.set(row.value, key)
|
|
81
|
+
return row.value
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getKey(value: string): string | undefined {
|
|
85
|
+
if (this.reverseMap.has(value)) {
|
|
86
|
+
const key = this.reverseMap.get(value)!
|
|
87
|
+
this.forwardMap.get(key)!.accessedAt = Date.now()
|
|
88
|
+
return key
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const row = this.stmt.byVal.get(value) as {
|
|
92
|
+
key: string
|
|
93
|
+
accessedAt: number
|
|
94
|
+
} | null
|
|
95
|
+
if (!row) return undefined
|
|
96
|
+
|
|
97
|
+
this.forwardMap.set(row.key, { value, accessedAt: Date.now() })
|
|
98
|
+
this.reverseMap.set(value, row.key)
|
|
99
|
+
return row.key
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
deleteByKey(key: string): boolean {
|
|
103
|
+
const entry = this.forwardMap.get(key)
|
|
104
|
+
|
|
105
|
+
if (entry) {
|
|
106
|
+
this.reverseMap.delete(entry.value)
|
|
107
|
+
this.forwardMap.delete(key)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.dirtyKeys.delete(key)
|
|
111
|
+
const info = this.stmt.delete.run(key)
|
|
112
|
+
return entry !== undefined || info.changes > 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
flushToDisk(): void {
|
|
116
|
+
if (this.dirtyKeys.size === 0) return
|
|
117
|
+
const keys = new Set(this.dirtyKeys)
|
|
118
|
+
this.dirtyKeys.clear()
|
|
119
|
+
db.transaction(() => {
|
|
120
|
+
for (const key of keys) {
|
|
121
|
+
const entry = this.forwardMap.get(key)
|
|
122
|
+
if (!entry) continue
|
|
123
|
+
this.stmt.insert.run(key, entry.value, entry.accessedAt)
|
|
124
|
+
}
|
|
125
|
+
})()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
flushAllToDisk(): void {
|
|
129
|
+
if (this.forwardMap.size === 0) return
|
|
130
|
+
db.transaction(() => {
|
|
131
|
+
for (const [key, entry] of this.forwardMap) {
|
|
132
|
+
this.stmt.insert.run(key, entry.value, entry.accessedAt)
|
|
133
|
+
}
|
|
134
|
+
this.dirtyKeys.clear()
|
|
135
|
+
})()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
close(): void {
|
|
139
|
+
clearInterval(this.flushTimer)
|
|
140
|
+
this.flushTimer = undefined
|
|
141
|
+
this.forwardMap.clear()
|
|
142
|
+
this.reverseMap.clear()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
[Symbol.dispose](): void {
|
|
146
|
+
this.close()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const Strings = new StringCache('string-cache', 30 * 60 * 1000)
|