tina4-nodejs 3.13.92 → 3.13.94

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 (134) hide show
  1. package/CLAUDE.md +16 -3
  2. package/README.md +1 -1
  3. package/package.json +12 -9
  4. package/packages/cli/dist/bin.js +1260 -969
  5. package/packages/core/dist/index.js +1260 -969
  6. package/packages/core/src/devMailbox.ts +20 -44
  7. package/packages/core/src/index.ts +2 -2
  8. package/packages/core/src/messenger.ts +72 -0
  9. package/packages/core/src/queueBackends/kafkaBackend.ts +108 -12
  10. package/packages/core/src/sessionHandlers/childError.ts +72 -0
  11. package/packages/core/src/sessionHandlers/mongoClient.ts +9 -3
  12. package/packages/core/src/sessionHandlers/redisHandler.ts +18 -5
  13. package/packages/core/src/sessionHandlers/respClient.ts +5 -1
  14. package/packages/frond/dist/index.js +74 -31
  15. package/packages/frond/src/engine.ts +99 -33
  16. package/packages/orm/dist/index.js +3055 -2764
  17. package/packages/orm/src/adapters/sqlite.ts +4 -1
  18. package/packages/orm/src/database.ts +108 -8
  19. package/types/cli/src/bin.d.ts +92 -0
  20. package/types/cli/src/commands/build.d.ts +2 -0
  21. package/types/cli/src/commands/generate.d.ts +47 -0
  22. package/types/cli/src/commands/init.d.ts +1 -0
  23. package/types/cli/src/commands/metrics.d.ts +6 -0
  24. package/types/cli/src/commands/migrate.d.ts +1 -0
  25. package/types/cli/src/commands/migrateCreate.d.ts +1 -0
  26. package/types/cli/src/commands/migrateRollback.d.ts +1 -0
  27. package/types/cli/src/commands/migrateStatus.d.ts +1 -0
  28. package/types/cli/src/commands/queue.d.ts +20 -0
  29. package/types/cli/src/commands/routes.d.ts +1 -0
  30. package/types/cli/src/commands/seed.d.ts +1 -0
  31. package/types/cli/src/commands/serve.d.ts +6 -0
  32. package/types/cli/src/commands/test.d.ts +1 -0
  33. package/types/core/src/ai.d.ts +64 -0
  34. package/types/core/src/api.d.ts +262 -0
  35. package/types/core/src/auth.d.ts +154 -0
  36. package/types/core/src/authGate.d.ts +20 -0
  37. package/types/core/src/background.d.ts +34 -0
  38. package/types/core/src/cache.d.ts +160 -0
  39. package/types/core/src/constants.d.ts +38 -0
  40. package/types/core/src/container.d.ts +44 -0
  41. package/types/core/src/context/chunker.d.ts +31 -0
  42. package/types/core/src/context/index.d.ts +93 -0
  43. package/types/core/src/devAdmin.d.ts +179 -0
  44. package/types/core/src/devMailbox.d.ts +54 -0
  45. package/types/core/src/docs.d.ts +141 -0
  46. package/types/core/src/docsAutoDiscovery.d.ts +6 -0
  47. package/types/core/src/dotenv.d.ts +65 -0
  48. package/types/core/src/env.d.ts +28 -0
  49. package/types/core/src/errorOverlay.d.ts +36 -0
  50. package/types/core/src/events.d.ts +75 -0
  51. package/types/core/src/fakeData.d.ts +55 -0
  52. package/types/core/src/feedback.d.ts +90 -0
  53. package/types/core/src/graphql.d.ts +207 -0
  54. package/types/core/src/health.d.ts +22 -0
  55. package/types/core/src/htmlElement.d.ts +75 -0
  56. package/types/core/src/i18n.d.ts +37 -0
  57. package/types/core/src/index.d.ts +93 -0
  58. package/types/core/src/job.d.ts +39 -0
  59. package/types/core/src/logger.d.ts +123 -0
  60. package/types/core/src/mcp.d.ts +248 -0
  61. package/types/core/src/messenger.d.ts +191 -0
  62. package/types/core/src/metrics.d.ts +77 -0
  63. package/types/core/src/middleware.d.ts +207 -0
  64. package/types/core/src/mqtt.d.ts +257 -0
  65. package/types/core/src/mqttMessage.d.ts +67 -0
  66. package/types/core/src/plan.d.ts +96 -0
  67. package/types/core/src/projectIndex.d.ts +56 -0
  68. package/types/core/src/queue.d.ts +219 -0
  69. package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
  70. package/types/core/src/queueBackends/liteBackend.d.ts +119 -0
  71. package/types/core/src/queueBackends/mongoBackend.d.ts +97 -0
  72. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
  73. package/types/core/src/rateLimiter.d.ts +49 -0
  74. package/types/core/src/request.d.ts +25 -0
  75. package/types/core/src/response.d.ts +28 -0
  76. package/types/core/src/routeDiscovery.d.ts +12 -0
  77. package/types/core/src/router.d.ts +355 -0
  78. package/types/core/src/scss.d.ts +19 -0
  79. package/types/core/src/server.d.ts +131 -0
  80. package/types/core/src/service.d.ts +115 -0
  81. package/types/core/src/session.d.ts +256 -0
  82. package/types/core/src/sessionHandlers/childError.d.ts +34 -0
  83. package/types/core/src/sessionHandlers/databaseHandler.d.ts +42 -0
  84. package/types/core/src/sessionHandlers/mongoClient.d.ts +24 -0
  85. package/types/core/src/sessionHandlers/mongoHandler.d.ts +61 -0
  86. package/types/core/src/sessionHandlers/redisHandler.d.ts +60 -0
  87. package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
  88. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
  89. package/types/core/src/static.d.ts +2 -0
  90. package/types/core/src/test.d.ts +94 -0
  91. package/types/core/src/testClient.d.ts +36 -0
  92. package/types/core/src/testing.d.ts +58 -0
  93. package/types/core/src/types.d.ts +219 -0
  94. package/types/core/src/validator.d.ts +52 -0
  95. package/types/core/src/websocket.d.ts +376 -0
  96. package/types/core/src/websocketBackplane.d.ts +166 -0
  97. package/types/core/src/websocketConnection.d.ts +54 -0
  98. package/types/core/src/wsdl.d.ts +101 -0
  99. package/types/frond/src/engine.d.ts +263 -0
  100. package/types/frond/src/index.d.ts +2 -0
  101. package/types/orm/src/adapters/firebird.d.ts +138 -0
  102. package/types/orm/src/adapters/mongodb.d.ts +81 -0
  103. package/types/orm/src/adapters/mssql.d.ts +70 -0
  104. package/types/orm/src/adapters/mysql.d.ts +66 -0
  105. package/types/orm/src/adapters/odbc.d.ts +97 -0
  106. package/types/orm/src/adapters/postgres.d.ts +85 -0
  107. package/types/orm/src/adapters/sqlite.d.ts +56 -0
  108. package/types/orm/src/autoCrud.d.ts +73 -0
  109. package/types/orm/src/baseModel.d.ts +391 -0
  110. package/types/orm/src/cachedDatabase.d.ts +177 -0
  111. package/types/orm/src/database.d.ts +609 -0
  112. package/types/orm/src/databaseResult.d.ts +85 -0
  113. package/types/orm/src/docstore.d.ts +182 -0
  114. package/types/orm/src/fakeData.d.ts +22 -0
  115. package/types/orm/src/index.d.ts +40 -0
  116. package/types/orm/src/migration.d.ts +275 -0
  117. package/types/orm/src/model.d.ts +7 -0
  118. package/types/orm/src/query.d.ts +14 -0
  119. package/types/orm/src/queryBuilder.d.ts +173 -0
  120. package/types/orm/src/realtime/index.d.ts +7 -0
  121. package/types/orm/src/realtime/models/attachment.d.ts +43 -0
  122. package/types/orm/src/realtime/models/channel.d.ts +32 -0
  123. package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
  124. package/types/orm/src/realtime/models/message.d.ts +36 -0
  125. package/types/orm/src/realtime/models/workspace.d.ts +26 -0
  126. package/types/orm/src/realtime/realtime.d.ts +24 -0
  127. package/types/orm/src/realtime/storage.d.ts +61 -0
  128. package/types/orm/src/seeder.d.ts +118 -0
  129. package/types/orm/src/sqlTranslator.d.ts +134 -0
  130. package/types/orm/src/types.d.ts +138 -0
  131. package/types/orm/src/validation.d.ts +6 -0
  132. package/types/swagger/src/generator.d.ts +46 -0
  133. package/types/swagger/src/index.d.ts +2 -0
  134. package/types/swagger/src/ui.d.ts +11 -0
