bunderstack 0.8.0 → 0.9.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 +26 -0
- package/package.json +1 -1
- package/src/index.ts +57 -14
- package/src/manifest.ts +4 -0
- package/src/realtime/facade.ts +16 -0
package/README.md
CHANGED
|
@@ -82,6 +82,32 @@ import { app } from './bunderstack'
|
|
|
82
82
|
await app.runWorker()
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
If a queue handler calls `ctx.realtime.publish()`, the web and worker processes
|
|
86
|
+
must share a realtime transport. Configure `REDIS_URL` (or
|
|
87
|
+
`realtime: { redis: "redis://..." }`). `realtime: true` without Redis uses a
|
|
88
|
+
process-local memory broker and is suitable only when the worker is embedded
|
|
89
|
+
with `app.startWorker()`.
|
|
90
|
+
|
|
91
|
+
Since 0.9.0, `app.runWorker()` rejects that unsafe combination by default. If
|
|
92
|
+
queue handlers never publish realtime events, acknowledge the process-local
|
|
93
|
+
behavior with `app.runWorker({ allowProcessLocalRealtime: true })`.
|
|
94
|
+
|
|
95
|
+
Inspect the active runtime with `app.realtime.transport` (`'disabled'`,
|
|
96
|
+
`'memory'`, or `'redis'`). Deploy tooling can read the configured transport
|
|
97
|
+
from `app.manifest.realtimeTransport`.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const app = await createBunderstack({
|
|
101
|
+
// ...
|
|
102
|
+
realtime: { redis: process.env.REDIS_URL! },
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
REDIS_URL=redis://localhost:6379 bun src/server.ts
|
|
108
|
+
REDIS_URL=redis://localhost:6379 bun src/worker.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
85
111
|
Cron tasks (`j.cron()`) are delivered by the host to
|
|
86
112
|
`POST /api/_bunderstack/cron/:name`; storage maintenance uses
|
|
87
113
|
`POST /api/_bunderstack/maintenance/storage-sweep`. Production requires the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunderstack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Batteries-included backend framework for Bun: CRUD APIs, auth, file storage, realtime, tRPC, email, and validated env from a single Drizzle schema and config object.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"backend",
|
package/src/index.ts
CHANGED
|
@@ -50,7 +50,11 @@ import {
|
|
|
50
50
|
PROVISION_INTERNALS,
|
|
51
51
|
type WithProvisionInternals,
|
|
52
52
|
} from './provision-internals'
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
createRealtimeFacade,
|
|
55
|
+
type RealtimeFacade,
|
|
56
|
+
type RealtimeTransport,
|
|
57
|
+
} from './realtime/facade'
|
|
54
58
|
import { createRealtimeBroker, buildRealtimeRouter } from './realtime/index'
|
|
55
59
|
import { createRedisRealtimeBroker } from './realtime/redis'
|
|
56
60
|
import { deleteFileWithDerivatives } from './storage/delete'
|
|
@@ -93,9 +97,9 @@ const DEFAULT_PENDING_TTL_MS = 30 * 60_000
|
|
|
93
97
|
* must also clean the file-meta row.
|
|
94
98
|
*/
|
|
95
99
|
export interface StorageFacade {
|
|
96
|
-
/** Delete
|
|
100
|
+
/** Delete a file row and purge all underlying storage derivatives. */
|
|
97
101
|
delete(fileId: string): Promise<void>
|
|
98
|
-
/**
|
|
102
|
+
/** Low-level access to the underlying storage adapter for a bucket. */
|
|
99
103
|
bucket(name: string): StorageAdapter | undefined
|
|
100
104
|
/**
|
|
101
105
|
* Reap stale `pending` uploads older than `olderThanMs` (default 30m). Runs
|
|
@@ -106,7 +110,15 @@ export interface StorageFacade {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
export type AppStartWorkerOptions = Omit<StartWorkerOptions, 'tick'>
|
|
109
|
-
export type AppRunWorkerOptions = AppStartWorkerOptions
|
|
113
|
+
export type AppRunWorkerOptions = AppStartWorkerOptions & {
|
|
114
|
+
/**
|
|
115
|
+
* Permit process-local realtime in a standalone worker.
|
|
116
|
+
*
|
|
117
|
+
* Use only when job handlers never call ctx.realtime.publish(). Publications
|
|
118
|
+
* made through the memory broker cannot reach SSE clients in another process.
|
|
119
|
+
*/
|
|
120
|
+
allowProcessLocalRealtime?: boolean
|
|
121
|
+
}
|
|
110
122
|
export type AppStartCronSchedulerOptions = Pick<
|
|
111
123
|
LocalCronSchedulerOptions,
|
|
112
124
|
'onError'
|
|
@@ -362,10 +374,15 @@ export async function createBunderstack<
|
|
|
362
374
|
typeof config.realtime === 'object'
|
|
363
375
|
? config.realtime.bufferSize
|
|
364
376
|
: undefined
|
|
365
|
-
const
|
|
366
|
-
config.realtime
|
|
367
|
-
|
|
368
|
-
|
|
377
|
+
const configuredRedisUrl = config.realtime
|
|
378
|
+
? resolveRealtimeRedisUrl(config.realtime, env)
|
|
379
|
+
: undefined
|
|
380
|
+
const configuredRealtimeTransport: RealtimeTransport = !config.realtime
|
|
381
|
+
? 'disabled'
|
|
382
|
+
: configuredRedisUrl
|
|
383
|
+
? 'redis'
|
|
384
|
+
: 'memory'
|
|
385
|
+
const redisUrl = introspect ? undefined : configuredRedisUrl
|
|
369
386
|
const broker = config.realtime
|
|
370
387
|
? redisUrl
|
|
371
388
|
? createRedisRealtimeBroker({
|
|
@@ -400,7 +417,15 @@ export async function createBunderstack<
|
|
|
400
417
|
bufferSize: realtimeBufferSize,
|
|
401
418
|
})
|
|
402
419
|
: undefined
|
|
403
|
-
const
|
|
420
|
+
const runtimeRealtimeTransport: RealtimeTransport = !broker
|
|
421
|
+
? 'disabled'
|
|
422
|
+
: redisUrl
|
|
423
|
+
? 'redis'
|
|
424
|
+
: 'memory'
|
|
425
|
+
const realtime = createRealtimeFacade<TSchema>(
|
|
426
|
+
broker,
|
|
427
|
+
runtimeRealtimeTransport,
|
|
428
|
+
)
|
|
404
429
|
const crudRouter = buildCrudRouter(options.schema, userDb, {
|
|
405
430
|
auth: authResolver,
|
|
406
431
|
access: resolvedAccess,
|
|
@@ -526,12 +551,24 @@ export async function createBunderstack<
|
|
|
526
551
|
const runWorker = async (
|
|
527
552
|
options: AppRunWorkerOptions = {},
|
|
528
553
|
): Promise<void> => {
|
|
529
|
-
|
|
554
|
+
if (
|
|
555
|
+
realtime.transport === 'memory' &&
|
|
556
|
+
!options.allowProcessLocalRealtime
|
|
557
|
+
) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
'[bunderstack] runWorker() cannot deliver realtime events through the in-memory broker. Configure REDIS_URL or realtime.redis, embed the worker with startWorker(), or pass allowProcessLocalRealtime: true only when jobs never publish realtime.',
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
const {
|
|
563
|
+
allowProcessLocalRealtime: _allowProcessLocalRealtime,
|
|
564
|
+
...workerOptions
|
|
565
|
+
} = options
|
|
566
|
+
const handle = await startWorker(workerOptions)
|
|
530
567
|
try {
|
|
531
|
-
const signal =
|
|
532
|
-
? AbortSignal.any([lifecycle.signal,
|
|
568
|
+
const signal = workerOptions.signal
|
|
569
|
+
? AbortSignal.any([lifecycle.signal, workerOptions.signal])
|
|
533
570
|
: lifecycle.signal
|
|
534
|
-
await waitForWorkerShutdown(signal, !
|
|
571
|
+
await waitForWorkerShutdown(signal, !workerOptions.signal)
|
|
535
572
|
} finally {
|
|
536
573
|
await handle.close()
|
|
537
574
|
await lifecycle.close()
|
|
@@ -613,6 +650,7 @@ export async function createBunderstack<
|
|
|
613
650
|
storage: config.storage,
|
|
614
651
|
envConfig: options.env as EnvConfigInput | undefined,
|
|
615
652
|
realtime: Boolean(config.realtime),
|
|
653
|
+
realtimeTransport: configuredRealtimeTransport,
|
|
616
654
|
jobs: jobsDefs,
|
|
617
655
|
}),
|
|
618
656
|
}
|
|
@@ -724,4 +762,9 @@ export type {
|
|
|
724
762
|
export type { TransformSpec } from './storage/thumbnails'
|
|
725
763
|
|
|
726
764
|
export type { RealtimeAction } from './realtime/index'
|
|
727
|
-
export
|
|
765
|
+
export { createRealtimeFacade } from './realtime/facade'
|
|
766
|
+
export type {
|
|
767
|
+
RealtimeFacade,
|
|
768
|
+
RealtimeTransport,
|
|
769
|
+
SchemaTable,
|
|
770
|
+
} from './realtime/facade'
|
package/src/manifest.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { getTableName, isTable } from 'drizzle-orm'
|
|
|
8
8
|
import type { Dialect } from './dialect'
|
|
9
9
|
import type { EnvConfigInput } from './env'
|
|
10
10
|
import type { JobsDefs } from './jobs/define'
|
|
11
|
+
import type { RealtimeTransport } from './realtime/facade'
|
|
11
12
|
import type { ResolvedBucket, ResolvedStorageBuckets } from './storage/buckets'
|
|
12
13
|
import {
|
|
13
14
|
bunderstackCronRuns,
|
|
@@ -29,6 +30,7 @@ export type BunderstackManifest = {
|
|
|
29
30
|
defaultBucket: string
|
|
30
31
|
buckets: { name: string; visibility: ResolvedBucket['visibility'] }[]
|
|
31
32
|
realtime: boolean
|
|
33
|
+
realtimeTransport: RealtimeTransport
|
|
32
34
|
env: { server: ManifestEnvVar[]; client: ManifestEnvVar[] }
|
|
33
35
|
background: {
|
|
34
36
|
jobs: { name: string }[]
|
|
@@ -60,6 +62,7 @@ export function buildManifest(args: {
|
|
|
60
62
|
storage: ResolvedStorageBuckets
|
|
61
63
|
envConfig: EnvConfigInput | undefined
|
|
62
64
|
realtime: boolean
|
|
65
|
+
realtimeTransport: RealtimeTransport
|
|
63
66
|
jobs: JobsDefs | undefined
|
|
64
67
|
}): BunderstackManifest {
|
|
65
68
|
return {
|
|
@@ -78,6 +81,7 @@ export function buildManifest(args: {
|
|
|
78
81
|
visibility: bucket.visibility,
|
|
79
82
|
})),
|
|
80
83
|
realtime: args.realtime,
|
|
84
|
+
realtimeTransport: args.realtimeTransport,
|
|
81
85
|
env: {
|
|
82
86
|
server: describeSection(args.envConfig?.server),
|
|
83
87
|
client: describeSection(args.envConfig?.client),
|
package/src/realtime/facade.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { getTableName, type InferSelectModel, type Table } from 'drizzle-orm'
|
|
|
2
2
|
|
|
3
3
|
import type { RealtimeAction, RealtimeBroker } from './index'
|
|
4
4
|
|
|
5
|
+
export type RealtimeTransport = 'disabled' | 'memory' | 'redis'
|
|
6
|
+
|
|
5
7
|
export type SchemaTable<TSchema extends Record<string, unknown>> = Extract<
|
|
6
8
|
TSchema[keyof TSchema],
|
|
7
9
|
Table
|
|
@@ -11,6 +13,7 @@ export interface RealtimeFacade<
|
|
|
11
13
|
TSchema extends Record<string, unknown> = Record<string, unknown>,
|
|
12
14
|
> {
|
|
13
15
|
readonly enabled: boolean
|
|
16
|
+
readonly transport: RealtimeTransport
|
|
14
17
|
|
|
15
18
|
publish<TTable extends SchemaTable<TSchema>>(
|
|
16
19
|
table: TTable,
|
|
@@ -21,9 +24,22 @@ export interface RealtimeFacade<
|
|
|
21
24
|
|
|
22
25
|
export function createRealtimeFacade<TSchema extends Record<string, unknown>>(
|
|
23
26
|
broker?: RealtimeBroker,
|
|
27
|
+
transport: RealtimeTransport = broker ? 'memory' : 'disabled',
|
|
24
28
|
): RealtimeFacade<TSchema> {
|
|
29
|
+
if (!broker && transport !== 'disabled') {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'[bunderstack] an enabled realtime transport requires a broker',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
if (broker && transport === 'disabled') {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'[bunderstack] a realtime broker cannot use the disabled transport',
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
return {
|
|
26
41
|
enabled: broker !== undefined,
|
|
42
|
+
transport,
|
|
27
43
|
async publish(table, action, record) {
|
|
28
44
|
if (!broker) return
|
|
29
45
|
await broker.publish(
|