@nats-kit/rpc 0.3.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/commonjs/client.d.ts +9 -0
- package/dist/commonjs/client.d.ts.map +1 -0
- package/dist/commonjs/client.js +307 -0
- package/dist/commonjs/client.js.map +1 -0
- package/dist/commonjs/contract.d.ts +59 -0
- package/dist/commonjs/contract.d.ts.map +1 -0
- package/dist/commonjs/contract.js +56 -0
- package/dist/commonjs/contract.js.map +1 -0
- package/dist/commonjs/errors.d.ts +58 -0
- package/dist/commonjs/errors.d.ts.map +1 -0
- package/dist/commonjs/errors.js +74 -0
- package/dist/commonjs/errors.js.map +1 -0
- package/dist/commonjs/eslint/index.d.ts +22 -0
- package/dist/commonjs/eslint/index.d.ts.map +1 -0
- package/dist/commonjs/eslint/index.js +33 -0
- package/dist/commonjs/eslint/index.js.map +1 -0
- package/dist/commonjs/events.d.ts +55 -0
- package/dist/commonjs/events.d.ts.map +1 -0
- package/dist/commonjs/events.js +82 -0
- package/dist/commonjs/events.js.map +1 -0
- package/dist/commonjs/headers.d.ts +53 -0
- package/dist/commonjs/headers.d.ts.map +1 -0
- package/dist/commonjs/headers.js +116 -0
- package/dist/commonjs/headers.js.map +1 -0
- package/dist/commonjs/index.d.ts +15 -0
- package/dist/commonjs/index.d.ts.map +1 -0
- package/dist/commonjs/index.js +61 -0
- package/dist/commonjs/index.js.map +1 -0
- package/dist/commonjs/kv.d.ts +13 -0
- package/dist/commonjs/kv.d.ts.map +1 -0
- package/dist/commonjs/kv.js +23 -0
- package/dist/commonjs/kv.js.map +1 -0
- package/dist/commonjs/package.json +4 -0
- package/dist/commonjs/server.d.ts +45 -0
- package/dist/commonjs/server.d.ts.map +1 -0
- package/dist/commonjs/server.js +433 -0
- package/dist/commonjs/server.js.map +1 -0
- package/dist/commonjs/types.d.ts +100 -0
- package/dist/commonjs/types.d.ts.map +1 -0
- package/dist/commonjs/types.js +3 -0
- package/dist/commonjs/types.js.map +1 -0
- package/dist/esm/client.d.ts +9 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +304 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/contract.d.ts +59 -0
- package/dist/esm/contract.d.ts.map +1 -0
- package/dist/esm/contract.js +48 -0
- package/dist/esm/contract.js.map +1 -0
- package/dist/esm/errors.d.ts +58 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +67 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/eslint/index.d.ts +22 -0
- package/dist/esm/eslint/index.d.ts.map +1 -0
- package/dist/esm/eslint/index.js +30 -0
- package/dist/esm/eslint/index.js.map +1 -0
- package/dist/esm/events.d.ts +55 -0
- package/dist/esm/events.d.ts.map +1 -0
- package/dist/esm/events.js +77 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/headers.d.ts +53 -0
- package/dist/esm/headers.d.ts.map +1 -0
- package/dist/esm/headers.js +108 -0
- package/dist/esm/headers.js.map +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +25 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/kv.d.ts +13 -0
- package/dist/esm/kv.d.ts.map +1 -0
- package/dist/esm/kv.js +20 -0
- package/dist/esm/kv.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/server.d.ts +45 -0
- package/dist/esm/server.d.ts.map +1 -0
- package/dist/esm/server.js +426 -0
- package/dist/esm/server.js.map +1 -0
- package/dist/esm/types.d.ts +100 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +103 -0
- package/src/client.ts +377 -0
- package/src/contract.ts +105 -0
- package/src/errors.ts +100 -0
- package/src/eslint/index.ts +30 -0
- package/src/events.ts +139 -0
- package/src/headers.ts +147 -0
- package/src/index.ts +76 -0
- package/src/kv.ts +33 -0
- package/src/server.ts +500 -0
- package/src/types.ts +119 -0
package/src/events.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { nuid } from "@nats-io/nuid";
|
|
2
|
+
import {
|
|
3
|
+
headers as buildMsgHdrs,
|
|
4
|
+
type MsgHdrs,
|
|
5
|
+
type NatsConnectionLike,
|
|
6
|
+
type NatsConnectionRunner,
|
|
7
|
+
type PubAck,
|
|
8
|
+
} from "@nats-kit/core";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
/** A CloudEvents-shaped event definition (§2.5, §4.8). */
|
|
12
|
+
export interface EventContract<T extends z.ZodType> {
|
|
13
|
+
/** `<app>.<entity>.<event>` — also the CloudEvents `type` and the subject. */
|
|
14
|
+
type: string;
|
|
15
|
+
/** CloudEvents `source`, e.g. "urn:anki:sandbox". */
|
|
16
|
+
source: string;
|
|
17
|
+
data: T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defineEvent<T extends z.ZodType>(def: EventContract<T>): EventContract<T> {
|
|
21
|
+
return def;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `publishEvent` needs a JetStream client to get a `PubAck` (design §2.5);
|
|
26
|
+
* `NatsConnectionLike` alone is Core-NATS-only, so we widen it with just the
|
|
27
|
+
* one extra accessor both `NatsConnectionRunner` and the NestJS `NatsService`
|
|
28
|
+
* (which delegates to it) already expose.
|
|
29
|
+
*/
|
|
30
|
+
export interface PublishEventOptions {
|
|
31
|
+
nats: NatsConnectionLike & Pick<NatsConnectionRunner, "getJetStream">;
|
|
32
|
+
/** CloudEvents `id` and `Nats-Msg-Id`. Defaults to a nuid. */
|
|
33
|
+
id?: string;
|
|
34
|
+
/** RFC3339. Defaults to `new Date().toISOString()`. */
|
|
35
|
+
time?: string;
|
|
36
|
+
/** CloudEvents optional `subject` attribute (a resource id, NOT the NATS subject). */
|
|
37
|
+
subject?: string;
|
|
38
|
+
traceparent?: string;
|
|
39
|
+
/** Override the publish subject. Defaults to `event.type`. */
|
|
40
|
+
natsSubject?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CloudEventEnvelope<T> {
|
|
44
|
+
specversion: "1.0";
|
|
45
|
+
id: string;
|
|
46
|
+
source: string;
|
|
47
|
+
type: string;
|
|
48
|
+
time: string;
|
|
49
|
+
subject?: string;
|
|
50
|
+
datacontenttype: "application/json";
|
|
51
|
+
data: T;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type CloudEvent<T> = CloudEventEnvelope<T>;
|
|
55
|
+
|
|
56
|
+
/** Validates the envelope shape (§4.8) around an arbitrary `data` payload. */
|
|
57
|
+
const CloudEventEnvelopeSchema = z.object({
|
|
58
|
+
specversion: z.literal("1.0"),
|
|
59
|
+
id: z.string().min(1),
|
|
60
|
+
source: z.string().min(1),
|
|
61
|
+
type: z.string().min(1),
|
|
62
|
+
time: z.string().min(1),
|
|
63
|
+
subject: z.string().optional(),
|
|
64
|
+
datacontenttype: z.literal("application/json"),
|
|
65
|
+
data: z.unknown(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* JSON has no representation for `NaN`/`Infinity` — `JSON.stringify` silently
|
|
70
|
+
* turns them into `null`, which would silently corrupt an event's data on
|
|
71
|
+
* the wire. Walk the value and reject before that can happen.
|
|
72
|
+
*/
|
|
73
|
+
function assertJsonSafe(value: unknown, path = "$"): void {
|
|
74
|
+
if (typeof value === "number") {
|
|
75
|
+
if (!Number.isFinite(value)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`publishEvent: non-finite number (${String(value)}) at ${path} is not valid JSON`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
value.forEach((v, i) => assertJsonSafe(v, `${path}[${i}]`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (value !== null && typeof value === "object") {
|
|
87
|
+
for (const [k, v] of Object.entries(value)) {
|
|
88
|
+
assertJsonSafe(v, `${path}.${k}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Publish a CloudEvents 1.0 structured-mode envelope (§4.8) via JetStream,
|
|
95
|
+
* with `Nats-Msg-Id` set to the CloudEvents `id` for stream-level dedup.
|
|
96
|
+
*/
|
|
97
|
+
export async function publishEvent<T extends z.ZodType>(
|
|
98
|
+
event: EventContract<T>,
|
|
99
|
+
data: z.input<T>,
|
|
100
|
+
options: PublishEventOptions,
|
|
101
|
+
): Promise<PubAck> {
|
|
102
|
+
const parsed = event.data.parse(data);
|
|
103
|
+
assertJsonSafe(parsed);
|
|
104
|
+
const id = options.id ?? nuid.next();
|
|
105
|
+
const envelope: CloudEventEnvelope<unknown> = {
|
|
106
|
+
specversion: "1.0",
|
|
107
|
+
id,
|
|
108
|
+
source: event.source,
|
|
109
|
+
type: event.type,
|
|
110
|
+
time: options.time ?? new Date().toISOString(),
|
|
111
|
+
subject: options.subject,
|
|
112
|
+
datacontenttype: "application/json",
|
|
113
|
+
data: parsed,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const h: MsgHdrs = buildMsgHdrs();
|
|
117
|
+
if (options.traceparent !== undefined) {
|
|
118
|
+
h.set("traceparent", options.traceparent);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const body = new TextEncoder().encode(JSON.stringify(envelope));
|
|
122
|
+
await options.nats.waitForReady();
|
|
123
|
+
const js = options.nats.getJetStream();
|
|
124
|
+
return js.publish(options.natsSubject ?? event.type, body, { msgID: id, headers: h });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse a received JetStream/Core message as a CloudEvents envelope,
|
|
129
|
+
* validating the full envelope (not just `data`) per §4.8.
|
|
130
|
+
*/
|
|
131
|
+
export function parseEvent<T extends z.ZodType>(
|
|
132
|
+
event: EventContract<T>,
|
|
133
|
+
msg: { data: Uint8Array; headers?: MsgHdrs },
|
|
134
|
+
): CloudEvent<z.output<T>> {
|
|
135
|
+
const raw: unknown = JSON.parse(new TextDecoder().decode(msg.data));
|
|
136
|
+
const envelope = CloudEventEnvelopeSchema.parse(raw);
|
|
137
|
+
const data = event.data.parse(envelope.data);
|
|
138
|
+
return { ...envelope, data };
|
|
139
|
+
}
|
package/src/headers.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { headers as buildMsgHdrs, type MsgHdrs } from "@nats-kit/core";
|
|
2
|
+
import { Match } from "@nats-io/nats-core";
|
|
3
|
+
|
|
4
|
+
import { type RpcCaller } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/** Header names (D5, §4.2). Never mint a bare `Nats-*` header — that prefix is server/ADR-32 territory. */
|
|
7
|
+
export const HEADER_REQUEST_ID = "Nk-Request-Id";
|
|
8
|
+
export const HEADER_CALLER_KIND = "Nk-Caller-Kind";
|
|
9
|
+
export const HEADER_CALLER_ID = "Nk-Caller-Id";
|
|
10
|
+
export const HEADER_CALLER_ROLE = "Nk-Caller-Role";
|
|
11
|
+
export const HEADER_DEADLINE = "Nk-Deadline";
|
|
12
|
+
export const HEADER_CONTENT_TYPE = "Content-Type";
|
|
13
|
+
export const HEADER_TRACEPARENT = "traceparent";
|
|
14
|
+
export const HEADER_TRACESTATE = "tracestate";
|
|
15
|
+
|
|
16
|
+
export const CONTENT_TYPE_JSON = "application/json";
|
|
17
|
+
|
|
18
|
+
const ROLE_RE = /^[A-Za-z0-9_:-]{1,64}$/;
|
|
19
|
+
const REQUEST_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
20
|
+
|
|
21
|
+
export interface BuildRequestHeadersInit {
|
|
22
|
+
requestId: string;
|
|
23
|
+
caller: RpcCaller;
|
|
24
|
+
deadlineMs?: number;
|
|
25
|
+
traceparent?: string;
|
|
26
|
+
tracestate?: string;
|
|
27
|
+
/** Extra `Nk-*` headers. `Nats-*` and reserved names are rejected by the caller (client.ts). */
|
|
28
|
+
extra?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Build the request `MsgHdrs` per §4.2. */
|
|
32
|
+
export function buildRequestHeaders(init: BuildRequestHeadersInit): MsgHdrs {
|
|
33
|
+
const h = buildMsgHdrs();
|
|
34
|
+
h.set(HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON);
|
|
35
|
+
h.set(HEADER_REQUEST_ID, init.requestId);
|
|
36
|
+
h.set(HEADER_CALLER_KIND, init.caller.kind);
|
|
37
|
+
if (init.caller.id !== undefined) {
|
|
38
|
+
h.set(HEADER_CALLER_ID, init.caller.id);
|
|
39
|
+
}
|
|
40
|
+
if (init.caller.roles && init.caller.roles.length > 0) {
|
|
41
|
+
h.set(HEADER_CALLER_ROLE, init.caller.roles.join(","));
|
|
42
|
+
}
|
|
43
|
+
if (init.deadlineMs !== undefined) {
|
|
44
|
+
h.set(HEADER_DEADLINE, String(Math.trunc(init.deadlineMs)));
|
|
45
|
+
}
|
|
46
|
+
if (init.traceparent !== undefined) {
|
|
47
|
+
h.set(HEADER_TRACEPARENT, init.traceparent);
|
|
48
|
+
}
|
|
49
|
+
if (init.tracestate !== undefined) {
|
|
50
|
+
h.set(HEADER_TRACESTATE, init.tracestate);
|
|
51
|
+
}
|
|
52
|
+
if (init.extra) {
|
|
53
|
+
for (const [k, v] of Object.entries(init.extra)) {
|
|
54
|
+
h.set(k, v);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return h;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse `Nk-Caller-Role` per §4.2: comma-separated, no spaces, absent/empty
|
|
62
|
+
* means an empty set. Guards the classic `"".split(",")` trap, which yields
|
|
63
|
+
* `[""]` rather than `[]`.
|
|
64
|
+
*/
|
|
65
|
+
export function parseRoles(headerValue: string | undefined | null): readonly string[] {
|
|
66
|
+
if (!headerValue) return [];
|
|
67
|
+
return headerValue.split(",").filter((r) => r.length > 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isValidRole(role: string): boolean {
|
|
71
|
+
return ROLE_RE.test(role);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isValidRequestId(id: string): boolean {
|
|
75
|
+
return REQUEST_ID_RE.test(id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ParsedRequestHeaders {
|
|
79
|
+
requestId: string;
|
|
80
|
+
callerKind: "user" | "service";
|
|
81
|
+
callerId?: string;
|
|
82
|
+
roles: readonly string[];
|
|
83
|
+
deadlineMs?: number;
|
|
84
|
+
traceparent?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type ParseHeadersResult =
|
|
88
|
+
| { ok: true; value: ParsedRequestHeaders }
|
|
89
|
+
| { ok: false; message: string };
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse and validate the required/conditional request headers per §4.2.
|
|
93
|
+
* Used by `createRpcServer` as dispatch step 1 (missing/malformed -> 400;
|
|
94
|
+
* `user` with no `Nk-Caller-Id` -> handled by the caller as 401).
|
|
95
|
+
*/
|
|
96
|
+
export function parseRequestHeaders(h: MsgHdrs | undefined): ParseHeadersResult {
|
|
97
|
+
if (!h) {
|
|
98
|
+
return { ok: false, message: "missing required headers" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const contentType = h.get(HEADER_CONTENT_TYPE, Match.IgnoreCase);
|
|
102
|
+
if (contentType && contentType !== CONTENT_TYPE_JSON) {
|
|
103
|
+
return { ok: false, message: `unsupported ${HEADER_CONTENT_TYPE}: "${contentType}"` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const requestId = h.get(HEADER_REQUEST_ID, Match.IgnoreCase);
|
|
107
|
+
if (!requestId || !isValidRequestId(requestId)) {
|
|
108
|
+
return { ok: false, message: `missing or malformed ${HEADER_REQUEST_ID}` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const callerKind = h.get(HEADER_CALLER_KIND, Match.IgnoreCase);
|
|
112
|
+
if (callerKind !== "user" && callerKind !== "service") {
|
|
113
|
+
return { ok: false, message: `missing or invalid ${HEADER_CALLER_KIND}` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const rawCallerId = h.get(HEADER_CALLER_ID, Match.IgnoreCase) || undefined;
|
|
117
|
+
if (rawCallerId !== undefined && (rawCallerId.length < 1 || rawCallerId.length > 256)) {
|
|
118
|
+
return { ok: false, message: `${HEADER_CALLER_ID} must be 1-256 chars` };
|
|
119
|
+
}
|
|
120
|
+
const callerId = rawCallerId;
|
|
121
|
+
|
|
122
|
+
const rawRoles = h.get(HEADER_CALLER_ROLE, Match.IgnoreCase);
|
|
123
|
+
const roles = parseRoles(rawRoles);
|
|
124
|
+
if (rawRoles) {
|
|
125
|
+
for (const role of roles) {
|
|
126
|
+
if (!isValidRole(role)) {
|
|
127
|
+
return { ok: false, message: `malformed role in ${HEADER_CALLER_ROLE}: "${role}"` };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const rawDeadline = h.get(HEADER_DEADLINE, Match.IgnoreCase);
|
|
133
|
+
let deadlineMs: number | undefined;
|
|
134
|
+
if (rawDeadline) {
|
|
135
|
+
if (!/^\d+$/.test(rawDeadline)) {
|
|
136
|
+
return { ok: false, message: `malformed ${HEADER_DEADLINE}` };
|
|
137
|
+
}
|
|
138
|
+
deadlineMs = Number(rawDeadline);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const traceparent = h.get(HEADER_TRACEPARENT, Match.IgnoreCase) || undefined;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
value: { requestId, callerKind, callerId, roles, deadlineMs, traceparent },
|
|
146
|
+
};
|
|
147
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Public surface of @nats-kit/rpc — typed request/reply RPC over NATS,
|
|
2
|
+
// built on @nats-io/services (ADR-32) with a reconnect supervisor.
|
|
3
|
+
//
|
|
4
|
+
// See docs/spec/nats-kit-wire-spec.md for the language-neutral wire spec this
|
|
5
|
+
// package implements (mirrors design doc §4).
|
|
6
|
+
|
|
7
|
+
// Contract
|
|
8
|
+
export {
|
|
9
|
+
defineService,
|
|
10
|
+
method,
|
|
11
|
+
methodSubject,
|
|
12
|
+
serviceName,
|
|
13
|
+
groupSubject,
|
|
14
|
+
SUBJECT_SEGMENT_RE,
|
|
15
|
+
} from "./contract.js";
|
|
16
|
+
export type { RpcMethodDef, ServiceContract } from "./contract.js";
|
|
17
|
+
|
|
18
|
+
// Client
|
|
19
|
+
export { createRpcClient } from "./client.js";
|
|
20
|
+
export type {
|
|
21
|
+
RpcCaller,
|
|
22
|
+
RpcCallOptions,
|
|
23
|
+
RpcClient,
|
|
24
|
+
RpcClientOptions,
|
|
25
|
+
} from "./types.js";
|
|
26
|
+
|
|
27
|
+
// Server
|
|
28
|
+
export { createRpcServer, ok, fail, newRequestId } from "./server.js";
|
|
29
|
+
export type {
|
|
30
|
+
RpcContext,
|
|
31
|
+
RpcResult,
|
|
32
|
+
RpcHandlers,
|
|
33
|
+
RpcAuthorize,
|
|
34
|
+
RpcServerOptions,
|
|
35
|
+
RpcServerHandle,
|
|
36
|
+
} from "./types.js";
|
|
37
|
+
|
|
38
|
+
// Errors
|
|
39
|
+
export {
|
|
40
|
+
RpcError,
|
|
41
|
+
isRpcError,
|
|
42
|
+
isDomainError,
|
|
43
|
+
isTransportError,
|
|
44
|
+
TRANSPORT_ERROR_CODES,
|
|
45
|
+
SERVICE_ERROR_CODES,
|
|
46
|
+
} from "./errors.js";
|
|
47
|
+
export type { RpcErrorKind, RpcErrorInit } from "./errors.js";
|
|
48
|
+
|
|
49
|
+
// Headers (exposed for consumers building their own low-level tooling, e.g.
|
|
50
|
+
// the interop test harness or a Go-side test client written in TS).
|
|
51
|
+
export {
|
|
52
|
+
HEADER_REQUEST_ID,
|
|
53
|
+
HEADER_CALLER_KIND,
|
|
54
|
+
HEADER_CALLER_ID,
|
|
55
|
+
HEADER_CALLER_ROLE,
|
|
56
|
+
HEADER_DEADLINE,
|
|
57
|
+
HEADER_CONTENT_TYPE,
|
|
58
|
+
HEADER_TRACEPARENT,
|
|
59
|
+
HEADER_TRACESTATE,
|
|
60
|
+
CONTENT_TYPE_JSON,
|
|
61
|
+
parseRoles,
|
|
62
|
+
parseRequestHeaders,
|
|
63
|
+
} from "./headers.js";
|
|
64
|
+
|
|
65
|
+
// Events
|
|
66
|
+
export { defineEvent, publishEvent, parseEvent } from "./events.js";
|
|
67
|
+
export type { EventContract, PublishEventOptions, CloudEvent } from "./events.js";
|
|
68
|
+
|
|
69
|
+
// KV
|
|
70
|
+
export { defineKvValue } from "./kv.js";
|
|
71
|
+
export type { KvValueContract } from "./kv.js";
|
|
72
|
+
|
|
73
|
+
// Re-export the ADR-32 error class from `@nats-io/services` so consumers can
|
|
74
|
+
// `instanceof`-check it without a direct dependency (mirrors the core
|
|
75
|
+
// barrel's `@nats-io/*` re-export rule).
|
|
76
|
+
export { ServiceError } from "@nats-io/services";
|
package/src/kv.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type z } from "zod";
|
|
2
|
+
|
|
3
|
+
const KV_KEY_PART_RE = /^[a-zA-Z0-9_-]+$/;
|
|
4
|
+
|
|
5
|
+
/** A typed KV value contract (§2.5, §4.9). No envelope; a breaking shape change means a new bucket. */
|
|
6
|
+
export interface KvValueContract<T extends z.ZodType> {
|
|
7
|
+
bucket: string;
|
|
8
|
+
value: T;
|
|
9
|
+
/** Builds a validated, dot-joined key from parts; each part must be `[a-zA-Z0-9_-]+`. */
|
|
10
|
+
key(...parts: string[]): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function defineKvValue<T extends z.ZodType>(def: {
|
|
14
|
+
bucket: string;
|
|
15
|
+
value: T;
|
|
16
|
+
}): KvValueContract<T> {
|
|
17
|
+
return {
|
|
18
|
+
bucket: def.bucket,
|
|
19
|
+
value: def.value,
|
|
20
|
+
key(...parts: string[]): string {
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (!KV_KEY_PART_RE.test(part)) {
|
|
23
|
+
throw new Error(`defineKvValue(${def.bucket}): invalid key part "${part}"`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const key = parts.join(".");
|
|
27
|
+
if (key.length === 0 || key.length > 255) {
|
|
28
|
+
throw new Error(`defineKvValue(${def.bucket}): key length out of bounds`);
|
|
29
|
+
}
|
|
30
|
+
return key;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|