@fishenv/http 0.0.2 → 0.0.3

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 (78) hide show
  1. package/dist/contract/builder.d.ts +9 -4
  2. package/dist/contract/builder.js +2 -0
  3. package/dist/contract/index.d.ts +3 -1
  4. package/dist/contract/index.js +1 -0
  5. package/dist/contract/types.d.ts +9 -1
  6. package/dist/contract/ws.d.ts +178 -0
  7. package/dist/contract/ws.js +164 -0
  8. package/dist/core/compose.js +5 -0
  9. package/dist/core/error.d.ts +21 -0
  10. package/dist/core/error.js +50 -7
  11. package/dist/core/index.d.ts +7 -3
  12. package/dist/core/index.js +5 -1
  13. package/dist/core/matcher.js +2 -1
  14. package/dist/core/route-builder.d.ts +4 -4
  15. package/dist/core/router.d.ts +16 -2
  16. package/dist/core/router.js +45 -6
  17. package/dist/core/transport.d.ts +39 -0
  18. package/dist/core/transport.js +68 -0
  19. package/dist/core/types.d.ts +22 -3
  20. package/dist/core/upgrade.d.ts +85 -0
  21. package/dist/core/upgrade.js +110 -0
  22. package/dist/core/utils/url.d.ts +4 -1
  23. package/dist/core/utils/url.js +14 -3
  24. package/dist/sse/hub.js +15 -33
  25. package/dist/sse/types.d.ts +3 -5
  26. package/dist/static/index.d.ts +27 -0
  27. package/dist/static/index.js +25 -0
  28. package/dist/static/negotiate.d.ts +23 -0
  29. package/dist/static/negotiate.js +85 -0
  30. package/dist/static/options.d.ts +44 -0
  31. package/dist/static/options.js +190 -0
  32. package/dist/static/resolve.d.ts +56 -0
  33. package/dist/static/resolve.js +113 -0
  34. package/dist/static/response.d.ts +53 -0
  35. package/dist/static/response.js +146 -0
  36. package/dist/static/router.d.ts +59 -0
  37. package/dist/static/router.js +217 -0
  38. package/dist/static/rules.d.ts +21 -0
  39. package/dist/static/rules.js +49 -0
  40. package/dist/static/types.d.ts +76 -0
  41. package/dist/static/types.js +0 -0
  42. package/dist/ws/channels/channel.d.ts +175 -0
  43. package/dist/ws/channels/channel.js +125 -0
  44. package/dist/ws/channels/handler.d.ts +33 -0
  45. package/dist/ws/channels/handler.js +486 -0
  46. package/dist/ws/channels/protocol.d.ts +96 -0
  47. package/dist/ws/channels/protocol.js +94 -0
  48. package/dist/ws/channels/target.d.ts +36 -0
  49. package/dist/ws/channels/target.js +56 -0
  50. package/dist/ws/connection.d.ts +56 -0
  51. package/dist/ws/connection.js +172 -0
  52. package/dist/ws/helpers.d.ts +17 -0
  53. package/dist/ws/helpers.js +48 -0
  54. package/dist/ws/hub.d.ts +172 -0
  55. package/dist/ws/hub.js +489 -0
  56. package/dist/ws/index.d.ts +65 -0
  57. package/dist/ws/index.js +66 -0
  58. package/dist/ws/lifecycle.d.ts +21 -0
  59. package/dist/ws/lifecycle.js +97 -0
  60. package/dist/ws/limits.d.ts +32 -0
  61. package/dist/ws/limits.js +67 -0
  62. package/dist/ws/message.d.ts +19 -0
  63. package/dist/ws/message.js +58 -0
  64. package/dist/ws/origin.d.ts +40 -0
  65. package/dist/ws/origin.js +152 -0
  66. package/dist/ws/presence.d.ts +96 -0
  67. package/dist/ws/presence.js +420 -0
  68. package/dist/ws/route-builder.d.ts +136 -0
  69. package/dist/ws/route-builder.js +265 -0
  70. package/dist/ws/router.d.ts +21 -0
  71. package/dist/ws/router.js +36 -0
  72. package/dist/ws/tickets.d.ts +82 -0
  73. package/dist/ws/tickets.js +161 -0
  74. package/dist/ws/typed.d.ts +54 -0
  75. package/dist/ws/typed.js +104 -0
  76. package/dist/ws/types.d.ts +69 -0
  77. package/dist/ws/types.js +70 -0
  78. package/package.json +14 -2
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A contract-typed view of a {@link WsConnection} for `c.ws().messages()`
3
+ * sockets: inbound JSON is validated against the client schema, outbound
4
+ * messages are typed (and validated in development).
5
+ */
6
+ import * as v from "valibot";
7
+ import { WsProtocolError, } from "./types.js";
8
+ /** Thrown in development when the server sends something off-contract. */
9
+ export class ContractViolationError extends Error {
10
+ issues;
11
+ constructor(what, issues) {
12
+ const first = issues[0];
13
+ const path = first?.path?.map((p) => String(p.key)).join(".") ?? "";
14
+ super(`${what} does not match the contract${path ? ` at "${path}"` : ""}: ${first?.message ?? "invalid"}`);
15
+ this.name = "ContractViolationError";
16
+ this.issues = issues;
17
+ }
18
+ }
19
+ /**
20
+ * Validate an inbound payload. Failure is the *client's* fault → protocol
21
+ * error (`1008`); the reason names the first issue only in development.
22
+ */
23
+ export function parseInbound(schema, value, development) {
24
+ const result = v.safeParse(schema, value);
25
+ if (result.success)
26
+ return result.output;
27
+ const issue = result.issues[0];
28
+ throw new WsProtocolError(development && issue
29
+ ? `Invalid message: ${issue.message}`
30
+ : "Invalid message");
31
+ }
32
+ /**
33
+ * In development, validate an outbound payload so contract drift surfaces
34
+ * immediately. A no-op in production (types are the guarantee there).
35
+ */
36
+ export function checkOutbound(schema, value, development, what) {
37
+ if (!development || schema == null)
38
+ return;
39
+ const result = v.safeParse(schema, value);
40
+ if (!result.success)
41
+ throw new ContractViolationError(what, result.issues);
42
+ }
43
+ export class TypedWsConnection {
44
+ /** The untyped connection — pass this to hubs. */
45
+ connection;
46
+ #client;
47
+ #server;
48
+ #development;
49
+ /** @internal Created by contract routes. */
50
+ constructor(connection, schemas, development) {
51
+ this.connection = connection;
52
+ this.#client = schemas.client;
53
+ this.#server = schemas.server;
54
+ this.#development = development;
55
+ }
56
+ get id() {
57
+ return this.connection.id;
58
+ }
59
+ get req() {
60
+ return this.connection.req;
61
+ }
62
+ get protocol() {
63
+ return this.connection.protocol;
64
+ }
65
+ get closed() {
66
+ return this.connection.closed;
67
+ }
68
+ get remoteAddress() {
69
+ return this.connection.remoteAddress;
70
+ }
71
+ get bufferedBytes() {
72
+ return this.connection.bufferedBytes;
73
+ }
74
+ get data() {
75
+ return this.connection.data;
76
+ }
77
+ set data(value) {
78
+ this.connection.data = value;
79
+ }
80
+ /**
81
+ * Validated inbound messages. Non-JSON or off-contract input closes the
82
+ * socket with `1008` before `fn` runs.
83
+ */
84
+ onMessage(fn) {
85
+ return this.connection.onMessage((message) => fn(parseInbound(this.#client, message.json(), this.#development()), message));
86
+ }
87
+ /** JSON-encode and send. Validated against the server schema in development. */
88
+ send(data) {
89
+ checkOutbound(this.#server, data, this.#development(), "Outbound message");
90
+ return this.connection.sendJson(data);
91
+ }
92
+ close(code, reason) {
93
+ this.connection.close(code, reason);
94
+ }
95
+ expireAt(when) {
96
+ this.connection.expireAt(when);
97
+ }
98
+ onClose(fn) {
99
+ this.connection.onClose(fn);
100
+ }
101
+ onDrain(fn) {
102
+ return this.connection.onDrain(fn);
103
+ }
104
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared WebSocket types, close codes and error → close-code mapping.
3
+ * Deliberately free of class references from other ws modules so everything
4
+ * can import from here without a cycle.
5
+ */
6
+ import { type UpgradedSocket } from "../core/index.ts";
7
+ export declare const WsCloseCode: {
8
+ readonly Normal: 1000;
9
+ readonly GoingAway: 1001;
10
+ /** Rate limit, protocol violation, invalid payload. */
11
+ readonly PolicyViolation: 1008;
12
+ readonly MessageTooBig: 1009;
13
+ readonly InternalError: 1011;
14
+ /** Backpressure / overload. */
15
+ readonly TryAgainLater: 1013;
16
+ /** `socket.expireAt()` reached. */
17
+ readonly SessionExpired: 4001;
18
+ };
19
+ /** Close reasons are limited to 123 UTF-8 bytes by RFC 6455. */
20
+ export declare const MAX_CLOSE_REASON_BYTES = 123;
21
+ /** Throw from any handler to close the socket with a specific code. */
22
+ export declare class WsCloseError extends Error {
23
+ readonly code: number;
24
+ constructor(code: number, reason?: string);
25
+ }
26
+ /** The client sent something that violates the expected protocol. */
27
+ export declare class WsProtocolError extends WsCloseError {
28
+ constructor(reason?: string);
29
+ }
30
+ /**
31
+ * Map a thrown value to a close frame. `HttpError`s become `4000 + status`
32
+ * with their public message; anything unknown becomes `1011` with a generic
33
+ * reason (the real message is only exposed in development).
34
+ */
35
+ export declare function closeFrameFor(err: unknown, development?: boolean): {
36
+ code: number;
37
+ reason: string;
38
+ };
39
+ /** Truncate to {@link MAX_CLOSE_REASON_BYTES} without splitting a code point. */
40
+ export declare function truncateReason(reason: string): string;
41
+ /** Return value is ignored; a returned promise is awaited only for error logging. */
42
+ export type WsCleanupFn = () => unknown;
43
+ /**
44
+ * - `sent` — written (or queued below the backpressure limit)
45
+ * - `backpressure` — queued, but the socket is over its buffer limit
46
+ * - `dropped` — not sent (Bun dropped it)
47
+ * - `closed` — the socket is gone
48
+ */
49
+ export type WsSendResult = "sent" | "backpressure" | "dropped" | "closed";
50
+ /** Whatever Bun's `ServerWebSocket.send()` accepts (string, buffers, Blob). */
51
+ export type WsSendable = Parameters<UpgradedSocket["send"]>[0];
52
+ export interface WsRateLimit {
53
+ /** Messages allowed per window (also the burst size). */
54
+ messages: number;
55
+ /** Window length in ms. */
56
+ perMs: number;
57
+ /** Default `"close"` → `1008`. `"drop"` silently discards excess messages. */
58
+ onExceed?: "close" | "drop";
59
+ }
60
+ export interface WsConnectionOptions {
61
+ /** Per-route inbound limit in bytes; must be ≤ the server-wide limit. */
62
+ maxMessageSize?: number;
63
+ rateLimit?: WsRateLimit;
64
+ /** Messages buffered while an async handler sets up. Default 256. */
65
+ maxQueuedMessages?: number;
66
+ /** Include error messages in close reasons. Set from `serve({ development })`. */
67
+ development?: boolean;
68
+ }
69
+ export declare const DEFAULT_MAX_QUEUED_MESSAGES = 256;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Shared WebSocket types, close codes and error → close-code mapping.
3
+ * Deliberately free of class references from other ws modules so everything
4
+ * can import from here without a cycle.
5
+ */
6
+ import { HttpError } from "../core/index.js";
7
+ // ── Close codes ──────────────────────────────────────────────────────────
8
+ export const WsCloseCode = {
9
+ Normal: 1000,
10
+ GoingAway: 1001,
11
+ /** Rate limit, protocol violation, invalid payload. */
12
+ PolicyViolation: 1008,
13
+ MessageTooBig: 1009,
14
+ InternalError: 1011,
15
+ /** Backpressure / overload. */
16
+ TryAgainLater: 1013,
17
+ /** `socket.expireAt()` reached. */
18
+ SessionExpired: 4001,
19
+ };
20
+ /** Close reasons are limited to 123 UTF-8 bytes by RFC 6455. */
21
+ export const MAX_CLOSE_REASON_BYTES = 123;
22
+ /** Throw from any handler to close the socket with a specific code. */
23
+ export class WsCloseError extends Error {
24
+ code;
25
+ constructor(code, reason = "") {
26
+ super(reason);
27
+ this.name = "WsCloseError";
28
+ this.code = code;
29
+ }
30
+ }
31
+ /** The client sent something that violates the expected protocol. */
32
+ export class WsProtocolError extends WsCloseError {
33
+ constructor(reason = "Protocol violation") {
34
+ super(WsCloseCode.PolicyViolation, reason);
35
+ this.name = "WsProtocolError";
36
+ }
37
+ }
38
+ /**
39
+ * Map a thrown value to a close frame. `HttpError`s become `4000 + status`
40
+ * with their public message; anything unknown becomes `1011` with a generic
41
+ * reason (the real message is only exposed in development).
42
+ */
43
+ export function closeFrameFor(err, development = false) {
44
+ if (err instanceof WsCloseError) {
45
+ return { code: err.code, reason: truncateReason(err.message) };
46
+ }
47
+ if (err instanceof HttpError && err.status >= 100 && err.status <= 999) {
48
+ return { code: 4000 + err.status, reason: truncateReason(err.message) };
49
+ }
50
+ const reason = development && err instanceof Error ? err.message : "Internal Error";
51
+ return { code: WsCloseCode.InternalError, reason: truncateReason(reason) };
52
+ }
53
+ /** Truncate to {@link MAX_CLOSE_REASON_BYTES} without splitting a code point. */
54
+ export function truncateReason(reason) {
55
+ const encoder = new TextEncoder();
56
+ if (encoder.encode(reason).byteLength <= MAX_CLOSE_REASON_BYTES) {
57
+ return reason;
58
+ }
59
+ let out = "";
60
+ let bytes = 0;
61
+ for (const char of reason) {
62
+ const size = encoder.encode(char).byteLength;
63
+ if (bytes + size > MAX_CLOSE_REASON_BYTES)
64
+ break;
65
+ out += char;
66
+ bytes += size;
67
+ }
68
+ return out;
69
+ }
70
+ export const DEFAULT_MAX_QUEUED_MESSAGES = 256;
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@fishenv/http",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Type-safe HTTP router for Bun with valibot validation, contracts and Server-Sent Events.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
+ "repository": {
9
+ "url": "https://github.com/freshfish70/fishenv.http"
10
+ },
8
11
  "files": [
9
12
  "dist"
10
13
  ],
@@ -17,13 +20,22 @@
17
20
  "types": "./dist/sse/index.d.ts",
18
21
  "default": "./dist/sse/index.js"
19
22
  },
23
+ "./ws": {
24
+ "types": "./dist/ws/index.d.ts",
25
+ "default": "./dist/ws/index.js"
26
+ },
20
27
  "./contract": {
21
28
  "types": "./dist/contract/index.d.ts",
22
29
  "default": "./dist/contract/index.js"
30
+ },
31
+ "./static": {
32
+ "types": "./dist/static/index.d.ts",
33
+ "default": "./dist/static/index.js"
23
34
  }
24
35
  },
25
36
  "publishConfig": {
26
- "access": "public"
37
+ "access": "public",
38
+ "provenance": true
27
39
  },
28
40
  "scripts": {
29
41
  "test": "bun test",