@lunora/runtime 1.0.0-alpha.34 → 1.0.0-alpha.36

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,165 @@
1
+ const defaultSleep = (ms) => new Promise((resolve) => {
2
+ setTimeout(resolve, ms);
3
+ });
4
+ const sanitizeChange = (raw) => {
5
+ const table = typeof raw["table"] === "string" ? raw["table"] : "";
6
+ const rawOp = typeof raw["op"] === "string" ? raw["op"] : "";
7
+ const op = rawOp === "delete" || rawOp === "insert" || rawOp === "update" ? rawOp : "upsert";
8
+ const id = typeof raw["id"] === "string" ? raw["id"] : void 0;
9
+ const documentRow = raw["doc"] && typeof raw["doc"] === "object" ? raw["doc"] : void 0;
10
+ const seq = typeof raw["seq"] === "number" && Number.isFinite(raw["seq"]) ? raw["seq"] : void 0;
11
+ const ts = typeof raw["ts"] === "number" && Number.isFinite(raw["ts"]) ? raw["ts"] : void 0;
12
+ return {
13
+ op,
14
+ table,
15
+ ...documentRow === void 0 ? {} : { doc: documentRow },
16
+ ...id === void 0 ? {} : { id },
17
+ ...seq === void 0 ? {} : { seq },
18
+ ...ts === void 0 ? {} : { ts }
19
+ };
20
+ };
21
+ const deliverWithRetry = async (sink, batch, maxRetries, initialBackoffMs, maxBackoffMs, sleep) => {
22
+ let attempt = 0;
23
+ while (true) {
24
+ try {
25
+ await sink.deliver(batch);
26
+ return;
27
+ } catch (error) {
28
+ if (attempt >= maxRetries) {
29
+ throw error instanceof Error ? error : new Error(String(error));
30
+ }
31
+ const delay = Math.min(initialBackoffMs * 2 ** attempt, maxBackoffMs);
32
+ await sleep(delay);
33
+ attempt += 1;
34
+ }
35
+ }
36
+ };
37
+ const runExportTap = async (options) => {
38
+ const {
39
+ coordinator,
40
+ cursorStore,
41
+ headers,
42
+ initialBackoffMs = 100,
43
+ limit,
44
+ maxBackoffMs = 5e3,
45
+ maxRetries = 3,
46
+ shardDO,
47
+ sink,
48
+ sleep = defaultSleep,
49
+ tables
50
+ } = options;
51
+ const priorCursors = await cursorStore.read(sink.name);
52
+ const feed = await coordinator.orchestrateCdcSync(shardDO, { cursors: priorCursors, headers, limit, tables });
53
+ const nextCursors = { ...priorCursors };
54
+ const failures = [];
55
+ let delivered = 0;
56
+ let hasMore = false;
57
+ for (const shard of feed.shards) {
58
+ if (shard.error) {
59
+ failures.push({ error: shard.error.message, shardKey: shard.shardKey });
60
+ hasMore = true;
61
+ continue;
62
+ }
63
+ const rawChanges = shard.changes ?? [];
64
+ if (rawChanges.length === 0) {
65
+ nextCursors[shard.shardKey] = shard.cursor;
66
+ continue;
67
+ }
68
+ const changes = rawChanges.map((change) => sanitizeChange(change));
69
+ const batch = { changes, cursor: shard.cursor, shardKey: shard.shardKey, sink: sink.name };
70
+ try {
71
+ await deliverWithRetry(sink, batch, maxRetries, initialBackoffMs, maxBackoffMs, sleep);
72
+ nextCursors[shard.shardKey] = shard.cursor;
73
+ delivered += changes.length;
74
+ if (limit !== void 0 && rawChanges.length >= limit) {
75
+ hasMore = true;
76
+ }
77
+ } catch (error) {
78
+ failures.push({ error: error instanceof Error ? error.message : String(error), shardKey: shard.shardKey });
79
+ hasMore = true;
80
+ }
81
+ }
82
+ await cursorStore.write(sink.name, nextCursors);
83
+ return { cursors: nextCursors, delivered, failures, hasMore, shards: feed.shards.length };
84
+ };
85
+ const defineExportSink = (config) => {
86
+ if (typeof config.name !== "string" || config.name.length === 0) {
87
+ throw new Error("defineExportSink: `name` must be a non-empty string");
88
+ }
89
+ if (typeof config.deliver !== "function") {
90
+ throw new TypeError("defineExportSink: `deliver` must be a function");
91
+ }
92
+ return { deliver: config.deliver, name: config.name };
93
+ };
94
+ const encodeNdjson = (changes) => `${changes.map((change) => JSON.stringify(change)).join("\n")}
95
+ `;
96
+ const webhookExportSink = (config) => {
97
+ const fetchImpl = config.fetchImpl ?? ((input, init) => fetch(input, init));
98
+ return defineExportSink({
99
+ deliver: async (batch) => {
100
+ const response = await fetchImpl(config.url, {
101
+ body: encodeNdjson(batch.changes),
102
+ headers: {
103
+ "content-type": "application/x-ndjson",
104
+ "x-lunora-cursor": String(batch.cursor),
105
+ "x-lunora-shard": batch.shardKey,
106
+ "x-lunora-sink": batch.sink,
107
+ ...config.headers
108
+ },
109
+ method: "POST"
110
+ });
111
+ if (!response.ok) {
112
+ throw new Error(`webhook export sink "${config.name}" returned ${String(response.status)}`);
113
+ }
114
+ },
115
+ name: config.name
116
+ });
117
+ };
118
+ const r2Sink = (config) => {
119
+ let prefix = config.prefix ?? "cdc";
120
+ while (prefix.endsWith("/")) {
121
+ prefix = prefix.slice(0, -1);
122
+ }
123
+ return defineExportSink({
124
+ deliver: async (batch) => {
125
+ const key = `${prefix}/${batch.shardKey}/${String(batch.cursor)}.ndjson`;
126
+ await config.bucket.put(key, encodeNdjson(batch.changes), { httpMetadata: { contentType: "application/x-ndjson" } });
127
+ },
128
+ name: config.name
129
+ });
130
+ };
131
+ const createMemoryCursorStore = () => {
132
+ const state = {};
133
+ return {
134
+ read: (sink) => Promise.resolve({ ...state[sink] }),
135
+ snapshot: () => structuredClone(state),
136
+ write: (sink, cursors) => {
137
+ state[sink] = { ...cursors };
138
+ return Promise.resolve();
139
+ }
140
+ };
141
+ };
142
+ const createKvCursorStore = (kv, options) => {
143
+ const keyPrefix = options?.keyPrefix ?? "__lunora_source_cursor:export";
144
+ const keyFor = (sink) => `${keyPrefix}:${sink}`;
145
+ return {
146
+ read: async (sink) => {
147
+ const raw = await kv.get(keyFor(sink), "json");
148
+ if (raw === null || typeof raw !== "object") {
149
+ return {};
150
+ }
151
+ const cursors = {};
152
+ for (const [shardKey, value] of Object.entries(raw)) {
153
+ if (typeof value === "number" && Number.isFinite(value)) {
154
+ cursors[shardKey] = value;
155
+ }
156
+ }
157
+ return cursors;
158
+ },
159
+ write: async (sink, cursors) => {
160
+ await kv.put(keyFor(sink), JSON.stringify(cursors));
161
+ }
162
+ };
163
+ };
164
+
165
+ export { createKvCursorStore, createMemoryCursorStore, defineExportSink, r2Sink, runExportTap, sanitizeChange, webhookExportSink };
@@ -0,0 +1 @@
1
+ export { e as emitLogEvent, a as emitRpcEvent } from './observability--NOFYBFc.mjs';
@@ -0,0 +1,3 @@
1
+ const methodGuard = (request, allowed) => allowed.includes(request.method) ? void 0 : new Response(void 0, { headers: { allow: allowed.join(", ") }, status: 405 });
2
+
3
+ export { methodGuard as m };
@@ -0,0 +1,47 @@
1
+ const DEFAULT_TRACE_HEAD_RATE = 1;
2
+ const traceIdToUnitInterval = (traceId) => {
3
+ const int = Number.parseInt(traceId.slice(0, 8), 16);
4
+ return Number.isFinite(int) ? int / 4294967296 : 0;
5
+ };
6
+ const isTraceHeadSampled = (traceId, headRate = DEFAULT_TRACE_HEAD_RATE) => {
7
+ if (headRate >= 1) {
8
+ return true;
9
+ }
10
+ if (headRate <= 0) {
11
+ return false;
12
+ }
13
+ return traceIdToUnitInterval(traceId) < headRate;
14
+ };
15
+ const resolveTraceSampling = (config, traceId) => {
16
+ return {
17
+ isTraced: isTraceHeadSampled(traceId, config?.headRate ?? DEFAULT_TRACE_HEAD_RATE),
18
+ keepErrors: config?.alwaysSampleErrors ?? true
19
+ };
20
+ };
21
+ const shouldExportTrace = (decision, traceHasError) => {
22
+ return decision.isTraced || decision.keepErrors && traceHasError;
23
+ };
24
+
25
+ const emitRpcEvent = (sink, event, context, sampling) => {
26
+ if (!sink?.onRpc) {
27
+ return;
28
+ }
29
+ if (sampling !== void 0 && event.traceId !== void 0 && !shouldExportTrace(resolveTraceSampling(sampling, event.traceId), !event.ok)) {
30
+ return;
31
+ }
32
+ try {
33
+ sink.onRpc(event, context);
34
+ } catch {
35
+ }
36
+ };
37
+ const emitLogEvent = (sink, event, context) => {
38
+ if (!sink?.onLog) {
39
+ return;
40
+ }
41
+ try {
42
+ sink.onLog(event, context);
43
+ } catch {
44
+ }
45
+ };
46
+
47
+ export { emitRpcEvent as a, emitLogEvent as e, resolveTraceSampling as r };
@@ -0,0 +1,163 @@
1
+ const OTLP_SEVERITY = {
2
+ debug: 5,
3
+ // DEBUG
4
+ error: 17,
5
+ // ERROR
6
+ fatal: 21,
7
+ // FATAL
8
+ info: 9,
9
+ // INFO
10
+ log: 9,
11
+ // INFO
12
+ trace: 1,
13
+ // TRACE
14
+ warn: 13
15
+ // WARN
16
+ };
17
+ const otlpUnixNano = (ms) => `${String(Math.round(ms))}000000`;
18
+ const otlpRandomHex = (bytes) => {
19
+ const buffer = new Uint8Array(bytes);
20
+ crypto.getRandomValues(buffer);
21
+ let hex = "";
22
+ for (const byte of buffer) {
23
+ hex += byte.toString(16).padStart(2, "0");
24
+ }
25
+ return hex;
26
+ };
27
+ const HEX_ONLY = /^[0-9a-f]+$/;
28
+ const buildTraceparent = (traceId, spanId, sampled = true) => `00-${traceId}-${spanId}-${sampled ? "01" : "00"}`;
29
+ const parseTraceparent = (header) => {
30
+ if (header === null || header === void 0) {
31
+ return void 0;
32
+ }
33
+ const parts = header.trim().toLowerCase().split("-");
34
+ const [version, traceId, parentSpanId, flags] = parts;
35
+ if (parts.length < 4 || version === void 0 || version.length !== 2 || !HEX_ONLY.test(version) || version === "ff" || // Version 00 forbids trailing fields; only a future version may carry them.
36
+ version === "00" && parts.length !== 4 || traceId === void 0 || parentSpanId === void 0 || flags === void 0 || flags.length !== 2 || !HEX_ONLY.test(flags) || traceId.length !== 32 || parentSpanId.length !== 16 || !HEX_ONLY.test(traceId) || !HEX_ONLY.test(parentSpanId) || traceId === "00000000000000000000000000000000" || parentSpanId === "0000000000000000") {
37
+ return void 0;
38
+ }
39
+ return { parentSpanId, sampled: (Number.parseInt(flags, 16) & 1) === 1, traceId };
40
+ };
41
+ const encodeAttribute = (key, value) => {
42
+ if (typeof value === "boolean") {
43
+ return { key, value: { boolValue: value } };
44
+ }
45
+ if (typeof value === "number") {
46
+ if (!Number.isFinite(value)) {
47
+ return { key, value: { stringValue: String(value) } };
48
+ }
49
+ return Number.isSafeInteger(value) ? { key, value: { intValue: String(value) } } : { key, value: { doubleValue: value } };
50
+ }
51
+ return { key, value: { stringValue: value } };
52
+ };
53
+ const mergeHeaders = (defaults, overrides, token) => {
54
+ const merged = {};
55
+ const seen = /* @__PURE__ */ new Map();
56
+ const put = (name, value) => {
57
+ const lower = name.toLowerCase();
58
+ const existing = seen.get(lower);
59
+ if (existing === void 0) {
60
+ seen.set(lower, name);
61
+ merged[name] = value;
62
+ } else {
63
+ merged[existing] = value;
64
+ }
65
+ };
66
+ for (const [name, value] of Object.entries(defaults)) {
67
+ put(name, value);
68
+ }
69
+ for (const [name, value] of Object.entries(overrides ?? {})) {
70
+ put(name, value);
71
+ }
72
+ if (token !== void 0 && token.length > 0) {
73
+ put("authorization", `Bearer ${token}`);
74
+ }
75
+ return merged;
76
+ };
77
+ const buildResourceAttributes = (serviceName, extra) => {
78
+ const merged = { "service.name": serviceName };
79
+ for (const [key, value] of Object.entries(extra ?? {})) {
80
+ merged[key] = value;
81
+ }
82
+ return Object.entries(merged).map(([key, value]) => encodeAttribute(key, value));
83
+ };
84
+ const wrapResourceSpans = (span, scopeName, serviceName, resourceAttributes) => {
85
+ return {
86
+ resourceSpans: [
87
+ {
88
+ resource: { attributes: buildResourceAttributes(serviceName, resourceAttributes) },
89
+ scopeSpans: [{ scope: { name: scopeName }, spans: [span] }]
90
+ }
91
+ ]
92
+ };
93
+ };
94
+ const wrapResourceLogs = (logRecord, scopeName, serviceName, resourceAttributes) => {
95
+ return {
96
+ resourceLogs: [
97
+ {
98
+ resource: { attributes: buildResourceAttributes(serviceName, resourceAttributes) },
99
+ scopeLogs: [{ logRecords: [logRecord], scope: { name: scopeName } }]
100
+ }
101
+ ]
102
+ };
103
+ };
104
+ const wrapResourceMetrics = (metric, scopeName, serviceName, resourceAttributes) => {
105
+ return {
106
+ resourceMetrics: [
107
+ {
108
+ resource: { attributes: buildResourceAttributes(serviceName, resourceAttributes) },
109
+ scopeMetrics: [{ metrics: [metric], scope: { name: scopeName } }]
110
+ }
111
+ ]
112
+ };
113
+ };
114
+
115
+ const readerFromRecord = (environment) => (key) => {
116
+ const value = environment?.[key];
117
+ return typeof value === "string" && value.length > 0 ? value : void 0;
118
+ };
119
+ const stringProperty = (object, key) => {
120
+ if (typeof object !== "object" || object === null) {
121
+ return void 0;
122
+ }
123
+ const value = object[key];
124
+ return typeof value === "string" && value.length > 0 ? value : void 0;
125
+ };
126
+ const detectServiceResource = (read) => {
127
+ const detected = {};
128
+ const serviceVersion = read("SERVICE_VERSION") ?? read("CF_VERSION_METADATA") ?? read("VERCEL_GIT_COMMIT_SHA") ?? read("GITHUB_SHA") ?? read("COMMIT_SHA");
129
+ if (serviceVersion !== void 0) {
130
+ detected["service.version"] = serviceVersion;
131
+ }
132
+ const deploymentEnvironment = read("DEPLOYMENT_ENVIRONMENT") ?? read("ENVIRONMENT") ?? read("NODE_ENV");
133
+ if (deploymentEnvironment !== void 0) {
134
+ detected["deployment.environment"] = deploymentEnvironment;
135
+ }
136
+ return detected;
137
+ };
138
+ const detectCloudflareResource = (read, cf) => {
139
+ const isCloudflare = cf !== void 0 || read("CLOUDFLARE") !== void 0 || read("CF_ACCOUNT_ID") !== void 0;
140
+ if (!isCloudflare) {
141
+ return {};
142
+ }
143
+ const detected = { "cloud.provider": "cloudflare" };
144
+ const colo = stringProperty(cf, "colo") ?? read("CF_COLO") ?? read("CLOUDFLARE_COLO");
145
+ if (colo !== void 0) {
146
+ detected["cloud.region"] = colo;
147
+ }
148
+ return detected;
149
+ };
150
+ const mergeResourceAttributes = (...bags) => {
151
+ const merged = {};
152
+ for (const bag of bags) {
153
+ if (bag === void 0) {
154
+ continue;
155
+ }
156
+ for (const [key, value] of Object.entries(bag)) {
157
+ merged[key] = value;
158
+ }
159
+ }
160
+ return merged;
161
+ };
162
+
163
+ export { OTLP_SEVERITY as O, detectServiceResource as a, buildTraceparent as b, wrapResourceMetrics as c, detectCloudflareResource as d, wrapResourceLogs as e, encodeAttribute as f, otlpUnixNano as g, mergeHeaders as h, mergeResourceAttributes as m, otlpRandomHex as o, parseTraceparent as p, readerFromRecord as r, wrapResourceSpans as w };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "1.0.0-alpha.34",
3
+ "version": "1.0.0-alpha.36",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,8 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/bindings": "1.0.0-alpha.9",
50
- "@lunora/errors": "1.0.0-alpha.6"
49
+ "@lunora/bindings": "1.0.0-alpha.10",
50
+ "@lunora/errors": "1.0.0-alpha.7"
51
51
  },
