@thoughtpivot/flight 2.0.1 → 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.
@@ -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 = dedupeStrings([
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 vite server:', error)
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 development server with hot module reload ${process.pid} started on 3001`)
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
  }