@volter/twin-temporal 0.1.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 (55) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +104 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +36 -0
  5. package/dist/src/generated/hpack.json +1 -0
  6. package/dist/src/generated/temporal-api.json +1 -0
  7. package/dist/src/index.d.ts +18 -0
  8. package/dist/src/index.js +70 -0
  9. package/dist/src/temporal-budget.d.ts +31 -0
  10. package/dist/src/temporal-budget.js +50 -0
  11. package/dist/src/temporal-capabilities.d.ts +7 -0
  12. package/dist/src/temporal-capabilities.js +650 -0
  13. package/dist/src/temporal-conformance.d.ts +8 -0
  14. package/dist/src/temporal-conformance.js +54 -0
  15. package/dist/src/temporal-connector.d.ts +32 -0
  16. package/dist/src/temporal-connector.js +121 -0
  17. package/dist/src/temporal-engine.d.ts +274 -0
  18. package/dist/src/temporal-engine.js +2132 -0
  19. package/dist/src/temporal-grpc.d.ts +51 -0
  20. package/dist/src/temporal-grpc.js +124 -0
  21. package/dist/src/temporal-h2.d.ts +40 -0
  22. package/dist/src/temporal-h2.js +644 -0
  23. package/dist/src/temporal-history.d.ts +27 -0
  24. package/dist/src/temporal-history.js +85 -0
  25. package/dist/src/temporal-http.d.ts +17 -0
  26. package/dist/src/temporal-http.js +160 -0
  27. package/dist/src/temporal-json.d.ts +7 -0
  28. package/dist/src/temporal-json.js +68 -0
  29. package/dist/src/temporal-proto.d.ts +17 -0
  30. package/dist/src/temporal-proto.js +46 -0
  31. package/dist/src/temporal-server.d.ts +25 -0
  32. package/dist/src/temporal-server.js +56 -0
  33. package/dist/src/temporal-twin.d.ts +25 -0
  34. package/dist/src/temporal-twin.js +100 -0
  35. package/dist/src/temporal-visibility.d.ts +49 -0
  36. package/dist/src/temporal-visibility.js +293 -0
  37. package/package.json +59 -0
  38. package/src/cli.ts +36 -0
  39. package/src/generated/hpack.json +1 -0
  40. package/src/generated/temporal-api.json +1 -0
  41. package/src/index.ts +81 -0
  42. package/src/temporal-budget.ts +75 -0
  43. package/src/temporal-capabilities.ts +679 -0
  44. package/src/temporal-conformance.ts +52 -0
  45. package/src/temporal-connector.ts +118 -0
  46. package/src/temporal-engine.ts +2176 -0
  47. package/src/temporal-grpc.ts +139 -0
  48. package/src/temporal-h2.ts +614 -0
  49. package/src/temporal-history.ts +99 -0
  50. package/src/temporal-http.ts +146 -0
  51. package/src/temporal-json.ts +77 -0
  52. package/src/temporal-proto.ts +53 -0
  53. package/src/temporal-server.ts +67 -0
  54. package/src/temporal-twin.ts +102 -0
  55. package/src/temporal-visibility.ts +245 -0
