@thoughtpivot/flight 2.0.2 → 3.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/CHANGELOG.md +41 -0
- package/README.md +182 -18
- package/dist/backend-discovery.js +61 -0
- package/dist/flight.js +8 -47
- package/dist/spa-pipeline.js +22 -4
- package/package.json +20 -10
- package/src/bun/app.test.ts +191 -0
- package/src/bun/app.ts +140 -0
- package/src/bun/backends.ts +93 -0
- package/src/bun/cache.ts +56 -0
- package/src/bun/config.ts +163 -0
- package/src/bun/koa-compat.ts +166 -0
- package/src/bun/middleware.ts +217 -0
- package/src/bun/server.ts +16 -0
- package/src/bun/session.ts +120 -0
- package/src/bun/static.ts +72 -0
- package/src/bun/store.ts +128 -0
- package/src/flight.ts +6 -47
package/src/bun/store.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Key/value store shared by sessions and the response cache. */
|
|
2
|
+
export interface KVStore {
|
|
3
|
+
get(key: string): Promise<string | null>
|
|
4
|
+
set(key: string, value: string, ttlSec: number): Promise<void>
|
|
5
|
+
del(key: string): Promise<void>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* In-process store used when Redis is unset or unreachable.
|
|
10
|
+
*/
|
|
11
|
+
class MemoryStore implements KVStore {
|
|
12
|
+
private entries = new Map<string, { value: string; exp: number }>()
|
|
13
|
+
|
|
14
|
+
/** Read a value, dropping it when the TTL has passed. */
|
|
15
|
+
async get(key: string): Promise<string | null> {
|
|
16
|
+
const entry = this.entries.get(key)
|
|
17
|
+
if (!entry) return null
|
|
18
|
+
if (entry.exp && entry.exp < Date.now()) {
|
|
19
|
+
this.entries.delete(key)
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
return entry.value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Store a value. A non-positive TTL means no expiry. */
|
|
26
|
+
async set(key: string, value: string, ttlSec: number): Promise<void> {
|
|
27
|
+
this.entries.set(key, { value, exp: ttlSec > 0 ? Date.now() + ttlSec * 1000 : 0 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Remove a key. */
|
|
31
|
+
async del(key: string): Promise<void> {
|
|
32
|
+
this.entries.delete(key)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bun's built-in Redis client. Typed loosely because the surface varies by Bun version.
|
|
38
|
+
*/
|
|
39
|
+
class RedisStore implements KVStore {
|
|
40
|
+
constructor(private client: any) {}
|
|
41
|
+
|
|
42
|
+
/** Read a Redis string. */
|
|
43
|
+
async get(key: string): Promise<string | null> {
|
|
44
|
+
return await this.client.get(key)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Write a Redis string with an optional EX expiry. */
|
|
48
|
+
async set(key: string, value: string, ttlSec: number): Promise<void> {
|
|
49
|
+
if (ttlSec > 0) await this.client.send('SET', [key, value, 'EX', String(ttlSec)])
|
|
50
|
+
else await this.client.set(key, value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Delete a Redis key. */
|
|
54
|
+
async del(key: string): Promise<void> {
|
|
55
|
+
await this.client.del(key)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Use Redis until a command fails, then stay on the in-memory store.
|
|
61
|
+
*/
|
|
62
|
+
class FallbackStore implements KVStore {
|
|
63
|
+
private degraded = false
|
|
64
|
+
|
|
65
|
+
constructor(
|
|
66
|
+
private primary: KVStore,
|
|
67
|
+
private fallback: KVStore,
|
|
68
|
+
private label: string
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
/** Log the first Redis failure and switch to memory. */
|
|
72
|
+
private trip(err: unknown) {
|
|
73
|
+
if (this.degraded) return
|
|
74
|
+
this.degraded = true
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
76
|
+
console.warn(`flight-bun: ${this.label} store degraded to in-memory (Redis error): ${message}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Read from Redis, or from memory after a failure. */
|
|
80
|
+
async get(key: string): Promise<string | null> {
|
|
81
|
+
if (this.degraded) return this.fallback.get(key)
|
|
82
|
+
try {
|
|
83
|
+
return await this.primary.get(key)
|
|
84
|
+
} catch (err) {
|
|
85
|
+
this.trip(err)
|
|
86
|
+
return this.fallback.get(key)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Write to Redis, or to memory after a failure. */
|
|
91
|
+
async set(key: string, value: string, ttlSec: number): Promise<void> {
|
|
92
|
+
if (this.degraded) return this.fallback.set(key, value, ttlSec)
|
|
93
|
+
try {
|
|
94
|
+
await this.primary.set(key, value, ttlSec)
|
|
95
|
+
} catch (err) {
|
|
96
|
+
this.trip(err)
|
|
97
|
+
await this.fallback.set(key, value, ttlSec)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Delete from Redis, or from memory after a failure. */
|
|
102
|
+
async del(key: string): Promise<void> {
|
|
103
|
+
if (this.degraded) return this.fallback.del(key)
|
|
104
|
+
try {
|
|
105
|
+
await this.primary.del(key)
|
|
106
|
+
} catch (err) {
|
|
107
|
+
this.trip(err)
|
|
108
|
+
await this.fallback.del(key)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Redis when `FLIGHT_REDIS_URL` is set, otherwise memory.
|
|
115
|
+
* A down Redis degrades the request instead of failing it.
|
|
116
|
+
*/
|
|
117
|
+
export function createStore(redisUrl: string | undefined, label: string): KVStore {
|
|
118
|
+
const memory = new MemoryStore()
|
|
119
|
+
if (!redisUrl) return memory
|
|
120
|
+
try {
|
|
121
|
+
const client = new Bun.RedisClient(redisUrl)
|
|
122
|
+
return new FallbackStore(new RedisStore(client), memory, label)
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
125
|
+
console.warn(`flight-bun: ${label} store could not init Redis, using in-memory: ${message}`)
|
|
126
|
+
return memory
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/flight.ts
CHANGED
|
@@ -21,6 +21,7 @@ import ratelimit from 'koa-ratelimit'
|
|
|
21
21
|
import serve from 'koa-static'
|
|
22
22
|
import session from 'koa-session'
|
|
23
23
|
|
|
24
|
+
import { backendDiscoveryIgnorePatterns, dedupeStrings, mergeExcludePaths } from './backend-discovery.js'
|
|
24
25
|
import {
|
|
25
26
|
applyTrustProxy,
|
|
26
27
|
httpCacheEnabledInSpaPipeline,
|
|
@@ -45,45 +46,6 @@ interface FlightArgv {
|
|
|
45
46
|
exclude_paths?: string[]
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function normalizeExcludePaths(value: unknown): string[] {
|
|
49
|
-
if (value == null || value === '') return []
|
|
50
|
-
const parts = Array.isArray(value) ? value : [value]
|
|
51
|
-
const out: string[] = []
|
|
52
|
-
for (const p of parts) {
|
|
53
|
-
const s = String(p).trim()
|
|
54
|
-
if (!s) continue
|
|
55
|
-
out.push(
|
|
56
|
-
...s
|
|
57
|
-
.split(',')
|
|
58
|
-
.map((x) => x.trim())
|
|
59
|
-
.filter(Boolean)
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
return out
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function dedupeStrings(items: string[]): string[] {
|
|
66
|
-
return [...new Set(items)]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Build fast-glob ignore globs for trees rooted under `appRootAbs`. */
|
|
70
|
-
function backendDiscoveryIgnorePatterns(appRootAbs: string, excludeRelativeDirs: string[]): string[] {
|
|
71
|
-
const patterns: string[] = []
|
|
72
|
-
for (const raw of excludeRelativeDirs) {
|
|
73
|
-
const trimmed = raw.trim()
|
|
74
|
-
if (!trimmed) continue
|
|
75
|
-
const resolved = path.resolve(appRootAbs, trimmed)
|
|
76
|
-
const rel = path.relative(appRootAbs, resolved)
|
|
77
|
-
const relPosix = rel.replace(/\\/g, '/')
|
|
78
|
-
if (!relPosix || relPosix.startsWith('..') || path.isAbsolute(rel)) {
|
|
79
|
-
console.warn(`Flight: exclude_paths entry skipped (outside app_home): ${trimmed}`)
|
|
80
|
-
continue
|
|
81
|
-
}
|
|
82
|
-
patterns.push(`${relPosix}/**`)
|
|
83
|
-
}
|
|
84
|
-
return patterns
|
|
85
|
-
}
|
|
86
|
-
|
|
87
49
|
// tsx (and similar loaders) may expose require('yargs/yargs') as { default: factory } instead of factory.
|
|
88
50
|
interface YargsInstance {
|
|
89
51
|
option(key: string, opts: Record<string, unknown>): YargsInstance
|
|
@@ -102,7 +64,7 @@ const argv = yargsEntry(process.argv.slice(2))
|
|
|
102
64
|
})
|
|
103
65
|
.option('mode', {
|
|
104
66
|
type: 'string',
|
|
105
|
-
describe: 'development (Vite HMR) or production'
|
|
67
|
+
describe: 'development (Vite HMR for Vue/React per vite.config) or production'
|
|
106
68
|
})
|
|
107
69
|
.option('port', {
|
|
108
70
|
type: 'number',
|
|
@@ -188,10 +150,7 @@ if (argv.disable_vite === undefined) {
|
|
|
188
150
|
argv.disable_vite = Boolean(argv.disable_vite)
|
|
189
151
|
|
|
190
152
|
const appHomePath = path.resolve(argv.app_home)
|
|
191
|
-
const excludePathsConfigured =
|
|
192
|
-
...normalizeExcludePaths(argv.exclude_paths),
|
|
193
|
-
...normalizeExcludePaths(process.env.FLIGHT_EXCLUDE_PATHS)
|
|
194
|
-
])
|
|
153
|
+
const excludePathsConfigured = mergeExcludePaths(argv.exclude_paths, process.env.FLIGHT_EXCLUDE_PATHS)
|
|
195
154
|
const backendDiscoveryIgnores = backendDiscoveryIgnorePatterns(appHomePath, excludePathsConfigured)
|
|
196
155
|
|
|
197
156
|
process.chdir(appHomePath)
|
|
@@ -359,12 +318,12 @@ if (cluster.isPrimary) {
|
|
|
359
318
|
})
|
|
360
319
|
|
|
361
320
|
viteProcess.on('error', (error) => {
|
|
362
|
-
console.error('Failed to start
|
|
321
|
+
console.error('Failed to start Vite dev server:', error)
|
|
363
322
|
})
|
|
364
323
|
|
|
365
324
|
viteProcess.on('exit', (code) => {
|
|
366
325
|
if (code !== 0) {
|
|
367
|
-
console.error(`Vite server exited with code ${code}`)
|
|
326
|
+
console.error(`Vite dev server exited with code ${code}`)
|
|
368
327
|
}
|
|
369
328
|
})
|
|
370
329
|
|
|
@@ -373,6 +332,6 @@ if (cluster.isPrimary) {
|
|
|
373
332
|
process.exit(0)
|
|
374
333
|
})
|
|
375
334
|
|
|
376
|
-
console.log(`Vite
|
|
335
|
+
console.log(`Vite dev server (HMR for Vue/React per your vite.config) for worker ${process.pid} on port 3001`)
|
|
377
336
|
}
|
|
378
337
|
}
|