@velajs/cloudflare 1.29.0 → 1.30.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 (89) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -1
  3. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/cloudflare-application.js +309 -0
  7. package/dist/cloudflare-application.js.map +1 -0
  8. package/dist/cloudflare-factory.js +109 -0
  9. package/dist/cloudflare-factory.js.map +1 -0
  10. package/dist/decorators/queue-consumer.js +39 -0
  11. package/dist/decorators/queue-consumer.js.map +1 -0
  12. package/dist/durable-objects.d.ts +3 -4
  13. package/dist/durable-objects.js +2 -524
  14. package/dist/environment.js +19 -0
  15. package/dist/environment.js.map +1 -0
  16. package/dist/index.d.ts +11 -9
  17. package/dist/index.js +21 -1276
  18. package/dist/nonce/durable-object-nonce.store.js +33 -0
  19. package/dist/nonce/durable-object-nonce.store.js.map +1 -0
  20. package/dist/nonce/nonce-validation.js +18 -0
  21. package/dist/nonce/nonce-validation.js.map +1 -0
  22. package/dist/nonce/nonce.durable-object.js +54 -0
  23. package/dist/nonce/nonce.durable-object.js.map +1 -0
  24. package/dist/{nonce.durable-object-Df4CZi-0.d.ts → nonce.durable-object-Ci9AGssY.d.ts} +2 -3
  25. package/dist/queue/cloudflare-queues.js +215 -0
  26. package/dist/queue/cloudflare-queues.js.map +1 -0
  27. package/dist/queues.d.ts +1 -2
  28. package/dist/queues.js +1 -214
  29. package/dist/rate-limit/cloudflare-rate-limit.store.js +40 -0
  30. package/dist/rate-limit/cloudflare-rate-limit.store.js.map +1 -0
  31. package/dist/schedule-diagnostics.js +51 -0
  32. package/dist/schedule-diagnostics.js.map +1 -0
  33. package/dist/scheduled-event.js +68 -0
  34. package/dist/scheduled-event.js.map +1 -0
  35. package/dist/services/flagship-flag.driver.js +52 -0
  36. package/dist/services/flagship-flag.driver.js.map +1 -0
  37. package/dist/services/kv-cache.store.js +82 -0
  38. package/dist/services/kv-cache.store.js.map +1 -0
  39. package/dist/services/kv-flag.driver.js +62 -0
  40. package/dist/services/kv-flag.driver.js.map +1 -0
  41. package/dist/storage/r2-storage.driver.js +67 -0
  42. package/dist/storage/r2-storage.driver.js.map +1 -0
  43. package/dist/storage/storage-key-claim.js +74 -0
  44. package/dist/storage/storage-key-claim.js.map +1 -0
  45. package/dist/storage/storage-manager.service.js +38 -0
  46. package/dist/storage/storage-manager.service.js.map +1 -0
  47. package/dist/storage/storage.controller.js +64 -0
  48. package/dist/storage/storage.controller.js.map +1 -0
  49. package/dist/storage/storage.module.js +25 -0
  50. package/dist/storage/storage.module.js.map +1 -0
  51. package/dist/storage/storage.service.js +64 -0
  52. package/dist/storage/storage.service.js.map +1 -0
  53. package/dist/storage/storage.tokens.js +7 -0
  54. package/dist/storage/storage.tokens.js.map +1 -0
  55. package/dist/vela-env-DFvyoNT3.d.ts +1 -2
  56. package/dist/websocket/broadcast.js +32 -0
  57. package/dist/websocket/broadcast.js.map +1 -0
  58. package/dist/websocket/cf-room-registry.js +137 -0
  59. package/dist/websocket/cf-room-registry.js.map +1 -0
  60. package/dist/websocket/cf-ws-client.js +94 -0
  61. package/dist/websocket/cf-ws-client.js.map +1 -0
  62. package/dist/websocket/cloudflare-websocket.module.js +25 -0
  63. package/dist/websocket/cloudflare-websocket.module.js.map +1 -0
  64. package/dist/websocket/do-bootstrap.js +56 -0
  65. package/dist/websocket/do-bootstrap.js.map +1 -0
  66. package/dist/websocket/do-live.js +184 -0
  67. package/dist/websocket/do-live.js.map +1 -0
  68. package/dist/websocket/do-pitr.js +81 -0
  69. package/dist/websocket/do-pitr.js.map +1 -0
  70. package/dist/websocket/do-state.js +7 -0
  71. package/dist/websocket/do-state.js.map +1 -0
  72. package/dist/websocket/do-websocket-host.js +169 -0
  73. package/dist/websocket/do-websocket-host.js.map +1 -0
  74. package/dist/websocket/room-id.js +27 -0
  75. package/dist/websocket/room-id.js.map +1 -0
  76. package/dist/websocket/websocket-routing.js +133 -0
  77. package/dist/websocket/websocket-routing.js.map +1 -0
  78. package/dist/websocket/websocket.durable-object.js +138 -0
  79. package/dist/websocket/websocket.durable-object.js.map +1 -0
  80. package/dist/websocket/ws-attachment.js +79 -0
  81. package/dist/websocket/ws-attachment.js.map +1 -0
  82. package/dist/websocket/ws-server-holder.js +38 -0
  83. package/dist/websocket/ws-server-holder.js.map +1 -0
  84. package/package.json +9 -7
  85. package/dist/durable-objects.js.map +0 -1
  86. package/dist/index.js.map +0 -1
  87. package/dist/nonce-validation-Dy8z05A9.js +0 -522
  88. package/dist/nonce-validation-Dy8z05A9.js.map +0 -1
  89. package/dist/queues.js.map +0 -1
