@velajs/cloudflare 1.5.0 → 1.8.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +8 -8
  3. package/dist/cloudflare-application.d.ts +25 -5
  4. package/dist/cloudflare-application.js +79 -35
  5. package/dist/cloudflare-factory.d.ts +15 -1
  6. package/dist/cloudflare-factory.js +45 -24
  7. package/dist/decorators/queue-consumer.js +9 -1
  8. package/dist/decorators/scheduled.js +8 -1
  9. package/dist/index.d.ts +8 -1
  10. package/dist/index.js +8 -1
  11. package/dist/modules/create-binding-module.js +19 -22
  12. package/dist/services/kv-cache.store.d.ts +19 -0
  13. package/dist/services/kv-cache.store.js +58 -0
  14. package/dist/storage/index.d.ts +7 -0
  15. package/dist/storage/index.js +6 -0
  16. package/dist/storage/r2-storage.driver.d.ts +19 -0
  17. package/dist/storage/r2-storage.driver.js +61 -0
  18. package/dist/storage/storage-manager.service.d.ts +16 -0
  19. package/dist/storage/storage-manager.service.js +56 -0
  20. package/dist/storage/storage.controller.d.ts +16 -0
  21. package/dist/storage/storage.controller.js +88 -0
  22. package/dist/storage/storage.module.d.ts +7 -0
  23. package/dist/storage/storage.module.js +37 -0
  24. package/dist/storage/storage.service.d.ts +20 -0
  25. package/dist/storage/storage.service.js +78 -0
  26. package/dist/storage/storage.tokens.d.ts +3 -0
  27. package/dist/storage/storage.tokens.js +2 -0
  28. package/dist/storage/storage.types.d.ts +17 -0
  29. package/dist/storage/storage.types.js +1 -0
  30. package/dist/websocket/broadcast.d.ts +15 -0
  31. package/dist/websocket/broadcast.js +26 -0
  32. package/dist/websocket/cf-room-registry.d.ts +23 -0
  33. package/dist/websocket/cf-room-registry.js +65 -0
  34. package/dist/websocket/cf-ws-client.d.ts +28 -0
  35. package/dist/websocket/cf-ws-client.js +83 -0
  36. package/dist/websocket/cloudflare-websocket.module.d.ts +11 -0
  37. package/dist/websocket/cloudflare-websocket.module.js +27 -0
  38. package/dist/websocket/do-bootstrap.d.ts +21 -0
  39. package/dist/websocket/do-bootstrap.js +50 -0
  40. package/dist/websocket/do-state.d.ts +25 -0
  41. package/dist/websocket/do-state.js +4 -0
  42. package/dist/websocket/do-websocket-host.d.ts +28 -0
  43. package/dist/websocket/do-websocket-host.js +69 -0
  44. package/dist/websocket/index.d.ts +13 -0
  45. package/dist/websocket/index.js +13 -0
  46. package/dist/websocket/room-id.d.ts +6 -0
  47. package/dist/websocket/room-id.js +12 -0
  48. package/dist/websocket/websocket-routing.d.ts +14 -0
  49. package/dist/websocket/websocket-routing.js +45 -0
  50. package/dist/websocket/websocket.durable-object.d.ts +15 -0
  51. package/dist/websocket/websocket.durable-object.js +72 -0
  52. package/dist/websocket/ws-server-holder.d.ts +16 -0
  53. package/dist/websocket/ws-server-holder.js +34 -0
  54. package/package.json +8 -17
