@benqoder/beam 0.1.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,68 @@
1
+ import type { ActionHandler, ModalHandler, DrawerHandler } from './types';
2
+ /**
3
+ * Type for glob import results from import.meta.glob
4
+ */
5
+ type GlobImport = Record<string, Record<string, unknown>>;
6
+ /**
7
+ * Collects action handlers from glob imports.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const actions = collectActions<Env>(
12
+ * import.meta.glob('./actions/*.tsx', { eager: true })
13
+ * )
14
+ * ```
15
+ *
16
+ * Each file can export multiple named functions that become actions:
17
+ * - `./actions/demo.tsx` exports `increment`, `decrement` → `increment`, `decrement`
18
+ * - `./actions/cart.tsx` exports `addToCart` → `addToCart`
19
+ */
20
+ export declare function collectActions<TEnv = object>(glob: GlobImport): Record<string, ActionHandler<TEnv>>;
21
+ /**
22
+ * Collects modal handlers from glob imports.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const modals = collectModals<Env>(
27
+ * import.meta.glob('./modals/*.tsx', { eager: true })
28
+ * )
29
+ * ```
30
+ */
31
+ export declare function collectModals<TEnv = object>(glob: GlobImport): Record<string, ModalHandler<TEnv>>;
32
+ /**
33
+ * Collects drawer handlers from glob imports.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const drawers = collectDrawers<Env>(
38
+ * import.meta.glob('./drawers/*.tsx', { eager: true })
39
+ * )
40
+ * ```
41
+ */
42
+ export declare function collectDrawers<TEnv = object>(glob: GlobImport): Record<string, DrawerHandler<TEnv>>;
43
+ /**
44
+ * Collects all handlers (actions, modals, drawers) from glob imports.
45
+ * This is a convenience function that collects all three at once.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const { actions, modals, drawers } = collectHandlers<Env>({
50
+ * actions: import.meta.glob('./actions/*.tsx', { eager: true }),
51
+ * modals: import.meta.glob('./modals/*.tsx', { eager: true }),
52
+ * drawers: import.meta.glob('./drawers/*.tsx', { eager: true }),
53
+ * })
54
+ *
55
+ * export const beam = createBeam<Env>({ actions, modals, drawers })
56
+ * ```
57
+ */
58
+ export declare function collectHandlers<TEnv = object>(globs: {
59
+ actions?: GlobImport;
60
+ modals?: GlobImport;
61
+ drawers?: GlobImport;
62
+ }): {
63
+ actions: Record<string, ActionHandler<TEnv>>;
64
+ modals: Record<string, ModalHandler<TEnv>>;
65
+ drawers: Record<string, DrawerHandler<TEnv>>;
66
+ };
67
+ export {};
68
+ //# sourceMappingURL=collect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../src/collect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEzE;;GAEG;AACH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAEzD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,GAAG,MAAM,EAC1C,IAAI,EAAE,UAAU,GACf,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAarC;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAG,MAAM,EACzC,IAAI,EAAE,UAAU,GACf,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAYpC;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,IAAI,GAAG,MAAM,EAC1C,IAAI,EAAE,UAAU,GACf,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAYrC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAG,MAAM,EAAE,KAAK,EAAE;IACpD,OAAO,CAAC,EAAE,UAAU,CAAA;IACpB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,OAAO,CAAC,EAAE,UAAU,CAAA;CACrB,GAAG;IACF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;IAC5C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAA;IAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;CAC7C,CAMA"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Collects action handlers from glob imports.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * const actions = collectActions<Env>(
7
+ * import.meta.glob('./actions/*.tsx', { eager: true })
8
+ * )
9
+ * ```
10
+ *
11
+ * Each file can export multiple named functions that become actions:
12
+ * - `./actions/demo.tsx` exports `increment`, `decrement` → `increment`, `decrement`
13
+ * - `./actions/cart.tsx` exports `addToCart` → `addToCart`
14
+ */
15
+ export function collectActions(glob) {
16
+ const handlers = {};
17
+ for (const [, module] of Object.entries(glob)) {
18
+ for (const [exportName, exportValue] of Object.entries(module)) {
19
+ // Skip non-function exports and default exports
20
+ if (typeof exportValue === 'function' && exportName !== 'default') {
21
+ handlers[exportName] = exportValue;
22
+ }
23
+ }
24
+ }
25
+ return handlers;
26
+ }
27
+ /**
28
+ * Collects modal handlers from glob imports.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const modals = collectModals<Env>(
33
+ * import.meta.glob('./modals/*.tsx', { eager: true })
34
+ * )
35
+ * ```
36
+ */
37
+ export function collectModals(glob) {
38
+ const handlers = {};
39
+ for (const [, module] of Object.entries(glob)) {
40
+ for (const [exportName, exportValue] of Object.entries(module)) {
41
+ if (typeof exportValue === 'function' && exportName !== 'default') {
42
+ handlers[exportName] = exportValue;
43
+ }
44
+ }
45
+ }
46
+ return handlers;
47
+ }
48
+ /**
49
+ * Collects drawer handlers from glob imports.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const drawers = collectDrawers<Env>(
54
+ * import.meta.glob('./drawers/*.tsx', { eager: true })
55
+ * )
56
+ * ```
57
+ */
58
+ export function collectDrawers(glob) {
59
+ const handlers = {};
60
+ for (const [, module] of Object.entries(glob)) {
61
+ for (const [exportName, exportValue] of Object.entries(module)) {
62
+ if (typeof exportValue === 'function' && exportName !== 'default') {
63
+ handlers[exportName] = exportValue;
64
+ }
65
+ }
66
+ }
67
+ return handlers;
68
+ }
69
+ /**
70
+ * Collects all handlers (actions, modals, drawers) from glob imports.
71
+ * This is a convenience function that collects all three at once.
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * const { actions, modals, drawers } = collectHandlers<Env>({
76
+ * actions: import.meta.glob('./actions/*.tsx', { eager: true }),
77
+ * modals: import.meta.glob('./modals/*.tsx', { eager: true }),
78
+ * drawers: import.meta.glob('./drawers/*.tsx', { eager: true }),
79
+ * })
80
+ *
81
+ * export const beam = createBeam<Env>({ actions, modals, drawers })
82
+ * ```
83
+ */
84
+ export function collectHandlers(globs) {
85
+ return {
86
+ actions: globs.actions ? collectActions(globs.actions) : {},
87
+ modals: globs.modals ? collectModals(globs.modals) : {},
88
+ drawers: globs.drawers ? collectDrawers(globs.drawers) : {},
89
+ };
90
+ }
@@ -0,0 +1,104 @@
1
+ import { RpcTarget } from 'capnweb';
2
+ import type { ActionHandler, ActionResponse, ModalHandler, DrawerHandler, BeamConfig, BeamInstance, BeamContext, BeamSession } from './types';
3
+ /**
4
+ * Session implementation using KV storage.
5
+ * Exported for users who need custom storage adapter.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // app/session-storage.ts
10
+ * import { KVSession } from '@benqoder/beam'
11
+ * export default (sessionId: string, env: Env) => new KVSession(sessionId, env.KV)
12
+ * ```
13
+ */
14
+ export declare class KVSession implements BeamSession {
15
+ private sessionId;
16
+ private kv;
17
+ constructor(sessionId: string, kv: KVNamespace);
18
+ private key;
19
+ get<T = unknown>(key: string): Promise<T | null>;
20
+ set<T = unknown>(key: string, value: T): Promise<void>;
21
+ delete(key: string): Promise<void>;
22
+ }
23
+ /**
24
+ * Cookie-based session storage (default).
25
+ * Stores all session data in a signed cookie (~4KB limit).
26
+ * Good for: cart, preferences, small state.
27
+ * For larger data, use KVSession or custom storage.
28
+ */
29
+ export declare class CookieSession implements BeamSession {
30
+ private data;
31
+ private _dirty;
32
+ constructor(initialData?: Record<string, unknown>);
33
+ get<T = unknown>(key: string): Promise<T | null>;
34
+ set<T = unknown>(key: string, value: T): Promise<void>;
35
+ delete(key: string): Promise<void>;
36
+ /** Check if session data has been modified */
37
+ isDirty(): boolean;
38
+ /** Get all session data (for serializing to cookie) */
39
+ getData(): Record<string, unknown>;
40
+ }
41
+ /**
42
+ * Beam RPC Server - extends RpcTarget for capnweb integration
43
+ *
44
+ * This enables:
45
+ * - Promise pipelining (multiple calls in one round-trip)
46
+ * - Bidirectional RPC (server can call client callbacks)
47
+ * - Pass-by-reference (RpcTarget objects stay on origin)
48
+ * - Function callbacks (pass functions over RPC)
49
+ */
50
+ declare class BeamServer<TEnv extends object> extends RpcTarget {
51
+ private ctx;
52
+ private actions;
53
+ private modals;
54
+ private drawers;
55
+ constructor(ctx: BeamContext<TEnv>, actions: Record<string, ActionHandler<TEnv>>, modals: Record<string, ModalHandler<TEnv>>, drawers: Record<string, DrawerHandler<TEnv>>);
56
+ /**
57
+ * Call an action handler
58
+ */
59
+ call(action: string, data?: Record<string, unknown>): Promise<ActionResponse>;
60
+ /**
61
+ * Open a modal
62
+ */
63
+ modal(modalId: string, data?: Record<string, unknown>): Promise<string>;
64
+ /**
65
+ * Open a drawer
66
+ */
67
+ drawer(drawerId: string, data?: Record<string, unknown>): Promise<string>;
68
+ /**
69
+ * Register a client callback for server-initiated updates
70
+ * This enables bidirectional communication - server can push to client
71
+ */
72
+ registerCallback(callback: (event: string, data: unknown) => void): void;
73
+ /**
74
+ * Notify connected client (if callback registered)
75
+ */
76
+ notify(event: string, data: unknown): Promise<void>;
77
+ }
78
+ /**
79
+ * Creates a Beam instance configured with actions, modals, and drawers.
80
+ * Uses capnweb for RPC, enabling promise pipelining and bidirectional calls.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * // app/beam.ts
85
+ * import { createBeam } from '@benqoder/beam'
86
+ * import type { Env } from './types'
87
+ *
88
+ * export const beam = createBeam<Env>({
89
+ * actions: { createProduct, deleteProduct },
90
+ * modals: { confirmDelete },
91
+ * drawers: { productDetails }
92
+ * })
93
+ *
94
+ * // app/server.ts
95
+ * import { createApp } from 'honox/server'
96
+ * import { beam } from './beam'
97
+ *
98
+ * const app = createApp({ init: beam.init })
99
+ * export default app
100
+ * ```
101
+ */
102
+ export declare function createBeam<TEnv extends object = object>(config: BeamConfig<TEnv>): BeamInstance<TEnv>;
103
+ export { BeamServer };
104
+ //# sourceMappingURL=createBeam.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createBeam.d.ts","sourceRoot":"","sources":["../src/createBeam.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAyB,MAAM,SAAS,CAAA;AAC1D,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,WAAW,EAEX,WAAW,EAIZ,MAAM,SAAS,CAAA;AAEhB;;;;;;;;;;GAUG;AACH,qBAAa,SAAU,YAAW,WAAW;IAC3C,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,EAAE,CAAa;gBAEX,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW;IAK9C,OAAO,CAAC,GAAG;IAIL,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAMhD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGzC;AAED;;;;;GAKG;AACH,qBAAa,aAAc,YAAW,WAAW;IAC/C,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,MAAM,CAAiB;gBAEnB,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAI/C,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAIhD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxC,8CAA8C;IAC9C,OAAO,IAAI,OAAO;IAIlB,uDAAuD;IACvD,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAGnC;AA4ED;;;;;;;;GAQG;AACH,cAAM,UAAU,CAAC,IAAI,SAAS,MAAM,CAAE,SAAQ,SAAS;IACrD,OAAO,CAAC,GAAG,CAAmB;IAC9B,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,MAAM,CAAoC;IAClD,OAAO,CAAC,OAAO,CAAqC;gBAGlD,GAAG,EAAE,WAAW,CAAC,IAAI,CAAC,EACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,EAC5C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,EAC1C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IAS9C;;OAEG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAavF;;OAEG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQjF;;OAEG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQnF;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAKxE;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAM1D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,UAAU,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EACrD,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,GACvB,YAAY,CAAC,IAAI,CAAC,CA8MpB;AAGD,OAAO,EAAE,UAAU,EAAE,CAAA"}
@@ -0,0 +1,421 @@
1
+ import { getSignedCookie, setSignedCookie } from 'hono/cookie';
2
+ import { RpcTarget, newWorkersRpcResponse } from 'capnweb';
3
+ /**
4
+ * Session implementation using KV storage.
5
+ * Exported for users who need custom storage adapter.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // app/session-storage.ts
10
+ * import { KVSession } from '@benqoder/beam'
11
+ * export default (sessionId: string, env: Env) => new KVSession(sessionId, env.KV)
12
+ * ```
13
+ */
14
+ export class KVSession {
15
+ sessionId;
16
+ kv;
17
+ constructor(sessionId, kv) {
18
+ this.sessionId = sessionId;
19
+ this.kv = kv;
20
+ }
21
+ key(k) {
22
+ return `beam:${this.sessionId}:${k}`;
23
+ }
24
+ async get(key) {
25
+ const data = await this.kv.get(this.key(key));
26
+ if (!data)
27
+ return null;
28
+ return JSON.parse(data);
29
+ }
30
+ async set(key, value) {
31
+ await this.kv.put(this.key(key), JSON.stringify(value));
32
+ }
33
+ async delete(key) {
34
+ await this.kv.delete(this.key(key));
35
+ }
36
+ }
37
+ /**
38
+ * Cookie-based session storage (default).
39
+ * Stores all session data in a signed cookie (~4KB limit).
40
+ * Good for: cart, preferences, small state.
41
+ * For larger data, use KVSession or custom storage.
42
+ */
43
+ export class CookieSession {
44
+ data;
45
+ _dirty = false;
46
+ constructor(initialData = {}) {
47
+ this.data = initialData;
48
+ }
49
+ async get(key) {
50
+ return this.data[key] ?? null;
51
+ }
52
+ async set(key, value) {
53
+ this.data[key] = value;
54
+ this._dirty = true;
55
+ }
56
+ async delete(key) {
57
+ delete this.data[key];
58
+ this._dirty = true;
59
+ }
60
+ /** Check if session data has been modified */
61
+ isDirty() {
62
+ return this._dirty;
63
+ }
64
+ /** Get all session data (for serializing to cookie) */
65
+ getData() {
66
+ return this.data;
67
+ }
68
+ }
69
+ /** Cookie name for session data (separate from session ID) */
70
+ const SESSION_DATA_COOKIE = 'beam_data';
71
+ /**
72
+ * Parse cookies from raw request (for WebSocket context)
73
+ */
74
+ function parseCookies(request) {
75
+ const cookieHeader = request.headers.get('Cookie');
76
+ if (!cookieHeader)
77
+ return {};
78
+ return Object.fromEntries(cookieHeader.split(';').map((c) => {
79
+ const [key, ...val] = c.trim().split('=');
80
+ return [key, val.join('=')];
81
+ }));
82
+ }
83
+ /**
84
+ * Parse session ID from raw request cookies (for WebSocket context)
85
+ */
86
+ function parseSessionFromRequest(request, cookieName) {
87
+ const cookies = parseCookies(request);
88
+ const signedValue = cookies[cookieName];
89
+ if (!signedValue)
90
+ return null;
91
+ // Hono signed cookie format: value.signature
92
+ const parts = signedValue.split('.');
93
+ if (parts.length !== 2)
94
+ return null;
95
+ return parts[0] || null;
96
+ }
97
+ /**
98
+ * Parse session data from raw request cookies (for WebSocket context)
99
+ */
100
+ function parseSessionDataFromRequest(request) {
101
+ const cookies = parseCookies(request);
102
+ const signedValue = cookies[SESSION_DATA_COOKIE];
103
+ if (!signedValue)
104
+ return {};
105
+ // Hono signed cookie format: value.signature
106
+ const parts = signedValue.split('.');
107
+ if (parts.length !== 2)
108
+ return {};
109
+ try {
110
+ return JSON.parse(decodeURIComponent(parts[0]));
111
+ }
112
+ catch {
113
+ return {};
114
+ }
115
+ }
116
+ /**
117
+ * Create a BeamContext with script() and render() helpers
118
+ */
119
+ function createBeamContext(base) {
120
+ return {
121
+ ...base,
122
+ script: (code) => ({ script: code }),
123
+ render: (html, options) => {
124
+ if (html instanceof Promise) {
125
+ return html.then((resolved) => ({ html: resolved, script: options?.script }));
126
+ }
127
+ return { html, script: options?.script };
128
+ },
129
+ redirect: (url) => ({ redirect: url }),
130
+ };
131
+ }
132
+ /**
133
+ * Beam RPC Server - extends RpcTarget for capnweb integration
134
+ *
135
+ * This enables:
136
+ * - Promise pipelining (multiple calls in one round-trip)
137
+ * - Bidirectional RPC (server can call client callbacks)
138
+ * - Pass-by-reference (RpcTarget objects stay on origin)
139
+ * - Function callbacks (pass functions over RPC)
140
+ */
141
+ class BeamServer extends RpcTarget {
142
+ ctx;
143
+ actions;
144
+ modals;
145
+ drawers;
146
+ constructor(ctx, actions, modals, drawers) {
147
+ super();
148
+ this.ctx = ctx;
149
+ this.actions = actions;
150
+ this.modals = modals;
151
+ this.drawers = drawers;
152
+ }
153
+ /**
154
+ * Call an action handler
155
+ */
156
+ async call(action, data = {}) {
157
+ const handler = this.actions[action];
158
+ if (!handler) {
159
+ throw new Error(`Unknown action: ${action}`);
160
+ }
161
+ const result = await handler(this.ctx, data);
162
+ // Normalize string responses to ActionResponse format
163
+ if (typeof result === 'string') {
164
+ return { html: result };
165
+ }
166
+ return result;
167
+ }
168
+ /**
169
+ * Open a modal
170
+ */
171
+ async modal(modalId, data = {}) {
172
+ const handler = this.modals[modalId];
173
+ if (!handler) {
174
+ throw new Error(`Unknown modal: ${modalId}`);
175
+ }
176
+ return await handler(this.ctx, data);
177
+ }
178
+ /**
179
+ * Open a drawer
180
+ */
181
+ async drawer(drawerId, data = {}) {
182
+ const handler = this.drawers[drawerId];
183
+ if (!handler) {
184
+ throw new Error(`Unknown drawer: ${drawerId}`);
185
+ }
186
+ return await handler(this.ctx, data);
187
+ }
188
+ /**
189
+ * Register a client callback for server-initiated updates
190
+ * This enables bidirectional communication - server can push to client
191
+ */
192
+ registerCallback(callback) {
193
+ // Store callback for later use by actions that need to push updates
194
+ ;
195
+ this._clientCallback = callback;
196
+ }
197
+ /**
198
+ * Notify connected client (if callback registered)
199
+ */
200
+ async notify(event, data) {
201
+ const callback = this._clientCallback;
202
+ if (callback) {
203
+ await callback(event, data);
204
+ }
205
+ }
206
+ }
207
+ /**
208
+ * Creates a Beam instance configured with actions, modals, and drawers.
209
+ * Uses capnweb for RPC, enabling promise pipelining and bidirectional calls.
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * // app/beam.ts
214
+ * import { createBeam } from '@benqoder/beam'
215
+ * import type { Env } from './types'
216
+ *
217
+ * export const beam = createBeam<Env>({
218
+ * actions: { createProduct, deleteProduct },
219
+ * modals: { confirmDelete },
220
+ * drawers: { productDetails }
221
+ * })
222
+ *
223
+ * // app/server.ts
224
+ * import { createApp } from 'honox/server'
225
+ * import { beam } from './beam'
226
+ *
227
+ * const app = createApp({ init: beam.init })
228
+ * export default app
229
+ * ```
230
+ */
231
+ export function createBeam(config) {
232
+ const { actions, modals, drawers = {}, auth, session: sessionConfig } = config;
233
+ // Session defaults
234
+ const cookieName = sessionConfig?.cookieName ?? 'beam_sid';
235
+ const maxAge = sessionConfig?.maxAge ?? 365 * 24 * 60 * 60; // 1 year
236
+ return {
237
+ actions,
238
+ modals,
239
+ drawers,
240
+ auth,
241
+ /**
242
+ * Middleware that resolves auth, session, and sets beam context in Hono.
243
+ * Use this to share context between Beam and regular Hono routes.
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * const app = createApp({
248
+ * init(app) {
249
+ * app.use('*', beam.authMiddleware())
250
+ * beam.init(app)
251
+ * }
252
+ * })
253
+ *
254
+ * // In a route handler:
255
+ * app.get('/api/products', (c) => {
256
+ * const { user, session } = c.get('beam')
257
+ * const cart = await session.get('cart')
258
+ * })
259
+ * ```
260
+ */
261
+ authMiddleware() {
262
+ return async (c, next) => {
263
+ // Resolve auth if resolver provided
264
+ const user = auth ? await auth(c.req.raw, c.env) : null;
265
+ // Resolve session
266
+ let sessionId = null;
267
+ let session;
268
+ let cookieSession = null;
269
+ if (sessionConfig) {
270
+ // Resolve secret from env if secretEnvKey provided, otherwise use static secret
271
+ const secret = sessionConfig.secretEnvKey
272
+ ? c.env[sessionConfig.secretEnvKey]
273
+ : sessionConfig.secret;
274
+ if (!secret) {
275
+ throw new Error(sessionConfig.secretEnvKey
276
+ ? `Session secret not found in env.${sessionConfig.secretEnvKey}`
277
+ : 'Session secret is required');
278
+ }
279
+ // Get or create session ID
280
+ const cookieValue = await getSignedCookie(c, secret, cookieName);
281
+ sessionId = typeof cookieValue === 'string' ? cookieValue : null;
282
+ if (!sessionId) {
283
+ sessionId = crypto.randomUUID();
284
+ await setSignedCookie(c, cookieName, sessionId, secret, {
285
+ maxAge,
286
+ httpOnly: true,
287
+ sameSite: 'Lax',
288
+ path: '/',
289
+ });
290
+ }
291
+ // Use custom storage factory if provided, otherwise use cookie storage
292
+ if (sessionConfig.storageFactory) {
293
+ session = sessionConfig.storageFactory(sessionId, c.env);
294
+ }
295
+ else {
296
+ // Default: cookie-based session storage
297
+ const existingDataCookie = await getSignedCookie(c, secret, SESSION_DATA_COOKIE);
298
+ let existingData = {};
299
+ if (typeof existingDataCookie === 'string') {
300
+ try {
301
+ existingData = JSON.parse(existingDataCookie);
302
+ }
303
+ catch {
304
+ existingData = {};
305
+ }
306
+ }
307
+ cookieSession = new CookieSession(existingData);
308
+ session = cookieSession;
309
+ }
310
+ }
311
+ else {
312
+ // No session config - provide a noop session
313
+ session = {
314
+ get: async () => null,
315
+ set: async () => { },
316
+ delete: async () => { },
317
+ };
318
+ }
319
+ // Create context with script() and render() helpers
320
+ const ctx = createBeamContext({
321
+ env: c.env,
322
+ user,
323
+ request: c.req.raw,
324
+ session,
325
+ });
326
+ // Set in Hono context for use by routes
327
+ c.set('beam', ctx);
328
+ await next();
329
+ // If using cookie session and data was modified, save it back to cookie
330
+ if (cookieSession && cookieSession.isDirty() && sessionConfig) {
331
+ const secret = sessionConfig.secretEnvKey
332
+ ? c.env[sessionConfig.secretEnvKey]
333
+ : sessionConfig.secret;
334
+ const dataString = JSON.stringify(cookieSession.getData());
335
+ await setSignedCookie(c, SESSION_DATA_COOKIE, dataString, secret, {
336
+ maxAge,
337
+ httpOnly: true,
338
+ sameSite: 'Lax',
339
+ path: '/',
340
+ });
341
+ }
342
+ };
343
+ },
344
+ /**
345
+ * Init function for HonoX createApp().
346
+ * Registers the WebSocket RPC endpoint using capnweb.
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * const app = createApp({
351
+ * init(app) {
352
+ * beam.init(app) // defaults to /beam
353
+ * beam.init(app, { endpoint: '/rpc' }) // custom endpoint
354
+ * }
355
+ * })
356
+ * ```
357
+ */
358
+ init(app, options) {
359
+ const endpoint = options?.endpoint ?? '/beam';
360
+ app.get(endpoint, async (c) => {
361
+ const upgradeHeader = c.req.header('Upgrade');
362
+ if (upgradeHeader !== 'websocket') {
363
+ return c.text('Expected WebSocket', 426);
364
+ }
365
+ // Try to get context from middleware, otherwise resolve fresh
366
+ let ctx;
367
+ const existingCtx = c.var.beam;
368
+ if (existingCtx) {
369
+ ctx = existingCtx;
370
+ }
371
+ else {
372
+ // Resolve auth
373
+ const user = auth ? await auth(c.req.raw, c.env) : null;
374
+ // Resolve session for WebSocket (cookie storage is read-only in WebSocket context)
375
+ let session;
376
+ if (sessionConfig) {
377
+ const sessionId = parseSessionFromRequest(c.req.raw, cookieName);
378
+ if (sessionId) {
379
+ // Use custom storage factory if provided
380
+ if (sessionConfig.storageFactory) {
381
+ session = sessionConfig.storageFactory(sessionId, c.env);
382
+ }
383
+ else {
384
+ // Default: cookie-based session (read-only - can't set cookies in WebSocket)
385
+ const existingData = parseSessionDataFromRequest(c.req.raw);
386
+ session = new CookieSession(existingData);
387
+ }
388
+ }
389
+ else {
390
+ // No session cookie - provide noop (rare edge case)
391
+ session = {
392
+ get: async () => null,
393
+ set: async () => { },
394
+ delete: async () => { },
395
+ };
396
+ }
397
+ }
398
+ else {
399
+ session = {
400
+ get: async () => null,
401
+ set: async () => { },
402
+ delete: async () => { },
403
+ };
404
+ }
405
+ ctx = createBeamContext({
406
+ env: c.env,
407
+ user,
408
+ request: c.req.raw,
409
+ session,
410
+ });
411
+ }
412
+ // Create BeamServer instance with capnweb RpcTarget
413
+ const server = new BeamServer(ctx, actions, modals, drawers);
414
+ // Use capnweb to handle the RPC connection
415
+ return newWorkersRpcResponse(c.req.raw, server);
416
+ });
417
+ },
418
+ };
419
+ }
420
+ // Export BeamServer for advanced usage (e.g., extending with custom methods)
421
+ export { BeamServer };