bunderstack 0.25.1 → 1.0.0-beta.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/CHANGELOG.md +43 -0
- package/dist/auth-password.d.ts +27 -0
- package/dist/auth-password.d.ts.map +1 -0
- package/dist/auth-password.js +60 -0
- package/dist/auth-password.js.map +1 -0
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -0
- package/dist/auth.js.map +1 -1
- package/dist/backend.d.ts +3 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +3 -1
- package/dist/backend.js.map +1 -1
- package/dist/cli.d.ts +11 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +97 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -18
- package/dist/config.js.map +1 -1
- package/dist/crud-operations.d.ts.map +1 -1
- package/dist/crud-operations.js +19 -3
- package/dist/crud-operations.js.map +1 -1
- package/dist/dev/binaries.d.ts +22 -0
- package/dist/dev/binaries.d.ts.map +1 -0
- package/dist/dev/binaries.js +119 -0
- package/dist/dev/binaries.js.map +1 -0
- package/dist/dev/env.d.ts +11 -0
- package/dist/dev/env.d.ts.map +1 -0
- package/dist/dev/env.js +71 -0
- package/dist/dev/env.js.map +1 -0
- package/dist/dev/index.d.ts +45 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +287 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/processes.d.ts +37 -0
- package/dist/dev/processes.d.ts.map +1 -0
- package/dist/dev/processes.js +104 -0
- package/dist/dev/processes.js.map +1 -0
- package/dist/dev/push.d.ts +2 -0
- package/dist/dev/push.d.ts.map +1 -0
- package/dist/dev/push.js +25 -0
- package/dist/dev/push.js.map +1 -0
- package/dist/env-probe.d.ts.map +1 -1
- package/dist/env-probe.js +0 -1
- package/dist/env-probe.js.map +1 -1
- package/dist/env.d.ts +0 -3
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +0 -7
- package/dist/env.js.map +1 -1
- package/dist/handler.d.ts +2 -0
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +1 -2
- package/dist/handler.js.map +1 -1
- package/dist/hosted-contract.d.ts.map +1 -1
- package/dist/hosted-contract.js +7 -3
- package/dist/hosted-contract.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jobs/define.d.ts +5 -0
- package/dist/jobs/define.d.ts.map +1 -1
- package/dist/jobs/define.js.map +1 -1
- package/dist/jobs/index.d.ts +2 -4
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +2 -3
- package/dist/jobs/index.js.map +1 -1
- package/dist/jobs/queue.d.ts +1 -0
- package/dist/jobs/queue.d.ts.map +1 -1
- package/dist/jobs/queue.js +6 -3
- package/dist/jobs/queue.js.map +1 -1
- package/dist/jobs/slots.d.ts +5 -0
- package/dist/jobs/slots.d.ts.map +1 -1
- package/dist/jobs/slots.js +12 -0
- package/dist/jobs/slots.js.map +1 -1
- package/dist/jobs/worker.d.ts +6 -0
- package/dist/jobs/worker.d.ts.map +1 -1
- package/dist/jobs/worker.js +32 -1
- package/dist/jobs/worker.js.map +1 -1
- package/dist/platform.d.ts +25 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +24 -0
- package/dist/platform.js.map +1 -0
- package/dist/query/client.js +1 -1
- package/dist/query/client.js.map +1 -1
- package/dist/rate-limit.d.ts +2 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +19 -22
- package/dist/rate-limit.js.map +1 -1
- package/dist/realtime/facade.d.ts +1 -1
- package/dist/realtime/facade.d.ts.map +1 -1
- package/dist/realtime/facade.js.map +1 -1
- package/dist/realtime/publisher.d.ts +0 -4
- package/dist/realtime/publisher.d.ts.map +1 -1
- package/dist/realtime/publisher.js +0 -9
- package/dist/realtime/publisher.js.map +1 -1
- package/dist/runtime.d.ts +5 -19
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +28 -102
- package/dist/runtime.js.map +1 -1
- package/dist/storage/registry.d.ts +2 -1
- package/dist/storage/registry.d.ts.map +1 -1
- package/dist/storage/registry.js +2 -2
- package/dist/storage/registry.js.map +1 -1
- package/dist/storage/s3.d.ts +11 -4
- package/dist/storage/s3.d.ts.map +1 -1
- package/dist/storage/s3.js +88 -34
- package/dist/storage/s3.js.map +1 -1
- package/dist/storage/sigv4.d.ts +24 -0
- package/dist/storage/sigv4.d.ts.map +1 -0
- package/dist/storage/sigv4.js +107 -0
- package/dist/storage/sigv4.js.map +1 -0
- package/dist/testing/fixture.d.ts.map +1 -1
- package/dist/testing/fixture.js +0 -3
- package/dist/testing/fixture.js.map +1 -1
- package/dist/testing/workers-fakes.d.ts +17 -0
- package/dist/testing/workers-fakes.d.ts.map +1 -0
- package/dist/testing/workers-fakes.js +98 -0
- package/dist/testing/workers-fakes.js.map +1 -0
- package/dist/typeid.d.ts +1 -0
- package/dist/typeid.d.ts.map +1 -1
- package/dist/typeid.js +34 -4
- package/dist/typeid.js.map +1 -1
- package/dist/vite.d.ts +30 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +37 -0
- package/dist/vite.js.map +1 -0
- package/dist/workers/app.d.ts +19 -0
- package/dist/workers/app.d.ts.map +1 -0
- package/dist/workers/app.js +56 -0
- package/dist/workers/app.js.map +1 -0
- package/dist/workers/index.d.ts +21 -0
- package/dist/workers/index.d.ts.map +1 -0
- package/dist/workers/index.js +33 -0
- package/dist/workers/index.js.map +1 -0
- package/dist/workers/r2.d.ts +23 -0
- package/dist/workers/r2.d.ts.map +1 -0
- package/dist/workers/r2.js +77 -0
- package/dist/workers/r2.js.map +1 -0
- package/dist/workers/rate-limiter.d.ts +9 -0
- package/dist/workers/rate-limiter.d.ts.map +1 -0
- package/dist/workers/rate-limiter.js +22 -0
- package/dist/workers/rate-limiter.js.map +1 -0
- package/dist/workers/realtime-hub.d.ts +20 -0
- package/dist/workers/realtime-hub.d.ts.map +1 -0
- package/dist/workers/realtime-hub.js +117 -0
- package/dist/workers/realtime-hub.js.map +1 -0
- package/dist/workers/scheduler.d.ts +13 -0
- package/dist/workers/scheduler.d.ts.map +1 -0
- package/dist/workers/scheduler.js +71 -0
- package/dist/workers/scheduler.js.map +1 -0
- package/dist/workers/types.d.ts +55 -0
- package/dist/workers/types.d.ts.map +1 -0
- package/dist/workers/types.js +4 -0
- package/dist/workers/types.js.map +1 -0
- package/dist/workers/wrangler.d.ts +64 -0
- package/dist/workers/wrangler.d.ts.map +1 -0
- package/dist/workers/wrangler.js +115 -0
- package/dist/workers/wrangler.js.map +1 -0
- package/llms-full.txt +10 -1
- package/package.json +11 -3
- package/dist/jobs/runtime.d.ts +0 -17
- package/dist/jobs/runtime.d.ts.map +0 -1
- package/dist/jobs/runtime.js +0 -56
- package/dist/jobs/runtime.js.map +0 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createMemoryRateLimitStore } from '../platform.js';
|
|
2
|
+
import { stub } from './types.js';
|
|
3
|
+
export class RateLimiter {
|
|
4
|
+
store = createMemoryRateLimitStore();
|
|
5
|
+
constructor(_state, _env) { }
|
|
6
|
+
async fetch(request) {
|
|
7
|
+
const { windowMs, max, now } = (await request.json());
|
|
8
|
+
return Response.json(await this.store.hit('window', windowMs, max, now));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function durableRateLimitStore(namespace) {
|
|
12
|
+
return {
|
|
13
|
+
async hit(key, windowMs, max, now = Date.now()) {
|
|
14
|
+
const res = await stub(namespace, key).fetch('https://rate-limiter/hit', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
body: JSON.stringify({ windowMs, max, now }),
|
|
17
|
+
});
|
|
18
|
+
return res.json();
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/workers/rate-limiter.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAI9B,MAAM,OAAO,WAAW;IACL,KAAK,GAAG,0BAA0B,EAAE,CAAA;IAErD,YAAY,MAA8B,EAAE,IAAa,IAAG,CAAC;IAE7D,KAAK,CAAC,KAAK,CAAC,OAAgB;QAC1B,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAe,CAAA;QACnE,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;IAC1E,CAAC;CACF;AAED,MAAM,UAAU,qBAAqB,CACnC,SAAqC;IAErC,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;YAC5C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,0BAA0B,EAAE;gBACvE,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,EAAuB,CAAC;aAClE,CAAC,CAAA;YACF,OAAO,GAAG,CAAC,IAAI,EAAE,CAAA;QACnB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["// src/workers/rate-limiter.ts — one Durable Object per client+path key. Its\n// memory is the window; a reset after eviction only forgives a few requests.\nimport type { RateLimitStore } from '../platform'\nimport type {\n DurableObjectNamespaceLike,\n DurableObjectStateLike,\n} from './types'\n\nimport { createMemoryRateLimitStore } from '../platform'\nimport { stub } from './types'\n\ntype HitRequest = { windowMs: number; max: number; now: number }\n\nexport class RateLimiter {\n private readonly store = createMemoryRateLimitStore()\n\n constructor(_state: DurableObjectStateLike, _env: unknown) {}\n\n async fetch(request: Request): Promise<Response> {\n const { windowMs, max, now } = (await request.json()) as HitRequest\n return Response.json(await this.store.hit('window', windowMs, max, now))\n }\n}\n\nexport function durableRateLimitStore(\n namespace: DurableObjectNamespaceLike,\n): RateLimitStore {\n return {\n async hit(key, windowMs, max, now = Date.now()) {\n const res = await stub(namespace, key).fetch('https://rate-limiter/hit', {\n method: 'POST',\n body: JSON.stringify({ windowMs, max, now } satisfies HitRequest),\n })\n return res.json()\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PublisherOptions, PublisherSubscribeListenerOptions } from '@orpc/publisher';
|
|
2
|
+
import { Publisher } from '@orpc/publisher';
|
|
3
|
+
import type { RealtimeEvents } from '../realtime/publisher.js';
|
|
4
|
+
import type { DurableObjectStateLike, FetcherLike } from './types.js';
|
|
5
|
+
export declare class RealtimeHub {
|
|
6
|
+
/** celld closes a stream after 60 s of silence. */
|
|
7
|
+
heartbeatMs: number;
|
|
8
|
+
private readonly publisher;
|
|
9
|
+
constructor(_state: DurableObjectStateLike, _env: unknown);
|
|
10
|
+
fetch(request: Request): Promise<Response>;
|
|
11
|
+
private subscribe;
|
|
12
|
+
}
|
|
13
|
+
export declare class HubPublisher extends Publisher<RealtimeEvents> {
|
|
14
|
+
private readonly hub;
|
|
15
|
+
constructor(hub: () => FetcherLike, options?: PublisherOptions);
|
|
16
|
+
private readonly serializer;
|
|
17
|
+
publish<K extends keyof RealtimeEvents & string>(event: K, payload: RealtimeEvents[K]): Promise<void>;
|
|
18
|
+
protected subscribeListener<K extends keyof RealtimeEvents & string>(event: K, listener: (payload: RealtimeEvents[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=realtime-hub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-hub.d.ts","sourceRoot":"","sources":["../../src/workers/realtime-hub.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,iCAAiC,EAClC,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAI3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAMlE,qBAAa,WAAW;IACtB,mDAAmD;IACnD,WAAW,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAExB;gBAEU,MAAM,EAAE,sBAAsB,EAAE,IAAI,EAAE,OAAO;IAEnD,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;YAelC,SAAS;CAgCxB;AAED,qBAAa,YAAa,SAAQ,SAAS,CAAC,cAAc,CAAC;IAEvD,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM,WAAW,EACvC,OAAO,CAAC,EAAE,gBAAgB;IAK5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0B;IAE/C,OAAO,CAAC,CAAC,SAAS,MAAM,cAAc,GAAG,MAAM,EACnD,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACzB,OAAO,CAAC,IAAI,CAAC;cAeA,iBAAiB,CAAC,CAAC,SAAS,MAAM,cAAc,GAAG,MAAM,EACvE,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,EAC9C,OAAO,CAAC,EAAE,iCAAiC,GAC1C,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CA6ChC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { RPCJsonSerializer } from '@orpc/client';
|
|
2
|
+
import { Publisher } from '@orpc/publisher';
|
|
3
|
+
import { MemoryPublisher } from '@orpc/publisher/memory';
|
|
4
|
+
import { getEventMeta, withEventMeta } from '@orpc/server';
|
|
5
|
+
export class RealtimeHub {
|
|
6
|
+
/** celld closes a stream after 60 s of silence. */
|
|
7
|
+
heartbeatMs = 20_000;
|
|
8
|
+
publisher = new MemoryPublisher({
|
|
9
|
+
resume: { enabled: true, seconds: 300 },
|
|
10
|
+
});
|
|
11
|
+
constructor(_state, _env) { }
|
|
12
|
+
async fetch(request) {
|
|
13
|
+
const url = new URL(request.url);
|
|
14
|
+
if (url.pathname === '/publish' && request.method === 'POST') {
|
|
15
|
+
const { event, payload } = (await request.json());
|
|
16
|
+
await this.publisher.publish(event, payload);
|
|
17
|
+
return new Response(null, { status: 204 });
|
|
18
|
+
}
|
|
19
|
+
if (url.pathname === '/subscribe')
|
|
20
|
+
return this.subscribe(url, request.signal);
|
|
21
|
+
return new Response('Not found', { status: 404 });
|
|
22
|
+
}
|
|
23
|
+
async subscribe(url, signal) {
|
|
24
|
+
const event = url.searchParams.get('event') ?? 'change';
|
|
25
|
+
const lastEventId = url.searchParams.get('lastEventId') ?? undefined;
|
|
26
|
+
const { readable, writable } = new TransformStream();
|
|
27
|
+
const writer = writable.getWriter();
|
|
28
|
+
const encoder = new TextEncoder();
|
|
29
|
+
let closed = false;
|
|
30
|
+
let timer;
|
|
31
|
+
let unsubscribe;
|
|
32
|
+
const close = () => {
|
|
33
|
+
if (closed)
|
|
34
|
+
return;
|
|
35
|
+
closed = true;
|
|
36
|
+
clearInterval(timer);
|
|
37
|
+
void unsubscribe?.();
|
|
38
|
+
void writer.close().catch(() => { });
|
|
39
|
+
};
|
|
40
|
+
const write = (line) => {
|
|
41
|
+
if (closed)
|
|
42
|
+
return;
|
|
43
|
+
writer.write(encoder.encode(`${JSON.stringify(line)}\n`)).catch(close);
|
|
44
|
+
};
|
|
45
|
+
unsubscribe = await this.publisher.subscribe(event, (payload) => write({ type: 'event', id: getEventMeta(payload)?.id, payload }), { lastEventId });
|
|
46
|
+
timer = setInterval(() => write({ type: 'heartbeat' }), this.heartbeatMs);
|
|
47
|
+
signal?.addEventListener('abort', close);
|
|
48
|
+
return new Response(readable, {
|
|
49
|
+
headers: { 'content-type': 'application/x-ndjson' },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class HubPublisher extends Publisher {
|
|
54
|
+
hub;
|
|
55
|
+
constructor(hub, options) {
|
|
56
|
+
super(options);
|
|
57
|
+
this.hub = hub;
|
|
58
|
+
}
|
|
59
|
+
serializer = new RPCJsonSerializer();
|
|
60
|
+
async publish(event, payload) {
|
|
61
|
+
const res = await this.hub().fetch('https://hub/publish', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
event,
|
|
65
|
+
payload: this.serializer.serialize(payload),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
throw new Error(`[bunderstack] realtime hub publish failed (${res.status})`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async subscribeListener(event, listener, options) {
|
|
73
|
+
const url = new URL('https://hub/subscribe');
|
|
74
|
+
url.searchParams.set('event', event);
|
|
75
|
+
if (options?.lastEventId) {
|
|
76
|
+
url.searchParams.set('lastEventId', options.lastEventId);
|
|
77
|
+
}
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const res = await this.hub().fetch(url, { signal: controller.signal });
|
|
80
|
+
if (!res.ok || !res.body) {
|
|
81
|
+
throw new Error(`[bunderstack] realtime hub subscribe failed (${res.status})`);
|
|
82
|
+
}
|
|
83
|
+
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
84
|
+
void (async () => {
|
|
85
|
+
let buffer = '';
|
|
86
|
+
try {
|
|
87
|
+
for (;;) {
|
|
88
|
+
const { value, done } = await reader.read();
|
|
89
|
+
if (done)
|
|
90
|
+
return;
|
|
91
|
+
buffer += value;
|
|
92
|
+
let newline;
|
|
93
|
+
while ((newline = buffer.indexOf('\n')) >= 0) {
|
|
94
|
+
const text = buffer.slice(0, newline);
|
|
95
|
+
buffer = buffer.slice(newline + 1);
|
|
96
|
+
if (!text)
|
|
97
|
+
continue;
|
|
98
|
+
const line = JSON.parse(text);
|
|
99
|
+
if (line.type !== 'event')
|
|
100
|
+
continue;
|
|
101
|
+
const payload = this.serializer.deserialize(line.payload);
|
|
102
|
+
listener(line.id ? withEventMeta(payload, { id: line.id }) : payload);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (!controller.signal.aborted)
|
|
108
|
+
options?.onError?.(error);
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
return async () => {
|
|
112
|
+
controller.abort();
|
|
113
|
+
await reader.cancel().catch(() => { });
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=realtime-hub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-hub.js","sourceRoot":"","sources":["../../src/workers/realtime-hub.ts"],"names":[],"mappings":"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","sourcesContent":["// src/workers/realtime-hub.ts — realtime fan-out for one app. The hub holds a\n// MemoryPublisher with resume; Workers reach it over a line-delimited stream.\n// Payloads cross as oRPC's RPC JSON, as with the Redis publisher, so Date and\n// BigInt values survive; the hub itself never looks inside them.\nimport type {\n PublisherOptions,\n PublisherSubscribeListenerOptions,\n} from '@orpc/publisher'\n\nimport { RPCJsonSerializer } from '@orpc/client'\nimport { Publisher } from '@orpc/publisher'\nimport { MemoryPublisher } from '@orpc/publisher/memory'\nimport { getEventMeta, withEventMeta } from '@orpc/server'\n\nimport type { RealtimeEvents } from '../realtime/publisher'\nimport type { DurableObjectStateLike, FetcherLike } from './types'\n\ntype HubLine =\n | { type: 'event'; id?: string; payload: object }\n | { type: 'heartbeat' }\n\nexport class RealtimeHub {\n /** celld closes a stream after 60 s of silence. */\n heartbeatMs = 20_000\n private readonly publisher = new MemoryPublisher<Record<string, object>>({\n resume: { enabled: true, seconds: 300 },\n })\n\n constructor(_state: DurableObjectStateLike, _env: unknown) {}\n\n async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url)\n if (url.pathname === '/publish' && request.method === 'POST') {\n const { event, payload } = (await request.json()) as {\n event: string\n payload: object\n }\n await this.publisher.publish(event, payload)\n return new Response(null, { status: 204 })\n }\n if (url.pathname === '/subscribe')\n return this.subscribe(url, request.signal)\n return new Response('Not found', { status: 404 })\n }\n\n private async subscribe(url: URL, signal: AbortSignal): Promise<Response> {\n const event = url.searchParams.get('event') ?? 'change'\n const lastEventId = url.searchParams.get('lastEventId') ?? undefined\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n const writer = writable.getWriter()\n const encoder = new TextEncoder()\n let closed = false\n let timer: ReturnType<typeof setInterval> | undefined\n let unsubscribe: (() => Promise<void>) | undefined\n const close = () => {\n if (closed) return\n closed = true\n clearInterval(timer)\n void unsubscribe?.()\n void writer.close().catch(() => {})\n }\n const write = (line: HubLine) => {\n if (closed) return\n writer.write(encoder.encode(`${JSON.stringify(line)}\\n`)).catch(close)\n }\n unsubscribe = await this.publisher.subscribe(\n event,\n (payload) =>\n write({ type: 'event', id: getEventMeta(payload)?.id, payload }),\n { lastEventId },\n )\n timer = setInterval(() => write({ type: 'heartbeat' }), this.heartbeatMs)\n signal?.addEventListener('abort', close)\n return new Response(readable, {\n headers: { 'content-type': 'application/x-ndjson' },\n })\n }\n}\n\nexport class HubPublisher extends Publisher<RealtimeEvents> {\n constructor(\n private readonly hub: () => FetcherLike,\n options?: PublisherOptions,\n ) {\n super(options)\n }\n\n private readonly serializer = new RPCJsonSerializer()\n\n async publish<K extends keyof RealtimeEvents & string>(\n event: K,\n payload: RealtimeEvents[K],\n ): Promise<void> {\n const res = await this.hub().fetch('https://hub/publish', {\n method: 'POST',\n body: JSON.stringify({\n event,\n payload: this.serializer.serialize(payload),\n }),\n })\n if (!res.ok) {\n throw new Error(\n `[bunderstack] realtime hub publish failed (${res.status})`,\n )\n }\n }\n\n protected async subscribeListener<K extends keyof RealtimeEvents & string>(\n event: K,\n listener: (payload: RealtimeEvents[K]) => void,\n options?: PublisherSubscribeListenerOptions,\n ): Promise<() => Promise<void>> {\n const url = new URL('https://hub/subscribe')\n url.searchParams.set('event', event)\n if (options?.lastEventId) {\n url.searchParams.set('lastEventId', options.lastEventId)\n }\n const controller = new AbortController()\n const res = await this.hub().fetch(url, { signal: controller.signal })\n if (!res.ok || !res.body) {\n throw new Error(\n `[bunderstack] realtime hub subscribe failed (${res.status})`,\n )\n }\n const reader = res.body.pipeThrough(new TextDecoderStream()).getReader()\n void (async () => {\n let buffer = ''\n try {\n for (;;) {\n const { value, done } = await reader.read()\n if (done) return\n buffer += value\n let newline: number\n while ((newline = buffer.indexOf('\\n')) >= 0) {\n const text = buffer.slice(0, newline)\n buffer = buffer.slice(newline + 1)\n if (!text) continue\n const line = JSON.parse(text) as HubLine\n if (line.type !== 'event') continue\n const payload = this.serializer.deserialize(\n line.payload as Parameters<RPCJsonSerializer['deserialize']>[0],\n ) as RealtimeEvents[K]\n listener(\n line.id ? withEventMeta(payload, { id: line.id }) : payload,\n )\n }\n }\n } catch (error) {\n if (!controller.signal.aborted) options?.onError?.(error as Error)\n }\n })()\n return async () => {\n controller.abort()\n await reader.cancel().catch(() => {})\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BunderstackBackend } from '../backend.js';
|
|
2
|
+
import type { DurableObjectStateLike, WorkerEnv } from './types.js';
|
|
3
|
+
export declare const TICK_BUDGET_MS = 25000;
|
|
4
|
+
export declare const NOTIFY_RETRY_MS = 1000;
|
|
5
|
+
export declare const SAFETY_MS = 3600000;
|
|
6
|
+
export declare const MIN_GAP_MS = 1000;
|
|
7
|
+
export interface SchedulerObject {
|
|
8
|
+
fetch(request: Request): Promise<Response>;
|
|
9
|
+
alarm(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export type SchedulerClass = new (state: DurableObjectStateLike, env: WorkerEnv) => SchedulerObject;
|
|
12
|
+
export declare function createSchedulerClass(backend: BunderstackBackend<any>): SchedulerClass;
|
|
13
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/workers/scheduler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIhE,eAAO,MAAM,cAAc,QAAS,CAAA;AACpC,eAAO,MAAM,eAAe,OAAQ,CAAA;AACpC,eAAO,MAAM,SAAS,UAAY,CAAA;AAClC,eAAO,MAAM,UAAU,OAAQ,CAAA;AAE/B,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,MAAM,MAAM,cAAc,GAAG,KAC3B,KAAK,EAAE,sBAAsB,EAC7B,GAAG,EAAE,SAAS,KACX,eAAe,CAAA;AAGpB,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,kBAAkB,CAAC,GAAG,CAAC,GAC/B,cAAc,CAkEhB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { appFor } from './app.js';
|
|
2
|
+
export const TICK_BUDGET_MS = 25_000;
|
|
3
|
+
export const NOTIFY_RETRY_MS = 1_000;
|
|
4
|
+
export const SAFETY_MS = 3_600_000;
|
|
5
|
+
export const MIN_GAP_MS = 1_000;
|
|
6
|
+
// An explicit return type keeps the private members out of the emitted .d.ts.
|
|
7
|
+
export function createSchedulerClass(backend) {
|
|
8
|
+
return class Scheduler {
|
|
9
|
+
state;
|
|
10
|
+
env;
|
|
11
|
+
notified = false;
|
|
12
|
+
retried = false;
|
|
13
|
+
constructor(state, env) {
|
|
14
|
+
this.state = state;
|
|
15
|
+
this.env = env;
|
|
16
|
+
}
|
|
17
|
+
app() {
|
|
18
|
+
// Enqueues from job handlers move this alarm directly, not via a stub.
|
|
19
|
+
return appFor(backend, this.env, 'scheduler', {
|
|
20
|
+
jobs: { notify: (runAt) => this.schedule(runAt) },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Only ever moves the alarm earlier. */
|
|
24
|
+
async schedule(runAt) {
|
|
25
|
+
const current = await this.state.storage.getAlarm();
|
|
26
|
+
if (current === null || runAt < current) {
|
|
27
|
+
await this.state.storage.setAlarm(runAt);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async fetch(request) {
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
if (url.pathname !== '/notify' || request.method !== 'POST') {
|
|
33
|
+
return new Response('Not found', { status: 404 });
|
|
34
|
+
}
|
|
35
|
+
const { runAt } = (await request.json());
|
|
36
|
+
this.notified = true;
|
|
37
|
+
this.retried = false;
|
|
38
|
+
await this.schedule(runAt);
|
|
39
|
+
return new Response(null, { status: 204 });
|
|
40
|
+
}
|
|
41
|
+
async alarm() {
|
|
42
|
+
// Clear the alarm that fired, so schedule() below can set a later one.
|
|
43
|
+
// Cloudflare has already cleared it; celld and the test fake may not.
|
|
44
|
+
const fired = await this.state.storage.getAlarm();
|
|
45
|
+
if (fired !== null && fired <= Date.now()) {
|
|
46
|
+
await this.state.storage.deleteAlarm();
|
|
47
|
+
}
|
|
48
|
+
const app = await this.app();
|
|
49
|
+
const started = Date.now();
|
|
50
|
+
let claimed = 0;
|
|
51
|
+
for (;;) {
|
|
52
|
+
const result = await app.jobs.tick(Date.now());
|
|
53
|
+
claimed += result.claimed;
|
|
54
|
+
if (result.claimed === 0 || Date.now() - started > TICK_BUDGET_MS) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
// An enqueue inside a transaction notifies before its commit.
|
|
60
|
+
if (this.notified && claimed === 0 && !this.retried) {
|
|
61
|
+
this.retried = true;
|
|
62
|
+
await this.schedule(now + NOTIFY_RETRY_MS);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.notified = false;
|
|
66
|
+
const next = await app.jobs.nextDueAt(now, now + SAFETY_MS);
|
|
67
|
+
await this.schedule(Math.max(next ?? now + SAFETY_MS, now + MIN_GAP_MS));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/workers/scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["// src/workers/scheduler.ts — the only place background work runs in a Worker.\n// One instance per app ('main'). Its alarm chain follows app.jobs.nextDueAt;\n// enqueues and Cron Triggers pull the alarm earlier through /notify.\nimport type { BunderstackBackend } from '../backend'\nimport type { DurableObjectStateLike, WorkerEnv } from './types'\n\nimport { appFor, type WorkerApp } from './app'\n\nexport const TICK_BUDGET_MS = 25_000\nexport const NOTIFY_RETRY_MS = 1_000\nexport const SAFETY_MS = 3_600_000\nexport const MIN_GAP_MS = 1_000\n\nexport interface SchedulerObject {\n fetch(request: Request): Promise<Response>\n alarm(): Promise<void>\n}\n\nexport type SchedulerClass = new (\n state: DurableObjectStateLike,\n env: WorkerEnv,\n) => SchedulerObject\n\n// An explicit return type keeps the private members out of the emitted .d.ts.\nexport function createSchedulerClass(\n backend: BunderstackBackend<any>,\n): SchedulerClass {\n return class Scheduler {\n private notified = false\n private retried = false\n\n constructor(\n private readonly state: DurableObjectStateLike,\n private readonly env: WorkerEnv,\n ) {}\n\n private app(): Promise<WorkerApp> {\n // Enqueues from job handlers move this alarm directly, not via a stub.\n return appFor(backend, this.env, 'scheduler', {\n jobs: { notify: (runAt) => this.schedule(runAt) },\n })\n }\n\n /** Only ever moves the alarm earlier. */\n private async schedule(runAt: number) {\n const current = await this.state.storage.getAlarm()\n if (current === null || runAt < current) {\n await this.state.storage.setAlarm(runAt)\n }\n }\n\n async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url)\n if (url.pathname !== '/notify' || request.method !== 'POST') {\n return new Response('Not found', { status: 404 })\n }\n const { runAt } = (await request.json()) as { runAt: number }\n this.notified = true\n this.retried = false\n await this.schedule(runAt)\n return new Response(null, { status: 204 })\n }\n\n async alarm(): Promise<void> {\n // Clear the alarm that fired, so schedule() below can set a later one.\n // Cloudflare has already cleared it; celld and the test fake may not.\n const fired = await this.state.storage.getAlarm()\n if (fired !== null && fired <= Date.now()) {\n await this.state.storage.deleteAlarm()\n }\n const app = await this.app()\n const started = Date.now()\n let claimed = 0\n for (;;) {\n const result = await app.jobs.tick(Date.now())\n claimed += result.claimed\n if (result.claimed === 0 || Date.now() - started > TICK_BUDGET_MS) {\n break\n }\n }\n const now = Date.now()\n // An enqueue inside a transaction notifies before its commit.\n if (this.notified && claimed === 0 && !this.retried) {\n this.retried = true\n await this.schedule(now + NOTIFY_RETRY_MS)\n return\n }\n this.notified = false\n const next = await app.jobs.nextDueAt(now, now + SAFETY_MS)\n await this.schedule(Math.max(next ?? now + SAFETY_MS, now + MIN_GAP_MS))\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface DurableObjectStorageLike {
|
|
2
|
+
getAlarm(): Promise<number | null>;
|
|
3
|
+
setAlarm(scheduledTime: number | Date): Promise<void>;
|
|
4
|
+
deleteAlarm(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface DurableObjectStateLike {
|
|
7
|
+
storage: DurableObjectStorageLike;
|
|
8
|
+
}
|
|
9
|
+
export interface FetcherLike {
|
|
10
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
11
|
+
}
|
|
12
|
+
export interface DurableObjectNamespaceLike {
|
|
13
|
+
idFromName(name: string): unknown;
|
|
14
|
+
get(id: unknown): FetcherLike;
|
|
15
|
+
}
|
|
16
|
+
export interface ExecutionContextLike {
|
|
17
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
18
|
+
}
|
|
19
|
+
export interface R2ObjectLike {
|
|
20
|
+
key: string;
|
|
21
|
+
size: number;
|
|
22
|
+
httpMetadata?: {
|
|
23
|
+
contentType?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface R2ObjectBodyLike extends R2ObjectLike {
|
|
27
|
+
body: ReadableStream;
|
|
28
|
+
}
|
|
29
|
+
export interface R2BucketLike {
|
|
30
|
+
put(key: string, value: ArrayBuffer | ReadableStream | string, options?: {
|
|
31
|
+
httpMetadata?: {
|
|
32
|
+
contentType?: string;
|
|
33
|
+
};
|
|
34
|
+
}): Promise<unknown>;
|
|
35
|
+
get(key: string): Promise<R2ObjectBodyLike | null>;
|
|
36
|
+
head(key: string): Promise<R2ObjectLike | null>;
|
|
37
|
+
delete(key: string): Promise<void>;
|
|
38
|
+
list(options?: {
|
|
39
|
+
prefix?: string;
|
|
40
|
+
cursor?: string;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
objects: R2ObjectLike[];
|
|
43
|
+
truncated: boolean;
|
|
44
|
+
cursor?: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
export type WorkerEnv = {
|
|
48
|
+
SCHEDULER?: DurableObjectNamespaceLike;
|
|
49
|
+
REALTIME?: DurableObjectNamespaceLike;
|
|
50
|
+
RATE_LIMITER?: DurableObjectNamespaceLike;
|
|
51
|
+
ASSETS?: FetcherLike;
|
|
52
|
+
[binding: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
export declare function stub(namespace: DurableObjectNamespaceLike, name: string): FetcherLike;
|
|
55
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/workers/types.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,wBAAwB;IACvC,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,wBAAwB,CAAA;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CACvE;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IACjC,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,WAAW,CAAA;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAC3C;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACxC;AAED,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,CACD,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,WAAW,GAAG,cAAc,GAAG,MAAM,EAC5C,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE;YAAE,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GACpD,OAAO,CAAC,OAAO,CAAC,CAAA;IACnB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAClD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAA;IAC/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAC5D,OAAO,EAAE,YAAY,EAAE,CAAA;QACvB,SAAS,EAAE,OAAO,CAAA;QAClB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAC,CAAA;CACH;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,CAAC,EAAE,0BAA0B,CAAA;IACtC,QAAQ,CAAC,EAAE,0BAA0B,CAAA;IACrC,YAAY,CAAC,EAAE,0BAA0B,CAAA;IACzC,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAA;CAC3B,CAAA;AAED,wBAAgB,IAAI,CAClB,SAAS,EAAE,0BAA0B,EACrC,IAAI,EAAE,MAAM,GACX,WAAW,CAEb"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/workers/types.ts"],"names":[],"mappings":"AA4DA,MAAM,UAAU,IAAI,CAClB,SAAqC,EACrC,IAAY;IAEZ,OAAO,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;AAClD,CAAC","sourcesContent":["// src/workers/types.ts — the small part of the Workers API that bunderstack\n// uses. Local interfaces keep `cloudflare:workers` and its type package out of\n// the core, so the same classes run under `bun test` with in-memory fakes.\nexport interface DurableObjectStorageLike {\n getAlarm(): Promise<number | null>\n setAlarm(scheduledTime: number | Date): Promise<void>\n deleteAlarm(): Promise<void>\n}\n\nexport interface DurableObjectStateLike {\n storage: DurableObjectStorageLike\n}\n\nexport interface FetcherLike {\n fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>\n}\n\nexport interface DurableObjectNamespaceLike {\n idFromName(name: string): unknown\n get(id: unknown): FetcherLike\n}\n\nexport interface ExecutionContextLike {\n waitUntil(promise: Promise<unknown>): void\n}\n\nexport interface R2ObjectLike {\n key: string\n size: number\n httpMetadata?: { contentType?: string }\n}\n\nexport interface R2ObjectBodyLike extends R2ObjectLike {\n body: ReadableStream\n}\n\nexport interface R2BucketLike {\n put(\n key: string,\n value: ArrayBuffer | ReadableStream | string,\n options?: { httpMetadata?: { contentType?: string } },\n ): Promise<unknown>\n get(key: string): Promise<R2ObjectBodyLike | null>\n head(key: string): Promise<R2ObjectLike | null>\n delete(key: string): Promise<void>\n list(options?: { prefix?: string; cursor?: string }): Promise<{\n objects: R2ObjectLike[]\n truncated: boolean\n cursor?: string\n }>\n}\n\nexport type WorkerEnv = {\n SCHEDULER?: DurableObjectNamespaceLike\n REALTIME?: DurableObjectNamespaceLike\n RATE_LIMITER?: DurableObjectNamespaceLike\n ASSETS?: FetcherLike\n [binding: string]: unknown\n}\n\nexport function stub(\n namespace: DurableObjectNamespaceLike,\n name: string,\n): FetcherLike {\n return namespace.get(namespace.idFromName(name))\n}\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { BunderstackManifest } from '../manifest.js';
|
|
2
|
+
export type WranglerConfig = ReturnType<typeof buildWranglerConfig>;
|
|
3
|
+
export declare function buildWranglerConfig(manifest: BunderstackManifest, options: {
|
|
4
|
+
name: string;
|
|
5
|
+
compatibilityDate: string;
|
|
6
|
+
main?: string;
|
|
7
|
+
assetsDirectory?: string;
|
|
8
|
+
}): {
|
|
9
|
+
triggers?: {
|
|
10
|
+
crons: string[];
|
|
11
|
+
} | undefined;
|
|
12
|
+
name: string;
|
|
13
|
+
main: string;
|
|
14
|
+
compatibility_date: string;
|
|
15
|
+
compatibility_flags: string[];
|
|
16
|
+
durable_objects: {
|
|
17
|
+
bindings: {
|
|
18
|
+
name: string;
|
|
19
|
+
class_name: string;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
migrations: {
|
|
23
|
+
tag: string;
|
|
24
|
+
new_sqlite_classes: string[];
|
|
25
|
+
}[];
|
|
26
|
+
r2_buckets: {
|
|
27
|
+
binding: string;
|
|
28
|
+
bucket_name: string;
|
|
29
|
+
}[];
|
|
30
|
+
assets: {
|
|
31
|
+
directory: string;
|
|
32
|
+
binding: string;
|
|
33
|
+
not_found_handling: string;
|
|
34
|
+
run_worker_first: string[];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export declare class WranglerCheckError extends Error {
|
|
38
|
+
constructor(path: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Imports the app's backend. Entry precedence: the argument,
|
|
42
|
+
* package.json#bunderstack.entry, src/bunderstack.ts.
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadBackend(directory: string, entry?: string): Promise<{
|
|
45
|
+
pkg: {
|
|
46
|
+
name?: string;
|
|
47
|
+
bunderstack?: {
|
|
48
|
+
entry?: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
backend: import("..").BunderstackBackend<unknown>;
|
|
52
|
+
}>;
|
|
53
|
+
export declare function runWranglerCommand(options: {
|
|
54
|
+
directory: string;
|
|
55
|
+
entry?: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
assets?: string;
|
|
58
|
+
output?: string;
|
|
59
|
+
check?: boolean;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
path: string;
|
|
62
|
+
changed: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
//# sourceMappingURL=wrangler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrangler.d.ts","sourceRoot":"","sources":["../../src/workers/wrangler.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAQtD,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAEnE,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE;IACP,IAAI,EAAE,MAAM,CAAA;IACZ,iBAAiB,EAAE,MAAM,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwDF;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,IAAI,EAAE,MAAM;CAIzB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;;eAGnD,MAAM;sBAAgB;YAAE,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE;;;GASvD;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA+B9C"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/workers/wrangler.ts — wrangler.json from the backend manifest. The same
|
|
2
|
+
// file deploys with `wrangler deploy` (Cloudflare) and `celld deploy`.
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { isBunderstackBackend } from '../backend.js';
|
|
7
|
+
import { bucketBindingName } from './r2.js';
|
|
8
|
+
/** Cloudflare's per-Worker Cron Trigger limit on the free plan. */
|
|
9
|
+
const MAX_CRONS = 5;
|
|
10
|
+
export function buildWranglerConfig(manifest, options) {
|
|
11
|
+
const hasStorage = manifest.storage.buckets.length > 0;
|
|
12
|
+
const crons = [
|
|
13
|
+
...new Set([
|
|
14
|
+
...manifest.background.cron.map((cron) => cron.schedule),
|
|
15
|
+
...(hasStorage
|
|
16
|
+
? manifest.background.maintenance.map((task) => task.schedule)
|
|
17
|
+
: []),
|
|
18
|
+
]),
|
|
19
|
+
].sort();
|
|
20
|
+
// Operations without their own path are served through /api/rpc.
|
|
21
|
+
const prefixes = [
|
|
22
|
+
...new Set([
|
|
23
|
+
'/api/*',
|
|
24
|
+
...manifest.api.operations
|
|
25
|
+
.filter((op) => op.path)
|
|
26
|
+
.map((op) => `/${op.path.split('/')[1]}/*`),
|
|
27
|
+
]),
|
|
28
|
+
];
|
|
29
|
+
return {
|
|
30
|
+
name: options.name,
|
|
31
|
+
main: options.main ?? 'src/worker.ts',
|
|
32
|
+
compatibility_date: options.compatibilityDate,
|
|
33
|
+
compatibility_flags: ['nodejs_compat'],
|
|
34
|
+
durable_objects: {
|
|
35
|
+
bindings: [
|
|
36
|
+
{ name: 'SCHEDULER', class_name: 'Scheduler' },
|
|
37
|
+
{ name: 'REALTIME', class_name: 'RealtimeHub' },
|
|
38
|
+
{ name: 'RATE_LIMITER', class_name: 'RateLimiter' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
migrations: [
|
|
42
|
+
{
|
|
43
|
+
tag: 'v1',
|
|
44
|
+
new_sqlite_classes: ['Scheduler', 'RealtimeHub', 'RateLimiter'],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
r2_buckets: manifest.storage.buckets.map((bucket) => ({
|
|
48
|
+
binding: bucketBindingName(bucket.name),
|
|
49
|
+
bucket_name: `${options.name}-${bucket.name}`,
|
|
50
|
+
})),
|
|
51
|
+
assets: {
|
|
52
|
+
directory: options.assetsDirectory ?? 'dist/client',
|
|
53
|
+
binding: 'ASSETS',
|
|
54
|
+
not_found_handling: 'single-page-application',
|
|
55
|
+
run_worker_first: prefixes,
|
|
56
|
+
},
|
|
57
|
+
...(crons.length > 0
|
|
58
|
+
? {
|
|
59
|
+
triggers: {
|
|
60
|
+
crons: crons.length > MAX_CRONS ? ['* * * * *'] : crons,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
: {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export class WranglerCheckError extends Error {
|
|
67
|
+
constructor(path) {
|
|
68
|
+
super(`[bunderstack] ${path} is out of date; run \`bunderstack wrangler\``);
|
|
69
|
+
this.name = 'WranglerCheckError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Imports the app's backend. Entry precedence: the argument,
|
|
74
|
+
* package.json#bunderstack.entry, src/bunderstack.ts.
|
|
75
|
+
*/
|
|
76
|
+
export async function loadBackend(directory, entry) {
|
|
77
|
+
const pkg = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
|
|
78
|
+
const path = entry ?? pkg.bunderstack?.entry ?? 'src/bunderstack.ts';
|
|
79
|
+
const module = (await import(pathToFileURL(join(directory, path)).href));
|
|
80
|
+
if (!isBunderstackBackend(module.backend)) {
|
|
81
|
+
throw new Error(`[bunderstack] ${path} must export backend`);
|
|
82
|
+
}
|
|
83
|
+
return { pkg, backend: module.backend };
|
|
84
|
+
}
|
|
85
|
+
export async function runWranglerCommand(options) {
|
|
86
|
+
const directory = resolve(options.directory);
|
|
87
|
+
const { pkg, backend } = await loadBackend(directory, options.entry);
|
|
88
|
+
const path = join(directory, options.output ?? 'wrangler.json');
|
|
89
|
+
const existing = await readFile(path, 'utf8').catch(() => undefined);
|
|
90
|
+
const previous = existing
|
|
91
|
+
? JSON.parse(existing)
|
|
92
|
+
: undefined;
|
|
93
|
+
const name = (options.name ?? pkg.name ?? 'app')
|
|
94
|
+
.replace(/^@[^/]+\//, '')
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/[^a-z0-9-]/g, '-');
|
|
97
|
+
const config = buildWranglerConfig(backend.inspect({ env: process.env }), {
|
|
98
|
+
name,
|
|
99
|
+
// Keep the date once chosen, so --check stays stable across days.
|
|
100
|
+
compatibilityDate: previous?.compatibility_date ?? new Date().toISOString().slice(0, 10),
|
|
101
|
+
// Keep a directory set once with --assets; `bunderstack dev` passes none.
|
|
102
|
+
assetsDirectory: options.assets ?? previous?.assets?.directory,
|
|
103
|
+
});
|
|
104
|
+
const text = `${JSON.stringify(config, null, 2)}\n`;
|
|
105
|
+
if (options.check) {
|
|
106
|
+
if (existing !== text)
|
|
107
|
+
throw new WranglerCheckError(path);
|
|
108
|
+
return { path, changed: false };
|
|
109
|
+
}
|
|
110
|
+
if (existing === text)
|
|
111
|
+
return { path, changed: false };
|
|
112
|
+
await writeFile(path, text);
|
|
113
|
+
return { path, changed: true };
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=wrangler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrangler.js","sourceRoot":"","sources":["../../src/workers/wrangler.ts"],"names":[],"mappings":"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","sourcesContent":["// src/workers/wrangler.ts — wrangler.json from the backend manifest. The same\n// file deploys with `wrangler deploy` (Cloudflare) and `celld deploy`.\nimport { readFile, writeFile } from 'node:fs/promises'\nimport { join, resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nimport type { BunderstackManifest } from '../manifest'\n\nimport { isBunderstackBackend } from '../backend'\nimport { bucketBindingName } from './r2'\n\n/** Cloudflare's per-Worker Cron Trigger limit on the free plan. */\nconst MAX_CRONS = 5\n\nexport type WranglerConfig = ReturnType<typeof buildWranglerConfig>\n\nexport function buildWranglerConfig(\n manifest: BunderstackManifest,\n options: {\n name: string\n compatibilityDate: string\n main?: string\n assetsDirectory?: string\n },\n) {\n const hasStorage = manifest.storage.buckets.length > 0\n const crons = [\n ...new Set([\n ...manifest.background.cron.map((cron) => cron.schedule),\n ...(hasStorage\n ? manifest.background.maintenance.map((task) => task.schedule)\n : []),\n ]),\n ].sort()\n // Operations without their own path are served through /api/rpc.\n const prefixes = [\n ...new Set([\n '/api/*',\n ...manifest.api.operations\n .filter((op) => op.path)\n .map((op) => `/${op.path!.split('/')[1]}/*`),\n ]),\n ]\n return {\n name: options.name,\n main: options.main ?? 'src/worker.ts',\n compatibility_date: options.compatibilityDate,\n compatibility_flags: ['nodejs_compat'],\n durable_objects: {\n bindings: [\n { name: 'SCHEDULER', class_name: 'Scheduler' },\n { name: 'REALTIME', class_name: 'RealtimeHub' },\n { name: 'RATE_LIMITER', class_name: 'RateLimiter' },\n ],\n },\n migrations: [\n {\n tag: 'v1',\n new_sqlite_classes: ['Scheduler', 'RealtimeHub', 'RateLimiter'],\n },\n ],\n r2_buckets: manifest.storage.buckets.map((bucket) => ({\n binding: bucketBindingName(bucket.name),\n bucket_name: `${options.name}-${bucket.name}`,\n })),\n assets: {\n directory: options.assetsDirectory ?? 'dist/client',\n binding: 'ASSETS',\n not_found_handling: 'single-page-application',\n run_worker_first: prefixes,\n },\n ...(crons.length > 0\n ? {\n triggers: {\n crons: crons.length > MAX_CRONS ? ['* * * * *'] : crons,\n },\n }\n : {}),\n }\n}\n\nexport class WranglerCheckError extends Error {\n constructor(path: string) {\n super(`[bunderstack] ${path} is out of date; run \\`bunderstack wrangler\\``)\n this.name = 'WranglerCheckError'\n }\n}\n\n/**\n * Imports the app's backend. Entry precedence: the argument,\n * package.json#bunderstack.entry, src/bunderstack.ts.\n */\nexport async function loadBackend(directory: string, entry?: string) {\n const pkg = JSON.parse(\n await readFile(join(directory, 'package.json'), 'utf8'),\n ) as { name?: string; bunderstack?: { entry?: string } }\n const path = entry ?? pkg.bunderstack?.entry ?? 'src/bunderstack.ts'\n const module = (await import(pathToFileURL(join(directory, path)).href)) as {\n backend?: unknown\n }\n if (!isBunderstackBackend(module.backend)) {\n throw new Error(`[bunderstack] ${path} must export backend`)\n }\n return { pkg, backend: module.backend }\n}\n\nexport async function runWranglerCommand(options: {\n directory: string\n entry?: string\n name?: string\n assets?: string\n output?: string\n check?: boolean\n}): Promise<{ path: string; changed: boolean }> {\n const directory = resolve(options.directory)\n const { pkg, backend } = await loadBackend(directory, options.entry)\n const path = join(directory, options.output ?? 'wrangler.json')\n const existing = await readFile(path, 'utf8').catch(() => undefined)\n const previous = existing\n ? (JSON.parse(existing) as {\n compatibility_date?: string\n assets?: { directory?: string }\n })\n : undefined\n const name = (options.name ?? pkg.name ?? 'app')\n .replace(/^@[^/]+\\//, '')\n .toLowerCase()\n .replace(/[^a-z0-9-]/g, '-')\n const config = buildWranglerConfig(backend.inspect({ env: process.env }), {\n name,\n // Keep the date once chosen, so --check stays stable across days.\n compatibilityDate:\n previous?.compatibility_date ?? new Date().toISOString().slice(0, 10),\n // Keep a directory set once with --assets; `bunderstack dev` passes none.\n assetsDirectory: options.assets ?? previous?.assets?.directory,\n })\n const text = `${JSON.stringify(config, null, 2)}\\n`\n if (options.check) {\n if (existing !== text) throw new WranglerCheckError(path)\n return { path, changed: false }\n }\n if (existing === text) return { path, changed: false }\n await writeFile(path, text)\n return { path, changed: true }\n}\n"]}
|
package/llms-full.txt
CHANGED
|
@@ -740,7 +740,16 @@ latest captured verification link for that user's email.
|
|
|
740
740
|
## Required schema tables
|
|
741
741
|
|
|
742
742
|
Include `user`, `session`, `account`, `verification` from BetterAuth in your schema.
|
|
743
|
-
|
|
743
|
+
Generate them with `npx auth generate`, or copy the definitions from the
|
|
744
|
+
[SaaS template](https://github.com/kirill-dev-pro/bunderstack/blob/main/templates/tanstack-start-saas/src/bunderstack/schema/auth.ts).
|
|
745
|
+
|
|
746
|
+
Better Auth 1.7.6 and later check these tables when the app starts and before
|
|
747
|
+
every auth call. A missing table or column, or a required column that Better
|
|
748
|
+
Auth never writes, fails the request with `Drizzle schema mismatch`. Apps
|
|
749
|
+
created with Better Auth 1.7.0–1.7.2 may still have a required
|
|
750
|
+
`account.issuer` column; follow the
|
|
751
|
+
[1.7 upgrade guide](https://www.better-auth.com/docs/guides/1-7-upgrade-guide)
|
|
752
|
+
before removing it.
|
|
744
753
|
|
|
745
754
|
An app whose schema has no `user` and `session` tables runs without Better Auth:
|
|
746
755
|
every session resolves to `null`, `/api/auth/*` answers 404, and the OpenAPI
|