lonnymq 0.0.6 → 0.0.8

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.
package/README.md CHANGED
@@ -1,20 +1,20 @@
1
1
  # LonnyMQ
2
2
 
3
- A high performance, multi-tenant Postgres message queue implementation for NodeJS/Typescript.
3
+ A high-performance, multi-tenant Postgres message queue implementation for Node.js/TypeScript.
4
4
 
5
5
  ## Features
6
6
 
7
- - High throughput.
8
- - Multi-tenant concurrency and capacity constraints.
9
- - Durable message processing.
10
- - Flexible message processing retries/deferrals.
11
- - Message de-duplication.
12
- - Queue actions as part of *existing* database transactions.
13
- - Database client agnostic.
14
- - Instant reactivity via `LISTEN/NOTIFY`.
15
- - Zero dependencies.
7
+ - High throughput
8
+ - Multi-tenant concurrency and capacity constraints
9
+ - Durable message processing
10
+ - Flexible message processing retries/deferrals
11
+ - Message de-duplication
12
+ - Queue actions as part of *existing* database transactions
13
+ - Database client agnostic
14
+ - Instant reactivity via `LISTEN/NOTIFY`
15
+ - Zero dependencies
16
16
 
17
- N.B. unlike other queue implementations, LonnyMQ provides direct access to queue methods vs. providing batteries-included Worker/Processor daemons.
17
+ N.B. Unlike other queue implementations, LonnyMQ provides direct access to queue methods rather than providing batteries-included Worker/Processor daemons.
18
18
 
19
19
  ## Quick Look
20
20
 
@@ -27,59 +27,84 @@ databaseClient satisfies DatabaseClient
27
27
 
28
28
  const queue = new Queue({ schema: "lonny" })
29
29
 