@@ -0,0 +1,139 @@
1
+ // THE gRPC WIRE — a DELIBERATELY THIN adapter (the smtp pack's rule, one layer up) over this pack's
2
+ // HTTP/2 framing (temporal-h2.ts).
3
+ //
4
+ // Everything this file knows is FRAMING: gRPC's length-prefixed messages over an HTTP/2 stream, the
5
+ // `grpc-timeout` header, and the status trailers (https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md).
6
+ // Which RPCs exist and what they answer is the twin's (`temporal-twin.ts` → `temporal-engine.ts`),
7
+ // so a test that calls the handler and a client on the socket run the same code.
8
+ //
9
+ // Loopback bind only, as smtp: this server fakes auth (Temporal's dev server has none either) and
10
+ // accepts any namespace's work, so it is never offered on another interface.
11
+ import type protobuf from 'protobufjs';
12
+ import type { H2Handler } from './temporal-h2.ts';
13
+ import { decodeMessage, encodeMessage, serviceMethods } from './temporal-proto.ts';
14
+
15
+ export const TEMPORAL_LOOPBACK_HOST = '127.0.0.1';
16
+
17
+ /** gRPC status codes (https://grpc.github.io/grpc/core/md_doc_statuscodes.html). */
18
+ export const GrpcStatus = {
19
+ OK: 0, CANCELLED: 1, UNKNOWN: 2, INVALID_ARGUMENT: 3, DEADLINE_EXCEEDED: 4, NOT_FOUND: 5, ALREADY_EXISTS: 6,
20
+ PERMISSION_DENIED: 7, RESOURCE_EXHAUSTED: 8, FAILED_PRECONDITION: 9, ABORTED: 10, OUT_OF_RANGE: 11,
21
+ UNIMPLEMENTED: 12, INTERNAL: 13, UNAVAILABLE: 14, DATA_LOSS: 15, UNAUTHENTICATED: 16,
22
+ } as const;
23
+
24
+ export class GrpcError extends Error {
25
+ readonly code: number;
26
+ constructor(code: number, message: string) {
27
+ super(message);
28
+ this.code = code;
29
+ }
30
+ }
31
+
32
+ export type GrpcCallContext = {
33
+ /** aborted when the client cancels the call or its deadline passes */
34
+ signal: AbortSignal;
35
+ /** the client's deadline as epoch ms, when it sent one */
36
+ deadline?: number;
37
+ metadata: Record<string, string>;
38
+ };
39
+
40
+ /** Answers one unary call; a method the handler does not serve throws UNIMPLEMENTED. */
41
+ export type GrpcHandler = (service: string, method: string, request: Record<string, unknown>, ctx: GrpcCallContext) => Promise<Record<string, unknown>>;
42
+
43
+ type MethodTable = Map<string, { requestType: protobuf.Type; responseType: protobuf.Type; requestStream: boolean; responseStream: boolean }>;
44
+
45
+ /** `grpc-timeout`: an integer and a unit (H M S m u n), PROTOCOL-HTTP2.md "Timeout". */
46
+ export function parseGrpcTimeout(value: string | undefined): number | undefined {
47
+ const match = /^(\d{1,8})([HMSmun])$/.exec(value ?? '');
48
+ if (!match) return undefined;
49
+ const amount = Number(match[1]);
50
+ const ms = { H: 3_600_000, M: 60_000, S: 1000, m: 1, u: 0.001, n: 0.000001 }[match[2] as 'H'];
51
+ return amount * ms;
52
+ }
53
+
54
+ /** Length-prefixed messages out of a request body: [compressed flag][u32 length][bytes]... */
55
+ export function splitGrpcFrames(body: Uint8Array): Array<{ compressed: boolean; message: Uint8Array }> {
56
+ const frames: Array<{ compressed: boolean; message: Uint8Array }> = [];
57
+ let offset = 0;
58
+ while (offset < body.length) {
59
+ if (body.length - offset < 5) throw new GrpcError(GrpcStatus.INTERNAL, 'truncated gRPC frame header');
60
+ const view = new DataView(body.buffer, body.byteOffset + offset, 5);
61
+ const length = view.getUint32(1);
62
+ if (body.length - offset - 5 < length) throw new GrpcError(GrpcStatus.INTERNAL, 'truncated gRPC message');
63
+ frames.push({ compressed: body[offset] === 1, message: body.subarray(offset + 5, offset + 5 + length) });
64
+ offset += 5 + length;
65
+ }
66
+ return frames;
67
+ }
68
+
69
+ export function grpcFrame(message: Uint8Array): Uint8Array {
70
+ const out = new Uint8Array(5 + message.length);
71
+ new DataView(out.buffer).setUint32(1, message.length);
72
+ out.set(message, 5);
73
+ return out;
74
+ }
75
+
76
+ /** grpc-message is percent-encoded (PROTOCOL-HTTP2.md "Status-Message"). */
77
+ export function encodeGrpcMessage(text: string): string {
78
+ return Array.from(new TextEncoder().encode(text), (byte) => (byte >= 0x20 && byte <= 0x7e && byte !== 0x25 ? String.fromCharCode(byte) : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`)).join('');
79
+ }
80
+
81
+ /** The HTTP/2 stream handler that answers gRPC for `services` (fully-qualified proto service names)
82
+ * through `handler`; the listener that feeds it sockets is temporal-server.ts's. */
83
+ export function grpcStreamHandler(options: { services: string[]; handler: GrpcHandler }): H2Handler {
84
+ const tables = new Map<string, MethodTable>(options.services.map((name) => [name, serviceMethods(name)]));
85
+ return (request, response) => {
86
+ const headers = request.headers;
87
+ const fail = (code: number, message: string): void => {
88
+ if (response.closed) return;
89
+ response.headers([[':status', '200'], ['content-type', 'application/grpc'], ['grpc-status', String(code)], ['grpc-message', encodeGrpcMessage(message)]], true);
90
+ };
91
+ const contentType = headers['content-type'] ?? '';
92
+ if (headers[':method'] !== 'POST' || !contentType.startsWith('application/grpc')) {
93
+ response.headers([[':status', '415'], ['content-type', 'text/plain']], true);
94
+ return;
95
+ }
96
+ const path = headers[':path'] ?? '';
97
+ const match = /^\/([^/]+)\/([^/]+)$/.exec(path);
98
+ const table = match ? tables.get(match[1]!) : undefined;
99
+ const method = match && table ? table.get(match[2]!) : undefined;
100
+ if (!match || !table || !method) { fail(GrpcStatus.UNIMPLEMENTED, `unknown method ${path}`); return; }
101
+ if (method.requestStream || method.responseStream) {
102
+ fail(GrpcStatus.UNIMPLEMENTED, `${match[1]}/${match[2]} is a streaming method; this server answers unary calls only`);
103
+ return;
104
+ }
105
+ const timeout = parseGrpcTimeout(headers['grpc-timeout']);
106
+ const controller = new AbortController();
107
+ const timer = timeout !== undefined ? setTimeout(() => controller.abort(), timeout) : undefined;
108
+ request.signal.addEventListener('abort', () => controller.abort(), { once: true });
109
+ const metadata: Record<string, string> = {};
110
+ for (const [key, value] of Object.entries(headers)) if (!key.startsWith(':')) metadata[key] = value;
111
+ let decoded: Record<string, unknown>;
112
+ try {
113
+ const frames = splitGrpcFrames(request.body);
114
+ if (frames.length !== 1) throw new GrpcError(GrpcStatus.INTERNAL, `a unary call carries exactly one message (got ${frames.length})`);
115
+ if (frames[0]!.compressed) throw new GrpcError(GrpcStatus.UNIMPLEMENTED, 'compressed messages are not accepted (this server advertises no grpc-encoding)');
116
+ decoded = decodeMessage(method.requestType, frames[0]!.message);
117
+ } catch (error) {
118
+ if (timer) clearTimeout(timer);
119
+ fail(error instanceof GrpcError ? error.code : GrpcStatus.INTERNAL, (error as Error).message);
120
+ return;
121
+ }
122
+ void (async () => {
123
+ try {
124
+ const answer = await options.handler(match[1]!, match[2]!, decoded, { signal: controller.signal, ...(timeout !== undefined ? { deadline: Date.now() + timeout } : {}), metadata });
125
+ if (response.closed) return;
126
+ const bytes = grpcFrame(encodeMessage(method.responseType, answer));
127
+ response.headers([[':status', '200'], ['content-type', 'application/grpc']]);
128
+ response.data(bytes);
129
+ response.trailers([['grpc-status', '0']]);
130
+ } catch (error) {
131
+ if (error instanceof GrpcError) fail(error.code, error.message);
132
+ else fail(GrpcStatus.INTERNAL, `internal error: ${(error as Error)?.message ?? String(error)}`);
133
+ } finally {
134
+ if (timer) clearTimeout(timer);
135
+ }
136
+ })();
137
+ };
138
+ }
139
+