52
52
  "engines": {
53
53
  "node": "^22.15.0 || >=24.11.0"
@@ -1,20 +0,0 @@
1
- const emitRpcEvent = (sink, event, context) => {
2
- if (!sink?.onRpc) {
3
- return;
4
- }
5
- try {
6
- sink.onRpc(event, context);
7
- } catch {
8
- }
9
- };
10
- const emitLogEvent = (sink, event, context) => {
11
- if (!sink?.onLog) {
12
- return;
13
- }
14
- try {
15
- sink.onLog(event, context);
16
- } catch {
17
- }
18
- };
19
-
20
- export { emitLogEvent, emitRpcEvent };
@@ -1,95 +0,0 @@
1
- const OTLP_SEVERITY = {
2
- debug: 5,
3
- // DEBUG
4
- error: 17,
5
- // ERROR
6
- fatal: 21,
7
- // FATAL
8
- info: 9,
9
- // INFO
10
- log: 9,
11
- // INFO
12
- trace: 1,
13
- // TRACE
14
- warn: 13
15
- // WARN
16
- };
17
- const otlpUnixNano = (ms) => `${String(Math.round(ms))}000000`;
18
- const otlpRandomHex = (bytes) => {
19
- const buffer = new Uint8Array(bytes);
20
- crypto.getRandomValues(buffer);
21
- let hex = "";
22
- for (const byte of buffer) {
23
- hex += byte.toString(16).padStart(2, "0");
24
- }
25
- return hex;
26
- };
27
- const buildTraceparent = (traceId, spanId) => `00-${traceId}-${spanId}-01`;
28
- const encodeAttribute = (key, value) => {
29
- if (typeof value === "boolean") {
30
- return { key, value: { boolValue: value } };
31
- }
32
- if (typeof value === "number") {
33
- if (!Number.isFinite(value)) {
34
- return { key, value: { stringValue: String(value) } };
35
- }
36
- return Number.isSafeInteger(value) ? { key, value: { intValue: String(value) } } : { key, value: { doubleValue: value } };
37
- }
38
- return { key, value: { stringValue: value } };
39
- };
40
- const mergeHeaders = (defaults, overrides, token) => {
41
- const merged = {};
42
- const seen = /* @__PURE__ */ new Map();
43
- const put = (name, value) => {
44
- const lower = name.toLowerCase();
45
- const existing = seen.get(lower);
46
- if (existing === void 0) {
47
- seen.set(lower, name);
48
- merged[name] = value;
49
- } else {
50
- merged[existing] = value;
51
- }
52
- };
53
- for (const [name, value] of Object.entries(defaults)) {
54
- put(name, value);
55
- }
56
- for (const [name, value] of Object.entries(overrides ?? {})) {
57
- put(name, value);
58
- }
59
- if (token !== void 0 && token.length > 0) {
60
- put("authorization", `Bearer ${token}`);
61
- }
62
- return merged;
63
- };
64
- const wrapResourceSpans = (span, scopeName, serviceName) => {
65
- return {
66
- resourceSpans: [
67
- {
68
- resource: { attributes: [encodeAttribute("service.name", serviceName)] },
69
- scopeSpans: [{ scope: { name: scopeName }, spans: [span] }]
70
- }
71
- ]
72
- };
73
- };
74
- const wrapResourceLogs = (logRecord, scopeName, serviceName) => {
75
- return {
76
- resourceLogs: [
77
- {
78
- resource: { attributes: [encodeAttribute("service.name", serviceName)] },
79
- scopeLogs: [{ logRecords: [logRecord], scope: { name: scopeName } }]
80
- }
81
- ]
82
- };
83
- };
84
- const wrapResourceMetrics = (metric, scopeName, serviceName) => {
85
- return {
86
- resourceMetrics: [
87
- {
88
- resource: { attributes: [encodeAttribute("service.name", serviceName)] },
89
- scopeMetrics: [{ metrics: [metric], scope: { name: scopeName } }]
90
- }
91
- ]
92
- };
93
- };
94
-
95
- export { OTLP_SEVERITY as O, wrapResourceMetrics as a, buildTraceparent as b, wrapResourceLogs as c, otlpUnixNano as d, encodeAttribute as e, mergeHeaders as m, otlpRandomHex as o, wrapResourceSpans as w };