@@ -0,0 +1,219 @@
1
+ import { type QueueJob } from "./job.js";
2
+ export { LiteBackend } from "./queueBackends/liteBackend.js";
3
+ export { type QueueJob } from "./job.js";
4
+ export interface QueueConfig {
5
+ backend?: string;
6
+ path?: string;
7
+ topic?: string;
8
+ maxRetries?: number;
9
+ /**
10
+ * Seconds to delay a failed job's automatic re-enqueue. 0 (the default)
11
+ * means retry immediately — the next pop()/consume() iteration picks it up
12
+ * straight away. Parity with Python's retry_backoff.
13
+ */
14
+ retryBackoff?: number;
15
+ /**
16
+ * Reservation/visibility timeout (seconds). A popped job is reserved for this
17
+ * long; if the consumer dies before complete()/fail() (crash, OOM, k8s
18
+ * eviction) the next pop() reclaims it — incrementing attempts and
19
+ * re-enqueuing, or dead-lettering past maxRetries (at-least-once delivery).
20
+ * Falls back to TINA4_QUEUE_VISIBILITY_TIMEOUT, else 300 (5 min). <= 0
21
+ * disables the reclaim (a reservation then lasts until the consumer acks —
22
+ * the old at-most-once behaviour). File + MongoDB backends only;
23
+ * RabbitMQ/Kafka delegate visibility to the broker. Parity with Python's
24
+ * visibility_timeout.
25
+ */
26
+ visibilityTimeout?: number;
27
+ }
28
+ export interface ProcessOptions {
29
+ pollInterval?: number;
30
+ maxJobs?: number;
31
+ maxRetries?: number;
32
+ batchSize?: number;
33
+ /**
34
+ * Override the queue's topic for this drain (parity with Python's
35
+ * process(handler, topic=...)). When set, process() retargets the queue so
36
+ * pop() reads the requested topic instead of the construction-time one.
37
+ */
38
+ topic?: string;
39
+ }
40
+ export interface ConsumeOptions {
41
+ /** Topic to consume (defaults to the constructor topic). */
42
+ topic?: string;
43
+ batchSize?: number;
44
+ pollInterval?: number;
45
+ iterations?: number;
46
+ id?: string;
47
+ }
48
+ export interface QueueBackendInterface {
49
+ push(queue: string, payload: unknown, delay?: number): string;
50
+ pop(queue: string): QueueJob | null;
51
+ size(queue: string): number;
52
+ clear(queue: string): void;
53
+ complete?(queue: string, id: string): void;
54
+ fail?(queue: string, id: string, error: string, maxRetries: number, retryBackoff: number): void;
55
+ retry?(queue: string, id: string, delaySeconds?: number): void;
56
+ deadLetters?(queue: string, maxRetries?: number): QueueJob[];
57
+ failed?(queue: string, maxRetries?: number): QueueJob[];
58
+ retryFailed?(queue: string, maxRetries?: number): number;
59
+ purge?(queue: string, status?: string): number;
60
+ }
61
+ export declare class Queue {
62
+ private backendName;
63
+ private basePath;
64
+ private topic;
65
+ private _maxRetries;
66
+ private _retryBackoff;
67
+ private _visibilityTimeout;
68
+ private externalBackend;
69
+ private liteBackend;
70
+ /**
71
+ * Unified Queue constructor.
72
+ *
73
+ * Accepts either:
74
+ * - new Queue({ topic: "tasks", backend: "rabbitmq" })
75
+ * - new Queue("rabbitmq", { path: "data/queue" }) // legacy
76
+ * - new Queue() // file backend, default topic
77
+ */
78
+ constructor(backendOrConfig?: string | QueueConfig, config?: QueueConfig);
79
+ /**
80
+ * Point this queue at ``topic`` in place.
81
+ *
82
+ * produce()/consume()/process() call this so a topic argument actually
83
+ * changes which topic is read or written. Without it the argument was
84
+ * accepted but ignored on the read path — pop() always used the
85
+ * construction-time topic, so consume("other") silently drained the wrong
86
+ * queue. The lite + external backends are topic-per-call (every push/pop/size
87
+ * takes the queue name), so changing this.topic retargets all of them; the
88
+ * job lifecycle (complete()/fail()/retry()) routes by the job's own .topic, so
89
+ * it is unaffected. Mirrors Python's Queue._retarget().
90
+ */
91
+ private retarget;
92
+ /**
93
+ * Add a job to the queue. Returns job ID.
94
+ *
95
+ * Can be called as:
96
+ * queue.push(payload) — uses constructor topic
97
+ * queue.push(payload, delay) — uses constructor topic with delay
98
+ * queue.push(payload, delay, priority) — with delay and priority
99
+ *
100
+ * @param priority — Higher value = higher priority. Default 0.
101
+ */
102
+ push(payload: unknown, delay?: number, priority?: number): string;
103
+ /**
104
+ * Atomically claim the next available job from this queue's topic. Returns null if empty.
105
+ */
106
+ pop(): QueueJob | null;
107
+ /**
108
+ * Pop up to count jobs at once. Returns a partial batch if fewer available.
109
+ */
110
+ popBatch(count: number): QueueJob[];
111
+ /**
112
+ * Process jobs from a queue with a handler function.
113
+ */
114
+ process(handler: (job: QueueJob | QueueJob[]) => Promise<void> | void, options?: ProcessOptions): void;
115
+ /**
116
+ * Count jobs filtered by status. Defaults to "pending".
117
+ */
118
+ size(status?: string): number;
119
+ /**
120
+ * Remove all jobs from this queue's topic. Returns the number cleared.
121
+ */
122
+ clear(): number;
123
+ /**
124
+ * Get jobs that failed at least once but are still being retried
125
+ * (0 < attempts < maxRetries). These live in the pending queue under the
126
+ * auto-retry lifecycle; dead-lettered jobs are returned by deadLetters().
127
+ */
128
+ failed(): QueueJob[];
129
+ /**
130
+ * Retry all dead letter jobs for this queue's topic.
131
+ * Moves failed jobs that exceeded max retries back to pending.
132
+ *
133
+ * @param delaySeconds - Optional delay before jobs become available
134
+ * @returns true if at least one job was re-queued, false if none found
135
+ */
136
+ retry(jobId?: string, delaySeconds?: number): boolean;
137
+ /**
138
+ * Get dead letter jobs — failed jobs that exceeded max retries.
139
+ */
140
+ deadLetters(maxRetries?: number): QueueJob[];
141
+ /**
142
+ * Delete messages by status (e.g. "completed", "failed", "dead").
143
+ */
144
+ purge(status: string, maxRetries?: number): number;
145
+ /**
146
+ * Re-queue failed jobs that haven't exceeded max retries back to pending.
147
+ */
148
+ retryFailed(maxRetries?: number): number;
149
+ /**
150
+ * Produce a message onto a topic. Convenience wrapper around push().
151
+ *
152
+ * Retargets to the requested topic (restoring the prior one afterwards) so it
153
+ * shares the same retarget path consume()/process() use — keeping produce and
154
+ * consume symmetric on the same topic argument.
155
+ */
156
+ produce(topic: string, payload: unknown, priority?: number, delay?: number): string;
157
+ /**
158
+ * Consume jobs from a topic using a generator (yield pattern).
159
+ *
160
+ * Usage:
161
+ * for (const job of queue.consume("emails")) {
162
+ * processEmail(job);
163
+ * }
164
+ *
165
+ * // Consume a specific job by ID:
166
+ * for (const job of queue.consume("emails", "job-id-123")) {
167
+ * processEmail(job);
168
+ * }
169
+ */
170
+ /**
171
+ * Long-running async generator that polls the queue continuously.
172
+ * When empty, sleeps for pollInterval ms before polling again.
173
+ * No external while-loop or sleep needed.
174
+ *
175
+ * @param topic Queue topic (defaults to constructor topic)
176
+ * @param id Optional job ID — single yield, no polling
177
+ * @param pollInterval Milliseconds to sleep when queue is empty (default 1000)
178
+ *
179
+ * Usage:
180
+ * for await (const job of queue.consume("emails")) { ... }
181
+ * for await (const job of queue.consume("emails", undefined, 5000)) { ... }
182
+ */
183
+ consume(topicOrOptions?: string | ConsumeOptions, id?: string, pollInterval?: number, iterations?: number, batchSize?: number): AsyncGenerator<QueueJob | QueueJob[]>;
184
+ /**
185
+ * Pop a specific job by ID from this queue's topic.
186
+ */
187
+ popById(id: string): QueueJob | null;
188
+ /**
189
+ * Get the configured topic name.
190
+ */
191
+ getTopic(): string;
192
+ getMaxRetries(): number;
193
+ getRetryBackoff(): number;
194
+ /**
195
+ * Resolved reservation/visibility timeout (seconds). <= 0 means the reclaim
196
+ * is disabled. File + MongoDB backends honour it; RabbitMQ/Kafka delegate to
197
+ * the broker.
198
+ */
199
+ getVisibilityTimeout(): number;
200
+ /**
201
+ * Record a failed attempt for a job. The backend increments `attempts`
202
+ * exactly once and decides whether to re-enqueue (attempts < maxRetries,
203
+ * after retryBackoff seconds) or dead-letter (attempts >= maxRetries).
204
+ */
205
+ _failJob(queue: string, job: QueueJob, error: string, maxRetries: number): void;
206
+ /**
207
+ * Re-queue a job back to the main queue directory with incremented attempts.
208
+ */
209
+ _retryJob(queue: string, job: QueueJob, delaySeconds?: number): void;
210
+ /**
211
+ * Acknowledge a completed job — drop its reservation so the visibility reclaim
212
+ * never re-delivers it. Routes to the active backend: a reservation-based
213
+ * external backend (MongoDB) acks there (without this its reserved doc would
214
+ * be re-delivered after the visibility window); RabbitMQ (no-ack on get) and
215
+ * Kafka (offset-based) expose no complete(), so the lite path is used and is a
216
+ * harmless no-op for them since they already acked/own redelivery.
217
+ */
218
+ _completeJob(queue: string, job: QueueJob): void;
219
+ }
@@ -0,0 +1,117 @@
1
+ import type { QueueJob } from "../queue.js";
2
+ export interface KafkaConfig {
3
+ brokers?: string;
4
+ groupId?: string;
5
+ /**
6
+ * Accepted for API parity with the file/MongoDB backends and IGNORED —
7
+ * consumer-group offsets own redelivery, so the framework-level visibility
8
+ * timeout does not apply here.
9
+ */
10
+ visibilityTimeout?: number;
11
+ }
12
+ /**
13
+ * librdkafka-style SSL/SASL client config (a TLS broker/proxy). Mirrors the
14
+ * keys produced by Python's `KafkaConnector._security_config`. Every key is
15
+ * optional — an unset env var leaves the key OUT (librdkafka defaults to the
16
+ * PLAINTEXT protocol with no SASL).
17
+ */
18
+ export interface KafkaSecurityConfig {
19
+ "security.protocol"?: string;
20
+ "ssl.ca.location"?: string;
21
+ "sasl.mechanism"?: string;
22
+ "sasl.username"?: string;
23
+ "sasl.password"?: string;
24
+ }
25
+ /** Resolved producer/consumer config — brokers, client id, and security keys. */
26
+ export interface KafkaClientConfig extends KafkaSecurityConfig {
27
+ "bootstrap.servers": string;
28
+ "client.id": string;
29
+ "group.id"?: string;
30
+ "auto.offset.reset"?: string;
31
+ "enable.auto.commit"?: boolean;
32
+ }
33
+ /**
34
+ * Build the SSL/SASL client config from the environment (for a TLS broker or
35
+ * proxy in front of Kafka). Each setting is read from the Tina4-namespaced env
36
+ * var FIRST (`TINA4_KAFKA_SECURITY_PROTOCOL` …) and falls back to the bare
37
+ * librdkafka-convention name (`KAFKA_SECURITY_PROTOCOL` …) that many Kafka
38
+ * deployments already set. Honours security.protocol (e.g. SSL, SASL_SSL),
39
+ * ssl.ca.location, and optional SASL (mechanism / username / password). Unset
40
+ * keys are omitted so librdkafka keeps its PLAINTEXT defaults.
41
+ *
42
+ * Exported for testing/introspection — and exact parity with Python's
43
+ * `_security_config` (same key set, same precedence, same omit-when-unset).
44
+ */
45
+ export declare function kafkaSecurityConfig(env?: NodeJS.ProcessEnv): KafkaSecurityConfig;
46
+ export interface QueueBackend {
47
+ push(queue: string, payload: unknown, delay?: number): string;
48
+ pop(queue: string): QueueJob | null;
49
+ size(queue: string): number;
50
+ clear(queue: string): void;
51
+ }
52
+ /**
53
+ * Kafka queue backend using raw Kafka protocol over TCP.
54
+ *
55
+ * Uses synchronous-style communication by spawning a child process
56
+ * for each operation, similar to the Redis session handler pattern.
57
+ */
58
+ export declare class KafkaBackend implements QueueBackend {
59
+ private brokers;
60
+ private groupId;
61
+ constructor(config?: KafkaConfig);
62
+ /**
63
+ * Resolved connection config — exposed for testing/introspection.
64
+ */
65
+ getConfig(): Required<Omit<KafkaConfig, "visibilityTimeout">>;
66
+ /**
67
+ * Resolved SSL/SASL client config from the environment (PLAINTEXT default).
68
+ * Mirrors Python's `KafkaConnector._security_config`.
69
+ */
70
+ securityConfig(): KafkaSecurityConfig;
71
+ /**
72
+ * Full producer config — brokers + client id + the resolved security block.
73
+ * The security keys are applied to BOTH producer and consumer (matching
74
+ * Python's `_connect_confluent`).
75
+ */
76
+ producerConfig(): KafkaClientConfig;
77
+ /**
78
+ * Full consumer config — brokers + client id + group id + the SAME resolved
79
+ * security block applied to the producer.
80
+ */
81
+ consumerConfig(): KafkaClientConfig;
82
+ /**
83
+ * Parse broker string into host:port.
84
+ */
85
+ private parseBroker;
86
+ /**
87
+ * Execute a Kafka operation synchronously via a child process.
88
+ *
89
+ * The wire protocol is hand-rolled (Tina4 is zero-dependency — no npm Kafka
90
+ * library). Produce uses Produce **v3** carrying a Kafka **v2 RecordBatch**
91
+ * (magic byte 2) with a **CRC-32C** (Castagnoli) checksum; Fetch uses Fetch
92
+ * **v4** and parses the v2 RecordBatch out of the response. Both formats are
93
+ * what a modern KRaft broker (apache/kafka 3.7.0) requires — the old
94
+ * Produce-v0 / message-format-v0 / CRC=0 batch is rejected by such brokers.
95
+ */
96
+ private execSync;
97
+ /**
98
+ * Sleep synchronously between produce retries.
99
+ *
100
+ * `push()` is synchronous (the whole backend drives its socket through a child
101
+ * process), so there is no event loop to await on. `Atomics.wait` on a
102
+ * SharedArrayBuffer is the stdlib way to block a thread for a fixed time --
103
+ * no dependency, no busy-wait burning CPU.
104
+ */
105
+ private static sleepSync;
106
+ /**
107
+ * Turn a sentinel from the protocol child into a thrown error, or return.
108
+ *
109
+ * The wording matches the Python and PHP backends exactly -- the parity rule
110
+ * covers user-visible error messages, not just behaviour.
111
+ */
112
+ private static assertNoError;
113
+ push(queue: string, payload: unknown, _delay?: number): string;
114
+ pop(queue: string): QueueJob | null;
115
+ size(_queue: string): number;
116
+ clear(_queue: string): void;
117
+ }
@@ -0,0 +1,119 @@
1
+ import { type QueueJob } from "../job.js";
2
+ import { type JobQueueBridge } from "../job.js";
3
+ export declare class LiteBackend {
4
+ private basePath;
5
+ private seq;
6
+ /**
7
+ * Reservation/visibility timeout (seconds). A popped job is held in reserved/
8
+ * with availableAt = now + visibilityTimeout. If the consumer dies before
9
+ * complete()/fail() (crash, OOM, k8s eviction) the next pop() reclaims it once
10
+ * the window expires — incrementing attempts and re-enqueuing, or
11
+ * dead-lettering past maxRetries. <= 0 disables the reclaim (a reservation
12
+ * then lasts until the consumer acks — the old at-most-once behaviour).
13
+ */
14
+ private visibilityTimeout;
15
+ constructor(basePath?: string, visibilityTimeout?: number);
16
+ private ensureDir;
17
+ private ensureFailedDir;
18
+ private ensureReservedDir;
19
+ private reservedPath;
20
+ private nowIso;
21
+ private futureIso;
22
+ private nextPrefix;
23
+ push(queue: string, payload: unknown, delay?: number, priority?: number): string;
24
+ /**
25
+ * Return [filename, jobData] for every pending, non-delayed job, ordered by
26
+ * the dequeue policy: highest priority first, ties broken oldest-first by
27
+ * createdAt. createdAt is an ISO-8601 string, so lexicographic comparison ==
28
+ * chronological order.
29
+ */
30
+ private availableCandidates;
31
+ /**
32
+ * Persist a reservation record so a dead consumer's job is reclaimable.
33
+ *
34
+ * Stores reservedAt + availableAt = now + visibilityTimeout. The next pop()
35
+ * reclaims this job once availableAt has passed (see reclaimExpired).
36
+ * complete()/fail()/retry() delete the record.
37
+ */
38
+ private writeReserved;
39
+ /**
40
+ * Return expired reservations to the queue (at-least-once delivery).
41
+ *
42
+ * A reserved job whose availableAt <= now means its consumer never
43
+ * acknowledged in time (crash / OOM / pod eviction). Atomically claim it
44
+ * (delete the reservation file), increment attempts, and either re-enqueue it
45
+ * (so the next pop picks it up) or dead-letter it once it has hit maxRetries.
46
+ * Disabled when visibilityTimeout <= 0.
47
+ */
48
+ private reclaimExpired;
49
+ pop(queue: string, bridge: JobQueueBridge): QueueJob | null;
50
+ popBatch(queue: string, bridge: JobQueueBridge, count: number): QueueJob[];
51
+ /**
52
+ * Delete a job's reservation record (best-effort).
53
+ */
54
+ private clearReservation;
55
+ /**
56
+ * Acknowledge a completed job — drop its reservation record so the visibility
57
+ * reclaim never re-delivers an already-acked job.
58
+ */
59
+ completeJob(queue: string, job: QueueJob): void;
60
+ private static readonly DEAD_STATES;
61
+ size(queue: string, status?: string): number;
62
+ clear(queue: string): number;
63
+ /**
64
+ * Jobs that have failed at least once but are still being retried.
65
+ *
66
+ * Under the auto-retry lifecycle a failed-but-retryable job lives in the
67
+ * pending queue (not the dead-letter dir), so this scans the queue dir for
68
+ * pending jobs with attempts > 0 that have not yet exhausted their retries.
69
+ * Dead-lettered jobs are returned by deadLetters().
70
+ */
71
+ failed(queue: string, maxRetries?: number): QueueJob[];
72
+ /**
73
+ * Revive a specific dead-letter job by id back to the pending queue.
74
+ *
75
+ * Manual override (Queue.retry(jobId) / job.retry()) — always revives a
76
+ * dead-letter regardless of attempt count. Returns false only if no
77
+ * dead-letter with that id exists.
78
+ */
79
+ retry(queue: string, jobId: string, delaySeconds?: number): boolean;
80
+ deadLetters(queue: string, maxRetries?: number): QueueJob[];
81
+ purge(queue: string, status: string, maxRetries?: number): number;
82
+ /**
83
+ * Re-queue dead-letter jobs that are under the (possibly raised) limit back
84
+ * to pending. Mirrors Python retry_failed(): a job dead-lettered at the
85
+ * original maxRetries needs a raised limit to qualify again.
86
+ */
87
+ retryFailed(queue: string, maxRetries?: number): number;
88
+ popById(queue: string, id: string): QueueJob | null;
89
+ /**
90
+ * Write the job back to the pending queue (queue dir).
91
+ *
92
+ * Re-enqueued jobs get a fresh createdAt so that within a priority tier they
93
+ * sort behind jobs that have not yet been attempted. `attempts` already
94
+ * reflects the latest failure count. The job carries its prior error.
95
+ */
96
+ private requeue;
97
+ /**
98
+ * Move the job to the dead-letter (failed/) directory. Terminal until a
99
+ * manual retryFailed()/retry() revives it.
100
+ */
101
+ private deadLetter;
102
+ /**
103
+ * Record a failed attempt.
104
+ *
105
+ * Increments `attempts` exactly once (the increment lives here, NOT in
106
+ * job.ts — see the double-increment fix). If the job still has retries left
107
+ * (attempts < maxRetries) it is automatically re-enqueued to pending, after
108
+ * an optional retryBackoff delay. Once it has been attempted maxRetries times
109
+ * (attempts >= maxRetries) it is moved to the dead-letter store.
110
+ */
111
+ failJob(queue: string, job: QueueJob, error: string, maxRetries: number, retryBackoff?: number): void;
112
+ /**
113
+ * Explicit re-queue requested by the caller (job.retry()).
114
+ *
115
+ * Always re-enqueues regardless of the retry limit — manual override,
116
+ * distinct from the automatic failJob() path.
117
+ */
118
+ retryJob(queue: string, job: QueueJob, delaySeconds?: number): void;
119
+ }
@@ -0,0 +1,97 @@
1
+ import type { QueueJob } from "../queue.js";
2
+ export interface MongoConfig {
3
+ host?: string;
4
+ port?: number;
5
+ uri?: string;
6
+ username?: string;
7
+ password?: string;
8
+ database?: string;
9
+ collection?: string;
10
+ /**
11
+ * Reservation/visibility timeout (seconds). A dequeued message is held
12
+ * reserved with availableAt = now + timeout; reclaim returns it once that
13
+ * passes (consumer died mid-flight, before complete()/fail()). <= 0 disables
14
+ * the reclaim. Falls back to TINA4_QUEUE_VISIBILITY_TIMEOUT, else 300.
15
+ */
16
+ visibilityTimeout?: number;
17
+ /** Max attempts before the reclaim dead-letters a job instead of re-delivering. */
18
+ maxRetries?: number;
19
+ }
20
+ export interface QueueBackend {
21
+ push(queue: string, payload: unknown, delay?: number): string;
22
+ pop(queue: string): QueueJob | null;
23
+ size(queue: string): number;
24
+ clear(queue: string): void;
25
+ }
26
+ /**
27
+ * MongoDB queue backend using the `mongodb` npm package.
28
+ *
29
+ * Uses synchronous-style communication by spawning a child process
30
+ * for each operation, similar to the RabbitMQ and Redis patterns.
31
+ * This keeps the interface synchronous as required by the Queue class.
32
+ */
33
+ export declare class MongoBackend implements QueueBackend {
34
+ private host;
35
+ private port;
36
+ private uri;
37
+ private username;
38
+ private password;
39
+ private database;
40
+ private collection;
41
+ private visibilityTimeout;
42
+ private maxRetries;
43
+ constructor(config?: MongoConfig);
44
+ /**
45
+ * Resolved connection config — exposed for testing/introspection.
46
+ */
47
+ getConfig(): {
48
+ uri: string;
49
+ database: string;
50
+ collection: string;
51
+ visibilityTimeout: number;
52
+ };
53
+ /**
54
+ * Resolved reservation/visibility timeout (seconds). <= 0 disables the
55
+ * reclaim. Exposed for testing/introspection.
56
+ */
57
+ getVisibilityTimeout(): number;
58
+ /**
59
+ * Build the Node script that performs one MongoDB queue operation in a child
60
+ * process. Exposed (not private) so tests can assert the visibility-timeout
61
+ * behaviour without a live MongoDB — the script's pop branch advances
62
+ * availableAt = now + visibilityTimeout and stamps reservedAt (the core fix),
63
+ * and the reclaim branch flips an expired { status: reserved } back to
64
+ * pending with attempts incremented (dead-lettering past maxRetries),
65
+ * disabled when visibilityTimeout <= 0.
66
+ */
67
+ buildScript(operation: string, queue: string, data?: string): string;
68
+ /**
69
+ * Execute a MongoDB operation synchronously via a child process.
70
+ */
71
+ private execSync;
72
+ push(queue: string, payload: unknown, delay?: number): string;
73
+ pop(queue: string): QueueJob | null;
74
+ size(queue: string): number;
75
+ clear(queue: string): void;
76
+ /**
77
+ * Acknowledge a completed job — drop its reservation so the reclaim never
78
+ * re-delivers it. Without this a Mongo-popped job stayed reserved and was
79
+ * re-delivered after the visibility window (the redelivery bug).
80
+ */
81
+ complete(queue: string, id: string): void;
82
+ /**
83
+ * Record a failed attempt: requeue (reset availableAt, ++attempts) while
84
+ * retries remain, else dead-letter. Mirrors the file/lite backend.
85
+ */
86
+ fail(queue: string, id: string, error: string, maxRetries: number, retryBackoff?: number): void;
87
+ /** Explicit manual re-queue (always re-enqueues regardless of the retry limit). */
88
+ retry(queue: string, id: string, delaySeconds?: number): void;
89
+ /** Jobs that exceeded max retries (the `<queue>.dead_letter` collection topic). */
90
+ deadLetters(queue: string, maxRetries?: number): QueueJob[];
91
+ /** Jobs that failed but are still eligible for retry (status=failed, attempts < max). */
92
+ failed(queue: string, maxRetries?: number): QueueJob[];
93
+ /** Revive dead-lettered jobs under the (possibly raised) limit. Returns count revived. */
94
+ retryFailed(queue: string, maxRetries?: number): number;
95
+ /** Remove jobs by status (default: every doc for the topic). Returns count removed. */
96
+ purge(queue: string, status?: string): number;
97
+ }
@@ -0,0 +1,55 @@
1
+ import type { QueueJob } from "../queue.js";
2
+ export interface RabbitMQConfig {
3
+ host?: string;
4
+ port?: number;
5
+ username?: string;
6
+ password?: string;
7
+ vhost?: string;
8
+ /**
9
+ * Accepted for API parity with the file/MongoDB backends and IGNORED — the
10
+ * broker owns redelivery (unacked messages requeue on channel close), so the
11
+ * framework-level visibility timeout does not apply here.
12
+ */
13
+ visibilityTimeout?: number;
14
+ }
15
+ /**
16
+ * Parse an AMQP URL (amqp://[user:pass@]host[:port][/vhost]) into a partial
17
+ * RabbitMQConfig. Mirrors the Python/PHP/Ruby `parse_amqp_url` semantics:
18
+ * strips a leading amqp:// or amqps:// scheme, splits optional credentials,
19
+ * and prepends a leading "/" to the vhost when missing. Only fields present
20
+ * in the URL are populated.
21
+ */
22
+ export declare function parseAmqpUrl(url: string): RabbitMQConfig;
23
+ export interface QueueBackend {
24
+ push(queue: string, payload: unknown, delay?: number): string;
25
+ pop(queue: string): QueueJob | null;
26
+ size(queue: string): number;
27
+ clear(queue: string): void;
28
+ }
29
+ /**
30
+ * RabbitMQ queue backend using raw AMQP 0-9-1 protocol.
31
+ *
32
+ * Uses synchronous-style communication by spawning a child process
33
+ * for each operation, similar to the Redis session handler pattern.
34
+ * This keeps the interface synchronous as required by the Queue class.
35
+ */
36
+ export declare class RabbitMQBackend implements QueueBackend {
37
+ private host;
38
+ private port;
39
+ private username;
40
+ private password;
41
+ private vhost;
42
+ constructor(config?: RabbitMQConfig);
43
+ /**
44
+ * Resolved connection config — exposed for testing/introspection.
45
+ */
46
+ getConfig(): Required<Omit<RabbitMQConfig, "visibilityTimeout">>;
47
+ /**
48
+ * Execute an AMQP operation synchronously via a child process.
49
+ */
50
+ private execSync;
51
+ push(queue: string, payload: unknown, _delay?: number): string;
52
+ pop(queue: string): QueueJob | null;
53
+ size(queue: string): number;
54
+ clear(queue: string): void;
55
+ }
@@ -0,0 +1,49 @@
1
+ import type { Middleware, Tina4Request, Tina4Response } from "./types.js";
2
+ /** Configuration for the rate limiter */
3
+ export interface RateLimiterConfig {
4
+ /** Maximum number of requests per window. Default: 100 (or TINA4_RATE_LIMIT env) */
5
+ limit?: number;
6
+ /** Window duration in seconds. Default: 60 (or TINA4_RATE_WINDOW env) */
7
+ windowSeconds?: number;
8
+ /** Cleanup interval in milliseconds. Default: 60000 (1 minute) */
9
+ cleanupIntervalMs?: number;
10
+ }
11
+ /**
12
+ * Create a rate limiter middleware using a sliding window algorithm.
13
+ * Tracks requests per IP in an in-memory Map.
14
+ *
15
+ * Response headers:
16
+ * X-RateLimit-Limit — Maximum requests per window
17
+ * X-RateLimit-Remaining — Requests remaining in the current window
18
+ * X-RateLimit-Reset — Unix timestamp (seconds) when the window resets
19
+ * Retry-After — Seconds to wait (only when rate limited)
20
+ *
21
+ * Returns 429 Too Many Requests when the limit is exceeded.
22
+ */
23
+ export declare function rateLimiter(config?: RateLimiterConfig): Middleware;
24
+ /** Rate limit check result */
25
+ export interface RateLimitResult {
26
+ allowed: boolean;
27
+ limit: number;
28
+ remaining: number;
29
+ reset: number;
30
+ retryAfter?: number;
31
+ }
32
+ /**
33
+ * Class-based rate limiter with check/reset/apply methods.
34
+ * Matches the Python/PHP/Ruby API surface.
35
+ */
36
+ export declare class RateLimiter {
37
+ readonly limit: number;
38
+ readonly window: number;
39
+ private store;
40
+ constructor(config?: RateLimiterConfig);
41
+ /** Check if a request from the given IP is allowed. */
42
+ check(ip: string): RateLimitResult;
43
+ /** Clear all tracked request data. */
44
+ reset(): void;
45
+ /** Apply rate limiting to a request/response pair. Sets headers and 429 if exceeded. */
46
+ apply(request: Tina4Request, response: Tina4Response): [Tina4Request, Tina4Response];
47
+ /** Middleware hook — enforces rate limiting before the route handler. */
48
+ beforeRateLimit(request: Tina4Request, response: Tina4Response): [Tina4Request, Tina4Response];
49
+ }
@@ -0,0 +1,25 @@
1
+ import type { IncomingMessage, IncomingHttpHeaders } from "node:http";
2
+ import type { Tina4Request, UploadedFile } from "./types.js";
3
+ /**
4
+ * Wrap Node's `IncomingHttpHeaders` in a Proxy so mixed-case lookups
5
+ * (`req.headers["Content-Type"]`) work alongside the canonical lowercase
6
+ * form Node already provides. Parity with PY-10-03 (Python ships a
7
+ * `CaseInsensitiveDict` for the same reason).
8
+ *
9
+ * The raw object is returned as-is by `Object.keys` / iteration — only
10
+ * string property reads/`in` checks are normalised.
11
+ */
12
+ export declare function makeCaseInsensitiveHeaders(raw: IncomingHttpHeaders): IncomingHttpHeaders;
13
+ export declare function createRequest(req: IncomingMessage): Tina4Request;
14
+ export declare class PayloadTooLargeError extends Error {
15
+ statusCode: number;
16
+ constructor(actual: number, limit: number);
17
+ }
18
+ /**
19
+ * Parse multipart/form-data body into fields and files.
20
+ * Zero-dependency implementation.
21
+ */
22
+ export declare function parseMultipart(body: Buffer, boundary: string): {
23
+ fields: Record<string, string>;
24
+ files: Record<string, UploadedFile | UploadedFile[]>;
25
+ };