30
- for(let ix = 0; ix < 500; ix += 1) {
31
- queue
30
+ // Run migrations first
31
+ for (const migration of queue.migrations()) {
32
+ for (const sql of migration.sql) {
33
+ await databaseClient.query(sql, [])
34
+ }
35
+ }
36
+
37
+ // Create messages
38
+ for (let ix = 0; ix < 500; ix += 1) {
39
+ await queue
32
40
  .channel("myChannel")
33
41
  .message
34
42
  .create({
35
- content: "Hello",
43
+ content: Buffer.from("Hello"),
36
44
  databaseClient,
37
45
  })
38
46
  }
39
47
 
40
- while(true) {
48
+ // Process messages
49
+ while (true) {
41
50
  const dequeueResult = await queue.dequeue({ databaseClient })
42
- if(dequeueResult.resultType === "MESSAGE_NOT_AVAILABLE") {
43
- await sleep(Math.min(1_000, dequeueResult.retryMs))
51
+ if (dequeueResult.resultType === "MESSAGE_NOT_AVAILABLE") {
52
+ const sleepMs = Math.min(1000, dequeueResult.retryMs ?? 5000)
53
+ await new Promise(resolve => setTimeout(resolve, sleepMs))
44
54
  continue
45
55
  }
46
56
 
47
- console.log(dequeueResult.message.content)
57
+ console.log(dequeueResult.message.content.toString())
48
58
  await dequeueResult.message.delete({ databaseClient })
49
59
  }
50
60
  ```
51
61
 
52
62
  ## Setup & Installation
53
63
 
54
- LonnyMQ can be installed from npm via:
64
+ LonnyMQ can be installed from npm:
55
65
 
56
66
  ```bash
57
67
  npm install lonnymq
58
68
  ```
59
69
 
60
- Once the package is installed, we need to install the requisite DB machinery. LonnyMQ is agnostic to DB client/migration process and thus simply provides users an ordered list of "Migrations" - each containing a unique name and some SQL fragments to be executed.
70
+ Once the package is installed, you need to install the requisite database machinery. LonnyMQ is agnostic to database client and migration process, providing users with an ordered list of "Migrations" - each containing a unique name and SQL fragments to be executed.
61
71
 
62
72
  ```typescript
63
73
  const queue = new Queue({ schema: "lonny" })
64
74
  const migrations = queue.migrations()
75
+
76
+ // Execute migrations (in a transaction for safety)
77
+ await databaseClient.query("BEGIN")
78
+ try {
79
+ for (const migration of migrations) {
80
+ for (const sql of migration.sql) {
81
+ await databaseClient.query(sql, [])
82
+ }
83
+ }
84
+ await databaseClient.query("COMMIT")
85
+ } catch (error) {
86
+ await databaseClient.query("ROLLBACK")
87
+ throw error
88
+ }
65
89
  ```
66
90
 
67
- N.B. Migration SQL is not idempotent and thus these migrations should be executed in the context of a transaction that can be rolled back.
91
+ **Note:** Migration SQL is not idempotent and should be executed within a transaction that can be rolled back if needed.
68
92
 
69
93
  ## Channels
70
94
 
71
- Channels are the mechanism by which LonnyMQ provides multi-tenancy support. They can be considered lightweight sub-queues that are read from in a round-robin fashion. There is no performance penalty associated with using large numbers of channels and thus can be assigned on a highly granular (i.e. per-user) basis to ensure work is scheduled fairly.
95
+ Channels provide LonnyMQ's multi-tenancy support. They can be considered lightweight sub-queues that are read from in a round-robin fashion. There is no performance penalty associated with using large numbers of channels, so they can be assigned on a highly granular basis (e.g., per-user) to ensure work is scheduled fairly.
72
96
 
73
- Channels can be configured with concurrency and capacity limits by setting their "channel policy".
97
+ Channels can be configured with concurrency, capacity, and rate limits by setting their "channel policy":
74
98
 
75
99
  ```typescript
76
100
  await queue
77
101
  .channel("my-channel")
78
102
  .policy
79
103
  .set({
80
- maxConcurrency: 1,
81
- maxSize: null,
82
- databaseClient
104
+ databaseClient,
105
+ maxConcurrency: 1,
106
+ maxSize: 100,
107
+ releaseIntervalMs: 1000
83
108
  })
84
109
 
85
110
  // Remove all constraints:
@@ -91,7 +116,7 @@ await queue
91
116
 
92
117
  ## Message creation
93
118
 
94
- We can add a message to the queue (and assign it to a particular channel) with the `create` function:
119
+ You can add a message to the queue (and assign it to a particular channel) using the `create` function:
95
120
 
96
121
  ```typescript
97
122
  await queue
@@ -99,82 +124,132 @@ await queue
99
124
  .message
100
125
  .create({
101
126
  databaseClient,
102
- content: "Hello, world"
127
+ content: Buffer.from("Hello, world"),
128
+ name: "optional-dedup-key",
129
+ lockMs: 30000, // 30 seconds
130
+ delayMs: 5000 // 5 second delay
103
131
  })
104
132
  ```
105
133
 
106
- A `name` argument can be provided for de-duplication purposes: if a message that has _never_ been dequeued exists with the same name, and within the same channel, no new message will be created.
134
+ The `name` argument can be provided for de-duplication purposes: if a message that has *never* been dequeued exists with the same name within the same channel, no new message will be created.
135
+
136
+ ## Message Processing
107
137
 
108
- ## Message processing
138
+ Messages can be fetched for processing by calling `dequeue` on the `Queue` - this locks the message. Once processing has completed, messages must be "finalized" via **deletion** or **deferral** (for further processing in the future).
109
139
 
110
- A message can be fetched for processing by calling `dequeue` on the `Queue` - locking the message. Once processing has completed, messages can then be "finalized" via **deletion** or **deferral** (for further processing in the future).
140
+ ```typescript
141
+ const dequeueResult = await queue.dequeue({ databaseClient })
142
+
143
+ if (dequeueResult.resultType === "MESSAGE_DEQUEUED") {
144
+ const { message } = dequeueResult
145
+ console.log(`Processing message: ${message.id}`)
146
+ console.log(`Content: ${message.content.toString()}`)
147
+ console.log(`Attempts: ${message.numAttempts}`)
148
+
149
+ try {
150
+ // Process the message...
151
+ await processMessage(message.content)
152
+
153
+ // Delete on success
154
+ await message.delete({ databaseClient })
155
+ } catch (error) {
156
+ // Defer for retry with updated state
157
+ await message.defer({
158
+ databaseClient,
159
+ delays: 30000, // Retry in 30 seconds
160
+ state: Buffer.from(JSON.stringify({ error: error.message }))
161
+ })
162
+ }
163
+ }
164
+ ```
111
165
 
112
- When deferring a message, we can optionally specify `deferMs` and `state` arguments. `deferMs` tells the queue how long to wait before allowing the message to be re-processed, and `state` allows us to "save our working" and implement durable and/or repeating/scheduled tasks.
166
+ When deferring a message, you can optionally specify `delayMs` and `state` arguments. The `delayMs` parameter tells the queue how long to wait before allowing the message to be re-processed, and `state` allows you to "save your work" and implement durable and/or repeating/scheduled tasks.
113
167
 
114
- ### Graceful shutdowns and message sweeping
168
+ ### Graceful Shutdowns and Message Sweeping
115
169
 
116
- If your program ends unexpectedly, messages that are in the middle of being processed may well be "orphaned" in a locked state - causing channel blockages and reducing throughput. To mitigate this problem, it is imperative that we gracefully shutdown by catching unhandled exceptions and signals (i.e. `SIGINT`/`SIGTERM`) - finalizing all outstanding messages prior to exiting.
170
+ If your program ends unexpectedly, messages that are currently being processed may become "orphaned" in a locked state - causing channel blockages and reducing throughput. To mitigate this problem, it's imperative that you gracefully shutdown by catching unhandled exceptions and signals (i.e., `SIGINT`/`SIGTERM`) and finalizing all outstanding messages prior to exiting.
117
171
 
118
- That said, despite our best efforts, should we run out of memory, suffer a loss of power, or receieve a `SIGKILL`, we will be unable to finalize messages that are currently locked. To mitigate this, we set a `lockMs` during message creation (by default this is 1 hour) which limits the maximum amount of time a message can be locked before being available again for dequeue. This facilities ensures that no matter the nature of the shutdown, the queue will always un-clog itself.
172
+ That said, despite our best efforts, if we run out of memory, suffer a loss of power, or receive a `SIGKILL`, we will be unable to finalize messages that are currently locked. To mitigate this, we set a `lockMs` during message creation (by default this is 1 hour) which limits the maximum amount of time a message can be locked before being available again for dequeue. This facility ensures that no matter the nature of the shutdown, the queue will always recover automatically.
119
173
 
120
- ## Improvements on polling
174
+ ## Improvements on Polling
121
175
 
122
- The simplest approach for processing messages is to call `dequeue` in a loop, and backing off with a sleep when no messages are available. The downside with this approach is that we lose reactivity as we increase the polling timeout interval.
176
+ The simplest approach for processing messages is to call `dequeue` in a loop, backing off with a sleep when no messages are available. The downside with this approach is that we lose reactivity as we increase the polling timeout interval.
123
177
 
124
- To improve reactivity, we can use the `retryMs` returned when we fail to dequeue a message. This will either be `null` or tell us how long until the next message is available for processing (a message might be deferred for a period time). Thus, we can tune our sleep to use the minimum of our `retryMs` and a default poll timeout.
178
+ To improve reactivity, you can use the `retryMs` returned when failing to dequeue a message. This will either be `null` or tell you how long until the next message is available for processing (a message might be deferred for a period of time). Thus, you can tune your sleep to use the minimum of your `retryMs` and a default poll timeout.
125
179
 
126
- Unfortunately, this doesn't help us in situations where a message is created/deferred while a worker is sleeping. However, if we deploy LonnyMQ, with the `useWake` parameter enabled, message creations and deferrals will trigger a payload to the `queue.wakeChannel()` Postgres channel with the amount of milliseconds until said message becomes available for processing encoded as a string payload.
180
+ Unfortunately, this doesn't help in situations where a message is created/deferred while a worker is sleeping. However, if you deploy LonnyMQ with the `useWake` parameter enabled, message creations and deferrals will trigger a payload to the `queue.wakeChannel()` Postgres channel with the number of milliseconds until said message becomes available for processing encoded as a string payload.
127
181
 
128
182
  ```typescript
129
183
  const queue = new Queue({ schema: "lonny" })
130
184
  const migrations = queue.migrations({ useWake: true })
131
185
 
186
+ // Run migrations with wake enabled...
187
+
132
188
  // LISTEN/NOTIFY only works with a single connection - not on a connection pool.
133
- const client = await pool.connect()
134
- await client.query(`LISTEN "${dep.wakeChannel()}"`)
189
+ const client = await databaseClient.connect()
190
+ await client.query(`LISTEN "${queue.wakeChannel()}"`)
135
191
  client.on("notification", (msg) => {
136
192
  if (msg.channel === queue.wakeChannel()) {
137
193
  const deferMs = parseInt(msg.payload as string, 10)
138
194
  console.log(`Should wake in ${deferMs} ms`)
195
+ // Wake up your worker loop here
139
196
  }
140
197
  })
141
198
  ```
142
199
 
143
200
  ## Deadlocks
144
201
 
145
- If all queue actions are isolated to their own transaction, there is 0 risk of deadlocks occurring. That being said, it is _possible_ to safely bulk perform the following actions within a single transaction if we ensure they are performed in a consistent lexicographical ordering with respect to channel name and message name (if provided):
202
+ If all queue actions are isolated to their own transaction, there is zero risk of deadlocks occurring. That being said, it is *possible* to safely bulk-perform the following actions within a single transaction if we ensure they are performed in a consistent lexicographical ordering with respect to channel name and message name (if provided):
146
203
 
147
- - Message create
148
- - Channel policy set
149
- - Channel policy clear
204
+ - Message create
205
+ - Channel policy set
206
+ - Channel policy clear
150
207
 
151
208
  Beyond the actions specified above, it is manifestly **unsafe** to bulk-perform any of the remaining actions within a single transaction. Each of these actions should be isolated within their **own** transaction:
152
209
 
153
- - Message dequeue
154
- - Message defer
155
- - Message delete
210
+ - Message dequeue
211
+ - Message defer
212
+ - Message delete
213
+
214
+ ## Batching Operations
156
215
 
157
- To help with ensuring commands are ordered consistently, we can create a "Batch" object by calling:
216
+ When you need to perform multiple safe operations (message creation and channel policy changes) within a single transaction, LonnyMQ provides a batching mechanism that automatically handles proper ordering to prevent deadlocks.
217
+
218
+ The batch interface mirrors the main queue interface - you call `queue.batch()` to create a batch, then use the same `.channel(name).message.create()` and `.channel(name).policy.set/clear()` methods you're already familiar with. The key difference is that batch operations are queued up and don't execute immediately.
219
+
220
+ The batch system ensures that all operations are executed in a consistent lexicographical order based on channel name and message name, eliminating the possibility of deadlocks when multiple workers are performing bulk operations simultaneously.
158
221
 
159
222
  ```typescript
160
223
  const batch = queue.batch()
161
- ```
162
224
 
163
- This batch object provides a familiar API for message creation and channel policy mutations, but doesn't execute the underlying commands until the underlying batch is explicitly "executed".
225
+ batch.channel("user-123").message.create({
226
+ content: Buffer.from("Welcome email")
227
+ })
164
228
 
165
- ```typescript
166
- const results = [
167
- batch.channel("foo").message({ content: "hi" }),
168
- batch.channel("bar").policy.clear(),
169
- batch.channel("bar").message({ content: "hi", name: "foo" }),
170
- batch.channel("bar").message({ content: "hi" }),
171
- ]
172
- ```
173
- Prior to execution, the batch object will perform a sort to ensure actions are ordered consistently
229
+ batch.channel("user-123").policy.set({
230
+ maxConcurrency: 5,
231
+ maxSize: 1000,
232
+ releaseIntervalMs: 100
233
+ })
234
+
235
+ batch.channel("notifications").message.create({
236
+ content: Buffer.from("Daily digest"),
237
+ name: "daily-digest-2025-08-29"
238
+ })
239
+
240
+ batch.channel("analytics").policy.clear()
174
241
 
175
- ```typescript
176
242
  await batch.execute({ databaseClient })
243
+ ```
177
244
 
178
- // Get the 3rd command that was submitted to the batch
179
- console.log(await results[2].get())
245
+ ## Database Clients
246
+
247
+ LonnyMQ is designed to be database client agnostic, requiring only a minimal interface that most PostgreSQL clients already implement. Your database client must provide a single `query` method with this signature:
248
+
249
+ ```typescript
250
+ interface DatabaseClient {
251
+ query(sql: string, params: Array<unknown>): Promise<{
252
+ rows: Array<Record<string, unknown>>
253
+ }>
254
+ }
180
255
  ```
package/dist/index.cjs CHANGED
@@ -1,15 +1,15 @@
1
- var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=Object.prototype.hasOwnProperty;var x=new WeakMap,J=(e)=>{var n=x.get(e),a;if(n)return n;if(n=d({},"__esModule",{value:!0}),e&&typeof e==="object"||typeof e==="function")V(e).map((_)=>!K.call(n,_)&&d(n,_,{get:()=>e[_],enumerable:!(a=X(e,_))||a.enumerable}));return x.set(e,n),n};var j=(e,n)=>{for(var a in n)d(e,a,{get:n[a],enumerable:!0,configurable:!0,set:(_)=>n[a]=()=>_})};var re={};j(re,{Queue:()=>y,MessageDequeueCommand:()=>i,MessageDeleteCommand:()=>l,MessageDeferCommand:()=>h,MessageCreateCommand:()=>o,ChannelPolicySetCommand:()=>m,ChannelPolicyClearCommand:()=>r});module.exports=J(re);var E=(e)=>({nodeType:"VALUE",value:e}),t=(e)=>({nodeType:"REF",value:e}),Z=(e)=>({nodeType:"RAW",value:e}),ee=(e)=>{return`'${e.replace(/'/g,"''")}'`},ne=(e)=>{if(e===null)return"NULL";else if(typeof e==="string")return ee(e);else if(typeof e==="number")return e.toString();else if(typeof e==="boolean")return e?"TRUE":"FALSE";else if(e instanceof Date)return`'${e.toISOString()}'`;else if(typeof e==="bigint")return e.toString();else throw new Error(`Unsupported value type: ${typeof e}`)},te=(e)=>{return`"${e.replace(/"/g,'""')}"`},ae=(e)=>{if(e.nodeType==="VALUE")return ne(e.value);else if(e.nodeType==="REF")return te(e.value);else if(e.nodeType==="RAW")return e.value;else throw new Error("Unsupported SQL node type")};var s=(e,...n)=>{let a=[];for(let _=0;_<e.length;_+=1)if(a.push(e[_]),_<n.length)a.push(ae(n[_]));return Z(a.join(""))};class r{schema;channelName;createdAt;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.createdAt=new Date}sortKeyGet(){return JSON.stringify([this.channelName,null,this.createdAt.toISOString()])}async execute(e){await e.query(s`
1
+ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=Object.prototype.hasOwnProperty;var y=new WeakMap,J=(e)=>{var n=y.get(e),s;if(n)return n;if(n=d({},"__esModule",{value:!0}),e&&typeof e==="object"||typeof e==="function")V(e).map((_)=>!K.call(n,_)&&d(n,_,{get:()=>e[_],enumerable:!(s=X(e,_))||s.enumerable}));return y.set(e,n),n};var j=(e,n)=>{for(var s in n)d(e,s,{get:n[s],enumerable:!0,configurable:!0,set:(_)=>n[s]=()=>_})};var re={};j(re,{Queue:()=>x,MessageDequeueCommand:()=>i,MessageDeleteCommand:()=>l,MessageDeferCommand:()=>h,MessageCreateCommand:()=>o,ChannelPolicySetCommand:()=>m,ChannelPolicyClearCommand:()=>r});module.exports=J(re);var E=(e)=>({nodeType:"VALUE",value:e}),t=(e)=>({nodeType:"REF",value:e}),Z=(e)=>({nodeType:"RAW",value:e}),ee=(e)=>{return`'${e.replace(/'/g,"''")}'`},ne=(e)=>{if(e===null)return"NULL";else if(typeof e==="string")return ee(e);else if(typeof e==="number")return e.toString();else if(typeof e==="boolean")return e?"TRUE":"FALSE";else if(e instanceof Date)return`'${e.toISOString()}'`;else if(typeof e==="bigint")return e.toString();else throw new Error(`Unsupported value type: ${typeof e}`)},te=(e)=>{return`"${e.replace(/"/g,'""')}"`},se=(e)=>{if(e.nodeType==="VALUE")return ne(e.value);else if(e.nodeType==="REF")return te(e.value);else if(e.nodeType==="RAW")return e.value;else throw new Error("Unsupported SQL node type")};var a=(e,...n)=>{let s=[];for(let _=0;_<e.length;_+=1)if(s.push(e[_]),_<n.length)s.push(se(n[_]));return Z(s.join(""))};class r{schema;channelName;createdAt;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.createdAt=new Date}sortKeyGet(){return JSON.stringify([this.channelName,null,this.createdAt.toISOString()])}async execute(e){await e.query(a`
2
2
  SELECT 1 FROM ${t(this.schema)}."channel_policy_clear"(
3
3
  $1
4
4
  )
5
- `.value,[this.channelName])}}class m{schema;channelName;maxSize;maxConcurrency;releaseIntervalMs;createdAt;constructor(e){this.schema=e.schema,this.channelName=e.channelName;let n=e.maxConcurrency??null;this.maxConcurrency=n!==null?Math.max(0,n):null;let a=e.maxSize??null;this.maxSize=a!==null?Math.max(0,a):null;let _=e.releaseIntervalMs??null;this.releaseIntervalMs=_!==null?Math.max(0,_):null,this.createdAt=new Date}sortKeyGet(){return JSON.stringify([this.channelName,null,this.createdAt.toISOString()])}async execute(e){await e.query(s`
5
+ `.value,[this.channelName])}}class m{schema;channelName;maxSize;maxConcurrency;releaseIntervalMs;createdAt;constructor(e){this.schema=e.schema,this.channelName=e.channelName;let n=e.maxConcurrency??null;this.maxConcurrency=n!==null?Math.max(0,n):null;let s=e.maxSize??null;this.maxSize=s!==null?Math.max(0,s):null;let _=e.releaseIntervalMs??null;this.releaseIntervalMs=_!==null?Math.max(0,_):null,this.createdAt=new Date}sortKeyGet(){return JSON.stringify([this.channelName,null,this.createdAt.toISOString()])}async execute(e){await e.query(a`
6
6
  SELECT 1 FROM ${t(this.schema)}."channel_policy_set"(
7
7
  $1,
8
8
  $2::INTEGER,
9
9
  $3::INTEGER,
10
10
  $4::INTEGER
11
11
  )
12
- `.value,[this.channelName,this.maxSize,this.maxConcurrency,this.releaseIntervalMs])}}var R=(e)=>{return e*1000},se=(e)=>{return R(e*60)},f=(e)=>{return se(e*60)};var C=require("node:crypto");class p{value;constructor(e){this.value=e}toString(e){return C.createHash("sha256").update(e).update(this.value).digest("base64").replace(/=/g,"")}}var u=new p("WAKE"),$=!1,N=R(0),q=f(1);var g=require("path"),__filename="/home/runner/work/lonnymq/lonnymq/src/core/path.ts",_e=g.dirname(g.dirname(__filename)),ce=new RegExp(`^${_e}/`),c=(e)=>{return e.replace(ce,"")};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/04-function-message-create.ts";var F={name:c(__filename),sql:(e)=>{return[s`
12
+ `.value,[this.channelName,this.maxSize,this.maxConcurrency,this.releaseIntervalMs])}}var R=(e)=>{return e*1000},ae=(e)=>{return R(e*60)},f=(e)=>{return ae(e*60)};var C=require("node:crypto");class p{value;constructor(e){this.value=e}toString(e){return C.createHash("sha256").update(e).update(this.value).digest("base64").replace(/=/g,"")}}var u=new p("WAKE"),q=!1,N=R(0),$=f(1);var g=require("path"),__filename="/home/runner/work/lonnymq/lonnymq/src/core/path.ts",_e=g.dirname(g.dirname(__filename)),ce=new RegExp(`^${_e}/`),c=(e)=>{return e.replace(ce,"")};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/04-function-message-create.ts";var F={name:c(__filename),sql:(e)=>{return[a`
13
13
  CREATE FUNCTION ${t(e.schema)}."message_create" (
14
14
  p_id UUID,
15
15
  p_channel_name TEXT,
@@ -65,7 +65,8 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
65
65
  "max_size",
66
66
  "max_concurrency",
67
67
  "message_next_id",
68
- "message_next_dequeue_after"
68
+ "message_next_dequeue_after",
69
+ "message_next_seq_no"
69
70
  INTO v_channel_state;
70
71
 
71
72
  IF v_channel_state."current_size" >= v_channel_policy."max_size" THEN
@@ -95,6 +96,7 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
95
96
  "name" = EXCLUDED."name"
96
97
  RETURNING
97
98
  "id",
99
+ "seq_no",
98
100
  "dequeue_after"
99
101
  INTO v_message;
100
102
 
@@ -106,12 +108,14 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
106
108
 
107
109
  IF
108
110
  v_channel_state."message_next_id" IS NULL OR
109
- v_channel_state."message_next_dequeue_after" > v_message."dequeue_after"
111
+ v_channel_state."message_next_dequeue_after" > v_message."dequeue_after" OR
112
+ (v_channel_state."message_next_dequeue_after" = v_message."dequeue_after" AND v_channel_state."message_next_seq_no" > v_message."seq_no")
110
113
  THEN
111
114
  UPDATE ${t(e.schema)}."channel_state" SET
112
115
  "current_size" = v_channel_state."current_size" + 1,
113
116
  "message_next_id" = v_message."id",
114
- "message_next_dequeue_after" = GREATEST(v_now, v_message."dequeue_after")
117
+ "message_next_dequeue_after" = GREATEST(v_now, v_message."dequeue_after"),
118
+ "message_next_seq_no" = v_message."seq_no"
115
119
  WHERE "id" = v_channel_state."id";
116
120
  ELSE
117
121
  UPDATE ${t(e.schema)}."channel_state" SET
@@ -126,7 +130,7 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
126
130
  RETURN;
127
131
  END;
128
132
  $$ LANGUAGE plpgsql;
129
- `]}};var M=require("node:crypto");class o{schema;channelName;name;content;lockMs;id;delayMs;createdAt;constructor(e){let n=e.name??null,a=e.lockMs===void 0?q:Math.max(0,e.lockMs),_=e.delayMs===void 0?N:e.delayMs;this.id=M.randomUUID(),this.schema=e.schema,this.channelName=e.channelName,this.content=e.content,this.name=n,this.lockMs=a,this.delayMs=_,this.createdAt=new Date}async execute(e){let n=await e.query(s`
133
+ `]}};var M=require("node:crypto");class o{schema;channelName;name;content;lockMs;id;delayMs;createdAt;constructor(e){let n=e.name??null,s=e.lockMs===void 0?$:Math.max(0,e.lockMs),_=e.delayMs===void 0?N:e.delayMs;this.id=M.randomUUID(),this.schema=e.schema,this.channelName=e.channelName,this.content=e.content,this.name=n,this.lockMs=s,this.delayMs=_,this.createdAt=new Date}async execute(e){let n=await e.query(a`
130
134
  SELECT * FROM ${t(this.schema)}."message_create"(
131
135
  $1,
132
136
  $2,
@@ -135,7 +139,7 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
135
139
  $5::INTEGER,
136
140
  $6::INTEGER
137
141
  )
138
- `.value,[this.id,this.channelName,this.name,this.content,this.lockMs,this.delayMs]).then((a)=>a.rows[0]);if(n.result_code===1)return{resultType:"MESSAGE_DROPPED"};else if(n.result_code===2)return{resultType:"MESSAGE_DEDUPLICATED"};else if(n.result_code===0)return{resultType:"MESSAGE_CREATED"};else throw new Error("Unexpected result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/05-function-message-dequeue.ts";var G={name:c(__filename),sql:(e)=>{return[s`
142
+ `.value,[this.id,this.channelName,this.name,this.content,this.lockMs,this.delayMs]).then((s)=>s.rows[0]);if(n.result_code===1)return{resultType:"MESSAGE_DROPPED"};else if(n.result_code===2)return{resultType:"MESSAGE_DEDUPLICATED"};else if(n.result_code===0)return{resultType:"MESSAGE_CREATED"};else throw new Error("Unexpected result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/05-function-message-dequeue.ts";var G={name:c(__filename),sql:(e)=>{return[a`
139
143
  CREATE FUNCTION ${t(e.schema)}."message_dequeue" ()
140
144
  RETURNS TABLE (
141
145
  result_code INTEGER,
@@ -181,14 +185,12 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
181
185
 
182
186
  RETURN QUERY SELECT
183
187
  ${E(1)},
184
- NULL::BYTEA,
185
- NULL::BYTEA,
188
+ v_message_locked.content,
189
+ v_message_locked.state,
186
190
  JSON_BUILD_OBJECT(
187
191
  'id', v_message_locked.id,
188
192
  'channel_name', v_message_locked.channel_name,
189
- 'state', v_message_locked.state,
190
- 'name', v_message_locked.name
191
- 'content', v_message_locked.content,
193
+ 'name', v_message_locked.name,
192
194
  'num_attempts', v_message_locked.num_attempts
193
195
  );
194
196
  RETURN;
@@ -248,11 +250,12 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
248
250
 
249
251
  SELECT
250
252
  "message"."id",
251
- "message"."dequeue_after"
253
+ "message"."dequeue_after",
254
+ "message"."seq_no"
252
255
  FROM ${t(e.schema)}."message"
253
256
  WHERE NOT "is_locked"
254
257
  AND "channel_name" = v_message_dequeue."channel_name"
255
- ORDER BY "dequeue_after" ASC, "id" ASC
258
+ ORDER BY "dequeue_after" ASC, "seq_no" ASC
256
259
  LIMIT 1
257
260
  INTO v_message_next_dequeue;
258
261
 
@@ -265,7 +268,8 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
265
268
  UPDATE ${t(e.schema)}."channel_state" SET
266
269
  "current_concurrency" = v_channel_state."current_concurrency" + 1,
267
270
  "message_next_id" = v_message_next_dequeue."id",
268
- "message_next_dequeue_after" = v_message_next_dequeue_after
271
+ "message_next_dequeue_after" = v_message_next_dequeue_after,
272
+ "message_next_seq_no" = v_message_next_dequeue."seq_no"
269
273
  WHERE "id" = v_channel_state."id";
270
274
  ELSE
271
275
  UPDATE ${t(e.schema)}."channel_state" SET
@@ -288,14 +292,14 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
288
292
  RETURN;
289
293
  END;
290
294
  $$ LANGUAGE plpgsql;
291
- `]}};class i{schema;constructor(e){this.schema=e.schema}async execute(e){let n=await e.query(s`
295
+ `]}};class i{schema;constructor(e){this.schema=e.schema}async execute(e){let n=await e.query(a`
292
296
  SELECT
293
297
  result_code,
294
298
  metadata,
295
299
  content,
296
300
  state
297
301
  FROM ${t(this.schema)}."message_dequeue"()
298
- `.value,[]).then((a)=>a.rows[0]);if(n.result_code===0)return{resultType:"MESSAGE_NOT_AVAILABLE",retryMs:n.metadata.retry_ms};else if(n.result_code===1)return{resultType:"MESSAGE_DEQUEUED",message:{id:n.metadata.id,channelName:n.metadata.channel_name,name:n.metadata.name,content:n.content,state:n.state,numAttempts:n.metadata.num_attempts}};else throw new Error("Unexpected dequeue result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/06-function-message-delete.ts";var B={name:c(__filename),sql:(e)=>{return[s`
302
+ `.value,[]).then((s)=>s.rows[0]);if(n.result_code===0)return{resultType:"MESSAGE_NOT_AVAILABLE",retryMs:n.metadata.retry_ms};else if(n.result_code===1)return{resultType:"MESSAGE_DEQUEUED",message:{id:n.metadata.id,channelName:n.metadata.channel_name,name:n.metadata.name,content:n.content,state:n.state,numAttempts:n.metadata.num_attempts}};else throw new Error("Unexpected dequeue result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/06-function-message-delete.ts";var B={name:c(__filename),sql:(e)=>{return[a`
299
303
  CREATE FUNCTION ${t(e.schema)}."message_delete" (
300
304
  p_id UUID
301
305
  )
@@ -360,11 +364,11 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
360
364
  RETURN;
361
365
  END;
362
366
  $$ LANGUAGE plpgsql;
363
- `]}};class l{schema;id;constructor(e){this.schema=e.schema,this.id=e.id}async execute(e){let n=await e.query(s`
367
+ `]}};class l{schema;id;constructor(e){this.schema=e.schema,this.id=e.id}async execute(e){let n=await e.query(a`
364
368
  SELECT * FROM ${t(this.schema)}."message_delete"(
365
369
  $1
366
370
  )
367
- `.value,[this.id]).then((a)=>a.rows[0]);if(n.result_code===0)return{resultType:"MESSAGE_NOT_FOUND"};else if(n.result_code===1)return{resultType:"STATE_INVALID"};else if(n.result_code===2)return{resultType:"MESSAGE_DELETED"};else throw new Error("Unexpected result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/07-function-message-defer.ts";var P={name:c(__filename),sql:(e)=>{return[s`
371
+ `.value,[this.id]).then((s)=>s.rows[0]);if(n.result_code===0)return{resultType:"MESSAGE_NOT_FOUND"};else if(n.result_code===1)return{resultType:"STATE_INVALID"};else if(n.result_code===2)return{resultType:"MESSAGE_DELETED"};else throw new Error("Unexpected result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/07-function-message-defer.ts";var P={name:c(__filename),sql:(e)=>{return[a`
368
372
  CREATE FUNCTION ${t(e.schema)}."message_defer" (
369
373
  p_id UUID,
370
374
  p_delay_ms INTEGER,
@@ -381,7 +385,8 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
381
385
  SELECT
382
386
  "message"."id",
383
387
  "message"."channel_name",
384
- "message"."is_locked"
388
+ "message"."is_locked",
389
+ "message"."seq_no"
385
390
  FROM ${t(e.schema)}."message"
386
391
  WHERE "id" = p_id
387
392
  FOR UPDATE
@@ -400,7 +405,8 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
400
405
  SELECT
401
406
  "channel_state"."current_concurrency",
402
407
  "channel_state"."message_next_id",
403
- "channel_state"."message_next_dequeue_after"
408
+ "channel_state"."message_next_dequeue_after",
409
+ "channel_state"."message_next_seq_no"
404
410
  FROM ${t(e.schema)}."channel_state"
405
411
  WHERE "name" = v_message."channel_name"
406
412
  FOR UPDATE
@@ -410,12 +416,14 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
410
416
 
411
417
  IF
412
418
  v_channel_state."message_next_id" IS NULL OR
413
- v_channel_state."message_next_dequeue_after" > v_dequeue_after
419
+ v_channel_state."message_next_dequeue_after" > v_dequeue_after OR
420
+ (v_channel_state."message_next_dequeue_after" = v_dequeue_after AND v_channel_state."message_next_seq_no" > v_message."seq_no")
414
421
  THEN
415
422
  UPDATE ${t(e.schema)}."channel_state" SET
416
423
  "current_concurrency" = v_channel_state."current_concurrency" - 1,
417
424
  "message_next_id" = v_message."id",
418
- "message_next_dequeue_after" = v_dequeue_after
425
+ "message_next_dequeue_after" = v_dequeue_after,
426
+ "message_next_seq_no" = v_message."seq_no"
419
427
  WHERE "name" = v_message."channel_name";
420
428
  ELSE
421
429
  UPDATE ${t(e.schema)}."channel_state" SET
@@ -436,15 +444,15 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
436
444
  RETURN;
437
445
  END;
438
446
  $$ LANGUAGE plpgsql;
439
- `]}};class h{schema;id;delayMs;state;constructor(e){let n=e.delayMs===void 0?N:e.delayMs;this.schema=e.schema,this.id=e.id,this.delayMs=n,this.state=e.state??null}async execute(e){let n=await e.query(s`
447
+ `]}};class h{schema;id;delayMs;state;constructor(e){let n=e.delayMs===void 0?N:e.delayMs;this.schema=e.schema,this.id=e.id,this.delayMs=n,this.state=e.state??null}async execute(e){let n=await e.query(a`
440
448
  SELECT * FROM ${t(this.schema)}."message_defer"(
441
449
  $1,
442
450
  $2,
443
451
  $3
444
452
  )
445
- `.value,[this.id,this.delayMs,this.state]).then((a)=>a.rows[0]);if(n.result_code===0)return{resultType:"MESSAGE_NOT_FOUND"};else if(n.result_code===1)return{resultType:"STATE_INVALID"};else if(n.result_code===2)return{resultType:"MESSAGE_DEFERRED"};else throw new Error("Unexpected result")}}var H=(e)=>{let n=e.split(`
446
- `),a=Number.MAX_SAFE_INTEGER;for(let _ of n){if(_.trim().length===0)continue;let T=_.search(/\S/);a=Math.min(a,T)}return n.map((_)=>_.slice(a)).join(`
447
- `).trim()};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/00-table-channel-policy.ts",k={name:c(__filename),sql:(e)=>{return[s`
453
+ `.value,[this.id,this.delayMs,this.state]).then((s)=>s.rows[0]);if(n.result_code===0)return{resultType:"MESSAGE_NOT_FOUND"};else if(n.result_code===1)return{resultType:"STATE_INVALID"};else if(n.result_code===2)return{resultType:"MESSAGE_DEFERRED"};else throw new Error("Unexpected result")}}var H=(e)=>{let n=e.split(`
454
+ `),s=Number.MAX_SAFE_INTEGER;for(let _ of n){if(_.trim().length===0)continue;let T=_.search(/\S/);s=Math.min(s,T)}return n.map((_)=>_.slice(s)).join(`
455
+ `).trim()};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/00-table-channel-policy.ts",k={name:c(__filename),sql:(e)=>{return[a`
448
456
  CREATE TABLE ${t(e.schema)}."channel_policy" (
449
457
  "id" UUID NOT NULL DEFAULT GEN_RANDOM_UUID(),
450
458
  "name" TEXT NOT NULL,
@@ -453,10 +461,10 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
453
461
  "release_interval_ms" INTEGER,
454
462
  PRIMARY KEY ("id")
455
463
  );
456
- `,s`
464
+ `,a`
457
465
  CREATE UNIQUE INDEX "channel_policy_name_ux"
458
466
  ON ${t(e.schema)}."channel_policy" ("name");
459
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/01-table-channel-state.ts",b={name:c(__filename),sql:(e)=>{return[s`
467
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/01-table-channel-state.ts",b={name:c(__filename),sql:(e)=>{return[a`
460
468
  CREATE TABLE ${t(e.schema)}."channel_state" (
461
469
  "id" UUID NOT NULL DEFAULT GEN_RANDOM_UUID(),
462
470
  "name" TEXT NOT NULL,
@@ -467,21 +475,23 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
467
475
  "current_concurrency" INTEGER NOT NULL,
468
476
  "message_next_id" UUID,
469
477
  "message_next_dequeue_after" TIMESTAMP,
478
+ "message_next_seq_no" BIGINT,
470
479
  PRIMARY KEY ("id")
471
480
  );
472
- `,s`
481
+ `,a`
473
482
  CREATE UNIQUE INDEX "channel_state_name_ux"
474
483
  ON ${t(e.schema)}."channel_state" ("name");
475
- `,s`
484
+ `,a`
476
485
  CREATE INDEX "channel_state_dequeue_ix"
477
486
  ON ${t(e.schema)}."channel_state" (
478
487
  "message_next_dequeue_after" ASC
479
488
  ) WHERE "message_next_id" IS NOT NULL
480
489
  AND ("max_concurrency" IS NULL OR "current_concurrency" < "max_concurrency");
481
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/02-table-message.ts",z={name:c(__filename),sql:(e)=>{return[s`
490
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/02-table-message.ts",z={name:c(__filename),sql:(e)=>{return[a`
482
491
  CREATE TABLE ${t(e.schema)}."message" (
483
492
  "id" UUID NOT NULL,
484
493
  "channel_name" TEXT NOT NULL,
494
+ "seq_no" BIGSERIAL NOT NULL,
485
495
  "name" TEXT,
486
496
  "content" BYTEA NOT NULL,
487
497
  "state" BYTEA,
@@ -491,25 +501,25 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
491
501
  "dequeue_after" TIMESTAMP NOT NULL,
492
502
  PRIMARY KEY ("id")
493
503
  );
494
- `,s`
504
+ `,a`
495
505
  CREATE UNIQUE INDEX "message_name_ux"
496
506
  ON ${t(e.schema)}."message" (
497
507
  "channel_name",
498
508
  "name"
499
509
  ) WHERE "num_attempts" = 0
500
- `,s`
510
+ `,a`
501
511
  CREATE INDEX "message_dequeue_ix"
502
512
  ON ${t(e.schema)}."message" (
503
513
  "channel_name",
504
514
  "dequeue_after" ASC,
505
- "id" ASC
515
+ "seq_no" ASC
506
516
  ) WHERE NOT "is_locked";
507
- `,s`
517
+ `,a`
508
518
  CREATE INDEX "message_locked_dequeue_ix"
509
519
  ON ${t(e.schema)}."message" (
510
520
  "dequeue_after" ASC
511
521
  ) WHERE "is_locked";
512
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/03-function-wake.ts",w={name:c(__filename),sql:(e)=>{let n=u.toString(e.schema);return[s`
522
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/03-function-wake.ts",w={name:c(__filename),sql:(e)=>{let n=u.toString(e.schema);return[a`
513
523
  CREATE FUNCTION ${t(e.schema)}."wake" (
514
524
  p_delay_ms INTEGER
515
525
  ) RETURNS VOID AS $$
@@ -519,7 +529,7 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
519
529
  END IF;
520
530
  END;
521
531
  $$ LANGUAGE plpgsql;
522
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/08-function-channel-policy-set.ts",W={name:c(__filename),sql:(e)=>{return[s`
532
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/08-function-channel-policy-set.ts",W={name:c(__filename),sql:(e)=>{return[a`
523
533
  CREATE FUNCTION ${t(e.schema)}."channel_policy_set" (
524
534
  p_name TEXT,
525
535
  p_max_size INTEGER,
@@ -551,7 +561,7 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
551
561
  PERFORM ${t(e.schema)}."wake"(0);
552
562
  END;
553
563
  $$ LANGUAGE plpgsql;
554
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/09-function-channel-policy-clear.ts",Y={name:c(__filename),sql:(e)=>{return[s`
564
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/09-function-channel-policy-clear.ts",Y={name:c(__filename),sql:(e)=>{return[a`
555
565
  CREATE FUNCTION ${t(e.schema)}."channel_policy_clear" (
556
566
  p_name TEXT
557
567
  ) RETURNS VOID AS $$
@@ -583,4 +593,4 @@ var{defineProperty:d,getOwnPropertyNames:V,getOwnPropertyDescriptor:X}=Object,K=
583
593
  PERFORM ${t(e.schema)}."wake"(0);
584
594
  END;
585
595
  $$ LANGUAGE plpgsql;
586
- `]}};class S{schema;channelName;registerFn;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.registerFn=e.registerFn}create(e){let n=new o({schema:this.schema,channelName:this.channelName,name:e.name,content:e.content,lockMs:e.lockMs,delayMs:e.delayMs}),a=new Promise((_)=>{this.registerFn({sortKey:JSON.stringify([n.channelName,n.name,n.createdAt.toISOString()]),execute:(T)=>n.execute(T).then((Q)=>_(Q))})});return{messageId:n.id,promise:a}}}class A{schema;channelName;registerFn;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.registerFn=e.registerFn}set(e){let n=new m({schema:this.schema,channelName:this.channelName,maxConcurrency:e.maxConcurrency,maxSize:e.maxSize,releaseIntervalMs:e.releaseIntervalMs});this.registerFn({sortKey:JSON.stringify([n.channelName,null,n.createdAt.toISOString()]),execute:async(a)=>{await n.execute(a)}})}clear(){let e=new r({schema:this.schema,channelName:this.channelName});this.registerFn({sortKey:JSON.stringify([e.channelName,null,e.createdAt.toISOString()]),execute:async(n)=>{await e.execute(n)}})}}class L{policy;message;constructor(e){this.message=new S(e),this.policy=new A(e)}}var Ee=(e,n)=>{return e.sortKey.localeCompare(n.sortKey)};class I{commands;schema;constructor(e){this.commands=[],this.schema=e.schema}channel(e){return new L({schema:this.schema,channelName:e,registerFn:(n)=>{this.commands.push(n)}})}async execute(e){for(let n of this.commands.sort(Ee))await n.execute(e.databaseClient)}}class U{schema;channelName;constructor(e){this.schema=e.schema,this.channelName=e.channelName}async create(e){return new o({schema:this.schema,channelName:this.channelName,name:e.name,content:e.content,lockMs:e.lockMs,delayMs:e.delayMs}).execute(e.databaseClient)}}class D{schema;channelName;constructor(e){this.schema=e.schema,this.channelName=e.channelName}set(e){return new m({schema:this.schema,channelName:this.channelName,maxConcurrency:e.maxConcurrency,maxSize:e.maxSize,releaseIntervalMs:e.releaseIntervalMs}).execute(e.databaseClient)}clear(e){return new r({schema:this.schema,channelName:this.channelName}).execute(e.databaseClient)}}class O{policy;message;constructor(e){this.message=new U({schema:e.schema,channelName:e.channelName}),this.policy=new D({schema:e.schema,channelName:e.channelName})}}class v{schema;id;channelName;name;content;state;numAttempts;constructor(e){this.schema=e.schema,this.id=e.id,this.channelName=e.channelName,this.name=e.name,this.content=e.content,this.state=e.state,this.numAttempts=e.numAttempts}async defer(e){return new h({schema:this.schema,id:this.id,delayMs:e.delayMs,state:e.state}).execute(e.databaseClient)}async delete(e){return new l({schema:this.schema,id:this.id}).execute(e.databaseClient)}}class y{schema;constructor(e){this.schema=e}async dequeue(e){let a=await new i({schema:this.schema}).execute(e.databaseClient);if(a.resultType==="MESSAGE_DEQUEUED")return{resultType:"MESSAGE_DEQUEUED",message:new v({schema:this.schema,id:a.message.id,channelName:a.message.channelName,name:a.message.name,content:a.message.content,state:a.message.state,numAttempts:a.message.numAttempts})};else return a}channel(e){return new O({schema:this.schema,channelName:e})}batch(){return new I({schema:this.schema})}migrations(e){return[k,b,z,w,F,G,B,P,W,Y].map((n)=>({name:n.name,sql:n.sql({schema:this.schema,useWake:e.useWake??$}).map((a)=>H(a.value))})).sort((n,a)=>n.name.localeCompare(a.name))}wakeChannel(){return u.toString(this.schema)}}
596
+ `]}};class S{schema;channelName;registerFn;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.registerFn=e.registerFn}create(e){let n=new o({schema:this.schema,channelName:this.channelName,name:e.name,content:e.content,lockMs:e.lockMs,delayMs:e.delayMs}),s=new Promise((_)=>{this.registerFn({sortKey:JSON.stringify([n.channelName,n.name,n.createdAt.toISOString()]),execute:(T)=>n.execute(T).then((Q)=>_(Q))})});return{messageId:n.id,promise:s}}}class A{schema;channelName;registerFn;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.registerFn=e.registerFn}set(e){let n=new m({schema:this.schema,channelName:this.channelName,maxConcurrency:e.maxConcurrency,maxSize:e.maxSize,releaseIntervalMs:e.releaseIntervalMs});this.registerFn({sortKey:JSON.stringify([n.channelName,null,n.createdAt.toISOString()]),execute:async(s)=>{await n.execute(s)}})}clear(){let e=new r({schema:this.schema,channelName:this.channelName});this.registerFn({sortKey:JSON.stringify([e.channelName,null,e.createdAt.toISOString()]),execute:async(n)=>{await e.execute(n)}})}}class I{policy;message;constructor(e){this.message=new S(e),this.policy=new A(e)}}var Ee=(e,n)=>{return e.sortKey.localeCompare(n.sortKey)};class L{commands;schema;constructor(e){this.commands=[],this.schema=e.schema}channel(e){return new I({schema:this.schema,channelName:e,registerFn:(n)=>{this.commands.push(n)}})}async execute(e){for(let n of this.commands.sort(Ee))await n.execute(e.databaseClient)}}class v{schema;channelName;constructor(e){this.schema=e.schema,this.channelName=e.channelName}async create(e){return new o({schema:this.schema,channelName:this.channelName,name:e.name,content:e.content,lockMs:e.lockMs,delayMs:e.delayMs}).execute(e.databaseClient)}}class U{schema;channelName;constructor(e){this.schema=e.schema,this.channelName=e.channelName}set(e){return new m({schema:this.schema,channelName:this.channelName,maxConcurrency:e.maxConcurrency,maxSize:e.maxSize,releaseIntervalMs:e.releaseIntervalMs}).execute(e.databaseClient)}clear(e){return new r({schema:this.schema,channelName:this.channelName}).execute(e.databaseClient)}}class O{policy;message;constructor(e){this.message=new v({schema:e.schema,channelName:e.channelName}),this.policy=new U({schema:e.schema,channelName:e.channelName})}}class D{schema;id;channelName;name;content;state;numAttempts;constructor(e){this.schema=e.schema,this.id=e.id,this.channelName=e.channelName,this.name=e.name,this.content=e.content,this.state=e.state,this.numAttempts=e.numAttempts}async defer(e){return new h({schema:this.schema,id:this.id,delayMs:e.delayMs,state:e.state}).execute(e.databaseClient)}async delete(e){return new l({schema:this.schema,id:this.id}).execute(e.databaseClient)}}class x{schema;constructor(e){this.schema=e.schema}async dequeue(e){let s=await new i({schema:this.schema}).execute(e.databaseClient);if(s.resultType==="MESSAGE_DEQUEUED")return{resultType:"MESSAGE_DEQUEUED",message:new D({schema:this.schema,id:s.message.id,channelName:s.message.channelName,name:s.message.name,content:s.message.content,state:s.message.state,numAttempts:s.message.numAttempts})};else return s}channel(e){return new O({schema:this.schema,channelName:e})}batch(){return new L({schema:this.schema})}migrations(e){return[k,b,z,w,F,G,B,P,W,Y].map((n)=>({name:n.name,sql:n.sql({schema:this.schema,useWake:e.useWake??q}).map((s)=>H(s.value))})).sort((n,s)=>n.name.localeCompare(s.name))}wakeChannel(){return u.toString(this.schema)}}
package/dist/index.d.ts CHANGED
@@ -268,7 +268,9 @@ export type MessageDequeueResult = {
268
268
  };
269
269
  export declare class Queue {
270
270
  private readonly schema;
271
- constructor(schema: string);
271
+ constructor(params: {
272
+ schema: string;
273
+ });
272
274
  dequeue(params: {
273
275
  databaseClient: DatabaseClient;
274
276
  }): Promise<MessageDequeueResult>;
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
- var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=>({nodeType:"RAW",value:e}),W=(e)=>{return`'${e.replace(/'/g,"''")}'`},Y=(e)=>{if(e===null)return"NULL";else if(typeof e==="string")return W(e);else if(typeof e==="number")return e.toString();else if(typeof e==="boolean")return e?"TRUE":"FALSE";else if(e instanceof Date)return`'${e.toISOString()}'`;else if(typeof e==="bigint")return e.toString();else throw new Error(`Unsupported value type: ${typeof e}`)},Q=(e)=>{return`"${e.replace(/"/g,'""')}"`},V=(e)=>{if(e.nodeType==="VALUE")return Y(e.value);else if(e.nodeType==="REF")return Q(e.value);else if(e.nodeType==="RAW")return e.value;else throw new Error("Unsupported SQL node type")};var a=(e,...t)=>{let s=[];for(let E=0;E<e.length;E+=1)if(s.push(e[E]),E<t.length)s.push(V(t[E]));return w(s.join(""))};class r{schema;channelName;createdAt;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.createdAt=new Date}sortKeyGet(){return JSON.stringify([this.channelName,null,this.createdAt.toISOString()])}async execute(e){await e.query(a`
1
+ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=>({nodeType:"RAW",value:e}),W=(e)=>{return`'${e.replace(/'/g,"''")}'`},Y=(e)=>{if(e===null)return"NULL";else if(typeof e==="string")return W(e);else if(typeof e==="number")return e.toString();else if(typeof e==="boolean")return e?"TRUE":"FALSE";else if(e instanceof Date)return`'${e.toISOString()}'`;else if(typeof e==="bigint")return e.toString();else throw new Error(`Unsupported value type: ${typeof e}`)},Q=(e)=>{return`"${e.replace(/"/g,'""')}"`},V=(e)=>{if(e.nodeType==="VALUE")return Y(e.value);else if(e.nodeType==="REF")return Q(e.value);else if(e.nodeType==="RAW")return e.value;else throw new Error("Unsupported SQL node type")};var s=(e,...t)=>{let a=[];for(let E=0;E<e.length;E+=1)if(a.push(e[E]),E<t.length)a.push(V(t[E]));return w(a.join(""))};class r{schema;channelName;createdAt;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.createdAt=new Date}sortKeyGet(){return JSON.stringify([this.channelName,null,this.createdAt.toISOString()])}async execute(e){await e.query(s`
2
2
  SELECT 1 FROM ${n(this.schema)}."channel_policy_clear"(
3
3
  $1
4
4
  )
5
- `.value,[this.channelName])}}class m{schema;channelName;maxSize;maxConcurrency;releaseIntervalMs;createdAt;constructor(e){this.schema=e.schema,this.channelName=e.channelName;let t=e.maxConcurrency??null;this.maxConcurrency=t!==null?Math.max(0,t):null;let s=e.maxSize??null;this.maxSize=s!==null?Math.max(0,s):null;let E=e.releaseIntervalMs??null;this.releaseIntervalMs=E!==null?Math.max(0,E):null,this.createdAt=new Date}sortKeyGet(){return JSON.stringify([this.channelName,null,this.createdAt.toISOString()])}async execute(e){await e.query(a`
5
+ `.value,[this.channelName])}}class m{schema;channelName;maxSize;maxConcurrency;releaseIntervalMs;createdAt;constructor(e){this.schema=e.schema,this.channelName=e.channelName;let t=e.maxConcurrency??null;this.maxConcurrency=t!==null?Math.max(0,t):null;let a=e.maxSize??null;this.maxSize=a!==null?Math.max(0,a):null;let E=e.releaseIntervalMs??null;this.releaseIntervalMs=E!==null?Math.max(0,E):null,this.createdAt=new Date}sortKeyGet(){return JSON.stringify([this.channelName,null,this.createdAt.toISOString()])}async execute(e){await e.query(s`
6
6
  SELECT 1 FROM ${n(this.schema)}."channel_policy_set"(
7
7
  $1,
8
8
  $2::INTEGER,
9
9
  $3::INTEGER,
10
10
  $4::INTEGER
11
11
  )
12
- `.value,[this.channelName,this.maxSize,this.maxConcurrency,this.releaseIntervalMs])}}var d=(e)=>{return e*1000},X=(e)=>{return d(e*60)},O=(e)=>{return X(e*60)};import{createHash as K}from"node:crypto";class v{value;constructor(e){this.value=e}toString(e){return K("sha256").update(e).update(this.value).digest("base64").replace(/=/g,"")}}var i=new v("WAKE"),y=!1,l=d(0),x=O(1);import{dirname as f}from"path";var __filename="/home/runner/work/lonnymq/lonnymq/src/core/path.ts",J=f(f(__filename)),j=new RegExp(`^${J}/`),_=(e)=>{return e.replace(j,"")};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/04-function-message-create.ts";var C={name:_(__filename),sql:(e)=>{return[a`
12
+ `.value,[this.channelName,this.maxSize,this.maxConcurrency,this.releaseIntervalMs])}}var d=(e)=>{return e*1000},X=(e)=>{return d(e*60)},O=(e)=>{return X(e*60)};import{createHash as K}from"node:crypto";class D{value;constructor(e){this.value=e}toString(e){return K("sha256").update(e).update(this.value).digest("base64").replace(/=/g,"")}}var i=new D("WAKE"),x=!1,l=d(0),y=O(1);import{dirname as f}from"path";var __filename="/home/runner/work/lonnymq/lonnymq/src/core/path.ts",J=f(f(__filename)),j=new RegExp(`^${J}/`),_=(e)=>{return e.replace(j,"")};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/04-function-message-create.ts";var C={name:_(__filename),sql:(e)=>{return[s`
13
13
  CREATE FUNCTION ${n(e.schema)}."message_create" (
14
14
  p_id UUID,
15
15
  p_channel_name TEXT,
@@ -65,7 +65,8 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
65
65
  "max_size",
66
66
  "max_concurrency",
67
67
  "message_next_id",
68
- "message_next_dequeue_after"
68
+ "message_next_dequeue_after",
69
+ "message_next_seq_no"
69
70
  INTO v_channel_state;
70
71
 
71
72
  IF v_channel_state."current_size" >= v_channel_policy."max_size" THEN
@@ -95,6 +96,7 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
95
96
  "name" = EXCLUDED."name"
96
97
  RETURNING
97
98
  "id",
99
+ "seq_no",
98
100
  "dequeue_after"
99
101
  INTO v_message;
100
102
 
@@ -106,12 +108,14 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
106
108
 
107
109
  IF
108
110
  v_channel_state."message_next_id" IS NULL OR
109
- v_channel_state."message_next_dequeue_after" > v_message."dequeue_after"
111
+ v_channel_state."message_next_dequeue_after" > v_message."dequeue_after" OR
112
+ (v_channel_state."message_next_dequeue_after" = v_message."dequeue_after" AND v_channel_state."message_next_seq_no" > v_message."seq_no")
110
113
  THEN
111
114
  UPDATE ${n(e.schema)}."channel_state" SET
112
115
  "current_size" = v_channel_state."current_size" + 1,
113
116
  "message_next_id" = v_message."id",
114
- "message_next_dequeue_after" = GREATEST(v_now, v_message."dequeue_after")
117
+ "message_next_dequeue_after" = GREATEST(v_now, v_message."dequeue_after"),
118
+ "message_next_seq_no" = v_message."seq_no"
115
119
  WHERE "id" = v_channel_state."id";
116
120
  ELSE
117
121
  UPDATE ${n(e.schema)}."channel_state" SET
@@ -126,7 +130,7 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
126
130
  RETURN;
127
131
  END;
128
132
  $$ LANGUAGE plpgsql;
129
- `]}};import{randomUUID as Z}from"node:crypto";class o{schema;channelName;name;content;lockMs;id;delayMs;createdAt;constructor(e){let t=e.name??null,s=e.lockMs===void 0?x:Math.max(0,e.lockMs),E=e.delayMs===void 0?l:e.delayMs;this.id=Z(),this.schema=e.schema,this.channelName=e.channelName,this.content=e.content,this.name=t,this.lockMs=s,this.delayMs=E,this.createdAt=new Date}async execute(e){let t=await e.query(a`
133
+ `]}};import{randomUUID as Z}from"node:crypto";class o{schema;channelName;name;content;lockMs;id;delayMs;createdAt;constructor(e){let t=e.name??null,a=e.lockMs===void 0?y:Math.max(0,e.lockMs),E=e.delayMs===void 0?l:e.delayMs;this.id=Z(),this.schema=e.schema,this.channelName=e.channelName,this.content=e.content,this.name=t,this.lockMs=a,this.delayMs=E,this.createdAt=new Date}async execute(e){let t=await e.query(s`
130
134
  SELECT * FROM ${n(this.schema)}."message_create"(
131
135
  $1,
132
136
  $2,
@@ -135,7 +139,7 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
135
139
  $5::INTEGER,
136
140
  $6::INTEGER
137
141
  )
138
- `.value,[this.id,this.channelName,this.name,this.content,this.lockMs,this.delayMs]).then((s)=>s.rows[0]);if(t.result_code===1)return{resultType:"MESSAGE_DROPPED"};else if(t.result_code===2)return{resultType:"MESSAGE_DEDUPLICATED"};else if(t.result_code===0)return{resultType:"MESSAGE_CREATED"};else throw new Error("Unexpected result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/05-function-message-dequeue.ts";var p={name:_(__filename),sql:(e)=>{return[a`
142
+ `.value,[this.id,this.channelName,this.name,this.content,this.lockMs,this.delayMs]).then((a)=>a.rows[0]);if(t.result_code===1)return{resultType:"MESSAGE_DROPPED"};else if(t.result_code===2)return{resultType:"MESSAGE_DEDUPLICATED"};else if(t.result_code===0)return{resultType:"MESSAGE_CREATED"};else throw new Error("Unexpected result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/05-function-message-dequeue.ts";var p={name:_(__filename),sql:(e)=>{return[s`
139
143
  CREATE FUNCTION ${n(e.schema)}."message_dequeue" ()
140
144
  RETURNS TABLE (
141
145
  result_code INTEGER,
@@ -181,14 +185,12 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
181
185
 
182
186
  RETURN QUERY SELECT
183
187
  ${c(1)},
184
- NULL::BYTEA,
185
- NULL::BYTEA,
188
+ v_message_locked.content,
189
+ v_message_locked.state,
186
190
  JSON_BUILD_OBJECT(
187
191
  'id', v_message_locked.id,
188
192
  'channel_name', v_message_locked.channel_name,
189
- 'state', v_message_locked.state,
190
- 'name', v_message_locked.name
191
- 'content', v_message_locked.content,
193
+ 'name', v_message_locked.name,
192
194
  'num_attempts', v_message_locked.num_attempts
193
195
  );
194
196
  RETURN;
@@ -248,11 +250,12 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
248
250
 
249
251
  SELECT
250
252
  "message"."id",
251
- "message"."dequeue_after"
253
+ "message"."dequeue_after",
254
+ "message"."seq_no"
252
255
  FROM ${n(e.schema)}."message"
253
256
  WHERE NOT "is_locked"
254
257
  AND "channel_name" = v_message_dequeue."channel_name"
255
- ORDER BY "dequeue_after" ASC, "id" ASC
258
+ ORDER BY "dequeue_after" ASC, "seq_no" ASC
256
259
  LIMIT 1
257
260
  INTO v_message_next_dequeue;
258
261
 
@@ -265,7 +268,8 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
265
268
  UPDATE ${n(e.schema)}."channel_state" SET
266
269
  "current_concurrency" = v_channel_state."current_concurrency" + 1,
267
270
  "message_next_id" = v_message_next_dequeue."id",
268
- "message_next_dequeue_after" = v_message_next_dequeue_after
271
+ "message_next_dequeue_after" = v_message_next_dequeue_after,
272
+ "message_next_seq_no" = v_message_next_dequeue."seq_no"
269
273
  WHERE "id" = v_channel_state."id";
270
274
  ELSE
271
275
  UPDATE ${n(e.schema)}."channel_state" SET
@@ -288,14 +292,14 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
288
292
  RETURN;
289
293
  END;
290
294
  $$ LANGUAGE plpgsql;
291
- `]}};class h{schema;constructor(e){this.schema=e.schema}async execute(e){let t=await e.query(a`
295
+ `]}};class h{schema;constructor(e){this.schema=e.schema}async execute(e){let t=await e.query(s`
292
296
  SELECT
293
297
  result_code,
294
298
  metadata,
295
299
  content,
296
300
  state
297
301
  FROM ${n(this.schema)}."message_dequeue"()
298
- `.value,[]).then((s)=>s.rows[0]);if(t.result_code===0)return{resultType:"MESSAGE_NOT_AVAILABLE",retryMs:t.metadata.retry_ms};else if(t.result_code===1)return{resultType:"MESSAGE_DEQUEUED",message:{id:t.metadata.id,channelName:t.metadata.channel_name,name:t.metadata.name,content:t.content,state:t.state,numAttempts:t.metadata.num_attempts}};else throw new Error("Unexpected dequeue result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/06-function-message-delete.ts";var $={name:_(__filename),sql:(e)=>{return[a`
302
+ `.value,[]).then((a)=>a.rows[0]);if(t.result_code===0)return{resultType:"MESSAGE_NOT_AVAILABLE",retryMs:t.metadata.retry_ms};else if(t.result_code===1)return{resultType:"MESSAGE_DEQUEUED",message:{id:t.metadata.id,channelName:t.metadata.channel_name,name:t.metadata.name,content:t.content,state:t.state,numAttempts:t.metadata.num_attempts}};else throw new Error("Unexpected dequeue result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/06-function-message-delete.ts";var q={name:_(__filename),sql:(e)=>{return[s`
299
303
  CREATE FUNCTION ${n(e.schema)}."message_delete" (
300
304
  p_id UUID
301
305
  )
@@ -360,11 +364,11 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
360
364
  RETURN;
361
365
  END;
362
366
  $$ LANGUAGE plpgsql;
363
- `]}};class u{schema;id;constructor(e){this.schema=e.schema,this.id=e.id}async execute(e){let t=await e.query(a`
367
+ `]}};class u{schema;id;constructor(e){this.schema=e.schema,this.id=e.id}async execute(e){let t=await e.query(s`
364
368
  SELECT * FROM ${n(this.schema)}."message_delete"(
365
369
  $1
366
370
  )
367
- `.value,[this.id]).then((s)=>s.rows[0]);if(t.result_code===0)return{resultType:"MESSAGE_NOT_FOUND"};else if(t.result_code===1)return{resultType:"STATE_INVALID"};else if(t.result_code===2)return{resultType:"MESSAGE_DELETED"};else throw new Error("Unexpected result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/07-function-message-defer.ts";var q={name:_(__filename),sql:(e)=>{return[a`
371
+ `.value,[this.id]).then((a)=>a.rows[0]);if(t.result_code===0)return{resultType:"MESSAGE_NOT_FOUND"};else if(t.result_code===1)return{resultType:"STATE_INVALID"};else if(t.result_code===2)return{resultType:"MESSAGE_DELETED"};else throw new Error("Unexpected result")}}var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/07-function-message-defer.ts";var $={name:_(__filename),sql:(e)=>{return[s`
368
372
  CREATE FUNCTION ${n(e.schema)}."message_defer" (
369
373
  p_id UUID,
370
374
  p_delay_ms INTEGER,
@@ -381,7 +385,8 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
381
385
  SELECT
382
386
  "message"."id",
383
387
  "message"."channel_name",
384
- "message"."is_locked"
388
+ "message"."is_locked",
389
+ "message"."seq_no"
385
390
  FROM ${n(e.schema)}."message"
386
391
  WHERE "id" = p_id
387
392
  FOR UPDATE
@@ -400,7 +405,8 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
400
405
  SELECT
401
406
  "channel_state"."current_concurrency",
402
407
  "channel_state"."message_next_id",
403
- "channel_state"."message_next_dequeue_after"
408
+ "channel_state"."message_next_dequeue_after",
409
+ "channel_state"."message_next_seq_no"
404
410
  FROM ${n(e.schema)}."channel_state"
405
411
  WHERE "name" = v_message."channel_name"
406
412
  FOR UPDATE
@@ -410,12 +416,14 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
410
416
 
411
417
  IF
412
418
  v_channel_state."message_next_id" IS NULL OR
413
- v_channel_state."message_next_dequeue_after" > v_dequeue_after
419
+ v_channel_state."message_next_dequeue_after" > v_dequeue_after OR
420
+ (v_channel_state."message_next_dequeue_after" = v_dequeue_after AND v_channel_state."message_next_seq_no" > v_message."seq_no")
414
421
  THEN
415
422
  UPDATE ${n(e.schema)}."channel_state" SET
416
423
  "current_concurrency" = v_channel_state."current_concurrency" - 1,
417
424
  "message_next_id" = v_message."id",
418
- "message_next_dequeue_after" = v_dequeue_after
425
+ "message_next_dequeue_after" = v_dequeue_after,
426
+ "message_next_seq_no" = v_message."seq_no"
419
427
  WHERE "name" = v_message."channel_name";
420
428
  ELSE
421
429
  UPDATE ${n(e.schema)}."channel_state" SET
@@ -436,15 +444,15 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
436
444
  RETURN;
437
445
  END;
438
446
  $$ LANGUAGE plpgsql;
439
- `]}};class N{schema;id;delayMs;state;constructor(e){let t=e.delayMs===void 0?l:e.delayMs;this.schema=e.schema,this.id=e.id,this.delayMs=t,this.state=e.state??null}async execute(e){let t=await e.query(a`
447
+ `]}};class N{schema;id;delayMs;state;constructor(e){let t=e.delayMs===void 0?l:e.delayMs;this.schema=e.schema,this.id=e.id,this.delayMs=t,this.state=e.state??null}async execute(e){let t=await e.query(s`
440
448
  SELECT * FROM ${n(this.schema)}."message_defer"(
441
449
  $1,
442
450
  $2,
443
451
  $3
444
452
  )
445
- `.value,[this.id,this.delayMs,this.state]).then((s)=>s.rows[0]);if(t.result_code===0)return{resultType:"MESSAGE_NOT_FOUND"};else if(t.result_code===1)return{resultType:"STATE_INVALID"};else if(t.result_code===2)return{resultType:"MESSAGE_DEFERRED"};else throw new Error("Unexpected result")}}var F=(e)=>{let t=e.split(`
446
- `),s=Number.MAX_SAFE_INTEGER;for(let E of t){if(E.trim().length===0)continue;let T=E.search(/\S/);s=Math.min(s,T)}return t.map((E)=>E.slice(s)).join(`
447
- `).trim()};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/00-table-channel-policy.ts",M={name:_(__filename),sql:(e)=>{return[a`
453
+ `.value,[this.id,this.delayMs,this.state]).then((a)=>a.rows[0]);if(t.result_code===0)return{resultType:"MESSAGE_NOT_FOUND"};else if(t.result_code===1)return{resultType:"STATE_INVALID"};else if(t.result_code===2)return{resultType:"MESSAGE_DEFERRED"};else throw new Error("Unexpected result")}}var F=(e)=>{let t=e.split(`
454
+ `),a=Number.MAX_SAFE_INTEGER;for(let E of t){if(E.trim().length===0)continue;let T=E.search(/\S/);a=Math.min(a,T)}return t.map((E)=>E.slice(a)).join(`
455
+ `).trim()};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/00-table-channel-policy.ts",M={name:_(__filename),sql:(e)=>{return[s`
448
456
  CREATE TABLE ${n(e.schema)}."channel_policy" (
449
457
  "id" UUID NOT NULL DEFAULT GEN_RANDOM_UUID(),
450
458
  "name" TEXT NOT NULL,
@@ -453,10 +461,10 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
453
461
  "release_interval_ms" INTEGER,
454
462
  PRIMARY KEY ("id")
455
463
  );
456
- `,a`
464
+ `,s`
457
465
  CREATE UNIQUE INDEX "channel_policy_name_ux"
458
466
  ON ${n(e.schema)}."channel_policy" ("name");
459
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/01-table-channel-state.ts",G={name:_(__filename),sql:(e)=>{return[a`
467
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/01-table-channel-state.ts",G={name:_(__filename),sql:(e)=>{return[s`
460
468
  CREATE TABLE ${n(e.schema)}."channel_state" (
461
469
  "id" UUID NOT NULL DEFAULT GEN_RANDOM_UUID(),
462
470
  "name" TEXT NOT NULL,
@@ -467,21 +475,23 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
467
475
  "current_concurrency" INTEGER NOT NULL,
468
476
  "message_next_id" UUID,
469
477
  "message_next_dequeue_after" TIMESTAMP,
478
+ "message_next_seq_no" BIGINT,
470
479
  PRIMARY KEY ("id")
471
480
  );
472
- `,a`
481
+ `,s`
473
482
  CREATE UNIQUE INDEX "channel_state_name_ux"
474
483
  ON ${n(e.schema)}."channel_state" ("name");
475
- `,a`
484
+ `,s`
476
485
  CREATE INDEX "channel_state_dequeue_ix"
477
486
  ON ${n(e.schema)}."channel_state" (
478
487
  "message_next_dequeue_after" ASC
479
488
  ) WHERE "message_next_id" IS NOT NULL
480
489
  AND ("max_concurrency" IS NULL OR "current_concurrency" < "max_concurrency");
481
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/02-table-message.ts",B={name:_(__filename),sql:(e)=>{return[a`
490
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/02-table-message.ts",B={name:_(__filename),sql:(e)=>{return[s`
482
491
  CREATE TABLE ${n(e.schema)}."message" (
483
492
  "id" UUID NOT NULL,
484
493
  "channel_name" TEXT NOT NULL,
494
+ "seq_no" BIGSERIAL NOT NULL,
485
495
  "name" TEXT,
486
496
  "content" BYTEA NOT NULL,
487
497
  "state" BYTEA,
@@ -491,25 +501,25 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
491
501
  "dequeue_after" TIMESTAMP NOT NULL,
492
502
  PRIMARY KEY ("id")
493
503
  );
494
- `,a`
504
+ `,s`
495
505
  CREATE UNIQUE INDEX "message_name_ux"
496
506
  ON ${n(e.schema)}."message" (
497
507
  "channel_name",
498
508
  "name"
499
509
  ) WHERE "num_attempts" = 0
500
- `,a`
510
+ `,s`
501
511
  CREATE INDEX "message_dequeue_ix"
502
512
  ON ${n(e.schema)}."message" (
503
513
  "channel_name",
504
514
  "dequeue_after" ASC,
505
- "id" ASC
515
+ "seq_no" ASC
506
516
  ) WHERE NOT "is_locked";
507
- `,a`
517
+ `,s`
508
518
  CREATE INDEX "message_locked_dequeue_ix"
509
519
  ON ${n(e.schema)}."message" (
510
520
  "dequeue_after" ASC
511
521
  ) WHERE "is_locked";
512
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/03-function-wake.ts",P={name:_(__filename),sql:(e)=>{let t=i.toString(e.schema);return[a`
522
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/03-function-wake.ts",P={name:_(__filename),sql:(e)=>{let t=i.toString(e.schema);return[s`
513
523
  CREATE FUNCTION ${n(e.schema)}."wake" (
514
524
  p_delay_ms INTEGER
515
525
  ) RETURNS VOID AS $$
@@ -519,7 +529,7 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
519
529
  END IF;
520
530
  END;
521
531
  $$ LANGUAGE plpgsql;
522
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/08-function-channel-policy-set.ts",H={name:_(__filename),sql:(e)=>{return[a`
532
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/08-function-channel-policy-set.ts",H={name:_(__filename),sql:(e)=>{return[s`
523
533
  CREATE FUNCTION ${n(e.schema)}."channel_policy_set" (
524
534
  p_name TEXT,
525
535
  p_max_size INTEGER,
@@ -551,7 +561,7 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
551
561
  PERFORM ${n(e.schema)}."wake"(0);
552
562
  END;
553
563
  $$ LANGUAGE plpgsql;
554
- `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/09-function-channel-policy-clear.ts",k={name:_(__filename),sql:(e)=>{return[a`
564
+ `]}};var __filename="/home/runner/work/lonnymq/lonnymq/src/migration/09-function-channel-policy-clear.ts",k={name:_(__filename),sql:(e)=>{return[s`
555
565
  CREATE FUNCTION ${n(e.schema)}."channel_policy_clear" (
556
566
  p_name TEXT
557
567
  ) RETURNS VOID AS $$
@@ -583,4 +593,4 @@ var c=(e)=>({nodeType:"VALUE",value:e}),n=(e)=>({nodeType:"REF",value:e}),w=(e)=
583
593
  PERFORM ${n(e.schema)}."wake"(0);
584
594
  END;
585
595
  $$ LANGUAGE plpgsql;
586
- `]}};class R{schema;channelName;registerFn;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.registerFn=e.registerFn}create(e){let t=new o({schema:this.schema,channelName:this.channelName,name:e.name,content:e.content,lockMs:e.lockMs,delayMs:e.delayMs}),s=new Promise((E)=>{this.registerFn({sortKey:JSON.stringify([t.channelName,t.name,t.createdAt.toISOString()]),execute:(T)=>t.execute(T).then((z)=>E(z))})});return{messageId:t.id,promise:s}}}class g{schema;channelName;registerFn;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.registerFn=e.registerFn}set(e){let t=new m({schema:this.schema,channelName:this.channelName,maxConcurrency:e.maxConcurrency,maxSize:e.maxSize,releaseIntervalMs:e.releaseIntervalMs});this.registerFn({sortKey:JSON.stringify([t.channelName,null,t.createdAt.toISOString()]),execute:async(s)=>{await t.execute(s)}})}clear(){let e=new r({schema:this.schema,channelName:this.channelName});this.registerFn({sortKey:JSON.stringify([e.channelName,null,e.createdAt.toISOString()]),execute:async(t)=>{await e.execute(t)}})}}class S{policy;message;constructor(e){this.message=new R(e),this.policy=new g(e)}}var ee=(e,t)=>{return e.sortKey.localeCompare(t.sortKey)};class A{commands;schema;constructor(e){this.commands=[],this.schema=e.schema}channel(e){return new S({schema:this.schema,channelName:e,registerFn:(t)=>{this.commands.push(t)}})}async execute(e){for(let t of this.commands.sort(ee))await t.execute(e.databaseClient)}}class L{schema;channelName;constructor(e){this.schema=e.schema,this.channelName=e.channelName}async create(e){return new o({schema:this.schema,channelName:this.channelName,name:e.name,content:e.content,lockMs:e.lockMs,delayMs:e.delayMs}).execute(e.databaseClient)}}class I{schema;channelName;constructor(e){this.schema=e.schema,this.channelName=e.channelName}set(e){return new m({schema:this.schema,channelName:this.channelName,maxConcurrency:e.maxConcurrency,maxSize:e.maxSize,releaseIntervalMs:e.releaseIntervalMs}).execute(e.databaseClient)}clear(e){return new r({schema:this.schema,channelName:this.channelName}).execute(e.databaseClient)}}class U{policy;message;constructor(e){this.message=new L({schema:e.schema,channelName:e.channelName}),this.policy=new I({schema:e.schema,channelName:e.channelName})}}class D{schema;id;channelName;name;content;state;numAttempts;constructor(e){this.schema=e.schema,this.id=e.id,this.channelName=e.channelName,this.name=e.name,this.content=e.content,this.state=e.state,this.numAttempts=e.numAttempts}async defer(e){return new N({schema:this.schema,id:this.id,delayMs:e.delayMs,state:e.state}).execute(e.databaseClient)}async delete(e){return new u({schema:this.schema,id:this.id}).execute(e.databaseClient)}}class b{schema;constructor(e){this.schema=e}async dequeue(e){let s=await new h({schema:this.schema}).execute(e.databaseClient);if(s.resultType==="MESSAGE_DEQUEUED")return{resultType:"MESSAGE_DEQUEUED",message:new D({schema:this.schema,id:s.message.id,channelName:s.message.channelName,name:s.message.name,content:s.message.content,state:s.message.state,numAttempts:s.message.numAttempts})};else return s}channel(e){return new U({schema:this.schema,channelName:e})}batch(){return new A({schema:this.schema})}migrations(e){return[M,G,B,P,C,p,$,q,H,k].map((t)=>({name:t.name,sql:t.sql({schema:this.schema,useWake:e.useWake??y}).map((s)=>F(s.value))})).sort((t,s)=>t.name.localeCompare(s.name))}wakeChannel(){return i.toString(this.schema)}}export{b as Queue,h as MessageDequeueCommand,u as MessageDeleteCommand,N as MessageDeferCommand,o as MessageCreateCommand,m as ChannelPolicySetCommand,r as ChannelPolicyClearCommand};
596
+ `]}};class R{schema;channelName;registerFn;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.registerFn=e.registerFn}create(e){let t=new o({schema:this.schema,channelName:this.channelName,name:e.name,content:e.content,lockMs:e.lockMs,delayMs:e.delayMs}),a=new Promise((E)=>{this.registerFn({sortKey:JSON.stringify([t.channelName,t.name,t.createdAt.toISOString()]),execute:(T)=>t.execute(T).then((z)=>E(z))})});return{messageId:t.id,promise:a}}}class g{schema;channelName;registerFn;constructor(e){this.schema=e.schema,this.channelName=e.channelName,this.registerFn=e.registerFn}set(e){let t=new m({schema:this.schema,channelName:this.channelName,maxConcurrency:e.maxConcurrency,maxSize:e.maxSize,releaseIntervalMs:e.releaseIntervalMs});this.registerFn({sortKey:JSON.stringify([t.channelName,null,t.createdAt.toISOString()]),execute:async(a)=>{await t.execute(a)}})}clear(){let e=new r({schema:this.schema,channelName:this.channelName});this.registerFn({sortKey:JSON.stringify([e.channelName,null,e.createdAt.toISOString()]),execute:async(t)=>{await e.execute(t)}})}}class S{policy;message;constructor(e){this.message=new R(e),this.policy=new g(e)}}var ee=(e,t)=>{return e.sortKey.localeCompare(t.sortKey)};class A{commands;schema;constructor(e){this.commands=[],this.schema=e.schema}channel(e){return new S({schema:this.schema,channelName:e,registerFn:(t)=>{this.commands.push(t)}})}async execute(e){for(let t of this.commands.sort(ee))await t.execute(e.databaseClient)}}class I{schema;channelName;constructor(e){this.schema=e.schema,this.channelName=e.channelName}async create(e){return new o({schema:this.schema,channelName:this.channelName,name:e.name,content:e.content,lockMs:e.lockMs,delayMs:e.delayMs}).execute(e.databaseClient)}}class L{schema;channelName;constructor(e){this.schema=e.schema,this.channelName=e.channelName}set(e){return new m({schema:this.schema,channelName:this.channelName,maxConcurrency:e.maxConcurrency,maxSize:e.maxSize,releaseIntervalMs:e.releaseIntervalMs}).execute(e.databaseClient)}clear(e){return new r({schema:this.schema,channelName:this.channelName}).execute(e.databaseClient)}}class v{policy;message;constructor(e){this.message=new I({schema:e.schema,channelName:e.channelName}),this.policy=new L({schema:e.schema,channelName:e.channelName})}}class U{schema;id;channelName;name;content;state;numAttempts;constructor(e){this.schema=e.schema,this.id=e.id,this.channelName=e.channelName,this.name=e.name,this.content=e.content,this.state=e.state,this.numAttempts=e.numAttempts}async defer(e){return new N({schema:this.schema,id:this.id,delayMs:e.delayMs,state:e.state}).execute(e.databaseClient)}async delete(e){return new u({schema:this.schema,id:this.id}).execute(e.databaseClient)}}class b{schema;constructor(e){this.schema=e.schema}async dequeue(e){let a=await new h({schema:this.schema}).execute(e.databaseClient);if(a.resultType==="MESSAGE_DEQUEUED")return{resultType:"MESSAGE_DEQUEUED",message:new U({schema:this.schema,id:a.message.id,channelName:a.message.channelName,name:a.message.name,content:a.message.content,state:a.message.state,numAttempts:a.message.numAttempts})};else return a}channel(e){return new v({schema:this.schema,channelName:e})}batch(){return new A({schema:this.schema})}migrations(e){return[M,G,B,P,C,p,q,$,H,k].map((t)=>({name:t.name,sql:t.sql({schema:this.schema,useWake:e.useWake??x}).map((a)=>F(a.value))})).sort((t,a)=>t.name.localeCompare(a.name))}wakeChannel(){return i.toString(this.schema)}}export{b as Queue,h as MessageDequeueCommand,u as MessageDeleteCommand,N as MessageDeferCommand,o as MessageCreateCommand,m as ChannelPolicySetCommand,r as ChannelPolicyClearCommand};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lonnymq",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {