@bumail/queue 0.8.0 → 0.9.0
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 +25 -1
- package/dist/chunks/{index-e5wa9f3j.js → index-9yp15arf.js} +91 -21
- package/dist/chunks/index-9yp15arf.js.map +14 -0
- package/dist/chunks/index-r8wb840a.js +24 -0
- package/dist/chunks/index-r8wb840a.js.map +10 -0
- package/dist/contract/checks.d.ts +2 -6
- package/dist/contract/checks.d.ts.map +1 -1
- package/dist/contract/dsn-checks.d.ts +4 -0
- package/dist/contract/dsn-checks.d.ts.map +1 -0
- package/dist/contract/dsn-column.d.ts +12 -0
- package/dist/contract/dsn-column.d.ts.map +1 -0
- package/dist/contract/storable.d.ts +7 -0
- package/dist/contract/storable.d.ts.map +1 -0
- package/dist/contract/types.d.ts +42 -0
- package/dist/contract/types.d.ts.map +1 -1
- package/dist/dsn/build.d.ts +2 -0
- package/dist/dsn/build.d.ts.map +1 -1
- package/dist/dsn/delivered.d.ts +39 -0
- package/dist/dsn/delivered.d.ts.map +1 -0
- package/dist/dsn/fields.d.ts +11 -2
- package/dist/dsn/fields.d.ts.map +1 -1
- package/dist/dsn/human.d.ts +7 -0
- package/dist/dsn/human.d.ts.map +1 -0
- package/dist/dsn/policy.d.ts +17 -0
- package/dist/dsn/policy.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +384 -254
- package/dist/index.js.map +18 -15
- package/dist/memory/index.js +1 -1
- package/dist/postgres/index.js +17 -9
- package/dist/postgres/index.js.map +6 -6
- package/dist/postgres/rows.d.ts +6 -1
- package/dist/postgres/rows.d.ts.map +1 -1
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/statements.d.ts +9 -8
- package/dist/postgres/statements.d.ts.map +1 -1
- package/dist/postgres/store.d.ts.map +1 -1
- package/dist/queue/envelope.d.ts +24 -4
- package/dist/queue/envelope.d.ts.map +1 -1
- package/dist/queue/events.d.ts +2 -1
- package/dist/queue/events.d.ts.map +1 -1
- package/dist/queue/notify.d.ts +6 -3
- package/dist/queue/notify.d.ts.map +1 -1
- package/dist/queue/outcome.d.ts.map +1 -1
- package/dist/queue/queue.d.ts.map +1 -1
- package/dist/queue/route.d.ts +7 -0
- package/dist/queue/route.d.ts.map +1 -1
- package/dist/queue/session.d.ts.map +1 -1
- package/dist/queue/settle.d.ts.map +1 -1
- package/dist/redis/index.js +26 -13
- package/dist/redis/index.js.map +5 -5
- package/dist/redis/items.d.ts +2 -0
- package/dist/redis/items.d.ts.map +1 -1
- package/dist/redis/scripts.d.ts +1 -1
- package/dist/redis/scripts.d.ts.map +1 -1
- package/dist/redis/store.d.ts.map +1 -1
- package/dist/sqlite/index.js +15 -8
- package/dist/sqlite/index.js.map +5 -5
- package/dist/sqlite/rows.d.ts +2 -1
- package/dist/sqlite/rows.d.ts.map +1 -1
- package/dist/sqlite/schema.d.ts.map +1 -1
- package/dist/sqlite/store.d.ts.map +1 -1
- package/docs/guide.md +72 -3
- package/docs/roadmap.md +8 -3
- package/docs/troubleshooting.md +41 -0
- package/package.json +3 -3
- package/dist/chunks/index-e5wa9f3j.js.map +0 -12
package/dist/redis/index.js.map
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sources": ["../src/redis/connect.ts", "../src/redis/items.ts", "../src/redis/scripts.ts", "../src/redis/run.ts", "../src/redis/store.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { invalid } from '../errors';\nimport { masked } from '../masked';\nimport type { RedisQueueClient, RedisQueueStoreOptions } from './options';\n\n/** The keys of one queue, each its prefix and a fixed name. */\nexport interface Keys {\n\treadonly prefix: string;\n\t/** Every item, scored by its next attempt: `list` and `count`. */\n\treadonly items: string;\n\t/** The items no lease holds, scored by their next attempt: what a claim takes first. */\n\treadonly ready: string;\n\t/** The leased items, scored by when their leases expire. */\n\treadonly leases: string;\n\t/** The counter that orders items equally due. */\n\treadonly seq: string;\n\t/** The layout version. */\n\treadonly schema: string;\n}\n\n/** The client a store uses, and whether it opened it (and so closes it). */\nexport interface Connection {\n\treadonly client: RedisQueueClient;\n\treadonly owned: boolean;\n\treadonly keys: Keys;\n\t/** The URL's password, so an error that repeats it is masked; `''` for a given client. */\n\treadonly password: string;\n}\n\nconst DEFAULT_PREFIX = 'bumail:queue:';\n\n/**\n * Nothing Redis reads specially: no `{`, so no Cluster hash tag; no `*`,\n * `?`, `[` or `\\`, so a `SCAN MATCH` on the prefix matches its keys only;\n * no space or control character.\n */\nconst PREFIX = /^[a-z][a-z0-9_:.-]{0,39}$/;\n\nexport function keysOf(value: unknown): Keys {\n\tconst prefix = value ?? DEFAULT_PREFIX;\n\tif (typeof prefix !== 'string' || !PREFIX.test(prefix)) {\n\t\tthrow invalid(\n\t\t\t`keyPrefix must be lowercase letters, digits, '_', ':', '.' and '-', starting with a letter, at most 40 characters, not ${JSON.stringify(prefix)}`,\n\t\t);\n\t}\n\treturn {\n\t\tprefix,\n\t\titems: `${prefix}items`,\n\t\tready: `${prefix}ready`,\n\t\tleases: `${prefix}leases`,\n\t\tseq: `${prefix}seq`,\n\t\tschema: `${prefix}schema`,\n\t};\n}\n\nconst NEEDS =\n\t'A Redis queue store needs client, a Bun.RedisClient, or url, a redis:// URL';\n\n/** The schemes `Bun.RedisClient` takes. */\nconst SCHEMES = [\n\t'redis:',\n\t'rediss:',\n\t'valkey:',\n\t'valkeys:',\n\t'redis+tls:',\n\t'redis+unix:',\n\t'redis+tls+unix:',\n];\n\n/** A Redis URL; never echoed, since it may hold a password. */\nfunction urlOf(value: unknown): URL {\n\tif (typeof value !== 'string' && !(value instanceof URL))\n\t\tthrow invalid(NEEDS);\n\tconst url = URL.parse(String(value));\n\tif (url === null || !SCHEMES.includes(url.protocol)) throw invalid(NEEDS);\n\treturn url;\n}\n\n/**\n * A client of the store's own for the URL. `Bun.RedisClient` refuses\n * some URLs at once (a database that is not a number): that is\n * `INVALID`, its reason kept and the password, should it be repeated,\n * masked.\n */\nfunction clientFor(url: URL): RedisQueueClient {\n\ttry {\n\t\treturn new Bun.RedisClient(url.href) as unknown as RedisQueueClient;\n\t} catch (error) {\n\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\tthrow invalid(\n\t\t\t`The URL in url cannot be opened: ${masked(reason, url.password)}`,\n\t\t);\n\t}\n}\n\nconst isClient = (value: unknown): value is RedisQueueClient =>\n\ttypeof value === 'object' &&\n\tvalue !== null &&\n\ttypeof (value as RedisQueueClient).send === 'function' &&\n\ttypeof (value as RedisQueueClient).getBuffer === 'function' &&\n\ttypeof (value as RedisQueueClient).close === 'function';\n\n/** Checks the options and opens a client for a URL; connects to nothing yet. */\nexport function connect(options: RedisQueueStoreOptions): Connection {\n\tif (typeof options !== 'object' || options === null) throw invalid(NEEDS);\n\tconst keys = keysOf(options.keyPrefix);\n\tconst { client, url } = options;\n\tif (client !== undefined && url !== undefined) {\n\t\tthrow invalid('A Redis queue store takes client or url, not both');\n\t}\n\tif (client !== undefined) {\n\t\tif (!isClient(client)) throw invalid(NEEDS);\n\t\treturn { client, owned: false, keys, password: '' };\n\t}\n\tconst parsed = urlOf(url);\n\treturn {\n\t\tclient: clientFor(parsed),\n\t\towned: true,\n\t\tkeys,\n\t\tpassword: parsed.password,\n\t};\n}\n",
|
|
6
|
-
"import { isDone } from '../contract/checks';\nimport type { QueueItem, RecipientState } from '../contract/types';\n\n/** An item's hash, as `HGETALL` gives it: a map, or a script's flat list of names and values. */\nexport type Fields = Readonly<Record<string, string>>;\n\n/** A script's `HGETALL`, `[name, value, …]`, as a map; a map as it is. */\nexport function fieldsOf(reply: unknown): Fields | undefined {\n\tif (Array.isArray(reply)) {\n\t\tconst fields: Record<string, string> = {};\n\t\tfor (let i = 0; i + 1 < reply.length; i += 2) {\n\t\t\tfields[String(reply[i])] = String(reply[i + 1]);\n\t\t}\n\t\treturn fieldsOf(fields);\n\t}\n\tif (typeof reply !== 'object' || reply === null) return undefined;\n\t// No id: no item, whatever else a hash left half written holds.\n\treturn (reply as Fields)['id'] === undefined ? undefined : (reply as Fields);\n}\n\nconst field = (fields: Fields, name: string): string => fields[name] ?? '';\n\n/** The item a hash holds. Times are kept as JavaScript wrote them, so each reads back exactly. */\nexport function itemOf(fields: Fields): QueueItem {\n\tconst item: QueueItem = {\n\t\tid: field(fields, 'id'),\n\t\tfrom: field(fields, 'from'),\n\t\trecipients: JSON.parse(field(fields, 'recipients')) as RecipientState[],\n\t\tsize: Number(field(fields, 'size')),\n\t\tcreatedAt: Number(field(fields, 'created')),\n\t\tnextAttemptAt: Number(field(fields, 'next')),\n\t\tattempts: Number(field(fields, 'attempts')),\n\t\tdelayNotified: field(fields, 'delay') === '1',\n\t};\n\tconst owner = fields['owner'];\n\tconst expires = fields['expires'];\n\tif (owner === undefined || expires === undefined) return item;\n\treturn { ...item, lease: { owner, expiresAt: Number(expires) } };\n}\n\n/**\n * The ids a store makes, `crypto.randomUUID()`'s. Any other id is one no\n * item has, so no id a caller gives ever reaches a key but an item's.\n */\nconst ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\nexport const isId = (id: unknown): id is string =>\n\ttypeof id === 'string' && ID.test(id);\n\n/** A time as a script takes it: the string JavaScript writes, which reads back exactly. */\nexport const time = (value: number) => String(value);\n\n/** What `COMPLETE` records of an attempt: the item dropped, or its new state. */\nexport const outcomeOf = (item: QueueItem): string[] =>\n\tisDone(item)\n\t\t? ['drop']\n\t\t: [\n\t\t\t\t'record',\n\t\t\t\tJSON.stringify(item.recipients),\n\t\t\t\ttime(item.nextAttemptAt),\n\t\t\t\t`${item.attempts}`,\n\t\t\t\titem.delayNotified ? '1' : '0',\n\t\t\t];\n",
|
|
7
|
-
"/**\n * The Lua scripts a Redis store runs: each operation that writes is one\n * script, so Redis runs it whole, with no other command between its\n * steps. The fixed keys come as `KEYS`; an item's own keys are its\n * prefix (`ARGV[1]`), `item:` or `message:`, and its id.\n *\n * An item is a hash, `<prefix>item:<id>`; its message a string,\n * `<prefix>message:<id>`, written from the bytes given, as they are. Its\n * member in the sorted sets is its sequence number, sixteen digits, `:`,\n * and its id: Redis orders members of one score by their bytes, so items\n * equally due come oldest first. Times travel as the strings JavaScript\n * writes for them and are only compared here (`tonumber`), never written\n * back from Lua, so none loses a digit.\n */\n\n/** What every script starts with. */\nconst PRELUDE = `\nlocal prefix = ARGV[1]\nlocal function itemKey(member) return prefix .. 'item:' .. string.sub(member, 18) end\nlocal function seqOf(member) return tonumber(string.sub(member, 1, 16)) end\n`;\n\n/** For CLAIM: when an item's due time, or false when its hash is gone (evicted, deleted by hand). */\nconst NEXT_OF = `\nlocal function nextOf(member)\n\tlocal found = redis.call('HGET', itemKey(member), 'next')\n\tif found then return tonumber(found) end\n\t-- A member whose hash is gone is dropped, never leased.\n\tredis.call('ZREM', KEYS[1], member)\n\tredis.call('ZREM', KEYS[2], member)\n\tredis.call('ZREM', KEYS[3], member)\n\tredis.call('DEL', prefix .. 'message:' .. string.sub(member, 18))\n\treturn false\nend\n`;\n\n/** A script, and the SHA-1 `EVALSHA` names it by. */\nexport interface Script {\n\treadonly source: string;\n\treadonly sha: string;\n}\n\nconst script = (body: string): Script => {\n\tconst source = PRELUDE + body;\n\tconst sha = new Bun.CryptoHasher('sha1').update(source).digest('hex');\n\treturn { source, sha };\n};\n\n/**\n * KEYS schema; ARGV prefix, version. Writes the version on a new queue;\n * returns the one found.\n */\nexport const SCHEMA = script(`\nlocal found = redis.call('GET', KEYS[1])\nif found then return found end\nredis.call('SET', KEYS[1], ARGV[2])\nreturn ARGV[2]\n`);\n\n/**\n * KEYS items, ready, seq; ARGV prefix, id, from, recipients, size,\n * created, message, max (or ''). `{'full', n}` when `max` items are\n * there already, else `{'ok'}`.\n */\nexport const ADD = script(`\nlocal max = tonumber(ARGV[8])\nif max then\n\tlocal n = redis.call('ZCARD', KEYS[1])\n\tif n >= max then return {'full', tostring(n)} end\nend\nlocal member = string.format('%016d', redis.call('INCR', KEYS[3])) .. ':' .. ARGV[2]\nredis.call('HSET', prefix .. 'item:' .. ARGV[2], 'id', ARGV[2], 'from', ARGV[3],\n\t'recipients', ARGV[4], 'size', ARGV[5], 'created', ARGV[6], 'next', ARGV[6],\n\t'attempts', '0', 'delay', '0', 'rev', '0', 'member', member)\nredis.call('SET', prefix .. 'message:' .. ARGV[2], ARGV[7])\nredis.call('ZADD', KEYS[1], ARGV[6], member)\nredis.call('ZADD', KEYS[2], ARGV[6], member)\nreturn {'ok'}\n`);\n\n/**\n * KEYS ready, leases, items; ARGV prefix, owner, now, expiry. The\n * earliest due, then the oldest, of the items no lease holds and of those\n * whose lease expired, leased to the owner; its fields, or false. The\n * expired leases are read whole: there are as many as the leases a\n * crashed or stalled worker left behind, and the leased items an admin\n * moved later, until they are due.\n */\nexport const CLAIM = script(`${NEXT_OF}\nlocal now = tonumber(ARGV[3])\nlocal best, bestNext\nwhile best == nil do\n\tlocal first = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[3], 'LIMIT', 0, 1)[1]\n\tif not first then break end\n\tlocal nextAt = nextOf(first)\n\tif nextAt then\n\t\tbest = first\n\t\tbestNext = nextAt\n\tend\nend\nfor _, member in ipairs(redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', ARGV[3])) do\n\tlocal nextAt = nextOf(member)\n\tif nextAt and nextAt <= now and (best == nil or nextAt < bestNext\n\t\tor (nextAt == bestNext and seqOf(member) < seqOf(best))) then\n\t\tbest = member\n\t\tbestNext = nextAt\n\tend\nend\nif best == nil then return false end\nredis.call('ZREM', KEYS[1], best)\nredis.call('ZADD', KEYS[2], ARGV[4], best)\nredis.call('HSET', itemKey(best), 'owner', ARGV[2], 'expires', ARGV[4])\nreturn redis.call('HGETALL', itemKey(best))\n`);\n\n/** KEYS leases; ARGV prefix, id, owner, expiry. 1 while the owner holds the lease, else 0. */\nexport const RENEW = script(`\nlocal key = prefix .. 'item:' .. ARGV[2]\nif redis.call('HGET', key, 'owner') ~= ARGV[3] then return 0 end\nredis.call('HSET', key, 'expires', ARGV[4])\nredis.call('ZADD', KEYS[1], ARGV[4], redis.call('HGET', key, 'member'))\nreturn 1\n`);\n\n/**\n * KEYS items, ready, leases; ARGV prefix, id, owner, rev, then 'drop',\n * or 'record' and recipients, next, attempts, delay. 0 when the owner\n * does not hold the lease; -1 when another outcome was recorded since\n * the item was read at `rev`, to read it again; 1 once recorded, the\n * lease let go of, or the item and its message dropped.\n */\nexport const COMPLETE = script(`\nlocal key = prefix .. 'item:' .. ARGV[2]\nif redis.call('HGET', key, 'owner') ~= ARGV[3] then return 0 end\nif redis.call('HGET', key, 'rev') ~= ARGV[4] then return -1 end\nlocal member = redis.call('HGET', key, 'member')\nredis.call('ZREM', KEYS[3], member)\nif ARGV[5] == 'drop' then\n\tredis.call('ZREM', KEYS[1], member)\n\tredis.call('ZREM', KEYS[2], member)\n\tredis.call('DEL', key, prefix .. 'message:' .. ARGV[2])\n\treturn 1\nend\nredis.call('HSET', key, 'recipients', ARGV[6], 'next', ARGV[7],\n\t'attempts', ARGV[8], 'delay', ARGV[9])\nredis.call('HINCRBY', key, 'rev', 1)\nredis.call('HDEL', key, 'owner', 'expires')\nredis.call('ZADD', KEYS[1], ARGV[7], member)\nredis.call('ZADD', KEYS[2], ARGV[7], member)\nreturn 1\n`);\n\n/**\n * KEYS items, ready, leases; ARGV prefix, id, at, owner (or ''). With an\n * owner, only while it holds the lease, which it lets go of; without,\n * the lease stays. 1 when moved, else 0.\n */\nexport const RESCHEDULE = script(`\nlocal key = prefix .. 'item:' .. ARGV[2]\nlocal member = redis.call('HGET', key, 'member')\nif not member then return 0 end\nlocal owner = redis.call('HGET', key, 'owner')\nif ARGV[4] ~= '' then\n\tif owner ~= ARGV[4] then return 0 end\n\tredis.call('HDEL', key, 'owner', 'expires')\n\tredis.call('ZREM', KEYS[3], member)\n\towner = false\nend\nredis.call('HSET', key, 'next', ARGV[3])\nredis.call('ZADD', KEYS[1], ARGV[3], member)\nif not owner then redis.call('ZADD', KEYS[2], ARGV[3], member) end\nreturn 1\n`);\n\n/** KEYS items, ready, leases; ARGV prefix, id. The item's fields as it stood, or false; dropped with its message. */\nexport const CANCEL = script(`\nlocal key = prefix .. 'item:' .. ARGV[2]\nlocal fields = redis.call('HGETALL', key)\nif #fields == 0 then return false end\nlocal member = redis.call('HGET', key, 'member')\nredis.call('ZREM', KEYS[1], member)\nredis.call('ZREM', KEYS[2], member)\nredis.call('ZREM', KEYS[3], member)\nredis.call('DEL', key, prefix .. 'message:' .. ARGV[2])\nreturn fields\n`);\n\n/** KEYS items; ARGV prefix, first, last. A page of items, each its fields, in one read. */\nexport const LIST = script(`\nlocal out = {}\nfor _, member in ipairs(redis.call('ZRANGE', KEYS[1], ARGV[2], ARGV[3])) do\n\tout[#out + 1] = redis.call('HGETALL', itemKey(member))\nend\nreturn out\n`);\n",
|
|
6
|
+
"import { isDone } from '../contract/checks';\nimport type { QueueItem, RecipientState } from '../contract/types';\n\n/** An item's hash, as `HGETALL` gives it: a map, or a script's flat list of names and values. */\nexport type Fields = Readonly<Record<string, string>>;\n\n/** A script's `HGETALL`, `[name, value, …]`, as a map; a map as it is. */\nexport function fieldsOf(reply: unknown): Fields | undefined {\n\tif (Array.isArray(reply)) {\n\t\tconst fields: Record<string, string> = {};\n\t\tfor (let i = 0; i + 1 < reply.length; i += 2) {\n\t\t\tfields[String(reply[i])] = String(reply[i + 1]);\n\t\t}\n\t\treturn fieldsOf(fields);\n\t}\n\tif (typeof reply !== 'object' || reply === null) return undefined;\n\t// No id: no item, whatever else a hash left half written holds.\n\treturn (reply as Fields)['id'] === undefined ? undefined : (reply as Fields);\n}\n\nconst field = (fields: Fields, name: string): string => fields[name] ?? '';\n\n/** `RET` and `ENVID`: absent from a hash written before they existed. */\nfunction dsnFields(fields: Fields): Pick<QueueItem, 'ret' | 'envid'> {\n\tconst { ret, envid } = fields;\n\treturn {\n\t\t...(ret === 'FULL' || ret === 'HDRS' ? { ret } : {}),\n\t\t...(envid ? { envid } : {}),\n\t};\n}\n\n/** The item a hash holds. Times are kept as JavaScript wrote them, so each reads back exactly. */\nexport function itemOf(fields: Fields): QueueItem {\n\tconst item: QueueItem = {\n\t\tid: field(fields, 'id'),\n\t\tfrom: field(fields, 'from'),\n\t\trecipients: JSON.parse(field(fields, 'recipients')) as RecipientState[],\n\t\tsize: Number(field(fields, 'size')),\n\t\tcreatedAt: Number(field(fields, 'created')),\n\t\tnextAttemptAt: Number(field(fields, 'next')),\n\t\tattempts: Number(field(fields, 'attempts')),\n\t\tdelayNotified: field(fields, 'delay') === '1',\n\t\t...dsnFields(fields),\n\t};\n\tconst owner = fields['owner'];\n\tconst expires = fields['expires'];\n\tif (owner === undefined || expires === undefined) return item;\n\treturn { ...item, lease: { owner, expiresAt: Number(expires) } };\n}\n\n/**\n * The ids a store makes, `crypto.randomUUID()`'s. Any other id is one no\n * item has, so no id a caller gives ever reaches a key but an item's.\n */\nconst ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\nexport const isId = (id: unknown): id is string =>\n\ttypeof id === 'string' && ID.test(id);\n\n/** A time as a script takes it: the string JavaScript writes, which reads back exactly. */\nexport const time = (value: number) => String(value);\n\n/** What `COMPLETE` records of an attempt: the item dropped, or its new state. */\nexport const outcomeOf = (item: QueueItem): string[] =>\n\tisDone(item)\n\t\t? ['drop']\n\t\t: [\n\t\t\t\t'record',\n\t\t\t\tJSON.stringify(item.recipients),\n\t\t\t\ttime(item.nextAttemptAt),\n\t\t\t\t`${item.attempts}`,\n\t\t\t\titem.delayNotified ? '1' : '0',\n\t\t\t];\n\n/** The arguments of the `ADD` script, after the prefix: see `ADD`. */\nexport const addArgs = (\n\tadded: QueueItem,\n\tmessage: Uint8Array,\n\tmax: number | undefined,\n): (string | Uint8Array)[] => [\n\tadded.id,\n\tadded.from,\n\tJSON.stringify(added.recipients),\n\t`${added.size}`,\n\ttime(added.createdAt),\n\tmessage,\n\tmax === undefined ? '' : `${max}`,\n\tadded.ret ?? '',\n\tadded.envid ?? '',\n];\n",
|
|
7
|
+
"/**\n * The Lua scripts a Redis store runs: each operation that writes is one\n * script, so Redis runs it whole, with no other command between its\n * steps. The fixed keys come as `KEYS`; an item's own keys are its\n * prefix (`ARGV[1]`), `item:` or `message:`, and its id.\n *\n * An item is a hash, `<prefix>item:<id>`; its message a string,\n * `<prefix>message:<id>`, written from the bytes given, as they are. Its\n * member in the sorted sets is its sequence number, sixteen digits, `:`,\n * and its id: Redis orders members of one score by their bytes, so items\n * equally due come oldest first. Times travel as the strings JavaScript\n * writes for them and are only compared here (`tonumber`), never written\n * back from Lua, so none loses a digit.\n */\n\n/** What every script starts with. */\nconst PRELUDE = `\nlocal prefix = ARGV[1]\nlocal function itemKey(member) return prefix .. 'item:' .. string.sub(member, 18) end\nlocal function seqOf(member) return tonumber(string.sub(member, 1, 16)) end\n`;\n\n/** For CLAIM: when an item's due time, or false when its hash is gone (evicted, deleted by hand). */\nconst NEXT_OF = `\nlocal function nextOf(member)\n\tlocal found = redis.call('HGET', itemKey(member), 'next')\n\tif found then return tonumber(found) end\n\t-- A member whose hash is gone is dropped, never leased.\n\tredis.call('ZREM', KEYS[1], member)\n\tredis.call('ZREM', KEYS[2], member)\n\tredis.call('ZREM', KEYS[3], member)\n\tredis.call('DEL', prefix .. 'message:' .. string.sub(member, 18))\n\treturn false\nend\n`;\n\n/** A script, and the SHA-1 `EVALSHA` names it by. */\nexport interface Script {\n\treadonly source: string;\n\treadonly sha: string;\n}\n\nconst script = (body: string): Script => {\n\tconst source = PRELUDE + body;\n\tconst sha = new Bun.CryptoHasher('sha1').update(source).digest('hex');\n\treturn { source, sha };\n};\n\n/**\n * KEYS schema; ARGV prefix, version. Writes the version on a new queue;\n * returns the one found.\n */\nexport const SCHEMA = script(`\nlocal found = redis.call('GET', KEYS[1])\nif found then return found end\nredis.call('SET', KEYS[1], ARGV[2])\nreturn ARGV[2]\n`);\n\n/**\n * KEYS items, ready, seq; ARGV prefix, id, from, recipients, size,\n * created, message, max (or ''), ret (or ''), envid (or ''). `{'full', n}` when `max` items are\n * there already, else `{'ok'}`.\n */\nexport const ADD = script(`\nlocal max = tonumber(ARGV[8])\nif max then\n\tlocal n = redis.call('ZCARD', KEYS[1])\n\tif n >= max then return {'full', tostring(n)} end\nend\nlocal member = string.format('%016d', redis.call('INCR', KEYS[3])) .. ':' .. ARGV[2]\nredis.call('HSET', prefix .. 'item:' .. ARGV[2], 'id', ARGV[2], 'from', ARGV[3],\n\t'recipients', ARGV[4], 'size', ARGV[5], 'created', ARGV[6], 'next', ARGV[6],\n\t'attempts', '0', 'delay', '0', 'rev', '0', 'member', member)\nif ARGV[9] ~= '' then redis.call('HSET', prefix .. 'item:' .. ARGV[2], 'ret', ARGV[9]) end\nif ARGV[10] ~= '' then redis.call('HSET', prefix .. 'item:' .. ARGV[2], 'envid', ARGV[10]) end\nredis.call('SET', prefix .. 'message:' .. ARGV[2], ARGV[7])\nredis.call('ZADD', KEYS[1], ARGV[6], member)\nredis.call('ZADD', KEYS[2], ARGV[6], member)\nreturn {'ok'}\n`);\n\n/**\n * KEYS ready, leases, items; ARGV prefix, owner, now, expiry. The\n * earliest due, then the oldest, of the items no lease holds and of those\n * whose lease expired, leased to the owner; its fields, or false. The\n * expired leases are read whole: there are as many as the leases a\n * crashed or stalled worker left behind, and the leased items an admin\n * moved later, until they are due.\n */\nexport const CLAIM = script(`${NEXT_OF}\nlocal now = tonumber(ARGV[3])\nlocal best, bestNext\nwhile best == nil do\n\tlocal first = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[3], 'LIMIT', 0, 1)[1]\n\tif not first then break end\n\tlocal nextAt = nextOf(first)\n\tif nextAt then\n\t\tbest = first\n\t\tbestNext = nextAt\n\tend\nend\nfor _, member in ipairs(redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', ARGV[3])) do\n\tlocal nextAt = nextOf(member)\n\tif nextAt and nextAt <= now and (best == nil or nextAt < bestNext\n\t\tor (nextAt == bestNext and seqOf(member) < seqOf(best))) then\n\t\tbest = member\n\t\tbestNext = nextAt\n\tend\nend\nif best == nil then return false end\nredis.call('ZREM', KEYS[1], best)\nredis.call('ZADD', KEYS[2], ARGV[4], best)\nredis.call('HSET', itemKey(best), 'owner', ARGV[2], 'expires', ARGV[4])\nreturn redis.call('HGETALL', itemKey(best))\n`);\n\n/** KEYS leases; ARGV prefix, id, owner, expiry. 1 while the owner holds the lease, else 0. */\nexport const RENEW = script(`\nlocal key = prefix .. 'item:' .. ARGV[2]\nif redis.call('HGET', key, 'owner') ~= ARGV[3] then return 0 end\nredis.call('HSET', key, 'expires', ARGV[4])\nredis.call('ZADD', KEYS[1], ARGV[4], redis.call('HGET', key, 'member'))\nreturn 1\n`);\n\n/**\n * KEYS items, ready, leases; ARGV prefix, id, owner, rev, then 'drop',\n * or 'record' and recipients, next, attempts, delay. 0 when the owner\n * does not hold the lease; -1 when another outcome was recorded since\n * the item was read at `rev`, to read it again; 1 once recorded, the\n * lease let go of, or the item and its message dropped.\n */\nexport const COMPLETE = script(`\nlocal key = prefix .. 'item:' .. ARGV[2]\nif redis.call('HGET', key, 'owner') ~= ARGV[3] then return 0 end\nif redis.call('HGET', key, 'rev') ~= ARGV[4] then return -1 end\nlocal member = redis.call('HGET', key, 'member')\nredis.call('ZREM', KEYS[3], member)\nif ARGV[5] == 'drop' then\n\tredis.call('ZREM', KEYS[1], member)\n\tredis.call('ZREM', KEYS[2], member)\n\tredis.call('DEL', key, prefix .. 'message:' .. ARGV[2])\n\treturn 1\nend\nredis.call('HSET', key, 'recipients', ARGV[6], 'next', ARGV[7],\n\t'attempts', ARGV[8], 'delay', ARGV[9])\nredis.call('HINCRBY', key, 'rev', 1)\nredis.call('HDEL', key, 'owner', 'expires')\nredis.call('ZADD', KEYS[1], ARGV[7], member)\nredis.call('ZADD', KEYS[2], ARGV[7], member)\nreturn 1\n`);\n\n/**\n * KEYS items, ready, leases; ARGV prefix, id, at, owner (or ''). With an\n * owner, only while it holds the lease, which it lets go of; without,\n * the lease stays. 1 when moved, else 0.\n */\nexport const RESCHEDULE = script(`\nlocal key = prefix .. 'item:' .. ARGV[2]\nlocal member = redis.call('HGET', key, 'member')\nif not member then return 0 end\nlocal owner = redis.call('HGET', key, 'owner')\nif ARGV[4] ~= '' then\n\tif owner ~= ARGV[4] then return 0 end\n\tredis.call('HDEL', key, 'owner', 'expires')\n\tredis.call('ZREM', KEYS[3], member)\n\towner = false\nend\nredis.call('HSET', key, 'next', ARGV[3])\nredis.call('ZADD', KEYS[1], ARGV[3], member)\nif not owner then redis.call('ZADD', KEYS[2], ARGV[3], member) end\nreturn 1\n`);\n\n/** KEYS items, ready, leases; ARGV prefix, id. The item's fields as it stood, or false; dropped with its message. */\nexport const CANCEL = script(`\nlocal key = prefix .. 'item:' .. ARGV[2]\nlocal fields = redis.call('HGETALL', key)\nif #fields == 0 then return false end\nlocal member = redis.call('HGET', key, 'member')\nredis.call('ZREM', KEYS[1], member)\nredis.call('ZREM', KEYS[2], member)\nredis.call('ZREM', KEYS[3], member)\nredis.call('DEL', key, prefix .. 'message:' .. ARGV[2])\nreturn fields\n`);\n\n/** KEYS items; ARGV prefix, first, last. A page of items, each its fields, in one read. */\nexport const LIST = script(`\nlocal out = {}\nfor _, member in ipairs(redis.call('ZRANGE', KEYS[1], ARGV[2], ARGV[3])) do\n\tout[#out + 1] = redis.call('HGETALL', itemKey(member))\nend\nreturn out\n`);\n",
|
|
8
8
|
"import { QueueError } from '../errors';\nimport type { Keys } from './connect';\nimport type { RedisQueueClient } from './options';\nimport { SCHEMA, type Script } from './scripts';\n\n/** Redis no longer has the script: restarted, failed over, or `SCRIPT FLUSH`ed. */\nconst isNoScript = (error: unknown) =>\n\terror instanceof Error && error.message.startsWith('NOSCRIPT');\n\n/**\n * Runs a script by its SHA-1 (`EVALSHA`), or, when Redis does not have\n * it, by its source (`EVAL`), which also keeps it for the next call.\n * Either way Redis runs it whole.\n */\nexport async function run(\n\tclient: RedisQueueClient,\n\tscript: Script,\n\tkeys: string[],\n\targs: (string | Uint8Array)[],\n): Promise<unknown> {\n\tconst rest = [`${keys.length}`, ...keys, ...args];\n\ttry {\n\t\treturn await client.send('EVALSHA', [script.sha, ...rest]);\n\t} catch (error) {\n\t\tif (!isNoScript(error)) throw error;\n\t\treturn client.send('EVAL', [script.source, ...rest]);\n\t}\n}\n\n/** The layout of the keys; a change is a new version, and a newer one is refused. */\nconst LAYOUT = 1;\n\n/** Reads the layout version, writing it on a new queue; a newer one is `INVALID`. */\nexport async function checkLayout(\n\tclient: RedisQueueClient,\n\tkeys: Keys,\n): Promise<void> {\n\tconst found = Number(\n\t\tawait run(client, SCHEMA, [keys.schema], [keys.prefix, `${LAYOUT}`]),\n\t);\n\tif (!Number.isInteger(found) || found < 1) {\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`The key ${keys.schema} does not hold a layout version: is the prefix another application's?`,\n\t\t);\n\t}\n\tif (found > LAYOUT) {\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`The database is at schema version ${found}, newer than this store's ${LAYOUT}`,\n\t\t);\n\t}\n}\n",
|
|
9
|
-
"import {\n\tapplyAttempt,\n\tcheckClaim,\n\tcheckList,\n\tcheckMaxItems,\n\tcheckNewItem,\n\tcheckOwner,\n\tcheckResult,\n\tcheckTime,\n\tnewItem,\n} from '../contract/checks';\nimport type { QueueStore } from '../contract/queue-store';\nimport type {\n\tAddOptions,\n\tAttemptResult,\n\tClaimRequest,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n} from '../contract/types';\nimport { QueueError } from '../errors';\nimport { masked } from '../masked';\nimport { isStorable } from '../text';\nimport { type Connection, connect, type Keys } from './connect';\nimport { fieldsOf, isId, itemOf, outcomeOf, time } from './items';\nimport type { RedisQueueClient, RedisQueueStoreOptions } from './options';\nimport { checkLayout, run } from './run';\nimport {\n\tADD,\n\tCANCEL,\n\tCLAIM,\n\tCOMPLETE,\n\tLIST,\n\tRENEW,\n\tRESCHEDULE,\n\ttype Script,\n} from './scripts';\n\n/**\n * A `QueueStore` on Redis through Bun's own `Bun.RedisClient`: several\n * instances, on one machine or many, share one queue. Every operation\n * that writes is one Lua script, which Redis runs whole, so two\n * instances never take the same item. The due items are in sorted sets,\n * each item a hash, its message a string written byte for byte. One\n * Redis, or a primary with replicas — not Redis Cluster.\n */\nexport class RedisQueueStore implements QueueStore {\n\treadonly #client: RedisQueueClient;\n\treadonly #owned: boolean;\n\treadonly #keys: Keys;\n\treadonly #password: string;\n\t#ready: Promise<void> | undefined;\n\t#closed = false;\n\n\tprivate constructor({ client, owned, keys, password }: Connection) {\n\t\tthis.#client = client;\n\t\tthis.#owned = owned;\n\t\tthis.#keys = keys;\n\t\tthis.#password = password;\n\t}\n\n\t/** Checks the options; connects to nothing until the first call. What is wrong is `INVALID`. */\n\tstatic open(options: RedisQueueStoreOptions): RedisQueueStore {\n\t\treturn new RedisQueueStore(connect(options));\n\t}\n\n\t/** Closes the client the store opened for a URL, never one it was given; closing twice is fine. */\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tif (this.#owned) await this.#client.close();\n\t}\n\n\t/**\n\t * Reads the layout version, writing it on a new queue; once per\n\t * store. A failure is `INVALID`, and the next call tries again.\n\t */\n\t#setUp(): Promise<void> {\n\t\tthis.#ready ??= checkLayout(this.#client, this.#keys).catch((error) => {\n\t\t\tthis.#ready = undefined;\n\t\t\tif (error instanceof QueueError) throw error;\n\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new QueueError(\n\t\t\t\t'INVALID',\n\t\t\t\t`The Redis queue cannot be set up: ${masked(reason, this.#password)}`,\n\t\t\t);\n\t\t});\n\t\treturn this.#ready;\n\t}\n\n\t/** The client, once the layout is checked; `CLOSED` after `close()`. */\n\tasync #redis(): Promise<RedisQueueClient> {\n\t\tif (this.#closed) throw closed();\n\t\tawait this.#setUp();\n\t\treturn this.#client;\n\t}\n\n\tasync #run(script: Script, keys: string[], args: (string | Uint8Array)[]) {\n\t\tconst client = await this.#redis();\n\t\treturn run(client, script, keys, [this.#keys.prefix, ...args]);\n\t}\n\n\tasync add(item: NewQueueItem, options: AddOptions = {}): Promise<QueueItem> {\n\t\tcheckNewItem(item);\n\t\tconst max = checkMaxItems(options.maxItems);\n\t\tconst added = newItem(crypto.randomUUID(), item);\n\t\tconst k = this.#keys;\n\t\tconst reply = (await this.#run(\n\t\t\tADD,\n\t\t\t[k.items, k.ready, k.seq],\n\t\t\
|
|
9
|
+
"import {\n\tapplyAttempt,\n\tcheckClaim,\n\tcheckList,\n\tcheckMaxItems,\n\tcheckNewItem,\n\tcheckOwner,\n\tcheckResult,\n\tcheckTime,\n\tnewItem,\n} from '../contract/checks';\nimport type { QueueStore } from '../contract/queue-store';\nimport type {\n\tAddOptions,\n\tAttemptResult,\n\tClaimRequest,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n} from '../contract/types';\nimport { QueueError } from '../errors';\nimport { masked } from '../masked';\nimport { isStorable } from '../text';\nimport { type Connection, connect, type Keys } from './connect';\nimport { addArgs, fieldsOf, isId, itemOf, outcomeOf, time } from './items';\nimport type { RedisQueueClient, RedisQueueStoreOptions } from './options';\nimport { checkLayout, run } from './run';\nimport {\n\tADD,\n\tCANCEL,\n\tCLAIM,\n\tCOMPLETE,\n\tLIST,\n\tRENEW,\n\tRESCHEDULE,\n\ttype Script,\n} from './scripts';\n\n/**\n * A `QueueStore` on Redis through Bun's own `Bun.RedisClient`: several\n * instances, on one machine or many, share one queue. Every operation\n * that writes is one Lua script, which Redis runs whole, so two\n * instances never take the same item. The due items are in sorted sets,\n * each item a hash, its message a string written byte for byte. One\n * Redis, or a primary with replicas — not Redis Cluster.\n */\nexport class RedisQueueStore implements QueueStore {\n\treadonly #client: RedisQueueClient;\n\treadonly #owned: boolean;\n\treadonly #keys: Keys;\n\treadonly #password: string;\n\t#ready: Promise<void> | undefined;\n\t#closed = false;\n\n\tprivate constructor({ client, owned, keys, password }: Connection) {\n\t\tthis.#client = client;\n\t\tthis.#owned = owned;\n\t\tthis.#keys = keys;\n\t\tthis.#password = password;\n\t}\n\n\t/** Checks the options; connects to nothing until the first call. What is wrong is `INVALID`. */\n\tstatic open(options: RedisQueueStoreOptions): RedisQueueStore {\n\t\treturn new RedisQueueStore(connect(options));\n\t}\n\n\t/** Closes the client the store opened for a URL, never one it was given; closing twice is fine. */\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tif (this.#owned) await this.#client.close();\n\t}\n\n\t/**\n\t * Reads the layout version, writing it on a new queue; once per\n\t * store. A failure is `INVALID`, and the next call tries again.\n\t */\n\t#setUp(): Promise<void> {\n\t\tthis.#ready ??= checkLayout(this.#client, this.#keys).catch((error) => {\n\t\t\tthis.#ready = undefined;\n\t\t\tif (error instanceof QueueError) throw error;\n\t\t\tconst reason = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new QueueError(\n\t\t\t\t'INVALID',\n\t\t\t\t`The Redis queue cannot be set up: ${masked(reason, this.#password)}`,\n\t\t\t);\n\t\t});\n\t\treturn this.#ready;\n\t}\n\n\t/** The client, once the layout is checked; `CLOSED` after `close()`. */\n\tasync #redis(): Promise<RedisQueueClient> {\n\t\tif (this.#closed) throw closed();\n\t\tawait this.#setUp();\n\t\treturn this.#client;\n\t}\n\n\tasync #run(script: Script, keys: string[], args: (string | Uint8Array)[]) {\n\t\tconst client = await this.#redis();\n\t\treturn run(client, script, keys, [this.#keys.prefix, ...args]);\n\t}\n\n\tasync add(item: NewQueueItem, options: AddOptions = {}): Promise<QueueItem> {\n\t\tcheckNewItem(item);\n\t\tconst max = checkMaxItems(options.maxItems);\n\t\tconst added = newItem(crypto.randomUUID(), item);\n\t\tconst k = this.#keys;\n\t\tconst reply = (await this.#run(\n\t\t\tADD,\n\t\t\t[k.items, k.ready, k.seq],\n\t\t\taddArgs(added, item.message, max),\n\t\t)) as string[];\n\t\tif (reply[0] === 'full') {\n\t\t\tthrow new QueueError(\n\t\t\t\t'QUEUE_FULL',\n\t\t\t\t`The queue holds ${reply[1]} items, its limit`,\n\t\t\t);\n\t\t}\n\t\treturn added;\n\t}\n\n\tasync get(id: string): Promise<QueueItem | undefined> {\n\t\tif (!isId(id)) return undefined;\n\t\tconst client = await this.#redis();\n\t\tconst fields = fieldsOf(\n\t\t\tawait client.send('HGETALL', [`${this.#keys.prefix}item:${id}`]),\n\t\t);\n\t\treturn fields ? itemOf(fields) : undefined;\n\t}\n\n\tasync list(options?: QueueListOptions): Promise<QueueItem[]> {\n\t\tconst { offset, limit } = checkList(options);\n\t\tconst pages = (await this.#run(\n\t\t\tLIST,\n\t\t\t[this.#keys.items],\n\t\t\t[`${offset}`, `${offset + limit - 1}`],\n\t\t)) as unknown[];\n\t\treturn pages.flatMap((reply) => {\n\t\t\tconst fields = fieldsOf(reply);\n\t\t\treturn fields ? [itemOf(fields)] : [];\n\t\t});\n\t}\n\n\tasync count(): Promise<number> {\n\t\tconst client = await this.#redis();\n\t\treturn Number(await client.send('ZCARD', [this.#keys.items]));\n\t}\n\n\tasync readMessage(id: string): Promise<Uint8Array | undefined> {\n\t\tif (!isId(id)) return undefined;\n\t\tconst client = await this.#redis();\n\t\tconst bytes = await client.getBuffer(`${this.#keys.prefix}message:${id}`);\n\t\treturn bytes === null ? undefined : new Uint8Array(bytes);\n\t}\n\n\tasync claim(request: ClaimRequest): Promise<QueueItem | undefined> {\n\t\tcheckClaim(request);\n\t\tconst { owner, now, leaseMs } = request;\n\t\tconst k = this.#keys;\n\t\tconst fields = fieldsOf(\n\t\t\tawait this.#run(\n\t\t\t\tCLAIM,\n\t\t\t\t[k.ready, k.leases, k.items],\n\t\t\t\t[owner, time(now), time(now + leaseMs)],\n\t\t\t),\n\t\t);\n\t\treturn fields ? itemOf(fields) : undefined;\n\t}\n\n\tasync renew(id: string, owner: string, expiresAt: number): Promise<boolean> {\n\t\tcheckOwner(owner);\n\t\tcheckTime('expiresAt', expiresAt);\n\t\tif (!isId(id)) return false;\n\t\tconst k = this.#keys;\n\t\tconst done = await this.#run(\n\t\t\tRENEW,\n\t\t\t[k.leases],\n\t\t\t[id, owner, time(expiresAt)],\n\t\t);\n\t\treturn Number(done) === 1;\n\t}\n\n\tasync complete(\n\t\tid: string,\n\t\towner: string,\n\t\tresult: AttemptResult,\n\t): Promise<QueueItem | undefined> {\n\t\tcheckOwner(owner);\n\t\tcheckResult(result);\n\t\tif (!isId(id)) return undefined;\n\t\tconst k = this.#keys;\n\t\tconst client = await this.#redis();\n\t\tfor (let read = 0; read < MAX_READS; read++) {\n\t\t\tconst fields = fieldsOf(\n\t\t\t\tawait client.send('HGETALL', [`${k.prefix}item:${id}`]),\n\t\t\t);\n\t\t\tif (!fields || fields['owner'] !== owner) return undefined;\n\t\t\tconst rev = fields['rev']; // none: damaged by hand, nothing recorded\n\t\t\tif (rev === undefined) return undefined;\n\t\t\tconst item = applyAttempt(itemOf(fields), result);\n\t\t\tconst done = Number(\n\t\t\t\tawait this.#run(\n\t\t\t\t\tCOMPLETE,\n\t\t\t\t\t[k.items, k.ready, k.leases],\n\t\t\t\t\t[id, owner, rev, ...outcomeOf(item)],\n\t\t\t\t),\n\t\t\t);\n\t\t\tif (done === 1) return item;\n\t\t\tif (done === 0) return undefined;\n\t\t}\n\t\treturn undefined; // outrun MAX_READS times: told as a lease lost\n\t}\n\n\tasync reschedule(id: string, at: number, owner?: string): Promise<boolean> {\n\t\tcheckTime('at', at);\n\t\t// As PostgreSQL: an id no store could hold is unknown before the owner is checked.\n\t\tif (!isStorable(id)) return false;\n\t\tif (owner !== undefined) checkOwner(owner);\n\t\tif (!isId(id)) return false;\n\t\tconst k = this.#keys;\n\t\tconst done = await this.#run(\n\t\t\tRESCHEDULE,\n\t\t\t[k.items, k.ready, k.leases],\n\t\t\t[id, time(at), owner ?? ''],\n\t\t);\n\t\treturn Number(done) === 1;\n\t}\n\n\tasync cancel(id: string): Promise<QueueItem | undefined> {\n\t\tif (!isId(id)) return undefined;\n\t\tconst k = this.#keys;\n\t\tconst fields = fieldsOf(\n\t\t\tawait this.#run(CANCEL, [k.items, k.ready, k.leases], [id]),\n\t\t);\n\t\treturn fields ? itemOf(fields) : undefined;\n\t}\n}\n\n/** How often `complete` reads an item, another outcome recorded since each read. */\nconst MAX_READS = 8;\n\nconst closed = () => new QueueError('CLOSED', 'The queue store is closed');\n"
|
|
10
10
|
],
|
|
11
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AA4BA,IAAM,iBAAiB;AAOvB,IAAM,SAAS;AAER,SAAS,MAAM,CAAC,OAAsB;AAAA,EAC5C,MAAM,SAAS,SAAS;AAAA,EACxB,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,KAAK,MAAM,GAAG;AAAA,IACvD,MAAM,QACL,0HAA0H,KAAK,UAAU,MAAM,GAChJ;AAAA,EACD;AAAA,EACA,OAAO;AAAA,IACN;AAAA,IACA,OAAO,GAAG;AAAA,IACV,OAAO,GAAG;AAAA,IACV,QAAQ,GAAG;AAAA,IACX,KAAK,GAAG;AAAA,IACR,QAAQ,GAAG;AAAA,EACZ;AAAA;AAGD,IAAM,QACL;AAGD,IAAM,UAAU;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAGA,SAAS,KAAK,CAAC,OAAqB;AAAA,EACnC,IAAI,OAAO,UAAU,YAAY,EAAE,iBAAiB;AAAA,IACnD,MAAM,QAAQ,KAAK;AAAA,EACpB,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,EACnC,IAAI,QAAQ,QAAQ,CAAC,QAAQ,SAAS,IAAI,QAAQ;AAAA,IAAG,MAAM,QAAQ,KAAK;AAAA,EACxE,OAAO;AAAA;AASR,SAAS,SAAS,CAAC,KAA4B;AAAA,EAC9C,IAAI;AAAA,IACH,OAAO,IAAI,IAAI,YAAY,IAAI,IAAI;AAAA,IAClC,OAAO,OAAO;AAAA,IACf,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IACpE,MAAM,QACL,oCAAoC,OAAO,QAAQ,IAAI,QAAQ,GAChE;AAAA;AAAA;AAIF,IAAM,WAAW,CAAC,UACjB,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA2B,SAAS,cAC5C,OAAQ,MAA2B,cAAc,cACjD,OAAQ,MAA2B,UAAU;AAGvC,SAAS,OAAO,CAAC,SAA6C;AAAA,EACpE,IAAI,OAAO,YAAY,YAAY,YAAY;AAAA,IAAM,MAAM,QAAQ,KAAK;AAAA,EACxE,MAAM,OAAO,OAAO,QAAQ,SAAS;AAAA,EACrC,QAAQ,QAAQ,QAAQ;AAAA,EACxB,IAAI,WAAW,aAAa,QAAQ,WAAW;AAAA,IAC9C,MAAM,QAAQ,mDAAmD;AAAA,EAClE;AAAA,EACA,IAAI,WAAW,WAAW;AAAA,IACzB,IAAI,CAAC,SAAS,MAAM;AAAA,MAAG,MAAM,QAAQ,KAAK;AAAA,IAC1C,OAAO,EAAE,QAAQ,OAAO,OAAO,MAAM,UAAU,GAAG;AAAA,EACnD;AAAA,EACA,MAAM,SAAS,MAAM,GAAG;AAAA,EACxB,OAAO;AAAA,IACN,QAAQ,UAAU,MAAM;AAAA,IACxB,OAAO;AAAA,IACP;AAAA,IACA,UAAU,OAAO;AAAA,EAClB;AAAA;;;AChHM,SAAS,QAAQ,CAAC,OAAoC;AAAA,EAC5D,IAAI,MAAM,QAAQ,KAAK,GAAG;AAAA,IACzB,MAAM,SAAiC,CAAC;AAAA,IACxC,SAAS,IAAI,EAAG,IAAI,IAAI,MAAM,QAAQ,KAAK,GAAG;AAAA,MAC7C,OAAO,OAAO,MAAM,EAAE,KAAK,OAAO,MAAM,IAAI,EAAE;AAAA,IAC/C;AAAA,IACA,OAAO,SAAS,MAAM;AAAA,EACvB;AAAA,EACA,IAAI,OAAO,UAAU,YAAY,UAAU;AAAA,IAAM;AAAA,EAEjD,OAAQ,MAAiB,UAAU,YAAY,YAAa;AAAA;AAG7D,IAAM,QAAQ,CAAC,QAAgB,SAAyB,OAAO,SAAS;
|
|
12
|
-
"debugId": "
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "709817593E83454864756E2164756E21",
|
|
13
13
|
"names": []
|
|
14
14
|
}
|
package/dist/redis/items.d.ts
CHANGED
|
@@ -10,4 +10,6 @@ export declare const isId: (id: unknown) => id is string;
|
|
|
10
10
|
export declare const time: (value: number) => string;
|
|
11
11
|
/** What `COMPLETE` records of an attempt: the item dropped, or its new state. */
|
|
12
12
|
export declare const outcomeOf: (item: QueueItem) => string[];
|
|
13
|
+
/** The arguments of the `ADD` script, after the prefix: see `ADD`. */
|
|
14
|
+
export declare const addArgs: (added: QueueItem, message: Uint8Array, max: number | undefined) => (string | Uint8Array)[];
|
|
13
15
|
//# sourceMappingURL=items.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/redis/items.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,mBAAmB,CAAC;AAEnE,iGAAiG;AACjG,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEtD,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAW3D;
|
|
1
|
+
{"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/redis/items.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,mBAAmB,CAAC;AAEnE,iGAAiG;AACjG,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEtD,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAW3D;AAaD,kGAAkG;AAClG,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAgBhD;AAQD,eAAO,MAAM,IAAI,GAAI,IAAI,OAAO,KAAG,EAAE,IAAI,MACH,CAAC;AAEvC,2FAA2F;AAC3F,eAAO,MAAM,IAAI,GAAI,OAAO,MAAM,WAAkB,CAAC;AAErD,iFAAiF;AACjF,eAAO,MAAM,SAAS,GAAI,MAAM,SAAS,KAAG,MAAM,EAS9C,CAAC;AAEL,sEAAsE;AACtE,eAAO,MAAM,OAAO,GACnB,OAAO,SAAS,EAChB,SAAS,UAAU,EACnB,KAAK,MAAM,GAAG,SAAS,KACrB,CAAC,MAAM,GAAG,UAAU,CAAC,EAUvB,CAAC"}
|
package/dist/redis/scripts.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface Script {
|
|
|
24
24
|
export declare const SCHEMA: Script;
|
|
25
25
|
/**
|
|
26
26
|
* KEYS items, ready, seq; ARGV prefix, id, from, recipients, size,
|
|
27
|
-
* created, message, max (or ''). `{'full', n}` when `max` items are
|
|
27
|
+
* created, message, max (or ''), ret (or ''), envid (or ''). `{'full', n}` when `max` items are
|
|
28
28
|
* there already, else `{'ok'}`.
|
|
29
29
|
*/
|
|
30
30
|
export declare const ADD: Script;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../../src/redis/scripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAuBH,qDAAqD;AACrD,MAAM,WAAW,MAAM;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACrB;AAQD;;;GAGG;AACH,eAAO,MAAM,MAAM,QAKjB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../../src/redis/scripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAuBH,qDAAqD;AACrD,MAAM,WAAW,MAAM;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACrB;AAQD;;;GAGG;AACH,eAAO,MAAM,MAAM,QAKjB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,GAAG,QAgBd,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,QAyBhB,CAAC;AAEH,8FAA8F;AAC9F,eAAO,MAAM,KAAK,QAMhB,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,QAmBnB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU,QAerB,CAAC;AAEH,qHAAqH;AACrH,eAAO,MAAM,MAAM,QAUjB,CAAC;AAEH,2FAA2F;AAC3F,eAAO,MAAM,IAAI,QAMf,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/redis/store.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAa1E;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,UAAU;;IAQjD,OAAO;IAOP,gGAAgG;IAChG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe;IAI7D,mGAAmG;IAC7F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCtB,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/redis/store.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,KAAK,EAAoB,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAa1E;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,UAAU;;IAQjD,OAAO;IAOP,gGAAgG;IAChG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe;IAI7D,mGAAmG;IAC7F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCtB,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC;IAmBrE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAS/C,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAatD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAKxB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAOxD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAc5D,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAarE,QAAQ,CACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IA2B3B,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAepE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;CAQxD"}
|
package/dist/sqlite/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
dsnColumn,
|
|
3
|
+
dsnParams
|
|
4
|
+
} from "../chunks/index-r8wb840a.js";
|
|
1
5
|
import {
|
|
2
6
|
QueueError2,
|
|
3
7
|
checkTime,
|
|
@@ -10,7 +14,7 @@ import {
|
|
|
10
14
|
isDone,
|
|
11
15
|
applyAttempt,
|
|
12
16
|
newItem
|
|
13
|
-
} from "../chunks/index-
|
|
17
|
+
} from "../chunks/index-9yp15arf.js";
|
|
14
18
|
|
|
15
19
|
// src/sqlite/open.ts
|
|
16
20
|
import { Database } from "bun:sqlite";
|
|
@@ -40,7 +44,8 @@ var MIGRATIONS = [
|
|
|
40
44
|
item_id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE,
|
|
41
45
|
content BLOB NOT NULL
|
|
42
46
|
) STRICT;
|
|
43
|
-
|
|
47
|
+
`,
|
|
48
|
+
`ALTER TABLE items ADD COLUMN dsn TEXT CHECK (dsn IS NULL OR json_valid(dsn))`
|
|
44
49
|
];
|
|
45
50
|
function migrate(db) {
|
|
46
51
|
db.transaction(() => {
|
|
@@ -108,7 +113,7 @@ function openDatabase(options) {
|
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
// src/sqlite/rows.ts
|
|
111
|
-
var COLUMNS = "id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at";
|
|
116
|
+
var COLUMNS = "id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at, dsn";
|
|
112
117
|
function itemOf(row) {
|
|
113
118
|
const item = {
|
|
114
119
|
id: row.id,
|
|
@@ -118,7 +123,8 @@ function itemOf(row) {
|
|
|
118
123
|
createdAt: row.created_at,
|
|
119
124
|
nextAttemptAt: row.next_attempt_at,
|
|
120
125
|
attempts: row.attempts,
|
|
121
|
-
delayNotified: row.delay_notified === 1
|
|
126
|
+
delayNotified: row.delay_notified === 1,
|
|
127
|
+
...dsnParams(row.dsn)
|
|
122
128
|
};
|
|
123
129
|
if (row.lease_owner === null || row.lease_expires_at === null)
|
|
124
130
|
return item;
|
|
@@ -190,15 +196,16 @@ class SqliteQueueStore {
|
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
db.query(`INSERT INTO items (id, sender, recipients, size, created_at,
|
|
193
|
-
next_attempt_at, attempts, delay_notified)
|
|
199
|
+
next_attempt_at, attempts, delay_notified, dsn)
|
|
194
200
|
VALUES ($id, $sender, $recipients, $size, $created_at,
|
|
195
|
-
$next_attempt_at, 0, 0)`).run({
|
|
201
|
+
$next_attempt_at, 0, 0, $dsn)`).run({
|
|
196
202
|
id: added.id,
|
|
197
203
|
sender: added.from,
|
|
198
204
|
recipients: JSON.stringify(added.recipients),
|
|
199
205
|
size: added.size,
|
|
200
206
|
created_at: added.createdAt,
|
|
201
|
-
next_attempt_at: added.nextAttemptAt
|
|
207
|
+
next_attempt_at: added.nextAttemptAt,
|
|
208
|
+
dsn: dsnColumn(added)
|
|
202
209
|
});
|
|
203
210
|
db.query("INSERT INTO messages (item_id, content) VALUES (?, ?)").run(added.id, item.message);
|
|
204
211
|
}).immediate();
|
|
@@ -282,5 +289,5 @@ export {
|
|
|
282
289
|
SqliteQueueStore
|
|
283
290
|
};
|
|
284
291
|
|
|
285
|
-
//# debugId=
|
|
292
|
+
//# debugId=92D7A7C42951038864756E2164756E21
|
|
286
293
|
//# sourceMappingURL=index.js.map
|
package/dist/sqlite/index.js.map
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
"sources": ["../src/sqlite/open.ts", "../src/sqlite/schema.ts", "../src/sqlite/rows.ts", "../src/sqlite/store.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { Database } from 'bun:sqlite';\nimport { chmodSync, existsSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { QueueError } from '../errors';\nimport type { SqliteQueueStoreOptions } from './options';\nimport { migrate } from './schema';\n\n/**\n * The queue's files, and the directory when the store makes it: its\n * owner's alone, as mail is. A directory that already exists keeps its\n * mode — it is the operator's, who may share it with a group on purpose.\n */\nconst PRIVATE_DIRECTORY = 0o700;\nconst PRIVATE_FILE = 0o600;\n\n/**\n * Several processes may share the database: WAL lets readers go on while\n * one writes, and a writer waits up to `busy_timeout` for another. Every\n * write is flushed before it is acknowledged.\n */\nfunction configure(db: Database, busyTimeout: number): void {\n\tdb.exec(`PRAGMA busy_timeout = ${busyTimeout}`);\n\tdb.exec('PRAGMA journal_mode = WAL');\n\tdb.exec('PRAGMA synchronous = FULL');\n\t// On macOS a plain fsync leaves the data in the drive's cache.\n\tdb.exec('PRAGMA fullfsync = ON');\n\tdb.exec('PRAGMA foreign_keys = ON');\n}\n\nfunction keepPrivate(file: string): void {\n\tfor (const path of [file, `${file}-wal`, `${file}-shm`]) {\n\t\tif (existsSync(path)) chmodSync(path, PRIVATE_FILE);\n\t}\n}\n\nfunction busyTimeoutOf(value: unknown): number {\n\tif (value === undefined) return 5000;\n\tif (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`busyTimeout must be an integer of at least 0, not ${value}`,\n\t\t);\n\t}\n\treturn value;\n}\n\nconst messageOf = (error: unknown) =>\n\terror instanceof Error ? error.message : String(error);\n\n/**\n * Opens the queue's database in `directory`, at the last migration.\n * Whatever fails is `INVALID`, naming the directory.\n */\nexport function openDatabase(options: SqliteQueueStoreOptions): Database {\n\tconst directory = (options as SqliteQueueStoreOptions | undefined)?.directory;\n\tif (typeof directory !== 'string' || directory === '') {\n\t\tthrow new QueueError('INVALID', 'A SQLite queue store needs a directory');\n\t}\n\tconst busyTimeout = busyTimeoutOf(options.busyTimeout);\n\tlet db: Database | undefined;\n\ttry {\n\t\tif (!existsSync(directory)) {\n\t\t\tmkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY });\n\t\t\t// The mode given to mkdir is narrowed by the umask, never widened: set it.\n\t\t\tchmodSync(directory, PRIVATE_DIRECTORY);\n\t\t}\n\t\tconst file = join(directory, 'queue.sqlite');\n\t\tdb = new Database(file, { create: true, readwrite: true, strict: true });\n\t\tconfigure(db, busyTimeout);\n\t\tkeepPrivate(file);\n\t\tmigrate(db);\n\t\treturn db;\n\t} catch (error) {\n\t\tdb?.close();\n\t\tif (error instanceof QueueError) throw error;\n\t\tthrow new QueueError(\n\t\t\t'INVALID',\n\t\t\t`The queue at \"${directory}\" cannot be opened: ${messageOf(error)}`,\n\t\t);\n\t}\n}\n",
|
|
6
|
-
"import type { Database } from 'bun:sqlite';\nimport { QueueError } from '../errors';\n\n/**\n * The migrations, in order: the database's `PRAGMA user_version` is how many\n * of them it has run. A migration is never edited once released; a change\n * is a new one at the end.\n */\nexport const MIGRATIONS: readonly string[] = [\n\t// 1: the items, and their messages in a table of their own.\n\t`\n\tCREATE TABLE items (\n\t\tseq INTEGER PRIMARY KEY,\n\t\tid TEXT NOT NULL UNIQUE,\n\t\tsender TEXT NOT NULL,\n\t\trecipients TEXT NOT NULL CHECK (json_valid(recipients)),\n\t\tsize INTEGER NOT NULL,\n\t\tcreated_at REAL NOT NULL,\n\t\tnext_attempt_at REAL NOT NULL,\n\t\tattempts INTEGER NOT NULL,\n\t\tdelay_notified INTEGER NOT NULL,\n\t\tlease_owner TEXT,\n\t\tlease_expires_at REAL,\n\t\tCHECK ((lease_owner IS NULL) = (lease_expires_at IS NULL))\n\t) STRICT;\n\tCREATE INDEX items_due ON items (next_attempt_at, seq);\n\n\tCREATE TABLE messages (\n\t\titem_id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE,\n\t\tcontent BLOB NOT NULL\n\t) STRICT;\n\t`,\n];\n\n/**\n * Brings the database to the last migration, in one transaction: a\n * migration that fails leaves the database as it was. A database written\n * by a newer store, with more migrations than this one knows, is refused.\n */\nexport function migrate(db: Database): void {\n\tdb.transaction(() => {\n\t\tconst { user_version: version } = db\n\t\t\t.query<{ user_version: number }, []>('PRAGMA user_version')\n\t\t\t.get() as { user_version: number };\n\t\tif (version > MIGRATIONS.length) {\n\t\t\tthrow new QueueError(\n\t\t\t\t'INVALID',\n\t\t\t\t`The database is at schema version ${version}, newer than this store's ${MIGRATIONS.length}`,\n\t\t\t);\n\t\t}\n\t\tfor (const sql of MIGRATIONS.slice(version)) db.exec(sql);\n\t\tif (version < MIGRATIONS.length) {\n\t\t\tdb.exec(`PRAGMA user_version = ${MIGRATIONS.length}`);\n\t\t}\n\t}).immediate();\n}\n",
|
|
7
|
-
"import type { QueueItem, RecipientState } from '../contract/types';\n\n/** A row of `items`, as `bun:sqlite` reads it. */\nexport interface ItemRow {\n\treadonly id: string;\n\treadonly sender: string;\n\treadonly recipients: string;\n\treadonly size: number;\n\treadonly created_at: number;\n\treadonly next_attempt_at: number;\n\treadonly attempts: number;\n\treadonly delay_notified: number;\n\treadonly lease_owner: string | null;\n\treadonly lease_expires_at: number | null;\n}\n\n/** The columns a query reads to build an item. */\nexport const COLUMNS =\n\t'id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at';\n\nexport function itemOf(row: ItemRow): QueueItem {\n\tconst item: QueueItem = {\n\t\tid: row.id,\n\t\tfrom: row.sender,\n\t\trecipients: JSON.parse(row.recipients) as RecipientState[],\n\t\tsize: row.size,\n\t\tcreatedAt: row.created_at,\n\t\tnextAttemptAt: row.next_attempt_at,\n\t\tattempts: row.attempts,\n\t\tdelayNotified: row.delay_notified === 1,\n\t};\n\tif (row.lease_owner === null || row.lease_expires_at === null) return item;\n\treturn {\n\t\t...item,\n\t\tlease: { owner: row.lease_owner, expiresAt: row.lease_expires_at },\n\t};\n}\n\n/** The named parameters that write an item's state back. */\nexport function stateOf(item: QueueItem) {\n\treturn {\n\t\tid: item.id,\n\t\trecipients: JSON.stringify(item.recipients),\n\t\tnext_attempt_at: item.nextAttemptAt,\n\t\tattempts: item.attempts,\n\t\tdelay_notified: item.delayNotified ? 1 : 0,\n\t\tlease_owner: item.lease?.owner ?? null,\n\t\tlease_expires_at: item.lease?.expiresAt ?? null,\n\t};\n}\n",
|
|
8
|
-
"import type { Database } from 'bun:sqlite';\nimport {\n\tapplyAttempt,\n\tcheckClaim,\n\tcheckList,\n\tcheckMaxItems,\n\tcheckNewItem,\n\tcheckOwner,\n\tcheckResult,\n\tcheckTime,\n\tisDone,\n\tnewItem,\n} from '../contract/checks';\nimport type { QueueStore } from '../contract/queue-store';\nimport type {\n\tAddOptions,\n\tAttemptResult,\n\tClaimRequest,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n} from '../contract/types';\nimport { QueueError } from '../errors';\nimport { openDatabase } from './open';\nimport type { SqliteQueueStoreOptions } from './options';\nimport { COLUMNS, type ItemRow, itemOf, stateOf } from './rows';\n\nexport type { SqliteQueueStoreOptions } from './options';\n\nconst DUE = `next_attempt_at <= $now\n\tAND (lease_owner IS NULL OR lease_expires_at <= $now)`;\n\n/**\n * A `QueueStore` on `bun:sqlite`: one directory holding `queue.sqlite`,\n * which several processes on one machine may open together. Every\n * operation is one transaction — a claim is a single `UPDATE … RETURNING`\n * — so two workers never take the same item. Messages live in a table of\n * their own, dropped with their item.\n */\nexport class SqliteQueueStore implements QueueStore {\n\treadonly #db: Database;\n\t#closed = false;\n\n\tprivate constructor(db: Database) {\n\t\tthis.#db = db;\n\t}\n\n\t/** Opens the store in `directory`, creating it if need be; what fails is `INVALID`. */\n\tstatic open(options: SqliteQueueStoreOptions): SqliteQueueStore {\n\t\treturn new SqliteQueueStore(openDatabase(options));\n\t}\n\n\t/** Lets go of the database; closing twice is fine. */\n\tclose(): void {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#db.close();\n\t}\n\n\tget #open(): Database {\n\t\tif (this.#closed) {\n\t\t\tthrow new QueueError('CLOSED', 'The queue store is closed');\n\t\t}\n\t\treturn this.#db;\n\t}\n\n\t#row(id: string, owner?: string): ItemRow | null {\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn db\n\t\t\t\t.query<ItemRow, [string]>(`SELECT ${COLUMNS} FROM items WHERE id = ?`)\n\t\t\t\t.get(id);\n\t\t}\n\t\treturn db\n\t\t\t.query<ItemRow, [string, string]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.get(id, owner);\n\t}\n\n\t#write(item: QueueItem): void {\n\t\tthis.#open\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET recipients = $recipients,\n\t\t\t\tnext_attempt_at = $next_attempt_at, attempts = $attempts,\n\t\t\t\tdelay_notified = $delay_notified, lease_owner = $lease_owner,\n\t\t\t\tlease_expires_at = $lease_expires_at WHERE id = $id`,\n\t\t\t)\n\t\t\t.run(stateOf(item));\n\t}\n\n\tasync add(item: NewQueueItem, options: AddOptions = {}): Promise<QueueItem> {\n\t\tcheckNewItem(item);\n\t\tconst max = checkMaxItems(options.maxItems);\n\t\tconst db = this.#open;\n\t\tconst added = newItem(crypto.randomUUID(), item);\n\t\tdb.transaction(() => {\n\t\t\tif (max !== undefined) {\n\t\t\t\tconst { n } = db\n\t\t\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t\t\t.get() as { n: number };\n\t\t\t\tif (n >= max) {\n\t\t\t\t\tthrow new QueueError(\n\t\t\t\t\t\t'QUEUE_FULL',\n\t\t\t\t\t\t`The queue holds ${n} items, its limit`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tdb.query(\n\t\t\t\t`INSERT INTO items (id, sender, recipients, size, created_at,\n\t\t\t\tnext_attempt_at, attempts, delay_notified)\n\t\t\t\tVALUES ($id, $sender, $recipients, $size, $created_at,\n\t\t\t\t$next_attempt_at, 0, 0)`,\n\t\t\t).run({\n\t\t\t\tid: added.id,\n\t\t\t\tsender: added.from,\n\t\t\t\trecipients: JSON.stringify(added.recipients),\n\t\t\t\tsize: added.size,\n\t\t\t\tcreated_at: added.createdAt,\n\t\t\t\tnext_attempt_at: added.nextAttemptAt,\n\t\t\t});\n\t\t\tdb.query('INSERT INTO messages (item_id, content) VALUES (?, ?)').run(\n\t\t\t\tadded.id,\n\t\t\t\titem.message,\n\t\t\t);\n\t\t}).immediate();\n\t\treturn added;\n\t}\n\n\tasync get(id: string): Promise<QueueItem | undefined> {\n\t\tconst row = this.#row(id);\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync list(options?: QueueListOptions): Promise<QueueItem[]> {\n\t\tconst { offset, limit } = checkList(options);\n\t\treturn this.#open\n\t\t\t.query<ItemRow, [number, number]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items ORDER BY next_attempt_at, seq\n\t\t\t\tLIMIT ? OFFSET ?`,\n\t\t\t)\n\t\t\t.all(limit, offset)\n\t\t\t.map(itemOf);\n\t}\n\n\tasync count(): Promise<number> {\n\t\tconst row = this.#open\n\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t.get();\n\t\treturn row?.n ?? 0;\n\t}\n\n\tasync readMessage(id: string): Promise<Uint8Array | undefined> {\n\t\tconst row = this.#open\n\t\t\t.query<{ content: Uint8Array }, [string]>(\n\t\t\t\t'SELECT content FROM messages WHERE item_id = ?',\n\t\t\t)\n\t\t\t.get(id);\n\t\treturn row ? new Uint8Array(row.content) : undefined;\n\t}\n\n\tasync claim(request: ClaimRequest): Promise<QueueItem | undefined> {\n\t\tcheckClaim(request);\n\t\tconst row = this.#open\n\t\t\t.query<ItemRow, Record<string, string | number>>(\n\t\t\t\t`UPDATE items SET lease_owner = $owner, lease_expires_at = $expires\n\t\t\t\tWHERE seq = (SELECT seq FROM items WHERE ${DUE}\n\t\t\t\t\tORDER BY next_attempt_at, seq LIMIT 1)\n\t\t\t\tRETURNING ${COLUMNS}`,\n\t\t\t)\n\t\t\t.get({\n\t\t\t\towner: request.owner,\n\t\t\t\tnow: request.now,\n\t\t\t\texpires: request.now + request.leaseMs,\n\t\t\t});\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync renew(id: string, owner: string, expiresAt: number): Promise<boolean> {\n\t\tcheckOwner(owner);\n\t\tcheckTime('expiresAt', expiresAt);\n\t\tconst { changes } = this.#open\n\t\t\t.query(\n\t\t\t\t'UPDATE items SET lease_expires_at = ? WHERE id = ? AND lease_owner = ?',\n\t\t\t)\n\t\t\t.run(expiresAt, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync complete(\n\t\tid: string,\n\t\towner: string,\n\t\tresult: AttemptResult,\n\t): Promise<QueueItem | undefined> {\n\t\tcheckOwner(owner);\n\t\tcheckResult(result);\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id, owner);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tconst item = applyAttempt(itemOf(row), result);\n\t\t\t\tif (isDone(item)) {\n\t\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\t} else {\n\t\t\t\t\tthis.#write(item);\n\t\t\t\t}\n\t\t\t\treturn item;\n\t\t\t})\n\t\t\t.immediate();\n\t}\n\n\tasync reschedule(id: string, at: number, owner?: string): Promise<boolean> {\n\t\tcheckTime('at', at);\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn (\n\t\t\t\tdb\n\t\t\t\t\t.query('UPDATE items SET next_attempt_at = ? WHERE id = ?')\n\t\t\t\t\t.run(at, id).changes > 0\n\t\t\t);\n\t\t}\n\t\tcheckOwner(owner);\n\t\tconst { changes } = db\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET next_attempt_at = ?, lease_owner = NULL,\n\t\t\t\tlease_expires_at = NULL WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.run(at, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync cancel(id: string): Promise<QueueItem | undefined> {\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\treturn itemOf(row);\n\t\t\t})\n\t\t\t.immediate();\n\t}\n}\n"
|
|
6
|
+
"import type { Database } from 'bun:sqlite';\nimport { QueueError } from '../errors';\n\n/**\n * The migrations, in order: the database's `PRAGMA user_version` is how many\n * of them it has run. A migration is never edited once released; a change\n * is a new one at the end.\n */\nexport const MIGRATIONS: readonly string[] = [\n\t// 1: the items, and their messages in a table of their own.\n\t`\n\tCREATE TABLE items (\n\t\tseq INTEGER PRIMARY KEY,\n\t\tid TEXT NOT NULL UNIQUE,\n\t\tsender TEXT NOT NULL,\n\t\trecipients TEXT NOT NULL CHECK (json_valid(recipients)),\n\t\tsize INTEGER NOT NULL,\n\t\tcreated_at REAL NOT NULL,\n\t\tnext_attempt_at REAL NOT NULL,\n\t\tattempts INTEGER NOT NULL,\n\t\tdelay_notified INTEGER NOT NULL,\n\t\tlease_owner TEXT,\n\t\tlease_expires_at REAL,\n\t\tCHECK ((lease_owner IS NULL) = (lease_expires_at IS NULL))\n\t) STRICT;\n\tCREATE INDEX items_due ON items (next_attempt_at, seq);\n\n\tCREATE TABLE messages (\n\t\titem_id TEXT PRIMARY KEY REFERENCES items (id) ON DELETE CASCADE,\n\t\tcontent BLOB NOT NULL\n\t) STRICT;\n\t`,\n\t// 2: the item's DSN parameters (RET, ENVID) as JSON; NULL for the rows of before.\n\t`ALTER TABLE items ADD COLUMN dsn TEXT CHECK (dsn IS NULL OR json_valid(dsn))`,\n];\n\n/**\n * Brings the database to the last migration, in one transaction: a\n * migration that fails leaves the database as it was. A database written\n * by a newer store, with more migrations than this one knows, is refused.\n */\nexport function migrate(db: Database): void {\n\tdb.transaction(() => {\n\t\tconst { user_version: version } = db\n\t\t\t.query<{ user_version: number }, []>('PRAGMA user_version')\n\t\t\t.get() as { user_version: number };\n\t\tif (version > MIGRATIONS.length) {\n\t\t\tthrow new QueueError(\n\t\t\t\t'INVALID',\n\t\t\t\t`The database is at schema version ${version}, newer than this store's ${MIGRATIONS.length}`,\n\t\t\t);\n\t\t}\n\t\tfor (const sql of MIGRATIONS.slice(version)) db.exec(sql);\n\t\tif (version < MIGRATIONS.length) {\n\t\t\tdb.exec(`PRAGMA user_version = ${MIGRATIONS.length}`);\n\t\t}\n\t}).immediate();\n}\n",
|
|
7
|
+
"import { dsnParams } from '../contract/dsn-column';\nimport type { QueueItem, RecipientState } from '../contract/types';\n\n/** A row of `items`, as `bun:sqlite` reads it. */\nexport interface ItemRow {\n\treadonly id: string;\n\treadonly sender: string;\n\treadonly recipients: string;\n\treadonly size: number;\n\treadonly created_at: number;\n\treadonly next_attempt_at: number;\n\treadonly attempts: number;\n\treadonly delay_notified: number;\n\treadonly lease_owner: string | null;\n\treadonly lease_expires_at: number | null;\n\treadonly dsn: string | null;\n}\n\n/** The columns a query reads to build an item. */\nexport const COLUMNS =\n\t'id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at, dsn';\n\nexport function itemOf(row: ItemRow): QueueItem {\n\tconst item: QueueItem = {\n\t\tid: row.id,\n\t\tfrom: row.sender,\n\t\trecipients: JSON.parse(row.recipients) as RecipientState[],\n\t\tsize: row.size,\n\t\tcreatedAt: row.created_at,\n\t\tnextAttemptAt: row.next_attempt_at,\n\t\tattempts: row.attempts,\n\t\tdelayNotified: row.delay_notified === 1,\n\t\t...dsnParams(row.dsn),\n\t};\n\tif (row.lease_owner === null || row.lease_expires_at === null) return item;\n\treturn {\n\t\t...item,\n\t\tlease: { owner: row.lease_owner, expiresAt: row.lease_expires_at },\n\t};\n}\n\n/** The named parameters that write an item's state back. */\nexport function stateOf(item: QueueItem) {\n\treturn {\n\t\tid: item.id,\n\t\trecipients: JSON.stringify(item.recipients),\n\t\tnext_attempt_at: item.nextAttemptAt,\n\t\tattempts: item.attempts,\n\t\tdelay_notified: item.delayNotified ? 1 : 0,\n\t\tlease_owner: item.lease?.owner ?? null,\n\t\tlease_expires_at: item.lease?.expiresAt ?? null,\n\t};\n}\n",
|
|
8
|
+
"import type { Database } from 'bun:sqlite';\nimport {\n\tapplyAttempt,\n\tcheckClaim,\n\tcheckList,\n\tcheckMaxItems,\n\tcheckNewItem,\n\tcheckOwner,\n\tcheckResult,\n\tcheckTime,\n\tisDone,\n\tnewItem,\n} from '../contract/checks';\nimport { dsnColumn } from '../contract/dsn-column';\nimport type { QueueStore } from '../contract/queue-store';\nimport type {\n\tAddOptions,\n\tAttemptResult,\n\tClaimRequest,\n\tNewQueueItem,\n\tQueueItem,\n\tQueueListOptions,\n} from '../contract/types';\nimport { QueueError } from '../errors';\nimport { openDatabase } from './open';\nimport type { SqliteQueueStoreOptions } from './options';\nimport { COLUMNS, type ItemRow, itemOf, stateOf } from './rows';\n\nexport type { SqliteQueueStoreOptions } from './options';\n\nconst DUE = `next_attempt_at <= $now\n\tAND (lease_owner IS NULL OR lease_expires_at <= $now)`;\n\n/**\n * A `QueueStore` on `bun:sqlite`: one directory holding `queue.sqlite`,\n * which several processes on one machine may open together. Every\n * operation is one transaction — a claim is a single `UPDATE … RETURNING`\n * — so two workers never take the same item. Messages live in a table of\n * their own, dropped with their item.\n */\nexport class SqliteQueueStore implements QueueStore {\n\treadonly #db: Database;\n\t#closed = false;\n\n\tprivate constructor(db: Database) {\n\t\tthis.#db = db;\n\t}\n\n\t/** Opens the store in `directory`, creating it if need be; what fails is `INVALID`. */\n\tstatic open(options: SqliteQueueStoreOptions): SqliteQueueStore {\n\t\treturn new SqliteQueueStore(openDatabase(options));\n\t}\n\n\t/** Lets go of the database; closing twice is fine. */\n\tclose(): void {\n\t\tif (this.#closed) return;\n\t\tthis.#closed = true;\n\t\tthis.#db.close();\n\t}\n\n\tget #open(): Database {\n\t\tif (this.#closed) {\n\t\t\tthrow new QueueError('CLOSED', 'The queue store is closed');\n\t\t}\n\t\treturn this.#db;\n\t}\n\n\t#row(id: string, owner?: string): ItemRow | null {\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn db\n\t\t\t\t.query<ItemRow, [string]>(`SELECT ${COLUMNS} FROM items WHERE id = ?`)\n\t\t\t\t.get(id);\n\t\t}\n\t\treturn db\n\t\t\t.query<ItemRow, [string, string]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.get(id, owner);\n\t}\n\n\t#write(item: QueueItem): void {\n\t\tthis.#open\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET recipients = $recipients,\n\t\t\t\tnext_attempt_at = $next_attempt_at, attempts = $attempts,\n\t\t\t\tdelay_notified = $delay_notified, lease_owner = $lease_owner,\n\t\t\t\tlease_expires_at = $lease_expires_at WHERE id = $id`,\n\t\t\t)\n\t\t\t.run(stateOf(item));\n\t}\n\n\tasync add(item: NewQueueItem, options: AddOptions = {}): Promise<QueueItem> {\n\t\tcheckNewItem(item);\n\t\tconst max = checkMaxItems(options.maxItems);\n\t\tconst db = this.#open;\n\t\tconst added = newItem(crypto.randomUUID(), item);\n\t\tdb.transaction(() => {\n\t\t\tif (max !== undefined) {\n\t\t\t\tconst { n } = db\n\t\t\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t\t\t.get() as { n: number };\n\t\t\t\tif (n >= max) {\n\t\t\t\t\tthrow new QueueError(\n\t\t\t\t\t\t'QUEUE_FULL',\n\t\t\t\t\t\t`The queue holds ${n} items, its limit`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tdb.query(\n\t\t\t\t`INSERT INTO items (id, sender, recipients, size, created_at,\n\t\t\t\tnext_attempt_at, attempts, delay_notified, dsn)\n\t\t\t\tVALUES ($id, $sender, $recipients, $size, $created_at,\n\t\t\t\t$next_attempt_at, 0, 0, $dsn)`,\n\t\t\t).run({\n\t\t\t\tid: added.id,\n\t\t\t\tsender: added.from,\n\t\t\t\trecipients: JSON.stringify(added.recipients),\n\t\t\t\tsize: added.size,\n\t\t\t\tcreated_at: added.createdAt,\n\t\t\t\tnext_attempt_at: added.nextAttemptAt,\n\t\t\t\tdsn: dsnColumn(added),\n\t\t\t});\n\t\t\tdb.query('INSERT INTO messages (item_id, content) VALUES (?, ?)').run(\n\t\t\t\tadded.id,\n\t\t\t\titem.message,\n\t\t\t);\n\t\t}).immediate();\n\t\treturn added;\n\t}\n\n\tasync get(id: string): Promise<QueueItem | undefined> {\n\t\tconst row = this.#row(id);\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync list(options?: QueueListOptions): Promise<QueueItem[]> {\n\t\tconst { offset, limit } = checkList(options);\n\t\treturn this.#open\n\t\t\t.query<ItemRow, [number, number]>(\n\t\t\t\t`SELECT ${COLUMNS} FROM items ORDER BY next_attempt_at, seq\n\t\t\t\tLIMIT ? OFFSET ?`,\n\t\t\t)\n\t\t\t.all(limit, offset)\n\t\t\t.map(itemOf);\n\t}\n\n\tasync count(): Promise<number> {\n\t\tconst row = this.#open\n\t\t\t.query<{ n: number }, []>('SELECT count(*) AS n FROM items')\n\t\t\t.get();\n\t\treturn row?.n ?? 0;\n\t}\n\n\tasync readMessage(id: string): Promise<Uint8Array | undefined> {\n\t\tconst row = this.#open\n\t\t\t.query<{ content: Uint8Array }, [string]>(\n\t\t\t\t'SELECT content FROM messages WHERE item_id = ?',\n\t\t\t)\n\t\t\t.get(id);\n\t\treturn row ? new Uint8Array(row.content) : undefined;\n\t}\n\n\tasync claim(request: ClaimRequest): Promise<QueueItem | undefined> {\n\t\tcheckClaim(request);\n\t\tconst row = this.#open\n\t\t\t.query<ItemRow, Record<string, string | number>>(\n\t\t\t\t`UPDATE items SET lease_owner = $owner, lease_expires_at = $expires\n\t\t\t\tWHERE seq = (SELECT seq FROM items WHERE ${DUE}\n\t\t\t\t\tORDER BY next_attempt_at, seq LIMIT 1)\n\t\t\t\tRETURNING ${COLUMNS}`,\n\t\t\t)\n\t\t\t.get({\n\t\t\t\towner: request.owner,\n\t\t\t\tnow: request.now,\n\t\t\t\texpires: request.now + request.leaseMs,\n\t\t\t});\n\t\treturn row ? itemOf(row) : undefined;\n\t}\n\n\tasync renew(id: string, owner: string, expiresAt: number): Promise<boolean> {\n\t\tcheckOwner(owner);\n\t\tcheckTime('expiresAt', expiresAt);\n\t\tconst { changes } = this.#open\n\t\t\t.query(\n\t\t\t\t'UPDATE items SET lease_expires_at = ? WHERE id = ? AND lease_owner = ?',\n\t\t\t)\n\t\t\t.run(expiresAt, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync complete(\n\t\tid: string,\n\t\towner: string,\n\t\tresult: AttemptResult,\n\t): Promise<QueueItem | undefined> {\n\t\tcheckOwner(owner);\n\t\tcheckResult(result);\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id, owner);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tconst item = applyAttempt(itemOf(row), result);\n\t\t\t\tif (isDone(item)) {\n\t\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\t} else {\n\t\t\t\t\tthis.#write(item);\n\t\t\t\t}\n\t\t\t\treturn item;\n\t\t\t})\n\t\t\t.immediate();\n\t}\n\n\tasync reschedule(id: string, at: number, owner?: string): Promise<boolean> {\n\t\tcheckTime('at', at);\n\t\tconst db = this.#open;\n\t\tif (owner === undefined) {\n\t\t\treturn (\n\t\t\t\tdb\n\t\t\t\t\t.query('UPDATE items SET next_attempt_at = ? WHERE id = ?')\n\t\t\t\t\t.run(at, id).changes > 0\n\t\t\t);\n\t\t}\n\t\tcheckOwner(owner);\n\t\tconst { changes } = db\n\t\t\t.query(\n\t\t\t\t`UPDATE items SET next_attempt_at = ?, lease_owner = NULL,\n\t\t\t\tlease_expires_at = NULL WHERE id = ? AND lease_owner = ?`,\n\t\t\t)\n\t\t\t.run(at, id, owner);\n\t\treturn changes > 0;\n\t}\n\n\tasync cancel(id: string): Promise<QueueItem | undefined> {\n\t\tconst db = this.#open;\n\t\treturn db\n\t\t\t.transaction(() => {\n\t\t\t\tconst row = this.#row(id);\n\t\t\t\tif (!row) return undefined;\n\t\t\t\tdb.query('DELETE FROM items WHERE id = ?').run(id);\n\t\t\t\treturn itemOf(row);\n\t\t\t})\n\t\t\t.immediate();\n\t}\n}\n"
|
|
9
9
|
],
|
|
10
|
-
"mappings": "
|
|
11
|
-
"debugId": "
|
|
10
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;AAAA;AACA;AACA;;;ACMO,IAAM,aAAgC;AAAA,EAE5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA;AACD;AAOO,SAAS,OAAO,CAAC,IAAoB;AAAA,EAC3C,GAAG,YAAY,MAAM;AAAA,IACpB,QAAQ,cAAc,YAAY,GAChC,MAAoC,qBAAqB,EACzD,IAAI;AAAA,IACN,IAAI,UAAU,WAAW,QAAQ;AAAA,MAChC,MAAM,IAAI,YACT,WACA,qCAAqC,oCAAoC,WAAW,QACrF;AAAA,IACD;AAAA,IACA,WAAW,OAAO,WAAW,MAAM,OAAO;AAAA,MAAG,GAAG,KAAK,GAAG;AAAA,IACxD,IAAI,UAAU,WAAW,QAAQ;AAAA,MAChC,GAAG,KAAK,yBAAyB,WAAW,QAAQ;AAAA,IACrD;AAAA,GACA,EAAE,UAAU;AAAA;;;AD5Cd,IAAM,oBAAoB;AAC1B,IAAM,eAAe;AAOrB,SAAS,SAAS,CAAC,IAAc,aAA2B;AAAA,EAC3D,GAAG,KAAK,yBAAyB,aAAa;AAAA,EAC9C,GAAG,KAAK,2BAA2B;AAAA,EACnC,GAAG,KAAK,2BAA2B;AAAA,EAEnC,GAAG,KAAK,uBAAuB;AAAA,EAC/B,GAAG,KAAK,0BAA0B;AAAA;AAGnC,SAAS,WAAW,CAAC,MAAoB;AAAA,EACxC,WAAW,QAAQ,CAAC,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG;AAAA,IACxD,IAAI,WAAW,IAAI;AAAA,MAAG,UAAU,MAAM,YAAY;AAAA,EACnD;AAAA;AAGD,SAAS,aAAa,CAAC,OAAwB;AAAA,EAC9C,IAAI,UAAU;AAAA,IAAW,OAAO;AAAA,EAChC,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GAAG;AAAA,IACvE,MAAM,IAAI,YACT,WACA,qDAAqD,OACtD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAGR,IAAM,YAAY,CAAC,UAClB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAM/C,SAAS,YAAY,CAAC,SAA4C;AAAA,EACxE,MAAM,YAAa,SAAiD;AAAA,EACpE,IAAI,OAAO,cAAc,YAAY,cAAc,IAAI;AAAA,IACtD,MAAM,IAAI,YAAW,WAAW,wCAAwC;AAAA,EACzE;AAAA,EACA,MAAM,cAAc,cAAc,QAAQ,WAAW;AAAA,EACrD,IAAI;AAAA,EACJ,IAAI;AAAA,IACH,IAAI,CAAC,WAAW,SAAS,GAAG;AAAA,MAC3B,UAAU,WAAW,EAAE,WAAW,MAAM,MAAM,kBAAkB,CAAC;AAAA,MAEjE,UAAU,WAAW,iBAAiB;AAAA,IACvC;AAAA,IACA,MAAM,OAAO,KAAK,WAAW,cAAc;AAAA,IAC3C,KAAK,IAAI,SAAS,MAAM,EAAE,QAAQ,MAAM,WAAW,MAAM,QAAQ,KAAK,CAAC;AAAA,IACvE,UAAU,IAAI,WAAW;AAAA,IACzB,YAAY,IAAI;AAAA,IAChB,QAAQ,EAAE;AAAA,IACV,OAAO;AAAA,IACN,OAAO,OAAO;AAAA,IACf,IAAI,MAAM;AAAA,IACV,IAAI,iBAAiB;AAAA,MAAY,MAAM;AAAA,IACvC,MAAM,IAAI,YACT,WACA,iBAAiB,gCAAgC,UAAU,KAAK,GACjE;AAAA;AAAA;;;AE3DK,IAAM,UACZ;AAEM,SAAS,MAAM,CAAC,KAAyB;AAAA,EAC/C,MAAM,OAAkB;AAAA,IACvB,IAAI,IAAI;AAAA,IACR,MAAM,IAAI;AAAA,IACV,YAAY,KAAK,MAAM,IAAI,UAAU;AAAA,IACrC,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,eAAe,IAAI;AAAA,IACnB,UAAU,IAAI;AAAA,IACd,eAAe,IAAI,mBAAmB;AAAA,OACnC,UAAU,IAAI,GAAG;AAAA,EACrB;AAAA,EACA,IAAI,IAAI,gBAAgB,QAAQ,IAAI,qBAAqB;AAAA,IAAM,OAAO;AAAA,EACtE,OAAO;AAAA,OACH;AAAA,IACH,OAAO,EAAE,OAAO,IAAI,aAAa,WAAW,IAAI,iBAAiB;AAAA,EAClE;AAAA;AAIM,SAAS,OAAO,CAAC,MAAiB;AAAA,EACxC,OAAO;AAAA,IACN,IAAI,KAAK;AAAA,IACT,YAAY,KAAK,UAAU,KAAK,UAAU;AAAA,IAC1C,iBAAiB,KAAK;AAAA,IACtB,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK,gBAAgB,IAAI;AAAA,IACzC,aAAa,KAAK,OAAO,SAAS;AAAA,IAClC,kBAAkB,KAAK,OAAO,aAAa;AAAA,EAC5C;AAAA;;;ACrBD,IAAM,MAAM;AAAA;AAAA;AAUL,MAAM,iBAAuC;AAAA,EAC1C;AAAA,EACT,UAAU;AAAA,EAEF,WAAW,CAAC,IAAc;AAAA,IACjC,KAAK,MAAM;AAAA;AAAA,SAIL,IAAI,CAAC,SAAoD;AAAA,IAC/D,OAAO,IAAI,iBAAiB,aAAa,OAAO,CAAC;AAAA;AAAA,EAIlD,KAAK,GAAS;AAAA,IACb,IAAI,KAAK;AAAA,MAAS;AAAA,IAClB,KAAK,UAAU;AAAA,IACf,KAAK,IAAI,MAAM;AAAA;AAAA,MAGZ,KAAK,GAAa;AAAA,IACrB,IAAI,KAAK,SAAS;AAAA,MACjB,MAAM,IAAI,YAAW,UAAU,2BAA2B;AAAA,IAC3D;AAAA,IACA,OAAO,KAAK;AAAA;AAAA,EAGb,IAAI,CAAC,IAAY,OAAgC;AAAA,IAChD,MAAM,KAAK,KAAK;AAAA,IAChB,IAAI,UAAU,WAAW;AAAA,MACxB,OAAO,GACL,MAAyB,UAAU,iCAAiC,EACpE,IAAI,EAAE;AAAA,IACT;AAAA,IACA,OAAO,GACL,MACA,UAAU,qDACX,EACC,IAAI,IAAI,KAAK;AAAA;AAAA,EAGhB,MAAM,CAAC,MAAuB;AAAA,IAC7B,KAAK,MACH,MACA;AAAA;AAAA;AAAA,wDAID,EACC,IAAI,QAAQ,IAAI,CAAC;AAAA;AAAA,QAGd,GAAG,CAAC,MAAoB,UAAsB,CAAC,GAAuB;AAAA,IAC3E,aAAa,IAAI;AAAA,IACjB,MAAM,MAAM,cAAc,QAAQ,QAAQ;AAAA,IAC1C,MAAM,KAAK,KAAK;AAAA,IAChB,MAAM,QAAQ,QAAQ,OAAO,WAAW,GAAG,IAAI;AAAA,IAC/C,GAAG,YAAY,MAAM;AAAA,MACpB,IAAI,QAAQ,WAAW;AAAA,QACtB,QAAQ,MAAM,GACZ,MAAyB,iCAAiC,EAC1D,IAAI;AAAA,QACN,IAAI,KAAK,KAAK;AAAA,UACb,MAAM,IAAI,YACT,cACA,mBAAmB,oBACpB;AAAA,QACD;AAAA,MACD;AAAA,MACA,GAAG,MACF;AAAA;AAAA;AAAA,kCAID,EAAE,IAAI;AAAA,QACL,IAAI,MAAM;AAAA,QACV,QAAQ,MAAM;AAAA,QACd,YAAY,KAAK,UAAU,MAAM,UAAU;AAAA,QAC3C,MAAM,MAAM;AAAA,QACZ,YAAY,MAAM;AAAA,QAClB,iBAAiB,MAAM;AAAA,QACvB,KAAK,UAAU,KAAK;AAAA,MACrB,CAAC;AAAA,MACD,GAAG,MAAM,uDAAuD,EAAE,IACjE,MAAM,IACN,KAAK,OACN;AAAA,KACA,EAAE,UAAU;AAAA,IACb,OAAO;AAAA;AAAA,QAGF,GAAG,CAAC,IAA4C;AAAA,IACrD,MAAM,MAAM,KAAK,KAAK,EAAE;AAAA,IACxB,OAAO,MAAM,OAAO,GAAG,IAAI;AAAA;AAAA,QAGtB,IAAI,CAAC,SAAkD;AAAA,IAC5D,QAAQ,QAAQ,UAAU,UAAU,OAAO;AAAA,IAC3C,OAAO,KAAK,MACV,MACA,UAAU;AAAA,qBAEX,EACC,IAAI,OAAO,MAAM,EACjB,IAAI,MAAM;AAAA;AAAA,QAGP,KAAK,GAAoB;AAAA,IAC9B,MAAM,MAAM,KAAK,MACf,MAAyB,iCAAiC,EAC1D,IAAI;AAAA,IACN,OAAO,KAAK,KAAK;AAAA;AAAA,QAGZ,WAAW,CAAC,IAA6C;AAAA,IAC9D,MAAM,MAAM,KAAK,MACf,MACA,gDACD,EACC,IAAI,EAAE;AAAA,IACR,OAAO,MAAM,IAAI,WAAW,IAAI,OAAO,IAAI;AAAA;AAAA,QAGtC,KAAK,CAAC,SAAuD;AAAA,IAClE,WAAW,OAAO;AAAA,IAClB,MAAM,MAAM,KAAK,MACf,MACA;AAAA,+CAC2C;AAAA;AAAA,gBAE/B,SACb,EACC,IAAI;AAAA,MACJ,OAAO,QAAQ;AAAA,MACf,KAAK,QAAQ;AAAA,MACb,SAAS,QAAQ,MAAM,QAAQ;AAAA,IAChC,CAAC;AAAA,IACF,OAAO,MAAM,OAAO,GAAG,IAAI;AAAA;AAAA,QAGtB,KAAK,CAAC,IAAY,OAAe,WAAqC;AAAA,IAC3E,WAAW,KAAK;AAAA,IAChB,UAAU,aAAa,SAAS;AAAA,IAChC,QAAQ,YAAY,KAAK,MACvB,MACA,wEACD,EACC,IAAI,WAAW,IAAI,KAAK;AAAA,IAC1B,OAAO,UAAU;AAAA;AAAA,QAGZ,QAAQ,CACb,IACA,OACA,QACiC;AAAA,IACjC,WAAW,KAAK;AAAA,IAChB,YAAY,MAAM;AAAA,IAClB,MAAM,KAAK,KAAK;AAAA,IAChB,OAAO,GACL,YAAY,MAAM;AAAA,MAClB,MAAM,MAAM,KAAK,KAAK,IAAI,KAAK;AAAA,MAC/B,IAAI,CAAC;AAAA,QAAK;AAAA,MACV,MAAM,OAAO,aAAa,OAAO,GAAG,GAAG,MAAM;AAAA,MAC7C,IAAI,OAAO,IAAI,GAAG;AAAA,QACjB,GAAG,MAAM,gCAAgC,EAAE,IAAI,EAAE;AAAA,MAClD,EAAE,KAAK;AAAA,QACN,KAAK,OAAO,IAAI;AAAA;AAAA,MAEjB,OAAO;AAAA,KACP,EACA,UAAU;AAAA;AAAA,QAGP,UAAU,CAAC,IAAY,IAAY,OAAkC;AAAA,IAC1E,UAAU,MAAM,EAAE;AAAA,IAClB,MAAM,KAAK,KAAK;AAAA,IAChB,IAAI,UAAU,WAAW;AAAA,MACxB,OACC,GACE,MAAM,mDAAmD,EACzD,IAAI,IAAI,EAAE,EAAE,UAAU;AAAA,IAE1B;AAAA,IACA,WAAW,KAAK;AAAA,IAChB,QAAQ,YAAY,GAClB,MACA;AAAA,6DAED,EACC,IAAI,IAAI,IAAI,KAAK;AAAA,IACnB,OAAO,UAAU;AAAA;AAAA,QAGZ,MAAM,CAAC,IAA4C;AAAA,IACxD,MAAM,KAAK,KAAK;AAAA,IAChB,OAAO,GACL,YAAY,MAAM;AAAA,MAClB,MAAM,MAAM,KAAK,KAAK,EAAE;AAAA,MACxB,IAAI,CAAC;AAAA,QAAK;AAAA,MACV,GAAG,MAAM,gCAAgC,EAAE,IAAI,EAAE;AAAA,MACjD,OAAO,OAAO,GAAG;AAAA,KACjB,EACA,UAAU;AAAA;AAEd;",
|
|
11
|
+
"debugId": "92D7A7C42951038864756E2164756E21",
|
|
12
12
|
"names": []
|
|
13
13
|
}
|
package/dist/sqlite/rows.d.ts
CHANGED
|
@@ -11,9 +11,10 @@ export interface ItemRow {
|
|
|
11
11
|
readonly delay_notified: number;
|
|
12
12
|
readonly lease_owner: string | null;
|
|
13
13
|
readonly lease_expires_at: number | null;
|
|
14
|
+
readonly dsn: string | null;
|
|
14
15
|
}
|
|
15
16
|
/** The columns a query reads to build an item. */
|
|
16
|
-
export declare const COLUMNS = "id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at";
|
|
17
|
+
export declare const COLUMNS = "id, sender, recipients, size, created_at, next_attempt_at, attempts, delay_notified, lease_owner, lease_expires_at, dsn";
|
|
17
18
|
export declare function itemOf(row: ItemRow): QueueItem;
|
|
18
19
|
/** The named parameters that write an item's state back. */
|
|
19
20
|
export declare function stateOf(item: QueueItem): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../src/sqlite/rows.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../src/sqlite/rows.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,mBAAmB,CAAC;AAEnE,kDAAkD;AAClD,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,kDAAkD;AAClD,eAAO,MAAM,OAAO,4HACsG,CAAC;AAE3H,wBAAgB,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAiB9C;AAED,4DAA4D;AAC5D,wBAAgB,OAAO,CAAC,IAAI,EAAE,SAAS;;;;;;;;EAUtC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/sqlite/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/sqlite/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,EA0BvC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAgB1C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite/store.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EACX,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAGzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAKzD;;;;;;GAMG;AACH,qBAAa,gBAAiB,YAAW,UAAU;;IAIlD,OAAO;IAIP,uFAAuF;IACvF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB;IAI/D,sDAAsD;IACtD,KAAK,IAAI,IAAI;IAsCP,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC;IAuCrE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAK/C,IAAI,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAWtD,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAOxB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IASxD,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAiB5D,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrE,QAAQ,CACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,aAAa,GACnB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAmB3B,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBpE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;CAWxD"}
|
package/docs/guide.md
CHANGED
|
@@ -59,6 +59,12 @@ const item = await queue.enqueue(message, { from: 'mary@example.net', to: ['joe@
|
|
|
59
59
|
for the null sender of a bounce you write yourself. `to` is one address
|
|
60
60
|
or an array. Each recipient is kept once: two addresses that differ
|
|
61
61
|
only in the case of their domain are one.
|
|
62
|
+
- **The DSN parameters** (RFC 3461) are optional: `ret` (`'FULL'` or
|
|
63
|
+
`'HDRS'`), `envid` (1 to 100 printable ASCII characters, 100 or fewer once xtext-encoded) and
|
|
64
|
+
`recipients`, a list of `{ address, notify?, orcpt? }` matched to `to`
|
|
65
|
+
by address, the shape of `@bumail/smtp`'s `Envelope.recipients`. They
|
|
66
|
+
are kept with the item by every store and described in
|
|
67
|
+
[Delivery status notifications](#delivery-status-notifications).
|
|
62
68
|
- **What comes back** is the item: an `id`, every recipient `pending`,
|
|
63
69
|
`nextAttemptAt` now. `start()` wakes at once to deliver it.
|
|
64
70
|
|
|
@@ -363,9 +369,56 @@ Last-Attempt-Date: Thu, 01 Oct 2026 12:00:03 +0000
|
|
|
363
369
|
cut after the last whole line within that bound, and every line past
|
|
364
370
|
998 bytes (RFC 5322 §2.1.1) is cut there, never inside a UTF-8
|
|
365
371
|
character, so the DSN itself is never refused for a long line.
|
|
372
|
+
- **`NOTIFY`** (RFC 3461 §4.1) filters them, per recipient:
|
|
373
|
+
`'NEVER'` sends none; absent, a failure and a delay, as before; a
|
|
374
|
+
list, only those events (`SUCCESS`, `FAILURE`, `DELAY`). The
|
|
375
|
+
`failed` and `dsn` events still fire; only the DSN is left out.
|
|
376
|
+
- **`SUCCESS`** (§5.2.2): when a recipient asked for it and the next hop
|
|
377
|
+
did not list `DSN` in its EHLO (`SendMailResult.dsnOffered` is `false`),
|
|
378
|
+
the queue sends a `relayed` DSN (`Action: relayed`, status `2.0.0`):
|
|
379
|
+
the message was handed on and no further notice can follow. When the
|
|
380
|
+
next hop offered DSN, it took the parameters and the responsibility, and
|
|
381
|
+
the queue sends none. A `dsn` event of kind `relayed` tells it.
|
|
382
|
+
- **`RET`** (§4.3): `HDRS` returns the header fields only, `FULL` the
|
|
383
|
+
whole message (still bounded by `limits.maxDsnReturn`, headers when it
|
|
384
|
+
is larger); absent, `dsn.returnContent`. It speaks of failure and delay DSNs
|
|
385
|
+
(§4.3): a `relayed` or `delivered` DSN returns the header fields
|
|
386
|
+
whatever `RET` says.
|
|
387
|
+
- **`ENVID` and `ORCPT`** (RFC 3464 §2.2.1, §2.3.1): `Original-Envelope-Id`
|
|
388
|
+
in the per-message fields and `Original-Recipient` (`rfc822; addr`) in
|
|
389
|
+
the recipient's, when the envelope gave them. An `ORCPT` too long for a
|
|
390
|
+
header line (over 900 characters once escaped) is left out of the DSN,
|
|
391
|
+
which is still sent.
|
|
392
|
+
- **Passed on**: `ret`, `envid` and the recipients' `notify` and `orcpt`
|
|
393
|
+
go to `sendMail`, which sends them when the next hop lists `DSN`.
|
|
366
394
|
- **Its From** is `Mail Delivery System <postmaster@<hostname>>`, or
|
|
367
395
|
`dsn.from`. It carries `Auto-Submitted: auto-replied` (RFC 3834).
|
|
368
396
|
|
|
397
|
+
### The `delivered` DSN of a local delivery
|
|
398
|
+
|
|
399
|
+
When the application puts a message in a local mailbox itself, it writes
|
|
400
|
+
the `delivered` DSN (`Action: delivered`, RFC 3461 §5.2.3) with
|
|
401
|
+
`deliveredDsn()` and enqueues it from the null sender:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { deliveredDsn } from '@bumail/queue';
|
|
405
|
+
|
|
406
|
+
const dsn = deliveredDsn({
|
|
407
|
+
reportingMta: 'mail.example.net',
|
|
408
|
+
from: 'postmaster@example.net',
|
|
409
|
+
to: envelope.from, // the reverse-path of the delivered message
|
|
410
|
+
arrival: received,
|
|
411
|
+
recipients: [{ address: 'joe@example.net', notify: ['SUCCESS'], orcpt }],
|
|
412
|
+
envid,
|
|
413
|
+
original: message, // optional; without it no third part
|
|
414
|
+
});
|
|
415
|
+
if (dsn) await queue.enqueue(dsn, { from: '', to: envelope.from });
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
It returns `undefined` when there is nothing to send: the reverse-path is
|
|
419
|
+
null, or no recipient asked for `SUCCESS`. Only the recipients that asked
|
|
420
|
+
are listed.
|
|
421
|
+
|
|
369
422
|
Nothing from outside reaches a header field raw: a reply's text, a host
|
|
370
423
|
name and an address have their control characters taken out (CR and LF
|
|
371
424
|
included), the `message/delivery-status` part is 7-bit (a UTF-8 address
|
|
@@ -445,7 +498,7 @@ off(); // stops listening
|
|
|
445
498
|
| `delivered` | `{ id, from, recipient, reply?, attempts, route?, fallback?, mxReply? }`: `route` is `'mx'` or `'smarthost'`, `fallback` is `true` after the [fallback](#falling-back-to-a-smarthost), `mxReply` the MX's reply that led to it |
|
|
446
499
|
| `deferred` | the same, and `nextAttemptAt`, and `mayHaveReachedMx: true` when the MX may hold a copy, `mayHaveReachedFallback: true` when the fallback smarthost may ([why](#falling-back-to-a-smarthost)) |
|
|
447
500
|
| `failed` | the same as `delivered`, and `mayHaveReachedMx` and `mayHaveReachedFallback` as for `deferred` |
|
|
448
|
-
| `dsn` | `{ kind: 'delayed' \| 'failed', id, of, to, recipients }`: `id` is the DSN's own item, `of` the item it reports on |
|
|
501
|
+
| `dsn` | `{ kind: 'delayed' \| 'failed' \| 'relayed', id, of, to, recipients }`: `id` is the DSN's own item, `of` the item it reports on |
|
|
449
502
|
| `error` | `{ error, id? }`: a store that failed (a lease renewal included), a DSN that could not be enqueued, a lease lost (`LEASE_LOST`), a message the store lost (`MESSAGE_UNREADABLE`), a route `sendMail` refused (`INVALID_OPTION`) |
|
|
450
503
|
|
|
451
504
|
Events about an outcome come once it is recorded, in this order: a
|
|
@@ -620,7 +673,17 @@ GRANT SELECT, INSERT, UPDATE, DELETE
|
|
|
620
673
|
```
|
|
621
674
|
|
|
622
675
|
After an upgrade that adds a migration, run that step again before the
|
|
623
|
-
workers start: theirs cannot make the change.
|
|
676
|
+
workers start: theirs cannot make the change. The second migration adds
|
|
677
|
+
one nullable `dsn jsonb` column to the items table (the item's `RET` and
|
|
678
|
+
`ENVID`); existing rows keep `NULL` and are delivered as before. A
|
|
679
|
+
worker of the previous version *started* after the migration is refused
|
|
680
|
+
(`schema version 2, newer than this store's 1`); one *still running*
|
|
681
|
+
keeps claiming items, ignores `NOTIFY`, `RET` and `ENVID`, and drops the
|
|
682
|
+
recipients' `notify` and `orcpt` when it completes an attempt. So stop
|
|
683
|
+
every old worker first, then start the new ones. The same holds for the
|
|
684
|
+
SQLite store, which migrates itself on open, and for Redis, which needs
|
|
685
|
+
no migration (an item's hash gains `ret` and `envid` fields) but where an
|
|
686
|
+
old worker drops `notify` and `orcpt` just alike.
|
|
624
687
|
|
|
625
688
|
### Several instances and leases
|
|
626
689
|
|
|
@@ -853,6 +916,7 @@ const send: Sender = async (_message, options) => {
|
|
|
853
916
|
port: 25,
|
|
854
917
|
tls: false,
|
|
855
918
|
authenticated: false,
|
|
919
|
+
dsnOffered: true,
|
|
856
920
|
};
|
|
857
921
|
};
|
|
858
922
|
const queue = createQueue({
|
|
@@ -906,6 +970,11 @@ codes, and keeps its promises:
|
|
|
906
970
|
smarthost for a recipient the MX may already hold. A 0.6 worker drops
|
|
907
971
|
the mark when it completes an attempt, so upgrade every worker on a
|
|
908
972
|
store together.
|
|
973
|
+
- `add` keeps the item's `ret` and `envid` and each recipient's `notify`
|
|
974
|
+
and `orcpt` (`NewQueueItem`), and `get`, `list`, `claim` and `complete`
|
|
975
|
+
return them unchanged, even after an outcome. The contract's
|
|
976
|
+
`checkNewItem` refuses a bad one as `INVALID`. An item stored before
|
|
977
|
+
they existed has none of them and is delivered as before.
|
|
909
978
|
- Times are given by the caller; the store keeps no clock.
|
|
910
979
|
- Nothing it returns is shared with what it keeps.
|
|
911
980
|
- `reschedule` with an owner refuses an empty owner as `INVALID`, even
|
|
@@ -949,5 +1018,5 @@ them yet.
|
|
|
949
1018
|
| `pollInterval` | `number`, to 2147483647 | 5000 | ms between passes of `start()` |
|
|
950
1019
|
| `owner` | `string` | a random UUID | this worker's name on its leases |
|
|
951
1020
|
| `clock` | `{ now(): number }` | `Date.now` | the time |
|
|
952
|
-
| `send` | `Sender` | `sendMail` | delivers one session |
|
|
1021
|
+
| `send` | `Sender` | `sendMail` | delivers one session; a custom one should return `dsnOffered` (leave it out and no `relayed` DSN is ever sent) |
|
|
953
1022
|
| `random` | `() => number` | `Math.random` | the jitter |
|
package/docs/roadmap.md
CHANGED
|
@@ -6,7 +6,7 @@ only number on it.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
Nothing
|
|
9
|
+
Nothing is under way besides what is marked shipped below.
|
|
10
10
|
|
|
11
11
|
## Next
|
|
12
12
|
|
|
@@ -28,8 +28,6 @@ Nothing in progress.
|
|
|
28
28
|
|
|
29
29
|
- **REQUIRETLS (RFC 8689)** — a message that asks for TLS all the way is
|
|
30
30
|
never sent in clear, and fails rather than downgrade.
|
|
31
|
-
- **DSN requests (RFC 3461)** — `NOTIFY`, `RET` and `ENVID` given at
|
|
32
|
-
enqueue and passed on, with a "delivered" DSN when asked for.
|
|
33
31
|
- **Retries per recipient** — a schedule of its own for each recipient,
|
|
34
32
|
rather than one per message.
|
|
35
33
|
- **A history** — delivered and failed items kept for a while, for an
|
|
@@ -53,6 +51,13 @@ Nothing in progress.
|
|
|
53
51
|
|
|
54
52
|
### Unreleased
|
|
55
53
|
|
|
54
|
+
- **DSN requests (RFC 3461)**: the queue honours a sender's `NOTIFY`, `RET`,
|
|
55
|
+
`ENVID` and `ORCPT` given at enqueue. `NOTIFY=NEVER` silences failure
|
|
56
|
+
DSNs; a message handed to a next hop without DSN is reported with a
|
|
57
|
+
`relayed` DSN; DSNs carry `Original-Envelope-Id` and
|
|
58
|
+
`Original-Recipient`; and `deliveredDsn()` builds the `delivered` DSN for
|
|
59
|
+
a local delivery. The SQLite and PostgreSQL stores run migration 2: stop
|
|
60
|
+
every old worker before starting the new ones.
|
|
56
61
|
- **The MX's doubt is remembered** — a recipient whose session by MX ended
|
|
57
62
|
after the final dot with no reply is marked `mayHaveReachedMx`, kept by
|
|
58
63
|
every store: no later attempt falls back to the smarthost on its own, it
|