@@ -0,0 +1,19 @@
1
+ import { type DownloadResult, type PresignedUrlResult, type PresignMethod, type StorageBody, type StorageDriver, type UploadOptions, type UploadResult } from '@velajs/vela/storage';
2
+ /** Base path of the StorageController presign-proxy route. */
3
+ export declare const STORAGE_ROUTE_BASE = "/storage";
4
+ export interface R2StorageDriverConfig {
5
+ disk: string;
6
+ bucket: R2Bucket;
7
+ /** HMAC secret for presigned URLs (typically env.APP_SECRET). */
8
+ secret?: string;
9
+ }
10
+ /** {@link StorageDriver} over a Cloudflare R2 bucket. */
11
+ export declare class R2StorageDriver implements StorageDriver {
12
+ private readonly config;
13
+ constructor(config: R2StorageDriverConfig);
14
+ upload(body: StorageBody, path: string, options: UploadOptions): Promise<UploadResult>;
15
+ download(path: string): Promise<DownloadResult>;
16
+ delete(path: string): Promise<void>;
17
+ exists(path: string): Promise<boolean>;
18
+ getPresignedUrl(path: string, method: PresignMethod, expiresIn: number): Promise<PresignedUrlResult>;
19
+ }
@@ -0,0 +1,61 @@
1
+ import { signUrl } from "@velajs/vela/storage";
2
+ /** Base path of the StorageController presign-proxy route. */ export const STORAGE_ROUTE_BASE = '/storage';
3
+ /** {@link StorageDriver} over a Cloudflare R2 bucket. */ export class R2StorageDriver {
4
+ config;
5
+ constructor(config){
6
+ this.config = config;
7
+ }
8
+ async upload(body, path, options) {
9
+ await this.config.bucket.put(path, body, {
10
+ httpMetadata: options.mimeType ? {
11
+ contentType: options.mimeType
12
+ } : undefined,
13
+ customMetadata: options.metadata
14
+ });
15
+ return {
16
+ path,
17
+ disk: this.config.disk,
18
+ size: options.size,
19
+ mimeType: options.mimeType ?? 'application/octet-stream',
20
+ uploadedAt: new Date()
21
+ };
22
+ }
23
+ async download(path) {
24
+ const obj = await this.config.bucket.get(path);
25
+ if (!obj) throw new Error(`Storage object not found at "${path}".`);
26
+ return {
27
+ toStream: ()=>obj.body,
28
+ toArrayBuffer: ()=>obj.arrayBuffer(),
29
+ toText: ()=>obj.text(),
30
+ contentType: obj.httpMetadata?.contentType ?? 'application/octet-stream',
31
+ size: obj.size,
32
+ metadata: obj.customMetadata
33
+ };
34
+ }
35
+ async delete(path) {
36
+ await this.config.bucket.delete(path);
37
+ }
38
+ async exists(path) {
39
+ return await this.config.bucket.head(path) !== null;
40
+ }
41
+ async getPresignedUrl(path, method, expiresIn) {
42
+ if (!this.config.secret) {
43
+ throw new Error('A signing secret is required for presigned URLs (set APP_SECRET).');
44
+ }
45
+ // Defend the direct-driver path too: a non-finite/non-positive expiry would
46
+ // make signUrl omit `expires`, yielding a never-expiring URL.
47
+ if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
48
+ throw new Error(`Invalid presigned URL expiry: ${expiresIn}s (must be a positive number).`);
49
+ }
50
+ const routePath = `${STORAGE_ROUTE_BASE}/${this.config.disk}/${path}`;
51
+ const url = await signUrl(`${routePath}?method=${method}`, this.config.secret, {
52
+ expiresIn
53
+ });
54
+ return {
55
+ url,
56
+ method,
57
+ expiresIn,
58
+ expiresAt: new Date(Date.now() + expiresIn * 1000)
59
+ };
60
+ }
61
+ }
@@ -0,0 +1,16 @@
1
+ import { EnvService } from '../services/env.service';
2
+ import { R2StorageDriver } from './r2-storage.driver';
3
+ import type { DiskConfig, StorageModuleOptions } from './storage.types';
4
+ /**
5
+ * Resolves R2 buckets by binding NAME (via {@link EnvService}) so multiple disks
6
+ * coexist without registering N R2Modules. Drivers are created per call — cheap,
7
+ * and avoids caching a per-request env value on this singleton.
8
+ */
9
+ export declare class StorageManagerService {
10
+ private readonly options;
11
+ private readonly env;
12
+ constructor(options: StorageModuleOptions, env: EnvService);
13
+ hasDisk(disk: string): boolean;
14
+ getDiskConfig(disk: string): DiskConfig;
15
+ getDriver(disk: string): R2StorageDriver;
16
+ }
@@ -0,0 +1,56 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { Inject, Injectable } from "@velajs/vela";
16
+ import { EnvService } from "../services/env.service.js";
17
+ import { R2StorageDriver } from "./r2-storage.driver.js";
18
+ import { STORAGE_OPTIONS } from "./storage.tokens.js";
19
+ export class StorageManagerService {
20
+ options;
21
+ env;
22
+ constructor(options, env){
23
+ this.options = options;
24
+ this.env = env;
25
+ }
26
+ hasDisk(disk) {
27
+ return this.options.disks.some((d)=>d.disk === disk);
28
+ }
29
+ getDiskConfig(disk) {
30
+ const config = this.options.disks.find((d)=>d.disk === disk);
31
+ if (!config) throw new Error(`Storage disk "${disk}" is not configured.`);
32
+ return config;
33
+ }
34
+ getDriver(disk) {
35
+ const config = this.getDiskConfig(disk);
36
+ const bucket = this.env.get(config.binding);
37
+ if (!bucket) {
38
+ throw new Error(`R2 binding "${config.binding}" for disk "${disk}" was not found in env.`);
39
+ }
40
+ return new R2StorageDriver({
41
+ disk,
42
+ bucket,
43
+ secret: this.env.get('APP_SECRET')
44
+ });
45
+ }
46
+ }
47
+ StorageManagerService = _ts_decorate([
48
+ Injectable(),
49
+ _ts_param(0, Inject(STORAGE_OPTIONS)),
50
+ _ts_param(1, Inject(EnvService)),
51
+ _ts_metadata("design:type", Function),
52
+ _ts_metadata("design:paramtypes", [
53
+ typeof StorageModuleOptions === "undefined" ? Object : StorageModuleOptions,
54
+ typeof EnvService === "undefined" ? Object : EnvService
55
+ ])
56
+ ], StorageManagerService);
@@ -0,0 +1,16 @@
1
+ import type { Context } from 'hono';
2
+ import { EnvService } from '../services/env.service';
3
+ import { StorageManagerService } from './storage-manager.service';
4
+ /**
5
+ * Presign-proxy: serves objects for HMAC-signed URLs produced by
6
+ * `StorageService.url()`. R2 has no native presign, so a signed URL points here;
7
+ * this route verifies the signature (+expiry) before streaming the object.
8
+ * The wildcard is the FULL object path (root already applied at sign time), so
9
+ * it is passed straight to the driver.
10
+ */
11
+ export declare class StorageController {
12
+ private readonly manager;
13
+ private readonly env;
14
+ constructor(manager: StorageManagerService, env: EnvService);
15
+ download(c: Context): Promise<Response>;
16
+ }
@@ -0,0 +1,88 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { Controller, Get, Inject, Req } from "@velajs/vela";
16
+ import { verifySignedUrl } from "@velajs/vela/storage";
17
+ import { EnvService } from "../services/env.service.js";
18
+ import { StorageManagerService } from "./storage-manager.service.js";
19
+ export class StorageController {
20
+ manager;
21
+ env;
22
+ constructor(manager, env){
23
+ this.manager = manager;
24
+ this.env = env;
25
+ }
26
+ async download(c) {
27
+ const secret = this.env.get('APP_SECRET');
28
+ if (!secret) return new Response('Storage signing is not configured', {
29
+ status: 500
30
+ });
31
+ const disk = c.req.param('disk');
32
+ if (!disk || !this.manager.hasDisk(disk)) return new Response('Unknown disk', {
33
+ status: 404
34
+ });
35
+ if (!await verifySignedUrl(c.req.url, secret)) {
36
+ return new Response('Invalid or expired URL', {
37
+ status: 403
38
+ });
39
+ }
40
+ // Enforce the signed `method` scope: this proxy only serves reads, so a URL
41
+ // scoped to PUT/DELETE/HEAD must NOT be honored as a GET (it would over-grant
42
+ // read access relative to the token's intended scope). The param is part of
43
+ // the signed payload, so it is trustworthy once the signature verifies.
44
+ const url = new URL(c.req.url);
45
+ if ((url.searchParams.get('method') ?? 'GET') !== 'GET') {
46
+ return new Response('URL is not scoped for reads', {
47
+ status: 403
48
+ });
49
+ }
50
+ // The object key is everything after `/storage/:disk/` (Hono doesn't expose
51
+ // the `*` wildcard via param()). The key already includes the disk root.
52
+ const { pathname } = url;
53
+ const prefix = `/storage/${disk}/`;
54
+ const idx = pathname.indexOf(prefix);
55
+ const fullPath = idx >= 0 ? decodeURIComponent(pathname.slice(idx + prefix.length)) : '';
56
+ try {
57
+ const result = await this.manager.getDriver(disk).download(fullPath);
58
+ return new Response(result.toStream(), {
59
+ headers: {
60
+ 'content-type': result.contentType
61
+ }
62
+ });
63
+ } catch {
64
+ return new Response('Not found', {
65
+ status: 404
66
+ });
67
+ }
68
+ }
69
+ }
70
+ _ts_decorate([
71
+ Get('/:disk/*'),
72
+ _ts_param(0, Req()),
73
+ _ts_metadata("design:type", Function),
74
+ _ts_metadata("design:paramtypes", [
75
+ typeof Context === "undefined" ? Object : Context
76
+ ]),
77
+ _ts_metadata("design:returntype", Promise)
78
+ ], StorageController.prototype, "download", null);
79
+ StorageController = _ts_decorate([
80
+ Controller('storage'),
81
+ _ts_param(0, Inject(StorageManagerService)),
82
+ _ts_param(1, Inject(EnvService)),
83
+ _ts_metadata("design:type", Function),
84
+ _ts_metadata("design:paramtypes", [
85
+ typeof StorageManagerService === "undefined" ? Object : StorageManagerService,
86
+ typeof EnvService === "undefined" ? Object : EnvService
87
+ ])
88
+ ], StorageController);
@@ -0,0 +1,7 @@
1
+ import type { StorageModuleOptions } from './storage.types';
2
+ declare const ConfigurableModuleClass: import("@velajs/vela").ConfigurableModuleClassType<StorageModuleOptions, "forRoot", "create", {
3
+ isGlobal?: boolean;
4
+ }>;
5
+ export declare class StorageModule extends ConfigurableModuleClass {
6
+ }
7
+ export {};
@@ -0,0 +1,37 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ import { ConfigurableModuleBuilder, Module } from "@velajs/vela";
8
+ import { EnvModule } from "../modules/env.module.js";
9
+ import { StorageController } from "./storage.controller.js";
10
+ import { StorageManagerService } from "./storage-manager.service.js";
11
+ import { StorageService } from "./storage.service.js";
12
+ import { STORAGE_OPTIONS } from "./storage.tokens.js";
13
+ const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
14
+ moduleName: 'Storage',
15
+ optionsInjectionToken: STORAGE_OPTIONS
16
+ }).build();
17
+ export class StorageModule extends ConfigurableModuleClass {
18
+ }
19
+ StorageModule = _ts_decorate([
20
+ Module({
21
+ imports: [
22
+ EnvModule.forRoot()
23
+ ],
24
+ providers: [
25
+ StorageManagerService,
26
+ StorageService
27
+ ],
28
+ controllers: [
29
+ StorageController
30
+ ],
31
+ exports: [
32
+ StorageService,
33
+ StorageManagerService,
34
+ STORAGE_OPTIONS
35
+ ]
36
+ })
37
+ ], StorageModule);
@@ -0,0 +1,20 @@
1
+ import { type DownloadResult, type PresignedUrlResult, type PresignMethod, type StorageBody, type UploadOptions, type UploadResult } from '@velajs/vela/storage';
2
+ import { StorageManagerService } from './storage-manager.service';
3
+ import type { StorageModuleOptions } from './storage.types';
4
+ /**
5
+ * Multi-disk storage facade. Applies each disk's (templated) root, resolves the
6
+ * driver, and validates presign expiry. Injectable anywhere via `StorageService`.
7
+ */
8
+ export declare class StorageService {
9
+ private readonly options;
10
+ private readonly manager;
11
+ constructor(options: StorageModuleOptions, manager: StorageManagerService);
12
+ put(relativePath: string, body: StorageBody, options?: UploadOptions, disk?: string): Promise<UploadResult>;
13
+ get(relativePath: string, disk?: string): Promise<DownloadResult>;
14
+ delete(relativePath: string, disk?: string): Promise<void>;
15
+ exists(relativePath: string, disk?: string): Promise<boolean>;
16
+ url(relativePath: string, method?: PresignMethod, expiresIn?: number, disk?: string): Promise<PresignedUrlResult>;
17
+ private resolveDisk;
18
+ private fullPath;
19
+ private validateExpiry;
20
+ }
@@ -0,0 +1,78 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { Inject, Injectable } from "@velajs/vela";
16
+ import { joinStoragePath } from "@velajs/vela/storage";
17
+ import { StorageManagerService } from "./storage-manager.service.js";
18
+ import { STORAGE_OPTIONS } from "./storage.tokens.js";
19
+ const DEFAULT_PRESIGN = {
20
+ defaultExpiry: 3600,
21
+ maxExpiry: 86400
22
+ };
23
+ export class StorageService {
24
+ options;
25
+ manager;
26
+ constructor(options, manager){
27
+ this.options = options;
28
+ this.manager = manager;
29
+ }
30
+ put(relativePath, body, options = {}, disk) {
31
+ const name = this.resolveDisk(disk);
32
+ return this.manager.getDriver(name).upload(body, this.fullPath(relativePath, name), options);
33
+ }
34
+ get(relativePath, disk) {
35
+ const name = this.resolveDisk(disk);
36
+ return this.manager.getDriver(name).download(this.fullPath(relativePath, name));
37
+ }
38
+ delete(relativePath, disk) {
39
+ const name = this.resolveDisk(disk);
40
+ return this.manager.getDriver(name).delete(this.fullPath(relativePath, name));
41
+ }
42
+ exists(relativePath, disk) {
43
+ const name = this.resolveDisk(disk);
44
+ return this.manager.getDriver(name).exists(this.fullPath(relativePath, name));
45
+ }
46
+ url(relativePath, method = 'GET', expiresIn, disk) {
47
+ const name = this.resolveDisk(disk);
48
+ return this.manager.getDriver(name).getPresignedUrl(this.fullPath(relativePath, name), method, this.validateExpiry(expiresIn));
49
+ }
50
+ resolveDisk(disk) {
51
+ const name = disk ?? this.options.defaultDisk;
52
+ if (!this.manager.hasDisk(name)) throw new Error(`Storage disk "${name}" is not configured.`);
53
+ return name;
54
+ }
55
+ fullPath(relativePath, disk) {
56
+ return joinStoragePath(this.manager.getDiskConfig(disk).root, relativePath);
57
+ }
58
+ validateExpiry(expiresIn) {
59
+ const cfg = this.options.presignedUrl ?? DEFAULT_PRESIGN;
60
+ const value = expiresIn ?? cfg.defaultExpiry;
61
+ // `Number.isFinite` rejects NaN — otherwise `NaN < 1 || NaN > max` is false,
62
+ // NaN slips through, signUrl omits `expires`, and the URL never expires.
63
+ if (!Number.isFinite(value) || value < 1 || value > cfg.maxExpiry) {
64
+ throw new Error(`Presigned URL expiry ${value}s is out of range (1–${cfg.maxExpiry}s).`);
65
+ }
66
+ return value;
67
+ }
68
+ }
69
+ StorageService = _ts_decorate([
70
+ Injectable(),
71
+ _ts_param(0, Inject(STORAGE_OPTIONS)),
72
+ _ts_param(1, Inject(StorageManagerService)),
73
+ _ts_metadata("design:type", Function),
74
+ _ts_metadata("design:paramtypes", [
75
+ typeof StorageModuleOptions === "undefined" ? Object : StorageModuleOptions,
76
+ typeof StorageManagerService === "undefined" ? Object : StorageManagerService
77
+ ])
78
+ ], StorageService);
@@ -0,0 +1,3 @@
1
+ import { InjectionToken } from '@velajs/vela';
2
+ import type { StorageModuleOptions } from './storage.types';
3
+ export declare const STORAGE_OPTIONS: InjectionToken<StorageModuleOptions>;
@@ -0,0 +1,2 @@
1
+ import { InjectionToken } from "@velajs/vela";
2
+ export const STORAGE_OPTIONS = new InjectionToken('STORAGE_OPTIONS');
@@ -0,0 +1,17 @@
1
+ /** A named storage disk backed by an R2 bucket binding. */
2
+ export interface DiskConfig {
3
+ disk: string;
4
+ /** R2Bucket binding name from wrangler.toml. */
5
+ binding: string;
6
+ /** Optional root prefix; supports path-template tokens ({date}/{year}/…). */
7
+ root?: string;
8
+ }
9
+ export interface PresignedUrlConfig {
10
+ defaultExpiry: number;
11
+ maxExpiry: number;
12
+ }
13
+ export interface StorageModuleOptions {
14
+ disks: DiskConfig[];
15
+ defaultDisk: string;
16
+ presignedUrl?: PresignedUrlConfig;
17
+ }
@@ -0,0 +1 @@
1
+ /** A named storage disk backed by an R2 bucket binding. */ export { };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Push to a room from a Worker HTTP handler / cron / queue consumer (server-
3
+ * initiated emit). Resolves the room's Durable Object and calls its `broadcast`
4
+ * RPC method — the same canonical room→DO mapping the upgrade route uses, so it
5
+ * always reaches the DO holding those sockets.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // In a controller — ns from DurableObjectService.namespace
10
+ * await broadcastToRoom(ns, `org:${id}`, 'order.created', order);
11
+ * ```
12
+ */
13
+ export declare function broadcastToRoom(ns: DurableObjectNamespace, room: string, event: string, data?: unknown, options?: {
14
+ exceptIds?: string[];
15
+ }): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { roomToDurableId } from "./room-id.js";
2
+ /**
3
+ * Push to a room from a Worker HTTP handler / cron / queue consumer (server-
4
+ * initiated emit). Resolves the room's Durable Object and calls its `broadcast`
5
+ * RPC method — the same canonical room→DO mapping the upgrade route uses, so it
6
+ * always reaches the DO holding those sockets.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // In a controller — ns from DurableObjectService.namespace
11
+ * await broadcastToRoom(ns, `org:${id}`, 'order.created', order);
12
+ * ```
13
+ */ export async function broadcastToRoom(ns, room, event, data, options) {
14
+ const cmd = {
15
+ rooms: [
16
+ room
17
+ ],
18
+ exceptIds: options?.exceptIds,
19
+ frame: JSON.stringify({
20
+ event,
21
+ data
22
+ })
23
+ };
24
+ const stub = ns.get(roomToDurableId(ns, room));
25
+ await stub.broadcast(cmd);
26
+ }
@@ -0,0 +1,23 @@
1
+ import type { BroadcastCommand, RoomRegistry, WsClient } from '@velajs/vela/websocket';
2
+ import type { DoStateLike, WsLike } from './do-state';
3
+ /**
4
+ * Core `RoomRegistry` backed by a Durable Object's hibernatable sockets. Room
5
+ * membership lives in tags (hub room, set at accept) + the attachment (dynamic
6
+ * `join()`), never in DO instance fields — so it survives hibernation with zero
7
+ * rehydration.
8
+ */
9
+ export declare class CfRoomRegistry implements RoomRegistry {
10
+ private readonly ctx;
11
+ constructor(ctx: DoStateLike);
12
+ register(_client: WsClient): void;
13
+ leaveAll(_client: WsClient): void;
14
+ join(client: WsClient, room: string): void | Promise<void>;
15
+ leave(client: WsClient, room: string): void | Promise<void>;
16
+ localIdsInRoom(room: string): string[];
17
+ deliverLocal(cmd: BroadcastCommand): void;
18
+ private collectRooms;
19
+ private socketsForRoom;
20
+ private attachmentOf;
21
+ /** Reconstruct a `WsClient` for a raw socket (e.g. inside gateway lifecycle scans). */
22
+ clientFor(ws: WsLike): WsClient;
23
+ }
@@ -0,0 +1,65 @@
1
+ import { CfWsClient } from "./cf-ws-client.js";
2
+ /**
3
+ * Core `RoomRegistry` backed by a Durable Object's hibernatable sockets. Room
4
+ * membership lives in tags (hub room, set at accept) + the attachment (dynamic
5
+ * `join()`), never in DO instance fields — so it survives hibernation with zero
6
+ * rehydration.
7
+ */ export class CfRoomRegistry {
8
+ ctx;
9
+ constructor(ctx){
10
+ this.ctx = ctx;
11
+ }
12
+ // CF sockets are registered with the DO by `acceptWebSocket`; nothing to track.
13
+ register(_client) {}
14
+ leaveAll(_client) {}
15
+ join(client, room) {
16
+ return client.join(room);
17
+ }
18
+ leave(client, room) {
19
+ return client.leave(room);
20
+ }
21
+ localIdsInRoom(room) {
22
+ return this.socketsForRoom(room).map((ws)=>this.attachmentOf(ws).connId);
23
+ }
24
+ deliverLocal(cmd) {
25
+ const excludeIds = new Set(cmd.exceptIds ?? []);
26
+ const excludeRooms = cmd.exceptRooms ?? [];
27
+ // Empty `rooms` => GLOBAL (every socket in this DO). Otherwise the union of
28
+ // targeted rooms, deduped per connection.
29
+ const targets = cmd.rooms.length === 0 ? this.ctx.getWebSockets() : this.collectRooms(cmd.rooms);
30
+ const seen = new Set();
31
+ for (const ws of targets){
32
+ const att = this.attachmentOf(ws);
33
+ if (seen.has(att.connId)) continue;
34
+ seen.add(att.connId);
35
+ if (excludeIds.has(att.connId)) continue;
36
+ if (excludeRooms.some((r)=>att.rooms.includes(r))) continue;
37
+ ws.send(cmd.frame);
38
+ }
39
+ }
40
+ collectRooms(rooms) {
41
+ const set = new Set();
42
+ for (const room of rooms)for (const ws of this.socketsForRoom(room))set.add(ws);
43
+ return [
44
+ ...set
45
+ ];
46
+ }
47
+ socketsForRoom(room) {
48
+ // Membership is the attachment's `rooms`, NOT the hibernation tag: tags are
49
+ // immutable after acceptWebSocket, so a socket that left its hub room still
50
+ // carries the tag. Since one DO ≈ one room, scanning all sockets in the DO
51
+ // and filtering by attachment is both correct and cheap.
52
+ return this.ctx.getWebSockets().filter((ws)=>this.attachmentOf(ws).rooms.includes(room));
53
+ }
54
+ attachmentOf(ws) {
55
+ return ws.deserializeAttachment() ?? {
56
+ connId: '',
57
+ path: '',
58
+ rooms: [],
59
+ data: {}
60
+ };
61
+ }
62
+ /** Reconstruct a `WsClient` for a raw socket (e.g. inside gateway lifecycle scans). */ clientFor(ws) {
63
+ return new CfWsClient(this.ctx, ws);
64
+ }
65
+ }
@@ -0,0 +1,28 @@
1
+ import type { WsClient } from '@velajs/vela/websocket';
2
+ import type { DoStateLike, WsLike } from './do-state';
3
+ /**
4
+ * Core `WsClient` over a native Cloudflare `WebSocket` inside a Durable Object.
5
+ * Per-connection state lives in the hibernation attachment (survives eviction),
6
+ * so a fresh `CfWsClient` is reconstructed per message with no in-memory state.
7
+ */
8
+ export declare class CfWsClient<TData extends Record<string, unknown> = Record<string, unknown>> implements WsClient<TData> {
9
+ private readonly ctx;
10
+ private readonly ws;
11
+ private readonly attachment;
12
+ constructor(ctx: DoStateLike, ws: WsLike);
13
+ get id(): string;
14
+ /** The gateway route path this socket connected on (used to route messages). */
15
+ get path(): string;
16
+ get rooms(): ReadonlySet<string>;
17
+ get data(): TData;
18
+ set data(value: TData);
19
+ get raw(): unknown;
20
+ send(event: string, data?: unknown, id?: string): void;
21
+ sendRaw(payload: string): void;
22
+ join(room: string): void;
23
+ leave(room: string): void;
24
+ /** Persist `data`/room mutations to the hibernation attachment. */
25
+ commit(): void;
26
+ close(code?: number, reason?: string): void;
27
+ private persist;
28
+ }