@rdlabo/workers-hono-kit 0.8.0 → 0.9.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.
package/dist/index.d.ts CHANGED
@@ -51,6 +51,10 @@ export { acknowledgeHibernationWebSocketClose, broadcastHibernationWebSockets, c
51
51
  export type { HibernationAutoResponseOptions, HibernationUpgradeOptions, HibernationWebSocketLike, HibernationWebSocketStateLike, WebSocketAutoResponsePairFactory, WebSocketPairFactory, } from './realtime/hibernation.js';
52
52
  export { isRetryableDurableObjectError, retryDurableObjectOperation } from './realtime/retry.js';
53
53
  export type { DurableObjectErrorLike, DurableObjectRetryOptions } from './realtime/retry.js';
54
+ export { DurableObjectResponseError, invokeDurableObjectFetch } from './realtime/invoke.js';
55
+ export type { DurableObjectFetchRequest, DurableObjectFetchStubLike, InvokeDurableObjectFetchOptions, } from './realtime/invoke.js';
56
+ export { parseRealtimeWebSocketProtocolOffer, parseWebSocketProtocols } from './realtime/protocol.js';
57
+ export type { ParseRealtimeWebSocketProtocolOptions, RealtimeWebSocketProtocolOffer } from './realtime/protocol.js';
54
58
  export { KVCache } from './cache/kv-cache.js';
55
59
  export type { KVNamespace, KVCacheOptions } from './cache/kv-cache.js';
56
60
  export { createStripeClient, verifyStripeWebhook } from './stripe/client.js';
package/dist/index.js CHANGED
@@ -38,6 +38,8 @@ export { createSentryErrorReporter } from './http/http-error.js';
38
38
  // realtime
39
39
  export { acknowledgeHibernationWebSocketClose, broadcastHibernationWebSockets, closeHibernationWebSocket, configureHibernationAutoResponse, upgradeHibernationWebSocket, } from './realtime/hibernation.js';
40
40
  export { isRetryableDurableObjectError, retryDurableObjectOperation } from './realtime/retry.js';
41
+ export { DurableObjectResponseError, invokeDurableObjectFetch } from './realtime/invoke.js';
42
+ export { parseRealtimeWebSocketProtocolOffer, parseWebSocketProtocols } from './realtime/protocol.js';
41
43
  // cache
42
44
  export { KVCache } from './cache/kv-cache.js';
43
45
  // stripe
@@ -2,3 +2,7 @@ export { acknowledgeHibernationWebSocketClose, broadcastHibernationWebSockets, c
2
2
  export type { HibernationAutoResponseOptions, HibernationUpgradeOptions, HibernationWebSocketLike, HibernationWebSocketStateLike, WebSocketAutoResponsePairFactory, WebSocketPairFactory, } from './hibernation.js';
3
3
  export { isRetryableDurableObjectError, retryDurableObjectOperation } from './retry.js';
4
4
  export type { DurableObjectErrorLike, DurableObjectRetryOptions } from './retry.js';
5
+ export { DurableObjectResponseError, invokeDurableObjectFetch } from './invoke.js';
6
+ export type { DurableObjectFetchRequest, DurableObjectFetchStubLike, InvokeDurableObjectFetchOptions, } from './invoke.js';
7
+ export { parseRealtimeWebSocketProtocolOffer, parseWebSocketProtocols } from './protocol.js';
8
+ export type { ParseRealtimeWebSocketProtocolOptions, RealtimeWebSocketProtocolOffer } from './protocol.js';
@@ -1,2 +1,4 @@
1
1
  export { acknowledgeHibernationWebSocketClose, broadcastHibernationWebSockets, closeHibernationWebSocket, configureHibernationAutoResponse, upgradeHibernationWebSocket, } from './hibernation.js';
2
2
  export { isRetryableDurableObjectError, retryDurableObjectOperation } from './retry.js';
3
+ export { DurableObjectResponseError, invokeDurableObjectFetch } from './invoke.js';
4
+ export { parseRealtimeWebSocketProtocolOffer, parseWebSocketProtocols } from './protocol.js';
@@ -0,0 +1,30 @@
1
+ import type { DurableObjectRetryOptions } from './retry.js';
2
+ /** Minimal fetch surface implemented by a Durable Object stub. */
3
+ export interface DurableObjectFetchStubLike {
4
+ fetch(input: Request | string | URL, init?: RequestInit): Promise<Response>;
5
+ }
6
+ /** A freshly-created request for one Durable Object invocation attempt. */
7
+ export interface DurableObjectFetchRequest {
8
+ input: Request | string | URL;
9
+ init?: RequestInit;
10
+ }
11
+ /** Options for invoking a Durable Object fetch endpoint safely. */
12
+ export interface InvokeDurableObjectFetchOptions {
13
+ getStub: () => DurableObjectFetchStubLike;
14
+ createRequest: () => DurableObjectFetchRequest;
15
+ retry?: boolean;
16
+ retryOptions?: DurableObjectRetryOptions;
17
+ errorMessage?: string;
18
+ }
19
+ /** HTTP response failure returned explicitly by a Durable Object fetch handler. */
20
+ export declare class DurableObjectResponseError extends Error {
21
+ readonly response: Response;
22
+ constructor(message: string, response: Response);
23
+ }
24
+ /**
25
+ * Invoke a Durable Object fetch endpoint with a fresh stub and request for every attempt.
26
+ *
27
+ * Non-2xx responses always throw. Runtime retryable exceptions are retried only when `retry` is
28
+ * explicitly enabled, allowing callers to keep non-idempotent operations at-most-once.
29
+ */
30
+ export declare function invokeDurableObjectFetch(options: InvokeDurableObjectFetchOptions): Promise<Response>;
@@ -0,0 +1,27 @@
1
+ import { retryDurableObjectOperation } from './retry.js';
2
+ /** HTTP response failure returned explicitly by a Durable Object fetch handler. */
3
+ export class DurableObjectResponseError extends Error {
4
+ response;
5
+ constructor(message, response) {
6
+ super(message);
7
+ this.response = response;
8
+ this.name = 'DurableObjectResponseError';
9
+ }
10
+ }
11
+ /**
12
+ * Invoke a Durable Object fetch endpoint with a fresh stub and request for every attempt.
13
+ *
14
+ * Non-2xx responses always throw. Runtime retryable exceptions are retried only when `retry` is
15
+ * explicitly enabled, allowing callers to keep non-idempotent operations at-most-once.
16
+ */
17
+ export async function invokeDurableObjectFetch(options) {
18
+ const invoke = async () => {
19
+ const request = options.createRequest();
20
+ const response = await options.getStub().fetch(request.input, request.init);
21
+ if (!response.ok) {
22
+ throw new DurableObjectResponseError(`${options.errorMessage ?? 'Durable Object request failed'}: ${response.status}`, response);
23
+ }
24
+ return response;
25
+ };
26
+ return options.retry ? retryDurableObjectOperation(invoke, options.retryOptions) : invoke();
27
+ }
@@ -0,0 +1,22 @@
1
+ /** Parsed WebSocket subprotocol offer used by authenticated realtime endpoints. */
2
+ export interface RealtimeWebSocketProtocolOffer {
3
+ protocols: string[];
4
+ authToken?: string;
5
+ clientId?: string;
6
+ }
7
+ /** Options for validating an application/auth/client WebSocket subprotocol offer. */
8
+ export interface ParseRealtimeWebSocketProtocolOptions {
9
+ protocol: string;
10
+ authPrefix?: string;
11
+ clientPrefix: string;
12
+ requireAuth?: boolean;
13
+ clientIdPattern?: RegExp;
14
+ }
15
+ /** Split a `Sec-WebSocket-Protocol` header into trimmed, non-empty protocol tokens. */
16
+ export declare function parseWebSocketProtocols(header: string | undefined): string[];
17
+ /**
18
+ * Validate the standard application/auth/client WebSocket subprotocol offer.
19
+ *
20
+ * Returns `null` when the application protocol, required auth token, or client ID is invalid.
21
+ */
22
+ export declare function parseRealtimeWebSocketProtocolOffer(header: string | undefined, options: ParseRealtimeWebSocketProtocolOptions): RealtimeWebSocketProtocolOffer | null;
@@ -0,0 +1,33 @@
1
+ /** Split a `Sec-WebSocket-Protocol` header into trimmed, non-empty protocol tokens. */
2
+ export function parseWebSocketProtocols(header) {
3
+ return (header ?? '')
4
+ .split(',')
5
+ .map((value) => value.trim())
6
+ .filter(Boolean);
7
+ }
8
+ /**
9
+ * Validate the standard application/auth/client WebSocket subprotocol offer.
10
+ *
11
+ * Returns `null` when the application protocol, required auth token, or client ID is invalid.
12
+ */
13
+ export function parseRealtimeWebSocketProtocolOffer(header, options) {
14
+ const protocols = parseWebSocketProtocols(header);
15
+ if (!protocols.includes(options.protocol)) {
16
+ return null;
17
+ }
18
+ const authPrefix = options.authPrefix;
19
+ const authToken = authPrefix
20
+ ? protocols.find((value) => value.startsWith(authPrefix))?.slice(authPrefix.length)
21
+ : undefined;
22
+ if ((options.requireAuth ?? true) && !authToken) {
23
+ return null;
24
+ }
25
+ const clientId = protocols
26
+ .find((value) => value.startsWith(options.clientPrefix))
27
+ ?.slice(options.clientPrefix.length);
28
+ const clientIdPattern = options.clientIdPattern ?? /^[A-Za-z0-9_-]{1,64}$/;
29
+ if (clientId !== undefined && !clientIdPattern.test(clientId)) {
30
+ return null;
31
+ }
32
+ return { protocols, authToken, clientId };
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -41,7 +41,9 @@
41
41
  "bin": {
42
42
  "workers-hono-kit-sync-dev-aws": "./scripts/sync-dev-aws.mjs",
43
43
  "workers-hono-kit-check-subrequest-fanout": "./scripts/check-subrequest-fanout.mjs",
44
- "workers-hono-kit-db-baseline": "./scripts/db-baseline.mjs"
44
+ "workers-hono-kit-db-baseline": "./scripts/db-baseline.mjs",
45
+ "workers-hono-kit-check-realtime-bundle": "./scripts/check-realtime-bundle.mjs",
46
+ "workers-hono-kit-query-realtime-do-metrics": "./scripts/query-realtime-do-metrics.mjs"
45
47
  },
46
48
  "exports": {
47
49
  ".": {
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { stat } from 'node:fs/promises';
3
+
4
+ const bundlePath = process.argv[2] ?? 'dist-realtime/realtime-worker.js';
5
+ const maxBytes = Number(process.argv[3] ?? 32 * 1024);
6
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
7
+ throw new Error(`Invalid realtime bundle byte limit: ${process.argv[3]}`);
8
+ }
9
+
10
+ const { size } = await stat(bundlePath);
11
+ if (size > maxBytes) {
12
+ throw new Error(`Realtime Worker bundle is ${size} bytes; expected at most ${maxBytes}`);
13
+ }
14
+ console.log(`[realtime-bundle] ${size} bytes (limit ${maxBytes})`);
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ const token = process.env.CLOUDFLARE_API_TOKEN;
3
+ if (!token) {
4
+ throw new Error('CLOUDFLARE_API_TOKEN (Account Analytics:Read) is required');
5
+ }
6
+
7
+ const namespace = process.argv[2] ?? process.env.REALTIME_DO_CLASS;
8
+ if (!namespace) {
9
+ throw new Error('Durable Object class name is required as argv[2] or REALTIME_DO_CLASS');
10
+ }
11
+ const accountTag = process.env.CLOUDFLARE_ACCOUNT_ID;
12
+ if (!accountTag) {
13
+ throw new Error('CLOUDFLARE_ACCOUNT_ID is required');
14
+ }
15
+ const since = process.env.REALTIME_METRICS_SINCE ?? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
16
+ const query = `query RealtimeMetrics($accountTag: string!, $since: Time!, $namespace: string!) {
17
+ viewer {
18
+ accounts(filter: { accountTag: $accountTag }) {
19
+ invocations: durableObjectsInvocationsAdaptiveGroups(
20
+ filter: { datetime_geq: $since, name: $namespace }
21
+ limit: 10000
22
+ ) {
23
+ dimensions { datetimeHour type status }
24
+ sum { requests errors wallTime }
25
+ }
26
+ periodic: durableObjectsPeriodicGroups(
27
+ filter: { datetime_geq: $since, name: $namespace }
28
+ limit: 10000
29
+ ) {
30
+ dimensions { datetimeHour }
31
+ sum {
32
+ duration activeTime cpuTime rowsRead rowsWritten storageDeletes storageReadUnits storageWriteUnits
33
+ inboundWebsocketMsgCount outboundWebsocketMsgCount
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }`;
39
+
40
+ const response = await fetch('https://api.cloudflare.com/client/v4/graphql', {
41
+ method: 'POST',
42
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ query, variables: { accountTag, since, namespace } }),
44
+ });
45
+ const payload = await response.json();
46
+ if (!response.ok || payload.errors?.length) {
47
+ throw new Error(JSON.stringify(payload.errors ?? payload));
48
+ }
49
+
50
+ const metrics = payload.data?.viewer?.accounts?.[0] ?? { invocations: [], periodic: [] };
51
+ const sum = (groups, field) => groups.reduce((total, group) => total + Number(group.sum?.[field] ?? 0), 0);
52
+ const summary = {
53
+ requests: sum(metrics.invocations, 'requests'),
54
+ errors: sum(metrics.invocations, 'errors'),
55
+ wallTime: sum(metrics.invocations, 'wallTime'),
56
+ duration: sum(metrics.periodic, 'duration'),
57
+ activeTime: sum(metrics.periodic, 'activeTime'),
58
+ cpuTime: sum(metrics.periodic, 'cpuTime'),
59
+ rowsRead: sum(metrics.periodic, 'rowsRead'),
60
+ rowsWritten: sum(metrics.periodic, 'rowsWritten'),
61
+ storageDeletes: sum(metrics.periodic, 'storageDeletes'),
62
+ storageReadUnits: sum(metrics.periodic, 'storageReadUnits'),
63
+ storageWriteUnits: sum(metrics.periodic, 'storageWriteUnits'),
64
+ inboundWebsocketMsgCount: sum(metrics.periodic, 'inboundWebsocketMsgCount'),
65
+ outboundWebsocketMsgCount: sum(metrics.periodic, 'outboundWebsocketMsgCount'),
66
+ };
67
+
68
+ console.log(JSON.stringify({ since, namespace, summary, ...metrics }, null, 2));