@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function toKebabCase(str: string): string {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
4
|
+
.replace(/[\s_]+/g, '-')
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toCamelCase(str: string): string {
|
|
9
|
+
return str
|
|
10
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
11
|
+
.replace(/^[A-Z]/, match => match.toLowerCase())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toPascalCase(str: string): string {
|
|
15
|
+
const camel = toCamelCase(str)
|
|
16
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toSnakeCase(str: string): string {
|
|
20
|
+
return str
|
|
21
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
22
|
+
.replace(/[\s-]+/g, '_')
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type CaseType = 'kebab' | 'camel' | 'pascal' | 'snake'
|
|
27
|
+
|
|
28
|
+
export const Case = Object.assign(
|
|
29
|
+
function Case(type: CaseType, str: string): string {
|
|
30
|
+
switch (type) {
|
|
31
|
+
case 'kebab':
|
|
32
|
+
return toKebabCase(str)
|
|
33
|
+
case 'camel':
|
|
34
|
+
return toCamelCase(str)
|
|
35
|
+
case 'pascal':
|
|
36
|
+
return toPascalCase(str)
|
|
37
|
+
case 'snake':
|
|
38
|
+
return toSnakeCase(str)
|
|
39
|
+
default:
|
|
40
|
+
return str
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
kebab: toKebabCase,
|
|
45
|
+
camel: toCamelCase,
|
|
46
|
+
pascal: toPascalCase,
|
|
47
|
+
snake: toSnakeCase,
|
|
48
|
+
upper: (str: string) => str.toUpperCase(),
|
|
49
|
+
lower: (str: string) => str.toLowerCase(),
|
|
50
|
+
caps: (str: string) => str.toUpperCase().replace(/[\s_-]+/g, ''),
|
|
51
|
+
},
|
|
52
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escaping primitives shared by the server pipeline and the browser runtime.
|
|
3
|
+
*
|
|
4
|
+
* These live in `isomorphic/` because both sides need them: the HTML pipeline
|
|
5
|
+
* and the Vue plugin escape on the way out, and the client bundle escapes when
|
|
6
|
+
* building DOM strings. Nothing here may reference `Bun.*`, node builtins, or
|
|
7
|
+
* DOM globals — this module is compiled into the browser bundle as-is.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const HTML_ESCAPES: Record<string, string> = {
|
|
11
|
+
'&': '&',
|
|
12
|
+
'<': '<',
|
|
13
|
+
'>': '>',
|
|
14
|
+
'"': '"',
|
|
15
|
+
"'": ''',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Escape the five HTML-significant characters. Nullish input yields an empty
|
|
20
|
+
* string rather than throwing, so it is safe to call on optional values.
|
|
21
|
+
*/
|
|
22
|
+
export function escapeHtml(value: unknown): string {
|
|
23
|
+
if (value === null || value === undefined) return ''
|
|
24
|
+
return String(value).replace(/[&<>"']/g, char => HTML_ESCAPES[char] || char)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Serialize for embedding inside a `<script>` tag. Escaping `<` stops a
|
|
29
|
+
* `</script>` in the data from closing the tag; U+2028/2029 are line terminators
|
|
30
|
+
* to a JS parser but legal raw inside JSON.
|
|
31
|
+
*
|
|
32
|
+
* `utils/http/dom.ts` used to carry its own copy of this and is now a caller.
|
|
33
|
+
*/
|
|
34
|
+
export function escapeScriptJson(value: unknown): string {
|
|
35
|
+
const json = JSON.stringify(value, (_key, val) =>
|
|
36
|
+
val === undefined ? null : val,
|
|
37
|
+
)
|
|
38
|
+
if (json === undefined) return 'null'
|
|
39
|
+
return json
|
|
40
|
+
.replace(/</g, '\\u003c')
|
|
41
|
+
.replace(/\u2028/g, '\\u2028')
|
|
42
|
+
.replace(/\u2029/g, '\\u2029')
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utilities shared by the server and the browser bundle.
|
|
3
|
+
*
|
|
4
|
+
* Rule for this directory: no `Bun.*`, no node builtins, no DOM globals. These
|
|
5
|
+
* modules are compiled into the client bundle as-is, so anything that reaches
|
|
6
|
+
* for a runtime API belongs one layer up in `utils/` or in `client/`.
|
|
7
|
+
*/
|
|
8
|
+
export * from './case'
|
|
9
|
+
export * from './escape'
|
|
10
|
+
export * from './is'
|
|
11
|
+
export * from './match'
|
|
12
|
+
export * from './math'
|
|
13
|
+
export * from './misc'
|
|
14
|
+
export * from './stringify'
|
|
15
|
+
export * from './try'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type predicates, shared by the server and the browser bundle.
|
|
3
|
+
*
|
|
4
|
+
* Two implementations of this used to exist. This one keeps the *callable*
|
|
5
|
+
* shape from the client copy (`is(x, 'string')` as well as `is.string(x)`,
|
|
6
|
+
* which is what the global `ISFunction` type has always described) and the
|
|
7
|
+
* *semantics* of the server copy — notably `is.object([]) === true`, which
|
|
8
|
+
* `misc.test.ts` asserts and which callers like `router.ts` rely on to
|
|
9
|
+
* JSON-encode array response bodies.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const checks = {
|
|
13
|
+
string: (v: any): v is string => typeof v === 'string',
|
|
14
|
+
number: (v: any): v is number => typeof v === 'number',
|
|
15
|
+
boolean: (v: any): v is boolean => typeof v === 'boolean',
|
|
16
|
+
bigint: (v: any): v is bigint => typeof v === 'bigint',
|
|
17
|
+
symbol: (v: any): v is symbol => typeof v === 'symbol',
|
|
18
|
+
function: (v: any): v is Function => typeof v === 'function',
|
|
19
|
+
/** Arrays count as objects here — see the note above before changing this. */
|
|
20
|
+
object: (v: any): v is Record<string, any> =>
|
|
21
|
+
v !== null && typeof v === 'object',
|
|
22
|
+
array: Array.isArray,
|
|
23
|
+
null: (v: any): v is null => v === null,
|
|
24
|
+
undefined: (v: any): v is undefined => v === undefined,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const is: ISFunction = Object.assign(function is(
|
|
28
|
+
value: any,
|
|
29
|
+
type?: string,
|
|
30
|
+
) {
|
|
31
|
+
if (!type) return value !== null && value !== undefined
|
|
32
|
+
// Delegate to the table rather than falling through to `typeof`, so that
|
|
33
|
+
// is(null, 'object') agrees with is.object(null) instead of contradicting it.
|
|
34
|
+
const check = (checks as any)[type]
|
|
35
|
+
return check ? check(value) : typeof value === type
|
|
36
|
+
}, checks) as ISFunction
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Match } from '../../types'
|
|
2
|
+
|
|
3
|
+
const matchDefault = Symbol('matchDefault')
|
|
4
|
+
|
|
5
|
+
function handleStringCases(value: any, cases: any): any {
|
|
6
|
+
// hasOwn, not `in`: `in` walks the prototype chain, so match('toString', {…})
|
|
7
|
+
// would find and invoke Object.prototype.toString.
|
|
8
|
+
if (Object.hasOwn(cases, value)) {
|
|
9
|
+
const handler = cases[value]
|
|
10
|
+
return typeof handler === 'function' ? handler(value) : handler
|
|
11
|
+
}
|
|
12
|
+
if (matchDefault in cases) {
|
|
13
|
+
const handler = cases[matchDefault]
|
|
14
|
+
return typeof handler === 'function' ? handler(value) : handler
|
|
15
|
+
}
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function handleArrayCases(value: any, cases: any[]): any {
|
|
20
|
+
for (const [predicate, result] of cases) {
|
|
21
|
+
const isMatch =
|
|
22
|
+
predicate === match ||
|
|
23
|
+
predicate === matchDefault ||
|
|
24
|
+
predicate === value ||
|
|
25
|
+
(typeof predicate === 'function' && Boolean(predicate(value)))
|
|
26
|
+
|
|
27
|
+
if (isMatch) {
|
|
28
|
+
return typeof result === 'function' ? result(value) : result
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const match: Match<typeof matchDefault> = ((value: any, cases: any) => {
|
|
35
|
+
const isString = typeof value === 'string'
|
|
36
|
+
const isArray = Array.isArray(cases)
|
|
37
|
+
|
|
38
|
+
if (isString && !isArray) {
|
|
39
|
+
return handleStringCases(value, cases)
|
|
40
|
+
}
|
|
41
|
+
if (isArray) {
|
|
42
|
+
return handleArrayCases(value, cases)
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}) as any
|
|
46
|
+
|
|
47
|
+
match.default = matchDefault
|
|
48
|
+
match[Symbol.toPrimitive] = () => matchDefault
|
|
49
|
+
|
|
50
|
+
export { matchDefault }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const Math2 = {
|
|
2
|
+
clamp(value: number, min?: number, max?: number): number {
|
|
3
|
+
min ??= -Infinity
|
|
4
|
+
max ??= Infinity
|
|
5
|
+
return Math.min(Math.max(value, min), max)
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
step(value: number, step: number): number {
|
|
9
|
+
return Math.round(value / step) * step
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { is } from './is'
|
|
2
|
+
|
|
3
|
+
export function throws(message: string | Error): never {
|
|
4
|
+
throw is.string(message) ? new Error(message) : message
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function assert(condition: any, message?: string): asserts condition {
|
|
8
|
+
if (!condition) {
|
|
9
|
+
throw new Error(message || 'Assertion failed')
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Escape hatch for casting through `any` without spelling it out each time. */
|
|
14
|
+
export function any<T = any>(value: any): T {
|
|
15
|
+
return value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function repeat(n: number): number[]
|
|
19
|
+
export function repeat<T>(n: number, fn: (i: number) => T): T[]
|
|
20
|
+
export function repeat<T>(n: number, fn?: (i: number) => T): unknown[] {
|
|
21
|
+
return Array.from({ length: n }, (_, k) => (fn ? fn(k) : k))
|
|
22
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circular-safe JSON helpers.
|
|
3
|
+
*
|
|
4
|
+
* Three copies of this logic existed: the client `$fmt` formatter, the
|
|
5
|
+
* livereload log serializer, and ad-hoc replacers on the server. They drifted —
|
|
6
|
+
* the `$fmt` one shipped with a one-parameter replacer that made every object
|
|
7
|
+
* render as `""` — so the kernel lives here once and callers layer their own
|
|
8
|
+
* extras on top.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a `JSON.stringify` replacer that substitutes `'[Circular]'` for repeat
|
|
13
|
+
* references. `extra` runs after the cycle check and can rewrite values further.
|
|
14
|
+
*
|
|
15
|
+
* Note the two-parameter signature: `JSON.stringify` calls a replacer as
|
|
16
|
+
* `(key, value)`, so a single-parameter function silently binds to *key*.
|
|
17
|
+
*/
|
|
18
|
+
export function circularReplacer(
|
|
19
|
+
extra?: (key: string, value: any) => any,
|
|
20
|
+
): (key: string, value: any) => any {
|
|
21
|
+
const seen = new WeakSet<object>()
|
|
22
|
+
return (key, value) => {
|
|
23
|
+
if (typeof value === 'object' && value !== null) {
|
|
24
|
+
if (seen.has(value)) return '[Circular]'
|
|
25
|
+
seen.add(value)
|
|
26
|
+
}
|
|
27
|
+
return extra ? extra(key, value) : value
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort string form of any value, safe against cycles and throwing
|
|
33
|
+
* `toJSON`/getters. Primitives pass through `String()` unchanged.
|
|
34
|
+
*/
|
|
35
|
+
export function safeStringify(value: unknown): string {
|
|
36
|
+
if (typeof value !== 'object' || value === null) return String(value)
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(value, circularReplacer()) ?? String(value)
|
|
39
|
+
} catch {
|
|
40
|
+
return Object.prototype.toString.call(value)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { MixedPromise, Wrapped } from '../../types'
|
|
2
|
+
import { is } from './is'
|
|
3
|
+
|
|
4
|
+
// These were declared again, here, byte-for-byte identical to `types.d.ts` —
|
|
5
|
+
// the second copy convention 5 exists to prevent. `isomorphic.test.ts` catches
|
|
6
|
+
// a duplicated *value* by reference identity, and a type has no runtime
|
|
7
|
+
// identity to compare, so nothing saw it.
|
|
8
|
+
//
|
|
9
|
+
// A type-only import is safe in an isomorphic module: `verbatimModuleSyntax`
|
|
10
|
+
// makes the compiler reject a value-position import of these, so it cannot
|
|
11
|
+
// become a runtime module edge into the browser bundle.
|
|
12
|
+
|
|
13
|
+
type CatchReturn<T extends MixedPromise<any>> =
|
|
14
|
+
T extends Promise<infer V>
|
|
15
|
+
? Promise<[Error, null] | [null, V]>
|
|
16
|
+
: MixedPromise<[Error, null] | [null, T]>
|
|
17
|
+
|
|
18
|
+
function tryThrow<T>(
|
|
19
|
+
callback: () => MixedPromise<T>,
|
|
20
|
+
error?: string | Error,
|
|
21
|
+
): Promise<T> {
|
|
22
|
+
return Promise.try(callback).catch((err: any) => {
|
|
23
|
+
throw is.string(error) ? new Error(error) : error || err
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tryReturn<T extends MixedPromise<any>, D extends MixedPromise<any>>(
|
|
28
|
+
value: Wrapped<T>,
|
|
29
|
+
defaultValue: Wrapped<D, [Error]>,
|
|
30
|
+
): T | D {
|
|
31
|
+
try {
|
|
32
|
+
const unwrapped = is.function(value) ? (value as any)() : value
|
|
33
|
+
if (
|
|
34
|
+
unwrapped instanceof Promise ||
|
|
35
|
+
(is.object(unwrapped) && is.function((unwrapped as any).catch))
|
|
36
|
+
) {
|
|
37
|
+
return (unwrapped as any).catch((error: any) =>
|
|
38
|
+
is.function(defaultValue) ? (defaultValue as any)(error) : defaultValue,
|
|
39
|
+
) as any
|
|
40
|
+
}
|
|
41
|
+
return unwrapped
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
const unwrappedDefault = is.function(defaultValue)
|
|
44
|
+
? (defaultValue as any)(error)
|
|
45
|
+
: defaultValue
|
|
46
|
+
return unwrappedDefault
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function trySilent<T extends MixedPromise<any>>(value: Wrapped<T>): T | null {
|
|
51
|
+
return tryReturn(value, null as any)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type TryType = {
|
|
55
|
+
<T extends MixedPromise<any>>(value: Wrapped<T>): T | null
|
|
56
|
+
catch<T extends MixedPromise<any>>(value: Wrapped<T>): CatchReturn<T>
|
|
57
|
+
return<T extends MixedPromise<any>, D extends MixedPromise<any>>(
|
|
58
|
+
value: Wrapped<T>,
|
|
59
|
+
defaultValue: Wrapped<D, [Error]>,
|
|
60
|
+
): T | D
|
|
61
|
+
throw: typeof tryThrow
|
|
62
|
+
/**
|
|
63
|
+
* Alias for calling `Try` directly. Kept because the browser global has
|
|
64
|
+
* always exposed it; unifying the two copies is not the place to shrink a
|
|
65
|
+
* published surface.
|
|
66
|
+
*/
|
|
67
|
+
silent<T extends MixedPromise<any>>(value: Wrapped<T>): T | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const Try: TryType = Object.assign(
|
|
71
|
+
function Try<T extends MixedPromise<any>>(value: Wrapped<T>): T | null {
|
|
72
|
+
return trySilent(value)
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
catch<T extends MixedPromise<any>>(value: Wrapped<T>): CatchReturn<T> {
|
|
76
|
+
if (is.function(value)) {
|
|
77
|
+
return Promise.try(value as any)
|
|
78
|
+
.then(data => [null, data])
|
|
79
|
+
.catch(error => [error, null]) as any
|
|
80
|
+
}
|
|
81
|
+
if (value instanceof Promise) {
|
|
82
|
+
return value
|
|
83
|
+
.then(data => [null, data])
|
|
84
|
+
.catch(error => [error, null]) as any
|
|
85
|
+
}
|
|
86
|
+
return [null, value] as any
|
|
87
|
+
},
|
|
88
|
+
return: tryReturn,
|
|
89
|
+
throw: tryThrow,
|
|
90
|
+
silent: trySilent,
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
export const tryCatch = Try.catch
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function parseJSONC(jsonc: string): any {
|
|
2
|
+
if (!jsonc.trim()) return null
|
|
3
|
+
|
|
4
|
+
const clean = jsonc.replace(
|
|
5
|
+
/("(?:\\.|[^"\\])*")|(\/\/.*|\/\*[\s\S]*?\*\/)|,\s*(?=[\]}])/g,
|
|
6
|
+
(_, stringLiteral) => (stringLiteral ? stringLiteral : ''),
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
return JSON.parse(clean)
|
|
10
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const HEADER_INT_COUNT = 16
|
|
2
|
+
const COUNTERS_INT_COUNT = 256
|
|
3
|
+
const RATE_LIMIT_SLOT_COUNT = 1024
|
|
4
|
+
const HEADER_BYTES = HEADER_INT_COUNT * 4
|
|
5
|
+
const COUNTERS_BYTES = COUNTERS_INT_COUNT * 4
|
|
6
|
+
const RATE_LIMIT_BYTES = RATE_LIMIT_SLOT_COUNT * 2 * 4
|
|
7
|
+
const BUFFER_START_OFFSET = HEADER_BYTES + COUNTERS_BYTES + RATE_LIMIT_BYTES
|
|
8
|
+
|
|
9
|
+
export const COUNTER_SLOTS = {
|
|
10
|
+
TOTAL_REQUESTS: 0,
|
|
11
|
+
TOTAL_ERRORS: 1,
|
|
12
|
+
ACTIVE_CONNECTIONS: 2,
|
|
13
|
+
LATENCY_SUM_MS: 3,
|
|
14
|
+
} as const
|
|
15
|
+
|
|
16
|
+
export class SharedMemoryPool {
|
|
17
|
+
// Definite-assignment assertions because the adopt-an-existing-buffer path
|
|
18
|
+
// assigns all five through `bind()`, and TypeScript's initialization
|
|
19
|
+
// analysis does not follow a method call out of the constructor. Both
|
|
20
|
+
// constructor paths do assign every one of them before returning.
|
|
21
|
+
buffer!: SharedArrayBuffer
|
|
22
|
+
header!: Int32Array
|
|
23
|
+
counters!: Int32Array
|
|
24
|
+
rateLimits!: Int32Array
|
|
25
|
+
dataPool!: Uint8Array
|
|
26
|
+
|
|
27
|
+
constructor(sizeOrBuffer: number | SharedArrayBuffer = 1024 * 1024) {
|
|
28
|
+
if (typeof sizeOrBuffer === 'number') {
|
|
29
|
+
const size = Math.max(sizeOrBuffer, BUFFER_START_OFFSET + 1024)
|
|
30
|
+
this.buffer = new SharedArrayBuffer(size)
|
|
31
|
+
this.header = new Int32Array(this.buffer, 0, HEADER_INT_COUNT)
|
|
32
|
+
this.counters = new Int32Array(
|
|
33
|
+
this.buffer,
|
|
34
|
+
HEADER_BYTES,
|
|
35
|
+
COUNTERS_INT_COUNT,
|
|
36
|
+
)
|
|
37
|
+
this.rateLimits = new Int32Array(
|
|
38
|
+
this.buffer,
|
|
39
|
+
HEADER_BYTES + COUNTERS_BYTES,
|
|
40
|
+
RATE_LIMIT_SLOT_COUNT * 2,
|
|
41
|
+
)
|
|
42
|
+
this.dataPool = new Uint8Array(
|
|
43
|
+
this.buffer,
|
|
44
|
+
BUFFER_START_OFFSET,
|
|
45
|
+
size - BUFFER_START_OFFSET,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
Atomics.store(this.header, 0, 0x42414b45)
|
|
49
|
+
Atomics.store(this.header, 1, size)
|
|
50
|
+
Atomics.store(this.header, 2, BUFFER_START_OFFSET)
|
|
51
|
+
} else {
|
|
52
|
+
// Adopting a buffer someone else laid out is exactly what `bind` does —
|
|
53
|
+
// it read the header for the size rather than trusting `byteLength`, and
|
|
54
|
+
// so did the copy that used to sit here, character for character.
|
|
55
|
+
this.bind(sizeOrBuffer)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Point every view at `buffer`, taking its size from the header it carries. */
|
|
60
|
+
bind(buffer: SharedArrayBuffer): void {
|
|
61
|
+
this.buffer = buffer
|
|
62
|
+
this.header = new Int32Array(this.buffer, 0, HEADER_INT_COUNT)
|
|
63
|
+
this.counters = new Int32Array(
|
|
64
|
+
this.buffer,
|
|
65
|
+
HEADER_BYTES,
|
|
66
|
+
COUNTERS_INT_COUNT,
|
|
67
|
+
)
|
|
68
|
+
this.rateLimits = new Int32Array(
|
|
69
|
+
this.buffer,
|
|
70
|
+
HEADER_BYTES + COUNTERS_BYTES,
|
|
71
|
+
RATE_LIMIT_SLOT_COUNT * 2,
|
|
72
|
+
)
|
|
73
|
+
const size = Atomics.load(this.header, 1) || this.buffer.byteLength
|
|
74
|
+
this.dataPool = new Uint8Array(
|
|
75
|
+
this.buffer,
|
|
76
|
+
BUFFER_START_OFFSET,
|
|
77
|
+
size - BUFFER_START_OFFSET,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
incrementCounter(slot: number, delta = 1): number {
|
|
82
|
+
if (slot < 0 || slot >= COUNTERS_INT_COUNT) return 0
|
|
83
|
+
return Atomics.add(this.counters, slot, delta) + delta
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
decrementCounter(slot: number, delta = 1): number {
|
|
87
|
+
if (slot < 0 || slot >= COUNTERS_INT_COUNT) return 0
|
|
88
|
+
return Atomics.sub(this.counters, slot, delta) - delta
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getCounter(slot: number): number {
|
|
92
|
+
if (slot < 0 || slot >= COUNTERS_INT_COUNT) return 0
|
|
93
|
+
return Atomics.load(this.counters, slot)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setCounter(slot: number, value: number): number {
|
|
97
|
+
if (slot < 0 || slot >= COUNTERS_INT_COUNT) return 0
|
|
98
|
+
return Atomics.store(this.counters, slot, value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
consumeToken(
|
|
102
|
+
slot: number,
|
|
103
|
+
maxTokens: number,
|
|
104
|
+
refillRatePerSec: number,
|
|
105
|
+
tokensRequested = 1,
|
|
106
|
+
): boolean {
|
|
107
|
+
if (slot < 0 || slot >= RATE_LIMIT_SLOT_COUNT) return false
|
|
108
|
+
const tokenIndex = slot * 2
|
|
109
|
+
const timeIndex = slot * 2 + 1
|
|
110
|
+
|
|
111
|
+
const nowSec = Math.floor(Date.now() / 1000)
|
|
112
|
+
|
|
113
|
+
while (true) {
|
|
114
|
+
const currentTokens = Atomics.load(this.rateLimits, tokenIndex)
|
|
115
|
+
const lastTime = Atomics.load(this.rateLimits, timeIndex)
|
|
116
|
+
|
|
117
|
+
if (lastTime === 0 && currentTokens === 0) {
|
|
118
|
+
const initTokens = Math.max(0, maxTokens - tokensRequested)
|
|
119
|
+
if (
|
|
120
|
+
Atomics.compareExchange(this.rateLimits, timeIndex, 0, nowSec) === 0
|
|
121
|
+
) {
|
|
122
|
+
Atomics.store(this.rateLimits, tokenIndex, initTokens)
|
|
123
|
+
return tokensRequested <= maxTokens
|
|
124
|
+
}
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let availableTokens = currentTokens
|
|
129
|
+
const elapsed = Math.max(0, nowSec - lastTime)
|
|
130
|
+
if (elapsed > 0) {
|
|
131
|
+
availableTokens = Math.min(
|
|
132
|
+
maxTokens,
|
|
133
|
+
currentTokens + elapsed * refillRatePerSec,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (availableTokens < tokensRequested) {
|
|
138
|
+
if (elapsed > 0) {
|
|
139
|
+
Atomics.compareExchange(this.rateLimits, timeIndex, lastTime, nowSec)
|
|
140
|
+
Atomics.compareExchange(
|
|
141
|
+
this.rateLimits,
|
|
142
|
+
tokenIndex,
|
|
143
|
+
currentTokens,
|
|
144
|
+
availableTokens,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const newTokens = availableTokens - tokensRequested
|
|
151
|
+
if (
|
|
152
|
+
Atomics.compareExchange(
|
|
153
|
+
this.rateLimits,
|
|
154
|
+
tokenIndex,
|
|
155
|
+
currentTokens,
|
|
156
|
+
newTokens,
|
|
157
|
+
) === currentTokens
|
|
158
|
+
) {
|
|
159
|
+
if (elapsed > 0) {
|
|
160
|
+
Atomics.compareExchange(this.rateLimits, timeIndex, lastTime, nowSec)
|
|
161
|
+
}
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
allocateBuffer(length: number): { offset: number; view: Uint8Array } | null {
|
|
168
|
+
if (length <= 0) return null
|
|
169
|
+
const totalSize = Atomics.load(this.header, 1)
|
|
170
|
+
while (true) {
|
|
171
|
+
const currentOffset = Atomics.load(this.header, 2)
|
|
172
|
+
if (currentOffset + length > totalSize) {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
const newOffset = currentOffset + length
|
|
176
|
+
if (
|
|
177
|
+
Atomics.compareExchange(this.header, 2, currentOffset, newOffset) ===
|
|
178
|
+
currentOffset
|
|
179
|
+
) {
|
|
180
|
+
return {
|
|
181
|
+
offset: currentOffset,
|
|
182
|
+
view: new Uint8Array(this.buffer, currentOffset, length),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getBufferView(offset: number, length: number): Uint8Array | null {
|
|
189
|
+
const totalSize = Atomics.load(this.header, 1)
|
|
190
|
+
if (offset < BUFFER_START_OFFSET || offset + length > totalSize) return null
|
|
191
|
+
return new Uint8Array(this.buffer, offset, length)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": [
|
|
3
|
+
"Base tsconfig for applications built on Bakery. Extend it:",
|
|
4
|
+
" { \"extends\": \"@bakery-framework/core/tsconfig.app.json\" }",
|
|
5
|
+
"",
|
|
6
|
+
"The JSX settings are not optional. Bakery renders JSX server-side to a",
|
|
7
|
+
"string through its own `createElement`, so without these Bun falls back to",
|
|
8
|
+
"the automatic runtime and every .tsx page fails with",
|
|
9
|
+
"'Cannot find module react/jsx-dev-runtime'."
|
|
10
|
+
],
|
|
11
|
+
"_files_note": "Paths in an extended config resolve relative to THIS file, so extending packages inherit core ambient declarations (createElement, Fragment, the JSX namespace, Bakery, AppConfig) without naming a node_modules path. shared.d.ts carries the handful of types the browser runtime needs too (MapOf, Wrapped, JsonResponse, ISFunction) and is declared there exactly once.",
|
|
12
|
+
"files": ["./src/global.d.ts", "./src/shared.d.ts", "./src/types.d.ts"],
|
|
13
|
+
"compilerOptions": {
|
|
14
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
15
|
+
"target": "ESNext",
|
|
16
|
+
"module": "Preserve",
|
|
17
|
+
"moduleDetection": "force",
|
|
18
|
+
"moduleResolution": "bundler",
|
|
19
|
+
"types": ["bun-types"],
|
|
20
|
+
|
|
21
|
+
"jsx": "react",
|
|
22
|
+
"jsxFactory": "createElement",
|
|
23
|
+
"jsxFragmentFactory": "Fragment",
|
|
24
|
+
|
|
25
|
+
"allowImportingTsExtensions": true,
|
|
26
|
+
"verbatimModuleSyntax": true,
|
|
27
|
+
"noEmit": true,
|
|
28
|
+
"resolveJsonModule": true,
|
|
29
|
+
"strict": true,
|
|
30
|
+
"skipLibCheck": true,
|
|
31
|
+
"esModuleInterop": true,
|
|
32
|
+
"forceConsistentCasingInFileNames": true
|
|
33
|
+
}
|
|
34
|
+
}
|