@lunora/queue 1.0.0-alpha.4 → 1.0.0-alpha.41

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.
@@ -1,73 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
-
3
- const createDispatchLogger = (prefix) => {
4
- return {
5
- debug: (message, ...rest) => {
6
- console.debug(prefix, message, ...rest);
7
- },
8
- error: (message, ...rest) => {
9
- console.error(prefix, message, ...rest);
10
- },
11
- info: (message, ...rest) => {
12
- console.info(prefix, message, ...rest);
13
- },
14
- warn: (message, ...rest) => {
15
- console.warn(prefix, message, ...rest);
16
- }
17
- };
18
- };
19
-
20
- const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
21
- const trimTrailingSlashes = (value) => {
22
- let end = value.length;
23
- while (end > 0 && value[end - 1] === "/") {
24
- end -= 1;
25
- }
26
- return value.slice(0, end);
27
- };
28
- const createDispatchRunner = (options) => {
29
- const { label } = options;
30
- const globalFetch = globalThis.fetch;
31
- const fetchImpl = options.fetchImpl ?? (typeof globalFetch === "function" ? globalFetch.bind(globalThis) : void 0);
32
- return async (function_, args, runOptions = {}) => {
33
- if (typeof fetchImpl !== "function") {
34
- throw new TypeError(`${label}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);
35
- }
36
- const origin = options.env.LUNORA_ORIGIN_URL;
37
- if (typeof origin !== "string" || origin.length === 0) {
38
- throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);
39
- }
40
- const token = options.env.LUNORA_ADMIN_TOKEN;
41
- if (typeof token !== "string" || token.length === 0) {
42
- throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);
43
- }
44
- const url = `${trimTrailingSlashes(origin)}${SCHEDULER_DISPATCH_PATH}`;
45
- const response = await fetchImpl(url, {
46
- body: JSON.stringify({ args: args ?? {}, functionPath: function_.__lunoraRef, shardKey: runOptions.shardKey }),
47
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
48
- method: "POST"
49
- });
50
- if (!response.ok) {
51
- throw new LunoraError("INTERNAL", `${label}: function dispatch failed (${String(response.status)}): ${await response.text()}`);
52
- }
53
- const text = await response.text();
54
- if (text.length === 0) {
55
- return void 0;
56
- }
57
- try {
58
- return JSON.parse(text);
59
- } catch {
60
- return text;
61
- }
62
- };
63
- };
64
-
65
- const createQueueRunContext = (options) => {
66
- return {
67
- env: options.env,
68
- log: createDispatchLogger(`[queue:${options.exportName}]`),
69
- run: createDispatchRunner({ env: options.env, fetchImpl: options.fetchImpl, label: "@lunora/queue" })
70
- };
71
- };
72
-
73
- export { createQueueRunContext };
@@ -1,33 +0,0 @@
1
- const producerFor = (binding) => {
2
- return {
3
- send: async (body, options) => {
4
- await binding.send(body, options);
5
- },
6
- sendBatch: async (messages, options) => {
7
- await binding.sendBatch(messages, options);
8
- }
9
- };
10
- };
11
- const createQueues = (options) => {
12
- const bindings = options.bindings ?? {};
13
- const producers = /* @__PURE__ */ Object.create(null);
14
- for (const [exportName, binding] of Object.entries(bindings)) {
15
- producers[exportName] = producerFor(binding);
16
- }
17
- const known = Object.keys(producers);
18
- const missing = (name) => {
19
- const suffix = known.length === 0 ? "no queues are declared" : `known queues: ${known.join(", ")}`;
20
- const error = () => Promise.reject(new Error(`@lunora/queue: no queue named "${name}" (${suffix})`));
21
- return { send: error, sendBatch: error };
22
- };
23
- return /* @__PURE__ */ new Proxy(producers, {
24
- get(target, property) {
25
- if (typeof property !== "string") {
26
- return void 0;
27
- }
28
- return Object.hasOwn(target, property) ? target[property] : missing(property);
29
- }
30
- });
31
- };
32
-
33
- export { createQueues as default };
@@ -1,18 +0,0 @@
1
- const queueBindingName = (exportName) => `QUEUE_${exportName.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g, "_").toUpperCase()}`;
2
- const queueDefaultName = (exportName) => exportName.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g, "-").toLowerCase();
3
- const defineQueue = (config) => {
4
- const mode = config.mode ?? "push";
5
- if (mode !== "push" && mode !== "pull") {
6
- throw new TypeError(`defineQueue: \`mode\` must be "push" or "pull" (got ${JSON.stringify(config.mode)})`);
7
- }
8
- if (mode === "push" && typeof config.handler !== "function") {
9
- throw new TypeError('defineQueue: `handler` must be a function for a push consumer (omit it only when `mode: "pull"`)');
10
- }
11
- if (config.name !== void 0 && (typeof config.name !== "string" || config.name.length === 0)) {
12
- throw new TypeError("defineQueue: `name` must be a non-empty string when provided");
13
- }
14
- return { ...config, isLunoraQueue: true, mode };
15
- };
16
- const isQueueDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraQueue === true;
17
-
18
- export { defineQueue, isQueueDefinition, queueBindingName, queueDefaultName };
@@ -1,131 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
- import { createQueueRunContext } from './createQueueRunContext-_2hD-TK7.mjs';
3
-
4
- const DEFAULT_MAX_RETRIES = 3;
5
- const timestampToMs = (value) => {
6
- if (value instanceof Date) {
7
- return value.getTime();
8
- }
9
- const asNumber = typeof value === "number" ? value : Number(value);
10
- return Number.isFinite(asNumber) ? asNumber : 0;
11
- };
12
- const instrumentBatch = (batch) => {
13
- const dispositions = /* @__PURE__ */ new Map();
14
- const originals = batch.messages;
15
- const wrappedMessages = originals.map((message) => {
16
- return {
17
- ack: () => {
18
- dispositions.set(message, "ack");
19
- message.ack();
20
- },
21
- get attempts() {
22
- return message.attempts;
23
- },
24
- get body() {
25
- return message.body;
26
- },
27
- get id() {
28
- return message.id;
29
- },
30
- retry: (options) => {
31
- dispositions.set(message, "retry");
32
- message.retry(options);
33
- },
34
- get timestamp() {
35
- return message.timestamp;
36
- }
37
- };
38
- });
39
- const fillUndecided = (outcome) => {
40
- for (const message of originals) {
41
- if (!dispositions.has(message)) {
42
- dispositions.set(message, outcome);
43
- }
44
- }
45
- };
46
- const wrappedBatch = {
47
- ackAll: () => {
48
- fillUndecided("ack");
49
- batch.ackAll();
50
- },
51
- messages: wrappedMessages,
52
- queue: batch.queue,
53
- retryAll: (options) => {
54
- fillUndecided("retry");
55
- batch.retryAll(options);
56
- }
57
- };
58
- return { dispositions, originals, wrappedBatch };
59
- };
60
- const describeThrownError = (handlerError) => {
61
- if (handlerError instanceof Error) {
62
- return handlerError.message;
63
- }
64
- if (typeof handlerError === "string") {
65
- return handlerError;
66
- }
67
- if (handlerError !== null && typeof handlerError === "object") {
68
- try {
69
- return JSON.stringify(handlerError);
70
- } catch {
71
- return "[unserializable thrown value]";
72
- }
73
- }
74
- return String(handlerError);
75
- };
76
- const buildCaptureRecords = (harness, entry, queue, threw, handlerError) => {
77
- const errorMessage = threw ? describeThrownError(handlerError) : void 0;
78
- const maxRetries = typeof entry.definition.maxRetries === "number" ? entry.definition.maxRetries : DEFAULT_MAX_RETRIES;
79
- return harness.originals.map((message) => {
80
- const decided = harness.dispositions.get(message);
81
- const outcome = decided ?? (threw ? "error" : "ack");
82
- const attempts = typeof message.attempts === "number" ? message.attempts : 1;
83
- return {
84
- attempts,
85
- body: message.body,
86
- deadLettered: outcome !== "ack" && attempts >= maxRetries,
87
- error: outcome === "error" ? errorMessage : void 0,
88
- exportName: entry.exportName,
89
- messageId: message.id,
90
- outcome,
91
- queue,
92
- timestamp: timestampToMs(message.timestamp)
93
- };
94
- });
95
- };
96
- const dispatchQueueBatch = async (batch, registry, options) => {
97
- const entry = registry[batch.queue];
98
- if (entry === void 0) {
99
- const known = Object.keys(registry);
100
- const suffix = known.length === 0 ? "no push queues are declared" : `known push queues: ${known.join(", ")}`;
101
- throw new LunoraError("INTERNAL", `@lunora/queue: received a batch for queue "${batch.queue}" but no push handler is registered (${suffix})`);
102
- }
103
- const { handler } = entry.definition;
104
- if (typeof handler !== "function") {
105
- throw new TypeError(`@lunora/queue: queue "${batch.queue}" (${entry.exportName}) has no push handler — it is declared as a pull consumer`);
106
- }
107
- const context = createQueueRunContext({ env: options.env, exportName: entry.exportName, fetchImpl: options.fetchImpl });
108
- if (options.capture === void 0) {
109
- await handler(context, batch);
110
- return;
111
- }
112
- const harness = instrumentBatch(batch);
113
- let threw = false;
114
- let handlerError;
115
- try {
116
- await handler(context, harness.wrappedBatch);
117
- } catch (error) {
118
- threw = true;
119
- handlerError = error;
120
- }
121
- try {
122
- const records = buildCaptureRecords(harness, entry, batch.queue, threw, handlerError);
123
- await options.capture(records);
124
- } catch {
125
- }
126
- if (threw) {
127
- throw handlerError;
128
- }
129
- };
130
-
131
- export { dispatchQueueBatch };