@@ -0,0 +1,33 @@
1
+ import { MAX_NONCE_BYTES, isCanonicalBoundedText, isValidExpiry } from "./nonce-validation.js";
2
+ //#region src/nonce/durable-object-nonce.store.ts
3
+ const APP_NAMESPACE_PREFIX = "vela:nonce:v1:";
4
+ const MAX_APP_NAMESPACE_BYTES = 128;
5
+ /**
6
+ * Strict, cross-isolate {@link NonceStore} backed by one SQLite Durable Object
7
+ * per explicit application namespace.
8
+ *
9
+ * Invalid input, an unavailable/malformed binding, RPC failure, or a malformed
10
+ * RPC result all deny the claim (`false`). Only the literal boolean `true` from
11
+ * the Durable Object is accepted.
12
+ */
13
+ function durableObjectNonceStore(options) {
14
+ if (!options || typeof options !== "object") throw new TypeError("Durable Object nonce-store options are required");
15
+ if (!isCanonicalBoundedText(options.appNamespace, MAX_APP_NAMESPACE_BYTES)) throw new TypeError(`appNamespace must be canonical, non-empty, and at most ${MAX_APP_NAMESPACE_BYTES} UTF-8 bytes`);
16
+ if (typeof options.binding !== "function") throw new TypeError("A lazy Durable Object namespace binding resolver is required");
17
+ const objectName = `${APP_NAMESPACE_PREFIX}${options.appNamespace}`;
18
+ return { async claim(nonce, expEpochSeconds) {
19
+ const now = Math.floor(Date.now() / 1e3);
20
+ if (!isCanonicalBoundedText(nonce, MAX_NONCE_BYTES) || !isValidExpiry(expEpochSeconds, now)) return false;
21
+ try {
22
+ const namespace = await options.binding();
23
+ const id = namespace.idFromName(objectName);
24
+ return await namespace.get(id).claim(nonce, expEpochSeconds) === true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ } };
29
+ }
30
+ //#endregion
31
+ export { durableObjectNonceStore };
32
+
33
+ //# sourceMappingURL=durable-object-nonce.store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-object-nonce.store.js","names":[],"sources":["../../src/nonce/durable-object-nonce.store.ts"],"sourcesContent":["import type { VelaNonceDurableObject } from './nonce.durable-object';\nimport { MAX_NONCE_BYTES, isCanonicalBoundedText, isValidExpiry } from './nonce-validation';\nimport type { NonceStore } from '@velajs/vela/security';\n\nconst APP_NAMESPACE_PREFIX = 'vela:nonce:v1:';\nconst MAX_APP_NAMESPACE_BYTES = 128;\n/** The generated Workers binding type for {@link VelaNonceDurableObject}. */\nexport type DurableObjectNonceNamespace = DurableObjectNamespace<VelaNonceDurableObject>;\n\nexport interface DurableObjectNonceStoreOptions {\n /**\n * Stable application/environment boundary (for example `billing-api:prod`).\n * Claims are globally single-use inside this namespace and isolated from all\n * other application namespaces. It must be non-empty, canonical, and at most\n * 128 UTF-8 bytes.\n */\n appNamespace: string;\n\n /**\n * Resolve the Workers Durable Object namespace at claim time. The resolver is\n * intentionally not cached so request-scoped env/binding references stay safe.\n */\n binding: () => DurableObjectNonceNamespace | Promise<DurableObjectNonceNamespace>;\n}\n\n/**\n * Strict, cross-isolate {@link NonceStore} backed by one SQLite Durable Object\n * per explicit application namespace.\n *\n * Invalid input, an unavailable/malformed binding, RPC failure, or a malformed\n * RPC result all deny the claim (`false`). Only the literal boolean `true` from\n * the Durable Object is accepted.\n */\nexport function durableObjectNonceStore(options: DurableObjectNonceStoreOptions): NonceStore {\n if (!options || typeof options !== 'object') {\n throw new TypeError('Durable Object nonce-store options are required');\n }\n if (!isCanonicalBoundedText(options.appNamespace, MAX_APP_NAMESPACE_BYTES)) {\n throw new TypeError(\n `appNamespace must be canonical, non-empty, and at most ${MAX_APP_NAMESPACE_BYTES} UTF-8 bytes`,\n );\n }\n if (typeof options.binding !== 'function') {\n throw new TypeError('A lazy Durable Object namespace binding resolver is required');\n }\n\n const objectName = `${APP_NAMESPACE_PREFIX}${options.appNamespace}`;\n\n return {\n async claim(nonce: string, expEpochSeconds: number): Promise<boolean> {\n const now = Math.floor(Date.now() / 1_000);\n if (!isCanonicalBoundedText(nonce, MAX_NONCE_BYTES) || !isValidExpiry(expEpochSeconds, now)) {\n return false;\n }\n\n try {\n const namespace = await options.binding();\n\n const id = namespace.idFromName(objectName);\n const stub = namespace.get(id);\n const result = await stub.claim(nonce, expEpochSeconds);\n return result === true;\n } catch {\n return false;\n }\n },\n };\n}\n"],"mappings":";;AAIA,MAAM,uBAAuB;AAC7B,MAAM,0BAA0B;;;;;;;;;AA4BhC,SAAgB,wBAAwB,SAAqD;CAC3F,IAAI,CAAC,WAAW,OAAO,YAAY,UACjC,MAAM,IAAI,UAAU,iDAAiD;CAEvE,IAAI,CAAC,uBAAuB,QAAQ,cAAc,uBAAuB,GACvE,MAAM,IAAI,UACR,0DAA0D,wBAAwB,aACpF;CAEF,IAAI,OAAO,QAAQ,YAAY,YAC7B,MAAM,IAAI,UAAU,8DAA8D;CAGpF,MAAM,aAAa,GAAG,uBAAuB,QAAQ;CAErD,OAAO,EACL,MAAM,MAAM,OAAe,iBAA2C;EACpE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAK;EACzC,IAAI,CAAC,uBAAuB,OAAO,eAAe,KAAK,CAAC,cAAc,iBAAiB,GAAG,GACxF,OAAO;EAGT,IAAI;GACF,MAAM,YAAY,MAAM,QAAQ,QAAQ;GAExC,MAAM,KAAK,UAAU,WAAW,UAAU;GAG1C,OAAO,MAFM,UAAU,IAAI,EACH,CAAC,CAAC,MAAM,OAAO,eAAe,MACpC;EACpB,QAAQ;GACN,OAAO;EACT;CACF,EACF;AACF"}
@@ -0,0 +1,18 @@
1
+ //#region src/nonce/nonce-validation.ts
2
+ const encoder = new TextEncoder();
3
+ const MAX_NONCE_BYTES = 512;
4
+ function isCanonicalBoundedText(value, maxBytes) {
5
+ if (typeof value !== "string") return false;
6
+ for (const character of value) {
7
+ const codePoint = character.codePointAt(0);
8
+ if (codePoint !== void 0 && (codePoint <= 31 || codePoint === 127 || codePoint >= 55296 && codePoint <= 57343)) return false;
9
+ }
10
+ return value.length > 0 && value === value.trim() && encoder.encode(value).byteLength <= maxBytes;
11
+ }
12
+ function isValidExpiry(expEpochSeconds, nowEpochSeconds) {
13
+ return typeof expEpochSeconds === "number" && Number.isSafeInteger(expEpochSeconds) && expEpochSeconds > 0 && expEpochSeconds >= nowEpochSeconds;
14
+ }
15
+ //#endregion
16
+ export { MAX_NONCE_BYTES, isCanonicalBoundedText, isValidExpiry };
17
+
18
+ //# sourceMappingURL=nonce-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nonce-validation.js","names":[],"sources":["../../src/nonce/nonce-validation.ts"],"sourcesContent":["const encoder = new TextEncoder();\nexport const MAX_NONCE_BYTES = 512;\nexport function isCanonicalBoundedText(value: unknown, maxBytes: number): value is string {\n if (typeof value !== 'string') return false;\n for (const character of value) {\n const codePoint = character.codePointAt(0);\n if (\n codePoint !== undefined &&\n (codePoint <= 0x1f || codePoint === 0x7f || (codePoint >= 0xd800 && codePoint <= 0xdfff))\n ) {\n return false;\n }\n }\n return value.length > 0 && value === value.trim() && encoder.encode(value).byteLength <= maxBytes;\n}\n\nexport function isValidExpiry(\n expEpochSeconds: unknown,\n nowEpochSeconds: number,\n): expEpochSeconds is number {\n return (\n typeof expEpochSeconds === 'number' &&\n Number.isSafeInteger(expEpochSeconds) &&\n expEpochSeconds > 0 &&\n expEpochSeconds >= nowEpochSeconds\n );\n}\n"],"mappings":";AAAA,MAAM,UAAU,IAAI,YAAY;AAChC,MAAa,kBAAkB;AAC/B,SAAgB,uBAAuB,OAAgB,UAAmC;CACxF,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,YAAY,UAAU,YAAY,CAAC;EACzC,IACE,cAAc,KAAA,MACb,aAAa,MAAQ,cAAc,OAAS,aAAa,SAAU,aAAa,QAEjF,OAAO;CAEX;CACA,OAAO,MAAM,SAAS,KAAK,UAAU,MAAM,KAAK,KAAK,QAAQ,OAAO,KAAK,CAAC,CAAC,cAAc;AAC3F;AAEA,SAAgB,cACd,iBACA,iBAC2B;CAC3B,OACE,OAAO,oBAAoB,YAC3B,OAAO,cAAc,eAAe,KACpC,kBAAkB,KAClB,mBAAmB;AAEvB"}
@@ -0,0 +1,54 @@
1
+ import { MAX_NONCE_BYTES, isCanonicalBoundedText, isValidExpiry } from "./nonce-validation.js";
2
+ import { DurableObject } from "cloudflare:workers";
3
+ //#region src/nonce/nonce.durable-object.ts
4
+ const EXPIRED_DELETE_BATCH = 1024;
5
+ const CREATE_TABLE = "CREATE TABLE IF NOT EXISTS __vela_nonce_claims (nonce TEXT PRIMARY KEY NOT NULL, expires_at INTEGER NOT NULL CHECK (expires_at > 0)) WITHOUT ROWID";
6
+ const CREATE_EXPIRY_INDEX = "CREATE INDEX IF NOT EXISTS __vela_nonce_claims_expiry ON __vela_nonce_claims (expires_at)";
7
+ const DELETE_EXPIRED = "DELETE FROM __vela_nonce_claims WHERE nonce IN (SELECT nonce FROM __vela_nonce_claims WHERE expires_at < ? ORDER BY expires_at LIMIT ?)";
8
+ const INSERT_CLAIM = "INSERT INTO __vela_nonce_claims (nonce, expires_at) VALUES (?, ?) ON CONFLICT(nonce) DO NOTHING RETURNING nonce, expires_at";
9
+ /**
10
+ * SQLite Durable Object that atomically consumes nonces.
11
+ *
12
+ * Export this class from the Worker entry and register it through a Wrangler
13
+ * `new_sqlite_classes` migration. `INSERT ... ON CONFLICT DO NOTHING RETURNING`
14
+ * is the single-use decision; all SQL runs synchronously before the RPC method
15
+ * yields, and the nonce primary key is the final concurrency boundary.
16
+ */
17
+ var VelaNonceDurableObject = class extends DurableObject {
18
+ sql;
19
+ constructor(ctx, env) {
20
+ super(ctx, env);
21
+ try {
22
+ const sql = ctx.storage?.sql;
23
+ if (!sql || typeof sql.exec !== "function") return;
24
+ sql.exec(CREATE_TABLE);
25
+ sql.exec(CREATE_EXPIRY_INDEX);
26
+ this.sql = sql;
27
+ } catch {
28
+ this.sql = void 0;
29
+ }
30
+ }
31
+ async claim(nonce, expEpochSeconds) {
32
+ const sql = this.sql;
33
+ if (!sql) return false;
34
+ const now = Math.floor(Date.now() / 1e3);
35
+ if (!isCanonicalBoundedText(nonce, MAX_NONCE_BYTES) || !isValidExpiry(expEpochSeconds, now)) return false;
36
+ try {
37
+ sql.exec(DELETE_EXPIRED, now, EXPIRED_DELETE_BATCH);
38
+ const cursor = sql.exec(INSERT_CLAIM, nonce, expEpochSeconds);
39
+ if (!cursor || typeof cursor.toArray !== "function") return false;
40
+ const rows = cursor.toArray();
41
+ if (!Array.isArray(rows) || rows.length !== 1) return false;
42
+ const row = rows[0];
43
+ if (typeof row !== "object" || row === null) return false;
44
+ if (!Object.hasOwn(row, "nonce") || !Object.hasOwn(row, "expires_at") || !("nonce" in row) || !("expires_at" in row)) return false;
45
+ return row.nonce === nonce && row.expires_at === expEpochSeconds;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+ };
51
+ //#endregion
52
+ export { VelaNonceDurableObject };
53
+
54
+ //# sourceMappingURL=nonce.durable-object.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nonce.durable-object.js","names":[],"sources":["../../src/nonce/nonce.durable-object.ts"],"sourcesContent":["import { DurableObject } from 'cloudflare:workers';\nimport { MAX_NONCE_BYTES, isCanonicalBoundedText, isValidExpiry } from './nonce-validation';\nconst EXPIRED_DELETE_BATCH = 1_024;\n\nconst CREATE_TABLE =\n 'CREATE TABLE IF NOT EXISTS __vela_nonce_claims (' +\n 'nonce TEXT PRIMARY KEY NOT NULL, ' +\n 'expires_at INTEGER NOT NULL CHECK (expires_at > 0)' +\n ') WITHOUT ROWID';\nconst CREATE_EXPIRY_INDEX =\n 'CREATE INDEX IF NOT EXISTS __vela_nonce_claims_expiry ON __vela_nonce_claims (expires_at)';\nconst DELETE_EXPIRED =\n 'DELETE FROM __vela_nonce_claims WHERE nonce IN (' +\n 'SELECT nonce FROM __vela_nonce_claims ' +\n 'WHERE expires_at < ? ORDER BY expires_at LIMIT ?' +\n ')';\nconst INSERT_CLAIM =\n 'INSERT INTO __vela_nonce_claims (nonce, expires_at) VALUES (?, ?) ' +\n 'ON CONFLICT(nonce) DO NOTHING RETURNING nonce, expires_at';\n\ninterface NonceSqlCursor {\n toArray(): Record<string, unknown>[];\n}\n\ninterface NonceSqlStorage {\n exec(query: string, ...bindings: unknown[]): NonceSqlCursor;\n}\n\n/**\n * SQLite Durable Object that atomically consumes nonces.\n *\n * Export this class from the Worker entry and register it through a Wrangler\n * `new_sqlite_classes` migration. `INSERT ... ON CONFLICT DO NOTHING RETURNING`\n * is the single-use decision; all SQL runs synchronously before the RPC method\n * yields, and the nonce primary key is the final concurrency boundary.\n */\nexport class VelaNonceDurableObject extends DurableObject<Record<string, unknown>> {\n private sql?: NonceSqlStorage;\n\n constructor(ctx: DurableObjectState, env: Record<string, unknown>) {\n super(ctx, env);\n try {\n const sql = ctx.storage?.sql;\n if (!sql || typeof sql.exec !== 'function') return;\n sql.exec(CREATE_TABLE);\n sql.exec(CREATE_EXPIRY_INDEX);\n this.sql = sql;\n } catch {\n // A non-SQLite/misconfigured class stays fail-closed: every claim denies.\n this.sql = undefined;\n }\n }\n\n async claim(nonce: string, expEpochSeconds: number): Promise<boolean> {\n const sql = this.sql;\n if (!sql) return false;\n\n const now = Math.floor(Date.now() / 1_000);\n if (!isCanonicalBoundedText(nonce, MAX_NONCE_BYTES) || !isValidExpiry(expEpochSeconds, now)) {\n return false;\n }\n\n try {\n // Keep cleanup bounded so one claim cannot monopolize the DO. Entries at\n // exactly `now` remain: Vela's invocation verifier accepts `now === exp`.\n sql.exec(DELETE_EXPIRED, now, EXPIRED_DELETE_BATCH);\n\n const cursor = sql.exec(INSERT_CLAIM, nonce, expEpochSeconds);\n if (!cursor || typeof cursor.toArray !== 'function') return false;\n const rows: unknown = cursor.toArray();\n if (!Array.isArray(rows) || rows.length !== 1) return false;\n\n const row: unknown = rows[0];\n if (typeof row !== 'object' || row === null) return false;\n if (\n !Object.hasOwn(row, 'nonce') ||\n !Object.hasOwn(row, 'expires_at') ||\n !('nonce' in row) ||\n !('expires_at' in row)\n ) {\n return false;\n }\n return row.nonce === nonce && row.expires_at === expEpochSeconds;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":";;;AAEA,MAAM,uBAAuB;AAE7B,MAAM,eACJ;AAIF,MAAM,sBACJ;AACF,MAAM,iBACJ;AAIF,MAAM,eACJ;;;;;;;;;AAmBF,IAAa,yBAAb,cAA4C,cAAuC;CACjF;CAEA,YAAY,KAAyB,KAA8B;EACjE,MAAM,KAAK,GAAG;EACd,IAAI;GACF,MAAM,MAAM,IAAI,SAAS;GACzB,IAAI,CAAC,OAAO,OAAO,IAAI,SAAS,YAAY;GAC5C,IAAI,KAAK,YAAY;GACrB,IAAI,KAAK,mBAAmB;GAC5B,KAAK,MAAM;EACb,QAAQ;GAEN,KAAK,MAAM,KAAA;EACb;CACF;CAEA,MAAM,MAAM,OAAe,iBAA2C;EACpE,MAAM,MAAM,KAAK;EACjB,IAAI,CAAC,KAAK,OAAO;EAEjB,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAK;EACzC,IAAI,CAAC,uBAAuB,OAAO,eAAe,KAAK,CAAC,cAAc,iBAAiB,GAAG,GACxF,OAAO;EAGT,IAAI;GAGF,IAAI,KAAK,gBAAgB,KAAK,oBAAoB;GAElD,MAAM,SAAS,IAAI,KAAK,cAAc,OAAO,eAAe;GAC5D,IAAI,CAAC,UAAU,OAAO,OAAO,YAAY,YAAY,OAAO;GAC5D,MAAM,OAAgB,OAAO,QAAQ;GACrC,IAAI,CAAC,MAAM,QAAQ,IAAI,KAAK,KAAK,WAAW,GAAG,OAAO;GAEtD,MAAM,MAAe,KAAK;GAC1B,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM,OAAO;GACpD,IACE,CAAC,OAAO,OAAO,KAAK,OAAO,KAC3B,CAAC,OAAO,OAAO,KAAK,YAAY,KAChC,EAAE,WAAW,QACb,EAAE,gBAAgB,MAElB,OAAO;GAET,OAAO,IAAI,UAAU,SAAS,IAAI,eAAe;EACnD,QAAQ;GACN,OAAO;EACT;CACF;AACF"}
@@ -1,5 +1,5 @@
1
- import { DynamicModule, Type } from "@velajs/vela";
2
1
  import { DurableObject } from "cloudflare:workers";
2
+ import { DynamicModule, Type } from "@velajs/vela";
3
3
  //#region src/root-module.d.ts
4
4
  /**
5
5
  * The root a Worker or Durable Object is built from: a module class or a
@@ -140,5 +140,4 @@ declare class VelaNonceDurableObject extends DurableObject<Record<string, unknow
140
140
  claim(nonce: string, expEpochSeconds: number): Promise<boolean>;
141
141
  }
142
142
  //#endregion
143
- export { DoPitrId as a, DoPitrUnavailableError as c, isDoPitrUnavailable as d, readDoPitrBookmark as f, DoPitrBookmarkRead as i, VelaDoPitrRpc as l, DoPitrArmOptions as n, DoPitrNamespace as o, CloudflareRoot as p, DoPitrArmResult as r, DoPitrStorage as s, VelaNonceDurableObject as t, armDoPitr as u };
144
- //# sourceMappingURL=nonce.durable-object-Df4CZi-0.d.ts.map
143
+ export { DoPitrId as a, DoPitrUnavailableError as c, isDoPitrUnavailable as d, readDoPitrBookmark as f, DoPitrBookmarkRead as i, VelaDoPitrRpc as l, DoPitrArmOptions as n, DoPitrNamespace as o, CloudflareRoot as p, DoPitrArmResult as r, DoPitrStorage as s, VelaNonceDurableObject as t, armDoPitr as u };
@@ -0,0 +1,215 @@
1
+ import { QueueBatchError, observeMessage, parseQueueJob } from "@velajs/vela/queue";
2
+ //#region src/queue/cloudflare-queues.ts
3
+ /** The Cloudflare Queues send limits `addBulk` is chunked to (bytes are decimal kilobytes). */
4
+ const QUEUE_SEND_LIMITS = Object.freeze({
5
+ /** Messages per `sendBatch` call. */
6
+ messages: 100,
7
+ /** Estimated bytes per `sendBatch` call. */
8
+ batchBytes: 256e3,
9
+ /** Estimated bytes per message. */
10
+ messageBytes: 128e3
11
+ });
12
+ /** Allowance for the platform's per-message framing on top of the body estimate. */
13
+ const MESSAGE_MARGIN_BYTES = 256;
14
+ const MAX_DELAY_MS = 864e5;
15
+ /**
16
+ * A string's size as JSON (UTF-8, quoted, escaped) or as V8 serializes it
17
+ * (one byte per character, two for a string holding any character above
18
+ * U+00FF), whichever is larger.
19
+ */
20
+ function stringBytes(text) {
21
+ let json = 2;
22
+ let wide = false;
23
+ for (let index = 0; index < text.length; index++) {
24
+ const code = text.charCodeAt(index);
25
+ if (code > 255) wide = true;
26
+ if (code < 32) json += 6;
27
+ else if (code === 34 || code === 92) json += 2;
28
+ else if (code < 128) json += 1;
29
+ else if (code < 2048) json += 2;
30
+ else if (code >= 55296 && code <= 57343) json += 2;
31
+ else json += 3;
32
+ }
33
+ return Math.max(json, (wide ? text.length * 2 : text.length) + 5);
34
+ }
35
+ function measure(value, seen) {
36
+ switch (typeof value) {
37
+ case "string": return stringBytes(value);
38
+ case "number": return Math.max(String(value).length, 9);
39
+ case "bigint": return String(value).length + 9;
40
+ case "boolean": return 5;
41
+ case "object": break;
42
+ default: return 9;
43
+ }
44
+ if (value === null) return 4;
45
+ if (seen.has(value)) return 9;
46
+ seen.add(value);
47
+ let bytes = 2;
48
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) bytes += value.byteLength + 9;
49
+ else if (value instanceof Date) bytes += 26;
50
+ else if (value instanceof Map) for (const [key, item] of value) bytes += measure(key, seen) + measure(item, seen) + 2;
51
+ else if (value instanceof Set) for (const item of value) bytes += measure(item, seen) + 1;
52
+ else if (Array.isArray(value)) for (const item of value) bytes += measure(item, seen) + 1;
53
+ else for (const key of Object.keys(value)) bytes += stringBytes(key) + measure(Reflect.get(value, key), seen) + 2;
54
+ return bytes;
55
+ }
56
+ /**
57
+ * A conservative size estimate of one native queue message: the larger of the
58
+ * body's JSON and V8 sizes, plus a per-message margin.
59
+ */
60
+ function estimateQueueMessageBytes(body) {
61
+ return measure(body, /* @__PURE__ */ new Set()) + MESSAGE_MARGIN_BYTES;
62
+ }
63
+ function delaySeconds(options) {
64
+ const delayMs = options?.delayMs;
65
+ if (delayMs === void 0) return void 0;
66
+ if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > MAX_DELAY_MS) throw new RangeError("Queue delayMs must be between 0 and 86400000.");
67
+ return Math.ceil(delayMs / 1e3);
68
+ }
69
+ function isProducer(value) {
70
+ return typeof value === "object" && value !== null && typeof Reflect.get(value, "send") === "function";
71
+ }
72
+ function nativeBatch(payload) {
73
+ if (typeof payload !== "object" || payload === null || !("queue" in payload) || typeof payload.queue !== "string" || !("messages" in payload) || !Array.isArray(payload.messages)) throw new TypeError("Invalid native queue batch.");
74
+ const messages = [];
75
+ for (const message of payload.messages) {
76
+ if (typeof message !== "object" || message === null || !("body" in message) || !("ack" in message) || typeof message.ack !== "function" || !("retry" in message) || typeof message.retry !== "function" || !("id" in message) || typeof message.id !== "string" || !("timestamp" in message) || !(message.timestamp instanceof Date) || !("attempts" in message) || typeof message.attempts !== "number") throw new TypeError("Invalid native queue message.");
77
+ messages.push(message);
78
+ }
79
+ return {
80
+ queue: payload.queue,
81
+ messages
82
+ };
83
+ }
84
+ /** Split in order: a new call starts at 100 messages, at the byte budget, or at another queue. */
85
+ function chunk(messages) {
86
+ const chunks = [];
87
+ let current = [];
88
+ let bytes = 0;
89
+ for (const message of messages) {
90
+ if (current.length > 0 && (current.length === QUEUE_SEND_LIMITS.messages || bytes + message.bytes > QUEUE_SEND_LIMITS.batchBytes || current[0].queue !== message.queue)) {
91
+ chunks.push(current);
92
+ current = [];
93
+ bytes = 0;
94
+ }
95
+ current.push(message);
96
+ bytes += message.bytes;
97
+ }
98
+ if (current.length > 0) chunks.push(current);
99
+ return chunks;
100
+ }
101
+ function createDriver({ env, queues }) {
102
+ const pins = /* @__PURE__ */ new Map();
103
+ for (const queue of queues.all()) for (const physical of queue.consumers) pins.set(physical, [...pins.get(physical) ?? [], queue.name]);
104
+ const producerFor = (name) => {
105
+ const registration = queues.get(name);
106
+ if (!registration) throw new Error(`Queue '${name}' is not registered. Add QueueModule.registerQueue({ name: '${name}', binding }) to the module that produces it.`);
107
+ if (registration.binding === void 0) throw new Error(`Queue '${name}' has no producer binding. Register it with QueueModule.registerQueue({ name: '${name}', binding: 'YOUR_QUEUE' }), where the binding is a queues.producers[].binding of this Worker.`);
108
+ if (env === void 0) throw new Error(`cloudflareQueues() needs the Workers ENV to send '${name}' jobs. Build the application with createCloudflareWorker, createCloudflareApp or cloudflareAdapter.`);
109
+ const producer = Reflect.get(env, registration.binding);
110
+ if (!isProducer(producer)) throw new TypeError(`ENV.${registration.binding} is not a Cloudflare queue producer binding (queue '${name}'). Declare it under queues.producers in this Worker's Wrangler configuration.`);
111
+ return producer;
112
+ };
113
+ return {
114
+ kind: "cloudflare",
115
+ entrypoints: [{
116
+ kind: "cf:queue:module",
117
+ meta: { consumers: [...pins.keys()] }
118
+ }],
119
+ async consume(payload, dispatch) {
120
+ const batch = nativeBatch(payload);
121
+ const pinned = pins.get(batch.queue);
122
+ await consumeQueueBatch(batch, (job) => {
123
+ const expected = queues.get(job.queue)?.consumers ?? [];
124
+ if (expected.length > 0 && !expected.includes(batch.queue)) throw new Error(`Queue '${job.queue}' is consumed from ${expected.map((queue) => `'${queue}'`).join(", ")}, not '${batch.queue}'.`);
125
+ return dispatch(job);
126
+ }, pinned ? { queues: pinned } : {});
127
+ },
128
+ async enqueue(job, options) {
129
+ const delay = delaySeconds(options);
130
+ await producerFor(job.queue).send(parseQueueJob(job), delay === void 0 ? void 0 : { delaySeconds: delay });
131
+ },
132
+ async enqueueBatch(requests) {
133
+ const messages = requests.map(({ job, options }) => {
134
+ const body = parseQueueJob(job);
135
+ const bytes = estimateQueueMessageBytes(body);
136
+ if (bytes > QUEUE_SEND_LIMITS.messageBytes) throw new RangeError(`Queue job '${job.id}' is an estimated ${bytes} bytes, over the ${QUEUE_SEND_LIMITS.messageBytes}-byte Cloudflare message limit. Store large payloads elsewhere (for example in R2) and enqueue a reference.`);
137
+ const delay = delaySeconds(options);
138
+ return {
139
+ id: job.id,
140
+ queue: job.queue,
141
+ bytes,
142
+ request: delay === void 0 ? { body } : {
143
+ body,
144
+ delaySeconds: delay
145
+ }
146
+ };
147
+ });
148
+ const producers = /* @__PURE__ */ new Map();
149
+ for (const { queue } of messages) if (!producers.has(queue)) producers.set(queue, producerFor(queue));
150
+ const accepted = [];
151
+ for (const part of chunk(messages)) {
152
+ const producer = producers.get(part[0].queue);
153
+ try {
154
+ if (typeof producer.sendBatch === "function") {
155
+ await producer.sendBatch(part.map(({ request }) => request));
156
+ accepted.push(...part.map(({ id }) => id));
157
+ } else for (const { id, request } of part) {
158
+ const delay = request.delaySeconds;
159
+ await producer.send(request.body, delay === void 0 ? void 0 : { delaySeconds: delay });
160
+ accepted.push(id);
161
+ }
162
+ } catch (error) {
163
+ throw new QueueBatchError(accepted, messages.slice(accepted.length).map(({ id }) => id), error);
164
+ }
165
+ }
166
+ }
167
+ };
168
+ }
169
+ /**
170
+ * The Cloudflare Queues driver for `QueueModule`:
171
+ *
172
+ * ```ts
173
+ * QueueModule.forRoot({ driver: cloudflareQueues() })
174
+ * QueueModule.registerQueue({ name: 'email', binding: 'EMAIL_QUEUE' })
175
+ * ```
176
+ *
177
+ * Each application gets its own driver. Producing reads the registered binding
178
+ * from that application's `ENV` when a job is added and awaits the native
179
+ * send; `addBulk` uses `sendBatch`, chunked to {@link QUEUE_SEND_LIMITS}.
180
+ * Consuming is native too: the Worker's `queue()` handler routes every batch
181
+ * that no `@QueueConsumer` claims through `QueueModule`, one message at a time,
182
+ * by each job's logical `queue`, with the module's dispatch policy (signed
183
+ * dispatch included). A registration's `consumer` pins its queue to that
184
+ * physical queue: the queue's jobs are accepted only from it, and it only
185
+ * carries the queues pinned to it. A message that is not a job envelope,
186
+ * belongs to an unregistered queue, or fails stays unacknowledged, so
187
+ * Cloudflare retries it and then dead-letters it.
188
+ */
189
+ function cloudflareQueues() {
190
+ return (context) => createDriver(context);
191
+ }
192
+ /**
193
+ * Settle native messages one job at a time. Attempts come from the host.
194
+ * Process every message; ack successful/unsettled deliveries, then rethrow any
195
+ * failures so the host retries the unsettled remainder. Explicit settlement wins.
196
+ * The callback must await all work that should determine delivery success.
197
+ */
198
+ async function consumeQueueBatch(batch, dispatch, options = {}) {
199
+ const errors = [];
200
+ for (const message of batch.messages) try {
201
+ const job = parseQueueJob(message.body, message.attempts);
202
+ if (options.queues && !options.queues.includes(job.queue)) throw new Error(`Queue '${job.queue}' is not consumed from '${batch.queue}', which accepts ${options.queues.map((queue) => `'${queue}'`).join(", ")}.`);
203
+ const observed = observeMessage(message);
204
+ if ((await dispatch(job, observed.message))?.handled === 0) throw new Error(`Queue job '${job.name}' has no handler.`);
205
+ if (observed.disposition().outcome === "unsettled") observed.message.ack();
206
+ } catch (error) {
207
+ errors.push(error);
208
+ }
209
+ if (errors.length === 1) throw errors[0];
210
+ if (errors.length > 1) throw new AggregateError(errors, "Queue batch deliveries failed.");
211
+ }
212
+ //#endregion
213
+ export { QUEUE_SEND_LIMITS, cloudflareQueues, consumeQueueBatch, estimateQueueMessageBytes };
214
+
215
+ //# sourceMappingURL=cloudflare-queues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-queues.js","names":[],"sources":["../../src/queue/cloudflare-queues.ts"],"sourcesContent":["import { observeMessage, parseQueueJob, QueueBatchError } from '@velajs/vela/queue';\nimport type {\n AddJobOptions,\n QueueDispatchFn,\n QueueDispatchResult,\n QueueDriver,\n QueueDriverContext,\n QueueDriverFactory,\n QueueEnqueueRequest,\n QueueJob,\n QueueMessageLike,\n} from '@velajs/vela/queue';\n\n/** The native producer surface the driver uses: a Wrangler `queues.producers` binding. */\nexport interface CloudflareQueueProducer {\n // Both older void-returning bindings and current metric-returning bindings work.\n send(message: QueueJob, options?: QueueSendOptions): Promise<unknown>;\n sendBatch?(messages: Iterable<MessageSendRequest<QueueJob>>): Promise<unknown>;\n}\n\n/** The Cloudflare Queues send limits `addBulk` is chunked to (bytes are decimal kilobytes). */\nexport const QUEUE_SEND_LIMITS = Object.freeze({\n /** Messages per `sendBatch` call. */\n messages: 100,\n /** Estimated bytes per `sendBatch` call. */\n batchBytes: 256_000,\n /** Estimated bytes per message. */\n messageBytes: 128_000,\n});\n\n/** Allowance for the platform's per-message framing on top of the body estimate. */\nconst MESSAGE_MARGIN_BYTES = 256;\nconst MAX_DELAY_MS = 86_400_000;\n\n/**\n * A string's size as JSON (UTF-8, quoted, escaped) or as V8 serializes it\n * (one byte per character, two for a string holding any character above\n * U+00FF), whichever is larger.\n */\nfunction stringBytes(text: string): number {\n let json = 2;\n let wide = false;\n for (let index = 0; index < text.length; index++) {\n const code = text.charCodeAt(index);\n if (code > 0xff) wide = true;\n if (code < 0x20) json += 6;\n else if (code === 0x22 || code === 0x5c) json += 2;\n else if (code < 0x80) json += 1;\n else if (code < 0x800) json += 2;\n else if (code >= 0xd800 && code <= 0xdfff) json += 2;\n else json += 3;\n }\n return Math.max(json, (wide ? text.length * 2 : text.length) + 5);\n}\n\nfunction measure(value: unknown, seen: Set<object>): number {\n switch (typeof value) {\n case 'string':\n return stringBytes(value);\n case 'number':\n return Math.max(String(value).length, 9);\n case 'bigint':\n return String(value).length + 9;\n case 'boolean':\n return 5;\n case 'object':\n break;\n default:\n return 9;\n }\n if (value === null) return 4;\n // V8 serializes a repeated reference as a back-reference.\n if (seen.has(value)) return 9;\n seen.add(value);\n let bytes = 2;\n if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {\n bytes += value.byteLength + 9;\n } else if (value instanceof Date) {\n bytes += 26;\n } else if (value instanceof Map) {\n for (const [key, item] of value) bytes += measure(key, seen) + measure(item, seen) + 2;\n } else if (value instanceof Set) {\n for (const item of value) bytes += measure(item, seen) + 1;\n } else if (Array.isArray(value)) {\n for (const item of value) bytes += measure(item, seen) + 1;\n } else {\n for (const key of Object.keys(value)) {\n bytes += stringBytes(key) + measure(Reflect.get(value, key), seen) + 2;\n }\n }\n return bytes;\n}\n\n/**\n * A conservative size estimate of one native queue message: the larger of the\n * body's JSON and V8 sizes, plus a per-message margin.\n */\nexport function estimateQueueMessageBytes(body: unknown): number {\n return measure(body, new Set()) + MESSAGE_MARGIN_BYTES;\n}\n\nfunction delaySeconds(options: AddJobOptions | undefined): number | undefined {\n const delayMs = options?.delayMs;\n if (delayMs === undefined) return undefined;\n if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > MAX_DELAY_MS) {\n throw new RangeError('Queue delayMs must be between 0 and 86400000.');\n }\n return Math.ceil(delayMs / 1000);\n}\n\nfunction isProducer(value: unknown): value is CloudflareQueueProducer {\n return (\n typeof value === 'object' && value !== null && typeof Reflect.get(value, 'send') === 'function'\n );\n}\n\nfunction nativeBatch(payload: unknown): { queue: string; messages: QueueMessageLike[] } {\n if (\n typeof payload !== 'object' ||\n payload === null ||\n !('queue' in payload) ||\n typeof payload.queue !== 'string' ||\n !('messages' in payload) ||\n !Array.isArray(payload.messages)\n )\n throw new TypeError('Invalid native queue batch.');\n const messages: QueueMessageLike[] = [];\n for (const message of payload.messages as unknown[]) {\n if (\n typeof message !== 'object' ||\n message === null ||\n !('body' in message) ||\n !('ack' in message) ||\n typeof message.ack !== 'function' ||\n !('retry' in message) ||\n typeof message.retry !== 'function' ||\n !('id' in message) ||\n typeof message.id !== 'string' ||\n !('timestamp' in message) ||\n !(message.timestamp instanceof Date) ||\n !('attempts' in message) ||\n typeof message.attempts !== 'number'\n )\n throw new TypeError('Invalid native queue message.');\n messages.push(message as QueueMessageLike);\n }\n return { queue: payload.queue, messages };\n}\n\ninterface PreparedMessage {\n readonly id: string;\n readonly queue: string;\n readonly bytes: number;\n readonly request: MessageSendRequest<QueueJob>;\n}\n\n/** Split in order: a new call starts at 100 messages, at the byte budget, or at another queue. */\nfunction chunk(messages: readonly PreparedMessage[]): PreparedMessage[][] {\n const chunks: PreparedMessage[][] = [];\n let current: PreparedMessage[] = [];\n let bytes = 0;\n for (const message of messages) {\n if (\n current.length > 0 &&\n (current.length === QUEUE_SEND_LIMITS.messages ||\n bytes + message.bytes > QUEUE_SEND_LIMITS.batchBytes ||\n current[0]!.queue !== message.queue)\n ) {\n chunks.push(current);\n current = [];\n bytes = 0;\n }\n current.push(message);\n bytes += message.bytes;\n }\n if (current.length > 0) chunks.push(current);\n return chunks;\n}\n\nfunction createDriver({ env, queues }: QueueDriverContext): QueueDriver {\n // Physical queue -> the logical queues registrations pin to it.\n const pins = new Map<string, string[]>();\n for (const queue of queues.all()) {\n for (const physical of queue.consumers) {\n pins.set(physical, [...(pins.get(physical) ?? []), queue.name]);\n }\n }\n\n // Resolved on every send from this application's ENV, never cached.\n const producerFor = (name: string): CloudflareQueueProducer => {\n const registration = queues.get(name);\n if (!registration) {\n throw new Error(\n `Queue '${name}' is not registered. Add QueueModule.registerQueue({ name: '${name}', ` +\n `binding }) to the module that produces it.`,\n );\n }\n if (registration.binding === undefined) {\n throw new Error(\n `Queue '${name}' has no producer binding. Register it with ` +\n `QueueModule.registerQueue({ name: '${name}', binding: 'YOUR_QUEUE' }), where the ` +\n `binding is a queues.producers[].binding of this Worker.`,\n );\n }\n if (env === undefined) {\n throw new Error(\n `cloudflareQueues() needs the Workers ENV to send '${name}' jobs. Build the application ` +\n `with createCloudflareWorker, createCloudflareApp or cloudflareAdapter.`,\n );\n }\n const producer: unknown = Reflect.get(env, registration.binding);\n if (!isProducer(producer)) {\n throw new TypeError(\n `ENV.${registration.binding} is not a Cloudflare queue producer binding (queue ` +\n `'${name}'). Declare it under queues.producers in this Worker's Wrangler configuration.`,\n );\n }\n return producer;\n };\n\n return {\n kind: 'cloudflare',\n entrypoints: [{ kind: 'cf:queue:module', meta: { consumers: [...pins.keys()] } }],\n\n async consume(payload: unknown, dispatch: QueueDispatchFn): Promise<void> {\n const batch = nativeBatch(payload);\n const pinned = pins.get(batch.queue);\n await consumeQueueBatch(\n batch,\n (job) => {\n // A pinned queue is consumed only from the physical queues it names.\n const expected = queues.get(job.queue)?.consumers ?? [];\n if (expected.length > 0 && !expected.includes(batch.queue)) {\n throw new Error(\n `Queue '${job.queue}' is consumed from ` +\n `${expected.map((queue) => `'${queue}'`).join(', ')}, not '${batch.queue}'.`,\n );\n }\n return dispatch(job);\n },\n pinned ? { queues: pinned } : {},\n );\n },\n\n async enqueue(job: QueueJob, options?: AddJobOptions): Promise<void> {\n const delay = delaySeconds(options);\n await producerFor(job.queue).send(\n parseQueueJob(job),\n delay === undefined ? undefined : { delaySeconds: delay },\n );\n },\n\n async enqueueBatch(requests: readonly QueueEnqueueRequest[]): Promise<void> {\n // Validate every job and resolve every binding before the first send.\n const messages = requests.map(({ job, options }): PreparedMessage => {\n const body = parseQueueJob(job);\n const bytes = estimateQueueMessageBytes(body);\n if (bytes > QUEUE_SEND_LIMITS.messageBytes) {\n throw new RangeError(\n `Queue job '${job.id}' is an estimated ${bytes} bytes, over the ` +\n `${QUEUE_SEND_LIMITS.messageBytes}-byte Cloudflare message limit. Store large ` +\n `payloads elsewhere (for example in R2) and enqueue a reference.`,\n );\n }\n const delay = delaySeconds(options);\n return {\n id: job.id,\n queue: job.queue,\n bytes,\n request: delay === undefined ? { body } : { body, delaySeconds: delay },\n };\n });\n const producers = new Map<string, CloudflareQueueProducer>();\n for (const { queue } of messages) {\n if (!producers.has(queue)) producers.set(queue, producerFor(queue));\n }\n const accepted: string[] = [];\n for (const part of chunk(messages)) {\n const producer = producers.get(part[0]!.queue)!;\n try {\n if (typeof producer.sendBatch === 'function') {\n // eslint-disable-next-line no-await-in-loop\n await producer.sendBatch(part.map(({ request }) => request));\n accepted.push(...part.map(({ id }) => id));\n } else {\n for (const { id, request } of part) {\n const delay = request.delaySeconds;\n // eslint-disable-next-line no-await-in-loop\n await producer.send(\n request.body,\n delay === undefined ? undefined : { delaySeconds: delay },\n );\n accepted.push(id);\n }\n }\n } catch (error) {\n throw new QueueBatchError(\n accepted,\n messages.slice(accepted.length).map(({ id }) => id),\n error,\n );\n }\n }\n },\n };\n}\n\n/**\n * The Cloudflare Queues driver for `QueueModule`:\n *\n * ```ts\n * QueueModule.forRoot({ driver: cloudflareQueues() })\n * QueueModule.registerQueue({ name: 'email', binding: 'EMAIL_QUEUE' })\n * ```\n *\n * Each application gets its own driver. Producing reads the registered binding\n * from that application's `ENV` when a job is added and awaits the native\n * send; `addBulk` uses `sendBatch`, chunked to {@link QUEUE_SEND_LIMITS}.\n * Consuming is native too: the Worker's `queue()` handler routes every batch\n * that no `@QueueConsumer` claims through `QueueModule`, one message at a time,\n * by each job's logical `queue`, with the module's dispatch policy (signed\n * dispatch included). A registration's `consumer` pins its queue to that\n * physical queue: the queue's jobs are accepted only from it, and it only\n * carries the queues pinned to it. A message that is not a job envelope,\n * belongs to an unregistered queue, or fails stays unacknowledged, so\n * Cloudflare retries it and then dead-letters it.\n */\nexport function cloudflareQueues(): QueueDriverFactory {\n return (context) => createDriver(context);\n}\n\nexport interface ConsumeQueueBatchOptions {\n /**\n * Accept only jobs of these logical queues; any other message fails and is\n * retried. By default every job envelope is accepted.\n */\n readonly queues?: readonly string[];\n}\n\n/**\n * Settle native messages one job at a time. Attempts come from the host.\n * Process every message; ack successful/unsettled deliveries, then rethrow any\n * failures so the host retries the unsettled remainder. Explicit settlement wins.\n * The callback must await all work that should determine delivery success.\n */\nexport async function consumeQueueBatch(\n batch: { readonly queue: string; readonly messages: readonly QueueMessageLike[] },\n dispatch: (job: QueueJob, message: QueueMessageLike) => Promise<QueueDispatchResult | void>,\n options: ConsumeQueueBatchOptions = {},\n): Promise<void> {\n const errors: unknown[] = [];\n for (const message of batch.messages) {\n try {\n const job = parseQueueJob(message.body, message.attempts);\n if (options.queues && !options.queues.includes(job.queue)) {\n throw new Error(\n `Queue '${job.queue}' is not consumed from '${batch.queue}', which accepts ` +\n `${options.queues.map((queue) => `'${queue}'`).join(', ')}.`,\n );\n }\n const observed = observeMessage(message);\n // eslint-disable-next-line no-await-in-loop\n const result = await dispatch(job, observed.message);\n if (result?.handled === 0) throw new Error(`Queue job '${job.name}' has no handler.`);\n if (observed.disposition().outcome === 'unsettled') observed.message.ack();\n } catch (error) {\n errors.push(error);\n }\n }\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1) throw new AggregateError(errors, 'Queue batch deliveries failed.');\n}\n"],"mappings":"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"}
package/dist/queues.d.ts CHANGED
@@ -45,5 +45,4 @@ export declare function consumeQueueBatch(batch: {
45
45
  readonly messages: readonly QueueMessageLike[];
46
46
  }, dispatch: (job: QueueJob, message: QueueMessageLike) => Promise<QueueDispatchResult | void>, options?: ConsumeQueueBatchOptions): Promise<void>;
47
47
  //#endregion
48
- export type { CloudflareQueueProducer, ConsumeQueueBatchOptions };
49
- //# sourceMappingURL=queues.d.ts.map
48
+ export type { CloudflareQueueProducer, ConsumeQueueBatchOptions };
package/dist/queues.js CHANGED
@@ -1,215 +1,2 @@
1
- import { QueueBatchError, observeMessage, parseQueueJob } from "@velajs/vela/queue";
2
- //#region src/queue/cloudflare-queues.ts
3
- /** The Cloudflare Queues send limits `addBulk` is chunked to (bytes are decimal kilobytes). */
4
- const QUEUE_SEND_LIMITS = Object.freeze({
5
- /** Messages per `sendBatch` call. */
6
- messages: 100,
7
- /** Estimated bytes per `sendBatch` call. */
8
- batchBytes: 256e3,
9
- /** Estimated bytes per message. */
10
- messageBytes: 128e3
11
- });
12
- /** Allowance for the platform's per-message framing on top of the body estimate. */
13
- const MESSAGE_MARGIN_BYTES = 256;
14
- const MAX_DELAY_MS = 864e5;
15
- /**
16
- * A string's size as JSON (UTF-8, quoted, escaped) or as V8 serializes it
17
- * (one byte per character, two for a string holding any character above
18
- * U+00FF), whichever is larger.
19
- */
20
- function stringBytes(text) {
21
- let json = 2;
22
- let wide = false;
23
- for (let index = 0; index < text.length; index++) {
24
- const code = text.charCodeAt(index);
25
- if (code > 255) wide = true;
26
- if (code < 32) json += 6;
27
- else if (code === 34 || code === 92) json += 2;
28
- else if (code < 128) json += 1;
29
- else if (code < 2048) json += 2;
30
- else if (code >= 55296 && code <= 57343) json += 2;
31
- else json += 3;
32
- }
33
- return Math.max(json, (wide ? text.length * 2 : text.length) + 5);
34
- }
35
- function measure(value, seen) {
36
- switch (typeof value) {
37
- case "string": return stringBytes(value);
38
- case "number": return Math.max(String(value).length, 9);
39
- case "bigint": return String(value).length + 9;
40
- case "boolean": return 5;
41
- case "object": break;
42
- default: return 9;
43
- }
44
- if (value === null) return 4;
45
- if (seen.has(value)) return 9;
46
- seen.add(value);
47
- let bytes = 2;
48
- if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) bytes += value.byteLength + 9;
49
- else if (value instanceof Date) bytes += 26;
50
- else if (value instanceof Map) for (const [key, item] of value) bytes += measure(key, seen) + measure(item, seen) + 2;
51
- else if (value instanceof Set) for (const item of value) bytes += measure(item, seen) + 1;
52
- else if (Array.isArray(value)) for (const item of value) bytes += measure(item, seen) + 1;
53
- else for (const key of Object.keys(value)) bytes += stringBytes(key) + measure(Reflect.get(value, key), seen) + 2;
54
- return bytes;
55
- }
56
- /**
57
- * A conservative size estimate of one native queue message: the larger of the
58
- * body's JSON and V8 sizes, plus a per-message margin.
59
- */
60
- function estimateQueueMessageBytes(body) {
61
- return measure(body, /* @__PURE__ */ new Set()) + MESSAGE_MARGIN_BYTES;
62
- }
63
- function delaySeconds(options) {
64
- const delayMs = options?.delayMs;
65
- if (delayMs === void 0) return void 0;
66
- if (!Number.isFinite(delayMs) || delayMs < 0 || delayMs > MAX_DELAY_MS) throw new RangeError("Queue delayMs must be between 0 and 86400000.");
67
- return Math.ceil(delayMs / 1e3);
68
- }
69
- function isProducer(value) {
70
- return typeof value === "object" && value !== null && typeof Reflect.get(value, "send") === "function";
71
- }
72
- function nativeBatch(payload) {
73
- if (typeof payload !== "object" || payload === null || !("queue" in payload) || typeof payload.queue !== "string" || !("messages" in payload) || !Array.isArray(payload.messages)) throw new TypeError("Invalid native queue batch.");
74
- const messages = [];
75
- for (const message of payload.messages) {
76
- if (typeof message !== "object" || message === null || !("body" in message) || !("ack" in message) || typeof message.ack !== "function" || !("retry" in message) || typeof message.retry !== "function" || !("id" in message) || typeof message.id !== "string" || !("timestamp" in message) || !(message.timestamp instanceof Date) || !("attempts" in message) || typeof message.attempts !== "number") throw new TypeError("Invalid native queue message.");
77
- messages.push(message);
78
- }
79
- return {
80
- queue: payload.queue,
81
- messages
82
- };
83
- }
84
- /** Split in order: a new call starts at 100 messages, at the byte budget, or at another queue. */
85
- function chunk(messages) {
86
- const chunks = [];
87
- let current = [];
88
- let bytes = 0;
89
- for (const message of messages) {
90
- if (current.length > 0 && (current.length === QUEUE_SEND_LIMITS.messages || bytes + message.bytes > QUEUE_SEND_LIMITS.batchBytes || current[0].queue !== message.queue)) {
91
- chunks.push(current);
92
- current = [];
93
- bytes = 0;
94
- }
95
- current.push(message);
96
- bytes += message.bytes;
97
- }
98
- if (current.length > 0) chunks.push(current);
99
- return chunks;
100
- }
101
- function createDriver({ env, queues }) {
102
- const pins = /* @__PURE__ */ new Map();
103
- for (const queue of queues.all()) for (const physical of queue.consumers) pins.set(physical, [...pins.get(physical) ?? [], queue.name]);
104
- const producerFor = (name) => {
105
- const registration = queues.get(name);
106
- if (!registration) throw new Error(`Queue '${name}' is not registered. Add QueueModule.registerQueue({ name: '${name}', binding }) to the module that produces it.`);
107
- if (registration.binding === void 0) throw new Error(`Queue '${name}' has no producer binding. Register it with QueueModule.registerQueue({ name: '${name}', binding: 'YOUR_QUEUE' }), where the binding is a queues.producers[].binding of this Worker.`);
108
- if (env === void 0) throw new Error(`cloudflareQueues() needs the Workers ENV to send '${name}' jobs. Build the application with createCloudflareWorker, createCloudflareApp or cloudflareAdapter.`);
109
- const producer = Reflect.get(env, registration.binding);
110
- if (!isProducer(producer)) throw new TypeError(`ENV.${registration.binding} is not a Cloudflare queue producer binding (queue '${name}'). Declare it under queues.producers in this Worker's Wrangler configuration.`);
111
- return producer;
112
- };
113
- return {
114
- kind: "cloudflare",
115
- entrypoints: [{
116
- kind: "cf:queue:module",
117
- meta: { consumers: [...pins.keys()] }
118
- }],
119
- async consume(payload, dispatch) {
120
- const batch = nativeBatch(payload);
121
- const pinned = pins.get(batch.queue);
122
- await consumeQueueBatch(batch, (job) => {
123
- const expected = queues.get(job.queue)?.consumers ?? [];
124
- if (expected.length > 0 && !expected.includes(batch.queue)) throw new Error(`Queue '${job.queue}' is consumed from ${expected.map((queue) => `'${queue}'`).join(", ")}, not '${batch.queue}'.`);
125
- return dispatch(job);
126
- }, pinned ? { queues: pinned } : {});
127
- },
128
- async enqueue(job, options) {
129
- const delay = delaySeconds(options);
130
- await producerFor(job.queue).send(parseQueueJob(job), delay === void 0 ? void 0 : { delaySeconds: delay });
131
- },
132
- async enqueueBatch(requests) {
133
- const messages = requests.map(({ job, options }) => {
134
- const body = parseQueueJob(job);
135
- const bytes = estimateQueueMessageBytes(body);
136
- if (bytes > QUEUE_SEND_LIMITS.messageBytes) throw new RangeError(`Queue job '${job.id}' is an estimated ${bytes} bytes, over the ${QUEUE_SEND_LIMITS.messageBytes}-byte Cloudflare message limit. Store large payloads elsewhere (for example in R2) and enqueue a reference.`);
137
- const delay = delaySeconds(options);
138
- return {
139
- id: job.id,
140
- queue: job.queue,
141
- bytes,
142
- request: delay === void 0 ? { body } : {
143
- body,
144
- delaySeconds: delay
145
- }
146
- };
147
- });
148
- const producers = /* @__PURE__ */ new Map();
149
- for (const { queue } of messages) if (!producers.has(queue)) producers.set(queue, producerFor(queue));
150
- const accepted = [];
151
- for (const part of chunk(messages)) {
152
- const producer = producers.get(part[0].queue);
153
- try {
154
- if (typeof producer.sendBatch === "function") {
155
- await producer.sendBatch(part.map(({ request }) => request));
156
- accepted.push(...part.map(({ id }) => id));
157
- } else for (const { id, request } of part) {
158
- const delay = request.delaySeconds;
159
- await producer.send(request.body, delay === void 0 ? void 0 : { delaySeconds: delay });
160
- accepted.push(id);
161
- }
162
- } catch (error) {
163
- throw new QueueBatchError(accepted, messages.slice(accepted.length).map(({ id }) => id), error);
164
- }
165
- }
166
- }
167
- };
168
- }
169
- /**
170
- * The Cloudflare Queues driver for `QueueModule`:
171
- *
172
- * ```ts
173
- * QueueModule.forRoot({ driver: cloudflareQueues() })
174
- * QueueModule.registerQueue({ name: 'email', binding: 'EMAIL_QUEUE' })
175
- * ```
176
- *
177
- * Each application gets its own driver. Producing reads the registered binding
178
- * from that application's `ENV` when a job is added and awaits the native
179
- * send; `addBulk` uses `sendBatch`, chunked to {@link QUEUE_SEND_LIMITS}.
180
- * Consuming is native too: the Worker's `queue()` handler routes every batch
181
- * that no `@QueueConsumer` claims through `QueueModule`, one message at a time,
182
- * by each job's logical `queue`, with the module's dispatch policy (signed
183
- * dispatch included). A registration's `consumer` pins its queue to that
184
- * physical queue: the queue's jobs are accepted only from it, and it only
185
- * carries the queues pinned to it. A message that is not a job envelope,
186
- * belongs to an unregistered queue, or fails stays unacknowledged, so
187
- * Cloudflare retries it and then dead-letters it.
188
- */
189
- function cloudflareQueues() {
190
- return (context) => createDriver(context);
191
- }
192
- /**
193
- * Settle native messages one job at a time. Attempts come from the host.
194
- * Process every message; ack successful/unsettled deliveries, then rethrow any
195
- * failures so the host retries the unsettled remainder. Explicit settlement wins.
196
- * The callback must await all work that should determine delivery success.
197
- */
198
- async function consumeQueueBatch(batch, dispatch, options = {}) {
199
- const errors = [];
200
- for (const message of batch.messages) try {
201
- const job = parseQueueJob(message.body, message.attempts);
202
- if (options.queues && !options.queues.includes(job.queue)) throw new Error(`Queue '${job.queue}' is not consumed from '${batch.queue}', which accepts ${options.queues.map((queue) => `'${queue}'`).join(", ")}.`);
203
- const observed = observeMessage(message);
204
- if ((await dispatch(job, observed.message))?.handled === 0) throw new Error(`Queue job '${job.name}' has no handler.`);
205
- if (observed.disposition().outcome === "unsettled") observed.message.ack();
206
- } catch (error) {
207
- errors.push(error);
208
- }
209
- if (errors.length === 1) throw errors[0];
210
- if (errors.length > 1) throw new AggregateError(errors, "Queue batch deliveries failed.");
211
- }
212
- //#endregion
1
+ import { cloudflareQueues, consumeQueueBatch } from "./queue/cloudflare-queues.js";
213
2
  export { cloudflareQueues, consumeQueueBatch };
214
-
215
- //# sourceMappingURL=queues.js.map