@sequenceholdings/managed-functions 0.1.1

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.
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FUNCTION_EGRESS_SUMMARY = void 0;
4
+ exports.installFunctionTracing = installFunctionTracing;
5
+ exports.runWithFunctionTracing = runWithFunctionTracing;
6
+ const http_tracing_1 = require("./http-tracing");
7
+ const node_async_hooks_1 = require("node:async_hooks");
8
+ const promises_1 = require("node:timers/promises");
9
+ const api_1 = require("@opentelemetry/api");
10
+ const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
11
+ const invocation = new node_async_hooks_1.AsyncLocalStorage();
12
+ const provider = new sdk_trace_base_1.BasicTracerProvider();
13
+ const tracer = provider.getTracer('com.seqholdings.managed-functions', '1.0.0');
14
+ let originalFetch;
15
+ const MAX_BATCHES = 10;
16
+ const MAX_SPANS = 100;
17
+ const BATCH_DELAY_MS = 50;
18
+ const EXPORT_TIMEOUT_MS = 500;
19
+ const nanos = (ms) => (BigInt(ms) * 1000000n).toString();
20
+ function installFunctionTracing() {
21
+ if (originalFetch)
22
+ return;
23
+ originalFetch = globalThis.fetch.bind(globalThis);
24
+ const send = originalFetch;
25
+ globalThis.fetch = async (input, init) => {
26
+ const current = invocation.getStore();
27
+ if (!current || current.stopped)
28
+ return send(input, init);
29
+ const url = new URL(input instanceof Request ? input.url : input.toString());
30
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
31
+ const method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
32
+ const atlasRequest = url.origin === current.origin &&
33
+ (url.pathname.startsWith('/api/') || url.pathname.startsWith('/_data/'));
34
+ if (url.origin === current.origin &&
35
+ (url.pathname.startsWith('/api/') || url.pathname.startsWith('/_data/'))) {
36
+ for (const [key, value] of Object.entries(current.headers))
37
+ headers.set(key, value);
38
+ init = { ...init, headers, redirect: 'manual' };
39
+ if (!(0, http_tracing_1.shouldReportAtlasHttp)(url.pathname, method))
40
+ return send(input, init);
41
+ }
42
+ const operation = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(method)
43
+ ? method
44
+ : 'HTTP';
45
+ const span = tracer.startSpan(operation, { kind: api_1.SpanKind.CLIENT }, api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, {
46
+ ...current.parent,
47
+ traceFlags: 1,
48
+ isRemote: true,
49
+ }));
50
+ const started = Date.now();
51
+ let status = 0;
52
+ try {
53
+ const response = await send(input, init);
54
+ status = response.status;
55
+ span.setStatus({
56
+ code: status >= 400 ? api_1.SpanStatusCode.ERROR : api_1.SpanStatusCode.UNSET,
57
+ });
58
+ return response;
59
+ }
60
+ finally {
61
+ span.end();
62
+ if (!current.stopped &&
63
+ !current.finishing &&
64
+ current.captured < MAX_SPANS) {
65
+ current.captured++;
66
+ current.spans.push({
67
+ traceId: span.spanContext().traceId,
68
+ spanId: span.spanContext().spanId,
69
+ parentSpanId: current.parent.spanId,
70
+ name: operation,
71
+ kind: 3,
72
+ startTimeUnixNano: nanos(started),
73
+ endTimeUnixNano: nanos(Date.now()),
74
+ attributes: [
75
+ ...(atlasRequest
76
+ ? [
77
+ {
78
+ key: 'url.path',
79
+ value: {
80
+ stringValue: url.pathname.length <= 512
81
+ ? url.pathname
82
+ : '/api/[unmatched]',
83
+ },
84
+ },
85
+ {
86
+ key: 'com.seqholdings.atlas_api',
87
+ value: { stringValue: 'true' },
88
+ },
89
+ ]
90
+ : []),
91
+ { key: 'server.address', value: { stringValue: url.hostname } },
92
+ {
93
+ key: 'http.request.method',
94
+ value: {
95
+ stringValue: operation === 'HTTP' ? '_OTHER' : operation,
96
+ },
97
+ },
98
+ {
99
+ key: 'url.scheme',
100
+ value: { stringValue: url.protocol.slice(0, -1) },
101
+ },
102
+ ...(status
103
+ ? [
104
+ {
105
+ key: 'http.response.status_code',
106
+ value: { intValue: String(status) },
107
+ },
108
+ ]
109
+ : []),
110
+ ],
111
+ status: { code: status === 0 || status >= 400 ? 2 : 0 },
112
+ });
113
+ scheduleExport(current);
114
+ }
115
+ else if (!current.stopped)
116
+ current.dropped++;
117
+ }
118
+ };
119
+ }
120
+ async function runWithFunctionTracing(request, run, response) {
121
+ installFunctionTracing();
122
+ const read = (key) => {
123
+ const value = request.headers?.[key];
124
+ return typeof value === 'string' ? value : undefined;
125
+ };
126
+ const sequenceParent = read('x-sequence-traceparent');
127
+ const traceparent = sequenceParent ?? read('traceparent');
128
+ const match = traceparent?.match(/^00-([0-9a-f]{32})-([0-9a-f]{16})-01$/);
129
+ const attestation = read('x-sequence-trace-attestation');
130
+ const origin = read('x-sequence-trace-origin');
131
+ const principal = read('x-sequence-trace-principal');
132
+ if (!match || !traceparent || !attestation || !origin || !principal)
133
+ return run();
134
+ const current = {
135
+ parent: { traceId: match[1], spanId: match[2] },
136
+ headers: {
137
+ traceparent,
138
+ ...(sequenceParent === undefined
139
+ ? {}
140
+ : { 'x-sequence-traceparent': sequenceParent }),
141
+ 'x-sequence-trace-attestation': attestation,
142
+ },
143
+ origin,
144
+ principal,
145
+ spans: [],
146
+ dropped: 0,
147
+ captured: 0,
148
+ acknowledged: 0,
149
+ rejected: 0,
150
+ abandoned: 0,
151
+ timedOut: false,
152
+ batches: 0,
153
+ stopped: false,
154
+ abort: new AbortController(),
155
+ };
156
+ if (response) {
157
+ const end = response.end;
158
+ const onClose = () => {
159
+ stopExport(current);
160
+ void finishExport(current);
161
+ };
162
+ response.once('close', onClose);
163
+ response.end = function (...args) {
164
+ response.off('close', onClose);
165
+ response.end = end;
166
+ void finishExport(current);
167
+ if (!response.destroyed)
168
+ Reflect.apply(end, response, args);
169
+ return this;
170
+ };
171
+ }
172
+ try {
173
+ return await invocation.run(current, run);
174
+ }
175
+ catch (error) {
176
+ void finishExport(current);
177
+ throw error;
178
+ }
179
+ finally {
180
+ if (!response || current.finishing || response.destroyed)
181
+ void finishExport(current);
182
+ }
183
+ }
184
+ function scheduleExport(current) {
185
+ if (current.stopped ||
186
+ current.finishing ||
187
+ current.timer ||
188
+ current.inFlight ||
189
+ current.batches >= MAX_BATCHES - 1 ||
190
+ !current.spans.length)
191
+ return;
192
+ current.timer = setTimeout(() => {
193
+ current.timer = undefined;
194
+ startExport(current);
195
+ }, BATCH_DELAY_MS);
196
+ }
197
+ function startExport(current) {
198
+ if (current.inFlight)
199
+ return current.inFlight;
200
+ if (current.stopped || !current.spans.length)
201
+ return Promise.resolve();
202
+ const spans = current.spans.splice(0);
203
+ current.batches++;
204
+ const sending = exportEgress(current, spans).finally(() => {
205
+ current.inFlight = undefined;
206
+ scheduleExport(current);
207
+ });
208
+ current.inFlight = sending;
209
+ return sending;
210
+ }
211
+ function stopExport(current) {
212
+ current.stopped = true;
213
+ clearTimeout(current.timer);
214
+ current.timer = undefined;
215
+ current.abort.abort();
216
+ current.abandoned += current.spans.length;
217
+ current.spans.length = 0;
218
+ }
219
+ function finishExport(current) {
220
+ if (current.finishing)
221
+ return current.finishing;
222
+ clearTimeout(current.timer);
223
+ current.timer = undefined;
224
+ const started = Date.now();
225
+ const remaining = current.spans.splice(0);
226
+ current.finishing = Promise.all([
227
+ current.inFlight,
228
+ remaining.length ? exportEgress(current, remaining) : undefined,
229
+ ])
230
+ .catch(() => undefined)
231
+ .then(() => {
232
+ stopExport(current);
233
+ reportEgressDelivery({ current, finalExportMs: Date.now() - started });
234
+ });
235
+ return current.finishing;
236
+ }
237
+ exports.FUNCTION_EGRESS_SUMMARY = 'traces.function_egress.summary';
238
+ function reportEgressDelivery({ current, finalExportMs, }) {
239
+ if (!current.captured)
240
+ return;
241
+ try {
242
+ const unacknowledged = Math.max(0, current.captured -
243
+ current.acknowledged -
244
+ current.rejected -
245
+ current.abandoned);
246
+ const line = JSON.stringify({
247
+ message: exports.FUNCTION_EGRESS_SUMMARY,
248
+ metric: exports.FUNCTION_EGRESS_SUMMARY,
249
+ captured: current.captured,
250
+ acknowledged: current.acknowledged,
251
+ unacknowledged,
252
+ rejected: current.rejected,
253
+ abandoned: current.abandoned,
254
+ dropped: current.dropped,
255
+ exportTimedOut: current.timedOut,
256
+ finalExportMs,
257
+ });
258
+ if (current.rejected || current.abandoned || current.dropped)
259
+ console.warn(line);
260
+ else
261
+ console.info(line);
262
+ }
263
+ catch {
264
+ }
265
+ }
266
+ async function exportEgress(current, spans) {
267
+ if (!spans.length || !originalFetch || current.stopped)
268
+ return;
269
+ const body = JSON.stringify({
270
+ resourceSpans: [
271
+ {
272
+ resource: {
273
+ attributes: [
274
+ {
275
+ key: 'service.name',
276
+ value: { stringValue: 'managed-function' },
277
+ },
278
+ ],
279
+ },
280
+ scopeSpans: [
281
+ {
282
+ scope: {
283
+ name: 'com.seqholdings.managed-functions',
284
+ version: '1.0.0',
285
+ },
286
+ spans,
287
+ },
288
+ ],
289
+ },
290
+ ],
291
+ });
292
+ const timeout = new AbortController();
293
+ const timer = setTimeout(() => timeout.abort(), EXPORT_TIMEOUT_MS);
294
+ const signal = AbortSignal.any([current.abort.signal, timeout.signal]);
295
+ const send = originalFetch;
296
+ let onAbort;
297
+ const aborted = new Promise((resolve) => {
298
+ onAbort = resolve;
299
+ });
300
+ signal.addEventListener('abort', onAbort, { once: true });
301
+ const attempts = async () => {
302
+ let uncertain = false;
303
+ for (let attempt = 0; attempt < 2 && !signal.aborted; attempt++) {
304
+ try {
305
+ const response = await send(`${current.origin}/api/runtime-traces/v1/traces`, {
306
+ method: 'POST',
307
+ headers: {
308
+ ...current.headers,
309
+ 'x-sequence-trace-principal': current.principal,
310
+ 'Content-Type': 'application/json',
311
+ },
312
+ body,
313
+ redirect: 'error',
314
+ signal,
315
+ });
316
+ if (response.ok) {
317
+ const result = (await response.json().catch(() => ({})));
318
+ if (signal.aborted)
319
+ return;
320
+ const reported = Number(result.partialSuccess?.rejectedSpans ?? 0);
321
+ if (Number.isInteger(reported) && reported >= 0) {
322
+ const rejected = Math.min(reported, spans.length);
323
+ if (!uncertain)
324
+ current.rejected += rejected;
325
+ current.acknowledged += spans.length - rejected;
326
+ }
327
+ if (result.partialSuccess?.rejectedSpans)
328
+ console.warn('[traces] Egress spans outside capture scope', {
329
+ rejected: result.partialSuccess.rejectedSpans,
330
+ });
331
+ break;
332
+ }
333
+ await response.body?.cancel();
334
+ if (response.status !== 429 && response.status < 500) {
335
+ if (!uncertain)
336
+ current.rejected += spans.length;
337
+ break;
338
+ }
339
+ uncertain = true;
340
+ }
341
+ catch {
342
+ uncertain = true;
343
+ }
344
+ if (attempt === 0) {
345
+ try {
346
+ await (0, promises_1.setTimeout)(50, undefined, { signal });
347
+ }
348
+ catch {
349
+ break;
350
+ }
351
+ }
352
+ }
353
+ };
354
+ try {
355
+ await Promise.race([attempts(), aborted]);
356
+ }
357
+ finally {
358
+ if (timeout.signal.aborted)
359
+ current.timedOut = true;
360
+ signal.removeEventListener('abort', onAbort);
361
+ clearTimeout(timer);
362
+ }
363
+ }
@@ -0,0 +1,41 @@
1
+ import { checkServerIdentity } from 'node:tls';
2
+ import { type CacheStore } from './cache';
3
+ export interface ValkeyConnection {
4
+ host: string;
5
+ port: number;
6
+ serverIdentity: string;
7
+ caCertificates: string[];
8
+ keyPrefix: string;
9
+ }
10
+ export declare function valkeyTlsOptions(connection: ValkeyConnection): {
11
+ ca: string[];
12
+ servername: string | undefined;
13
+ rejectUnauthorized: boolean;
14
+ minVersion: "TLSv1.2";
15
+ checkServerIdentity: (_hostname: string, cert: Parameters<typeof checkServerIdentity>[1]) => Error | undefined;
16
+ };
17
+ export declare class ValkeyStore implements CacheStore {
18
+ private readonly connection;
19
+ private client;
20
+ private connecting;
21
+ private epoch;
22
+ private active;
23
+ private failures;
24
+ private openUntil;
25
+ private telemetry;
26
+ private readonly telemetryTimer;
27
+ private readonly auth;
28
+ constructor(connection: ValkeyConnection, local?: boolean);
29
+ private connect;
30
+ close(): void;
31
+ private resetConnection;
32
+ private execute;
33
+ get(key: string): Promise<string | null>;
34
+ set({ key, value, ttlSeconds, onlyIfAbsent, }: {
35
+ key: string;
36
+ value: string;
37
+ ttlSeconds: number;
38
+ onlyIfAbsent: boolean;
39
+ }): Promise<boolean>;
40
+ delete(key: string): Promise<boolean>;
41
+ }
package/dist/valkey.js ADDED
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ValkeyStore = void 0;
4
+ exports.valkeyTlsOptions = valkeyTlsOptions;
5
+ const node_net_1 = require("node:net");
6
+ const node_tls_1 = require("node:tls");
7
+ const google_auth_library_1 = require("google-auth-library");
8
+ const ioredis_1 = require("ioredis");
9
+ const cache_1 = require("./cache");
10
+ const COMMAND_TIMEOUT_MS = 150;
11
+ const CONNECT_TIMEOUT_MS = 1500;
12
+ const BREAKER_RESET_MS = 5000;
13
+ const MAX_PENDING = 128;
14
+ function valkeyTlsOptions(connection) {
15
+ return {
16
+ ca: connection.caCertificates,
17
+ servername: (0, node_net_1.isIP)(connection.serverIdentity)
18
+ ? undefined
19
+ : connection.serverIdentity,
20
+ rejectUnauthorized: true,
21
+ minVersion: 'TLSv1.2',
22
+ checkServerIdentity: (_hostname, cert) => (0, node_tls_1.checkServerIdentity)(connection.serverIdentity, cert),
23
+ };
24
+ }
25
+ class ValkeyStore {
26
+ connection;
27
+ client;
28
+ connecting;
29
+ epoch = 0;
30
+ active = 0;
31
+ failures = 0;
32
+ openUntil = 0;
33
+ telemetry = {
34
+ operations: 0,
35
+ errors: 0,
36
+ latencyTotalMs: 0,
37
+ latencyMaxMs: 0,
38
+ };
39
+ telemetryTimer;
40
+ auth;
41
+ constructor(connection, local = false) {
42
+ this.connection = connection;
43
+ if (!local && process.env.K_SERVICE) {
44
+ this.telemetryTimer = setInterval(() => {
45
+ if (this.telemetry.operations > 0)
46
+ console.info(JSON.stringify({
47
+ severity: 'INFO',
48
+ event: 'managed_function.cache',
49
+ ...this.telemetry,
50
+ }));
51
+ this.telemetry = {
52
+ operations: 0,
53
+ errors: 0,
54
+ latencyTotalMs: 0,
55
+ latencyMaxMs: 0,
56
+ };
57
+ }, 30000);
58
+ this.telemetryTimer.unref();
59
+ }
60
+ this.auth = local
61
+ ? new google_auth_library_1.GoogleAuth({
62
+ scopes: ['https://www.googleapis.com/auth/cloud-platform'],
63
+ })
64
+ : new google_auth_library_1.Compute({
65
+ scopes: ['https://www.googleapis.com/auth/cloud-platform'],
66
+ });
67
+ }
68
+ async connect() {
69
+ if (this.client?.status === 'ready')
70
+ return this.client;
71
+ if (this.connecting)
72
+ return this.connecting;
73
+ const epoch = this.epoch;
74
+ const connection = (async () => {
75
+ const access = await this.auth.getAccessToken();
76
+ const token = typeof access === 'string' ? access : access?.token;
77
+ if (!token || epoch !== this.epoch)
78
+ throw new cache_1.CacheError('UNAVAILABLE');
79
+ const client = new ioredis_1.Redis({
80
+ host: this.connection.host,
81
+ port: this.connection.port,
82
+ username: 'default',
83
+ password: token,
84
+ tls: valkeyTlsOptions(this.connection),
85
+ keyPrefix: this.connection.keyPrefix,
86
+ lazyConnect: true,
87
+ connectTimeout: CONNECT_TIMEOUT_MS,
88
+ commandTimeout: COMMAND_TIMEOUT_MS,
89
+ enableOfflineQueue: false,
90
+ enableReadyCheck: false,
91
+ retryStrategy: () => null,
92
+ maxRetriesPerRequest: 0,
93
+ autoResendUnfulfilledCommands: false,
94
+ autoResubscribe: false,
95
+ });
96
+ client.on('error', () => {
97
+ });
98
+ this.client = client;
99
+ await client.connect();
100
+ if (epoch !== this.epoch) {
101
+ client.disconnect();
102
+ throw new cache_1.CacheError('UNAVAILABLE');
103
+ }
104
+ return client;
105
+ })();
106
+ this.connecting = connection;
107
+ try {
108
+ return await connection;
109
+ }
110
+ finally {
111
+ if (this.connecting === connection)
112
+ this.connecting = undefined;
113
+ }
114
+ }
115
+ close() {
116
+ clearInterval(this.telemetryTimer);
117
+ this.resetConnection();
118
+ }
119
+ resetConnection() {
120
+ this.epoch++;
121
+ this.client?.disconnect();
122
+ this.client = undefined;
123
+ this.connecting = undefined;
124
+ }
125
+ async execute(mutation, command) {
126
+ if (Date.now() < this.openUntil || this.active >= MAX_PENDING)
127
+ throw new cache_1.CacheError('UNAVAILABLE');
128
+ this.active++;
129
+ const started = performance.now();
130
+ let sent = false;
131
+ let timer;
132
+ const deadline = this.client?.status === 'ready' ? COMMAND_TIMEOUT_MS : CONNECT_TIMEOUT_MS;
133
+ try {
134
+ const result = await Promise.race([
135
+ (async () => {
136
+ const client = await this.connect();
137
+ sent = true;
138
+ return command(client);
139
+ })(),
140
+ new Promise((_, reject) => {
141
+ timer = setTimeout(() => reject(new cache_1.CacheError('UNAVAILABLE')), deadline);
142
+ }),
143
+ ]);
144
+ this.failures = 0;
145
+ return result;
146
+ }
147
+ catch {
148
+ this.telemetry.errors++;
149
+ this.resetConnection();
150
+ if (++this.failures >= 3)
151
+ this.openUntil = Date.now() + BREAKER_RESET_MS;
152
+ throw new cache_1.CacheError(mutation && sent ? 'UNKNOWN_OUTCOME' : 'UNAVAILABLE');
153
+ }
154
+ finally {
155
+ clearTimeout(timer);
156
+ this.active--;
157
+ const elapsed = performance.now() - started;
158
+ this.telemetry.operations++;
159
+ this.telemetry.latencyTotalMs += elapsed;
160
+ this.telemetry.latencyMaxMs = Math.max(this.telemetry.latencyMaxMs, elapsed);
161
+ }
162
+ }
163
+ get(key) {
164
+ return this.execute(false, (client) => client.get(key));
165
+ }
166
+ async set({ key, value, ttlSeconds, onlyIfAbsent, }) {
167
+ return this.execute(true, async (client) => (onlyIfAbsent
168
+ ? await client.set(key, value, 'EX', ttlSeconds, 'NX')
169
+ : await client.set(key, value, 'EX', ttlSeconds)) === 'OK');
170
+ }
171
+ delete(key) {
172
+ return this.execute(true, async (client) => (await client.del(key)) > 0);
173
+ }
174
+ }
175
+ exports.ValkeyStore = ValkeyStore;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@sequenceholdings/managed-functions",
3
+ "version": "0.1.1",
4
+ "description": "Managed Function context and opt-in cache runtime",
5
+ "license": "UNLICENSED",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./config": {
14
+ "types": "./dist/config.d.ts",
15
+ "default": "./dist/config.js"
16
+ },
17
+ "./runtime": "./dist/runtime.cjs"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "dependencies": {
27
+ "google-auth-library": "^10.5.0",
28
+ "ioredis": "^5.10.0",
29
+ "zod": "^4.1.13",
30
+ "@opentelemetry/api": "^1.9.0",
31
+ "@opentelemetry/sdk-trace-base": "^2.5.1",
32
+ "@sequenceholdings/storage": "0.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.0.0",
36
+ "typescript": "^5.6.0",
37
+ "vitest": "^4.1.11",
38
+ "esbuild": "^0.25.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc && node scripts/bundle-runtime.mjs",
45
+ "type-check": "tsc --noEmit",
46
+ "test": "vitest run"
47
+ }
48
+ }