@vielzeug/postmaster 2.2.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,22 +1,18 @@
1
1
  # @vielzeug/postmaster
2
2
 
3
- Typed durable job outbox with leased processing, retries, and dead-letter recovery.
3
+ > Typed durable job outbox with leased processing, retries, and dead-letter recovery
4
4
 
5
- Postmaster coordinates delivery of application jobs that must survive reloads, resume later, retry according to an explicit policy, and retain terminal failures for recovery.
6
-
7
- ## Install
5
+ ## Installation
8
6
 
9
7
  ```sh
10
8
  pnpm add @vielzeug/postmaster
9
+ npm install @vielzeug/postmaster
10
+ yarn add @vielzeug/postmaster
11
11
  ```
12
12
 
13
- For browser persistence, also install `@vielzeug/vault`:
14
-
15
- ```sh
16
- pnpm add @vielzeug/postmaster @vielzeug/vault
17
- ```
13
+ For browser persistence, also install `@vielzeug/vault`.
18
14
 
19
- ## Usage
15
+ ## Quick Start
20
16
 
21
17
  ```ts
22
18
  import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
@@ -43,58 +39,21 @@ const store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });
43
39
  const postmaster = createPostmaster({ jobs, store });
44
40
 
45
41
  await postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });
46
- await postmaster.start();
47
-
48
- // Delayed eligibility — the job persists now but cannot be claimed until `availableAt`:
49
- await postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' }, {
50
- availableAt: Date.now() + 60_000,
51
- });
42
+ postmaster.start();
52
43
 
53
44
  // On page unload:
54
45
  await postmaster.dispose();
55
46
  await store.dispose();
56
47
  ```
57
48
 
58
- ## At-least-once delivery
49
+ ## Documentation
59
50
 
60
- Postmaster provides at-least-once delivery. Every job must derive a stable idempotency key, and handlers must send or otherwise enforce that key. Never assume exactly-once execution.
61
-
62
- ## Delayed eligibility
63
-
64
- `enqueue()` accepts an optional `availableAt` timestamp. The job persists immediately but cannot be claimed before that time. Postmaster does not guarantee execution at `availableAt` — only that the job will not be claimed earlier. A live processor (`start()` or `flush()`) is required for execution; in a browser, a closed page or suspended service worker will run the job when the processor next becomes active. Past timestamps remain immediately eligible.
65
-
66
- ```ts
67
- await postmaster.enqueue('sendDigest', { userId }, { availableAt: Date.now() + 60_000 });
68
- ```
51
+ - [Overview](https://vielzeug.dev/postmaster/)
52
+ - [Usage Guide](https://vielzeug.dev/postmaster/usage)
53
+ - [API Reference](https://vielzeug.dev/postmaster/api)
54
+ - [Examples](https://vielzeug.dev/postmaster/examples)
55
+ - [Migration Guide](https://vielzeug.dev/postmaster/migration)
69
56
 
70
- ## Entry points
57
+ ## License
71
58
 
72
- | Import | Purpose |
73
- | --- | --- |
74
- | `@vielzeug/postmaster` | Job definitions, processor, store contract, events, errors |
75
- | `@vielzeug/postmaster/indexeddb` | Durable browser store backed by Vault IndexedDB |
76
- | `@vielzeug/postmaster/testing` | Deterministic in-memory store and test helpers |
77
-
78
- ## Testing
79
-
80
- ```ts
81
- import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
82
- import { createMemoryPostmasterStore } from '@vielzeug/postmaster/testing';
83
-
84
- const store = createMemoryPostmasterStore();
85
- const postmaster = createPostmaster({
86
- jobs: defineJobs({
87
- send: {
88
- version: 1,
89
- validate: (v: unknown) => String(v),
90
- key: (p) => p,
91
- execute: async () => {},
92
- },
93
- }),
94
- store,
95
- });
96
-
97
- await postmaster.enqueue('send', 'hello');
98
- await postmaster.flush();
99
- await postmaster.dispose();
100
- ```
59
+ MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs");function t(t,n){if(t&&(!Number.isInteger(t.maxAttempts)||t.maxAttempts<1))throw new e.PostmasterError(`job "${n}" retry.maxAttempts must be a positive integer`)}function n(n,r){if(!Number.isInteger(r.version)||r.version<1)throw new e.PostmasterError(`job "${n}" version must be a positive integer`);if(typeof r.execute!=`function`||typeof r.key!=`function`)throw new e.PostmasterError(`job "${n}" must define execute and key`);t(r.retry,n)}function r(e){for(let[t,r]of Object.entries(e))n(t,r);return e}exports.defineJobs=r;
1
+ const e=require("./errors.cjs");function t(t,n){if(t&&(!Number.isInteger(t.maxAttempts)||t.maxAttempts<1))throw new e.PostmasterError(`job "${n}" retry.maxAttempts must be a positive integer`)}function n(t,n){let r=n.migrate;if(!r)return;let i=Object.keys(r);if(i.length===0)throw new e.PostmasterError(`job "${t}" migrations must not be empty`);let a=i.map(i=>{let a=Number(i);if(!Number.isInteger(a)||String(a)!==i)throw new e.PostmasterError(`job "${t}" has invalid migration key "${i}"`);if(a<1||a>=n.version)throw new e.PostmasterError(`job "${t}" has invalid migration key "${a}" (valid range: 1..${n.version-1})`);if(typeof r[a]!=`function`)throw new e.PostmasterError(`job "${t}" migration from version ${a} must be a function`);return a}).sort((e,t)=>e-t);for(let n=1;n<a.length;n++)if(a[n]!==a[n-1]+1)throw new e.PostmasterError(`job "${t}" missing migration from version ${a[n-1]+1}`);if(a.at(-1)!==n.version-1)throw new e.PostmasterError(`job "${t}" migrations must lead into version ${n.version}`)}function r(r,i){if(!Number.isInteger(i.version)||i.version<1)throw new e.PostmasterError(`job "${r}" version must be a positive integer`);if(typeof i.execute!=`function`||typeof i.key!=`function`)throw new e.PostmasterError(`job "${r}" must define execute and key`);t(i.retry,r),n(r,i)}function i(e){for(let[t,n]of Object.entries(e))r(t,n);return e}exports.defineJobs=i;
2
2
  //# sourceMappingURL=definitions.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.cjs","names":[],"sources":["../src/definitions.ts"],"sourcesContent":["import { PostmasterError } from './errors.ts';\nimport type { JobDefinition, JobDefinitions, RetryPolicy } from './types.ts';\n\nfunction validateRetry(retry: RetryPolicy | undefined, name: string): void {\n if (!retry) return;\n\n if (!Number.isInteger(retry.maxAttempts) || retry.maxAttempts < 1) {\n throw new PostmasterError(`job \"${name}\" retry.maxAttempts must be a positive integer`);\n }\n}\n\nfunction validateJob(name: string, job: JobDefinition<unknown>): void {\n if (!Number.isInteger(job.version) || job.version < 1) {\n throw new PostmasterError(`job \"${name}\" version must be a positive integer`);\n }\n\n if (typeof job.execute !== 'function' || typeof job.key !== 'function') {\n throw new PostmasterError(`job \"${name}\" must define execute and key`);\n }\n\n validateRetry(job.retry, name);\n}\n\nexport function defineJobs<const J extends JobDefinitions>(jobs: J): J {\n for (const [name, job] of Object.entries(jobs)) validateJob(name, job);\n\n return jobs;\n}\n"],"mappings":"gCAGA,SAAS,EAAc,EAAgC,EAAoB,CACpE,OAED,CAAC,OAAO,UAAU,EAAM,WAAW,GAAK,EAAM,YAAc,GAC9D,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,+CAA+C,CAE1F,CAEA,SAAS,EAAY,EAAc,EAAmC,CACpE,GAAI,CAAC,OAAO,UAAU,EAAI,OAAO,GAAK,EAAI,QAAU,EAClD,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,qCAAqC,EAG9E,GAAI,OAAO,EAAI,SAAY,YAAc,OAAO,EAAI,KAAQ,WAC1D,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,8BAA8B,EAGvE,EAAc,EAAI,MAAO,CAAI,CAC/B,CAEA,SAAgB,EAA2C,EAAY,CACrE,IAAK,GAAM,CAAC,EAAM,KAAQ,OAAO,QAAQ,CAAI,EAAG,EAAY,EAAM,CAAG,EAErE,OAAO,CACT"}
1
+ {"version":3,"file":"definitions.cjs","names":[],"sources":["../src/definitions.ts"],"sourcesContent":["import { PostmasterError } from './errors.ts';\nimport type { JobDefinition, JobDefinitions, RetryPolicy } from './types.ts';\n\nfunction validateRetry(retry: RetryPolicy | undefined, name: string): void {\n if (!retry) return;\n\n if (!Number.isInteger(retry.maxAttempts) || retry.maxAttempts < 1) {\n throw new PostmasterError(`job \"${name}\" retry.maxAttempts must be a positive integer`);\n }\n}\n\nfunction validateMigrations(name: string, job: JobDefinition<unknown>): void {\n const migrations = job.migrate;\n if (!migrations) return;\n\n const keys = Object.keys(migrations);\n if (keys.length === 0) throw new PostmasterError(`job \"${name}\" migrations must not be empty`);\n\n const versions = keys\n .map((key) => {\n const version = Number(key);\n if (!Number.isInteger(version) || String(version) !== key) {\n throw new PostmasterError(`job \"${name}\" has invalid migration key \"${key}\"`);\n }\n if (version < 1 || version >= job.version) {\n throw new PostmasterError(\n `job \"${name}\" has invalid migration key \"${version}\" (valid range: 1..${job.version - 1})`,\n );\n }\n if (typeof migrations[version] !== 'function') {\n throw new PostmasterError(`job \"${name}\" migration from version ${version} must be a function`);\n }\n return version;\n })\n .sort((a, b) => a - b);\n\n for (let index = 1; index < versions.length; index++) {\n if (versions[index] !== versions[index - 1] + 1) {\n throw new PostmasterError(`job \"${name}\" missing migration from version ${versions[index - 1] + 1}`);\n }\n }\n\n if (versions.at(-1) !== job.version - 1) {\n throw new PostmasterError(`job \"${name}\" migrations must lead into version ${job.version}`);\n }\n}\n\nfunction validateJob(name: string, job: JobDefinition<unknown>): void {\n if (!Number.isInteger(job.version) || job.version < 1) {\n throw new PostmasterError(`job \"${name}\" version must be a positive integer`);\n }\n\n if (typeof job.execute !== 'function' || typeof job.key !== 'function') {\n throw new PostmasterError(`job \"${name}\" must define execute and key`);\n }\n\n validateRetry(job.retry, name);\n validateMigrations(name, job);\n}\n\nexport function defineJobs<const J extends JobDefinitions>(jobs: J): J {\n for (const [name, job] of Object.entries(jobs)) validateJob(name, job);\n\n return jobs;\n}\n"],"mappings":"gCAGA,SAAS,EAAc,EAAgC,EAAoB,CACpE,OAED,CAAC,OAAO,UAAU,EAAM,WAAW,GAAK,EAAM,YAAc,GAC9D,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,+CAA+C,CAE1F,CAEA,SAAS,EAAmB,EAAc,EAAmC,CAC3E,IAAM,EAAa,EAAI,QACvB,GAAI,CAAC,EAAY,OAEjB,IAAM,EAAO,OAAO,KAAK,CAAU,EACnC,GAAI,EAAK,SAAW,EAAG,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,+BAA+B,EAE7F,IAAM,EAAW,EACd,IAAK,GAAQ,CACZ,IAAM,EAAU,OAAO,CAAG,EAC1B,GAAI,CAAC,OAAO,UAAU,CAAO,GAAK,OAAO,CAAO,IAAM,EACpD,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,+BAA+B,EAAI,EAAE,EAE9E,GAAI,EAAU,GAAK,GAAW,EAAI,QAChC,MAAM,IAAI,EAAA,gBACR,QAAQ,EAAK,+BAA+B,EAAQ,qBAAqB,EAAI,QAAU,EAAE,EAC3F,EAEF,GAAI,OAAO,EAAW,IAAa,WACjC,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,2BAA2B,EAAQ,oBAAoB,EAEhG,OAAO,CACT,CAAC,CAAC,CACD,MAAM,EAAG,IAAM,EAAI,CAAC,EAEvB,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAS,OAAQ,IAC3C,GAAI,EAAS,KAAW,EAAS,EAAQ,GAAK,EAC5C,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,mCAAmC,EAAS,EAAQ,GAAK,GAAG,EAIvG,GAAI,EAAS,GAAG,EAAE,IAAM,EAAI,QAAU,EACpC,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,sCAAsC,EAAI,SAAS,CAE9F,CAEA,SAAS,EAAY,EAAc,EAAmC,CACpE,GAAI,CAAC,OAAO,UAAU,EAAI,OAAO,GAAK,EAAI,QAAU,EAClD,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,qCAAqC,EAG9E,GAAI,OAAO,EAAI,SAAY,YAAc,OAAO,EAAI,KAAQ,WAC1D,MAAM,IAAI,EAAA,gBAAgB,QAAQ,EAAK,8BAA8B,EAGvE,EAAc,EAAI,MAAO,CAAI,EAC7B,EAAmB,EAAM,CAAG,CAC9B,CAEA,SAAgB,EAA2C,EAAY,CACrE,IAAK,GAAM,CAAC,EAAM,KAAQ,OAAO,QAAQ,CAAI,EAAG,EAAY,EAAM,CAAG,EAErE,OAAO,CACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../src/definitions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,cAAc,EAAe,MAAM,YAAY,CAAC;AAsB7E,wBAAgB,UAAU,CAAC,KAAK,CAAC,CAAC,SAAS,cAAc,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAIrE"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../src/definitions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,cAAc,EAAe,MAAM,YAAY,CAAC;AA2D7E,wBAAgB,UAAU,CAAC,KAAK,CAAC,CAAC,SAAS,cAAc,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAIrE"}
@@ -3,16 +3,31 @@ import { PostmasterError as e } from "./errors.js";
3
3
  function t(t, n) {
4
4
  if (t && (!Number.isInteger(t.maxAttempts) || t.maxAttempts < 1)) throw new e(`job "${n}" retry.maxAttempts must be a positive integer`);
5
5
  }
6
- function n(n, r) {
7
- if (!Number.isInteger(r.version) || r.version < 1) throw new e(`job "${n}" version must be a positive integer`);
8
- if (typeof r.execute != "function" || typeof r.key != "function") throw new e(`job "${n}" must define execute and key`);
9
- t(r.retry, n);
6
+ function n(t, n) {
7
+ let r = n.migrate;
8
+ if (!r) return;
9
+ let i = Object.keys(r);
10
+ if (i.length === 0) throw new e(`job "${t}" migrations must not be empty`);
11
+ let a = i.map((i) => {
12
+ let a = Number(i);
13
+ if (!Number.isInteger(a) || String(a) !== i) throw new e(`job "${t}" has invalid migration key "${i}"`);
14
+ if (a < 1 || a >= n.version) throw new e(`job "${t}" has invalid migration key "${a}" (valid range: 1..${n.version - 1})`);
15
+ if (typeof r[a] != "function") throw new e(`job "${t}" migration from version ${a} must be a function`);
16
+ return a;
17
+ }).sort((e, t) => e - t);
18
+ for (let n = 1; n < a.length; n++) if (a[n] !== a[n - 1] + 1) throw new e(`job "${t}" missing migration from version ${a[n - 1] + 1}`);
19
+ if (a.at(-1) !== n.version - 1) throw new e(`job "${t}" migrations must lead into version ${n.version}`);
10
20
  }
11
- function r(e) {
12
- for (let [t, r] of Object.entries(e)) n(t, r);
21
+ function r(r, i) {
22
+ if (!Number.isInteger(i.version) || i.version < 1) throw new e(`job "${r}" version must be a positive integer`);
23
+ if (typeof i.execute != "function" || typeof i.key != "function") throw new e(`job "${r}" must define execute and key`);
24
+ t(i.retry, r), n(r, i);
25
+ }
26
+ function i(e) {
27
+ for (let [t, n] of Object.entries(e)) r(t, n);
13
28
  return e;
14
29
  }
15
30
  //#endregion
16
- export { r as defineJobs };
31
+ export { i as defineJobs };
17
32
 
18
33
  //# sourceMappingURL=definitions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","names":[],"sources":["../src/definitions.ts"],"sourcesContent":["import { PostmasterError } from './errors.ts';\nimport type { JobDefinition, JobDefinitions, RetryPolicy } from './types.ts';\n\nfunction validateRetry(retry: RetryPolicy | undefined, name: string): void {\n if (!retry) return;\n\n if (!Number.isInteger(retry.maxAttempts) || retry.maxAttempts < 1) {\n throw new PostmasterError(`job \"${name}\" retry.maxAttempts must be a positive integer`);\n }\n}\n\nfunction validateJob(name: string, job: JobDefinition<unknown>): void {\n if (!Number.isInteger(job.version) || job.version < 1) {\n throw new PostmasterError(`job \"${name}\" version must be a positive integer`);\n }\n\n if (typeof job.execute !== 'function' || typeof job.key !== 'function') {\n throw new PostmasterError(`job \"${name}\" must define execute and key`);\n }\n\n validateRetry(job.retry, name);\n}\n\nexport function defineJobs<const J extends JobDefinitions>(jobs: J): J {\n for (const [name, job] of Object.entries(jobs)) validateJob(name, job);\n\n return jobs;\n}\n"],"mappings":";;AAGA,SAAS,EAAc,GAAgC,GAAoB;CACpE,UAED,CAAC,OAAO,UAAU,EAAM,WAAW,KAAK,EAAM,cAAc,IAC9D,MAAM,IAAI,EAAgB,QAAQ,EAAK,+CAA+C;AAE1F;AAEA,SAAS,EAAY,GAAc,GAAmC;CACpE,IAAI,CAAC,OAAO,UAAU,EAAI,OAAO,KAAK,EAAI,UAAU,GAClD,MAAM,IAAI,EAAgB,QAAQ,EAAK,qCAAqC;CAG9E,IAAI,OAAO,EAAI,WAAY,cAAc,OAAO,EAAI,OAAQ,YAC1D,MAAM,IAAI,EAAgB,QAAQ,EAAK,8BAA8B;CAGvE,EAAc,EAAI,OAAO,CAAI;AAC/B;AAEA,SAAgB,EAA2C,GAAY;CACrE,KAAK,IAAM,CAAC,GAAM,MAAQ,OAAO,QAAQ,CAAI,GAAG,EAAY,GAAM,CAAG;CAErE,OAAO;AACT"}
1
+ {"version":3,"file":"definitions.js","names":[],"sources":["../src/definitions.ts"],"sourcesContent":["import { PostmasterError } from './errors.ts';\nimport type { JobDefinition, JobDefinitions, RetryPolicy } from './types.ts';\n\nfunction validateRetry(retry: RetryPolicy | undefined, name: string): void {\n if (!retry) return;\n\n if (!Number.isInteger(retry.maxAttempts) || retry.maxAttempts < 1) {\n throw new PostmasterError(`job \"${name}\" retry.maxAttempts must be a positive integer`);\n }\n}\n\nfunction validateMigrations(name: string, job: JobDefinition<unknown>): void {\n const migrations = job.migrate;\n if (!migrations) return;\n\n const keys = Object.keys(migrations);\n if (keys.length === 0) throw new PostmasterError(`job \"${name}\" migrations must not be empty`);\n\n const versions = keys\n .map((key) => {\n const version = Number(key);\n if (!Number.isInteger(version) || String(version) !== key) {\n throw new PostmasterError(`job \"${name}\" has invalid migration key \"${key}\"`);\n }\n if (version < 1 || version >= job.version) {\n throw new PostmasterError(\n `job \"${name}\" has invalid migration key \"${version}\" (valid range: 1..${job.version - 1})`,\n );\n }\n if (typeof migrations[version] !== 'function') {\n throw new PostmasterError(`job \"${name}\" migration from version ${version} must be a function`);\n }\n return version;\n })\n .sort((a, b) => a - b);\n\n for (let index = 1; index < versions.length; index++) {\n if (versions[index] !== versions[index - 1] + 1) {\n throw new PostmasterError(`job \"${name}\" missing migration from version ${versions[index - 1] + 1}`);\n }\n }\n\n if (versions.at(-1) !== job.version - 1) {\n throw new PostmasterError(`job \"${name}\" migrations must lead into version ${job.version}`);\n }\n}\n\nfunction validateJob(name: string, job: JobDefinition<unknown>): void {\n if (!Number.isInteger(job.version) || job.version < 1) {\n throw new PostmasterError(`job \"${name}\" version must be a positive integer`);\n }\n\n if (typeof job.execute !== 'function' || typeof job.key !== 'function') {\n throw new PostmasterError(`job \"${name}\" must define execute and key`);\n }\n\n validateRetry(job.retry, name);\n validateMigrations(name, job);\n}\n\nexport function defineJobs<const J extends JobDefinitions>(jobs: J): J {\n for (const [name, job] of Object.entries(jobs)) validateJob(name, job);\n\n return jobs;\n}\n"],"mappings":";;AAGA,SAAS,EAAc,GAAgC,GAAoB;CACpE,UAED,CAAC,OAAO,UAAU,EAAM,WAAW,KAAK,EAAM,cAAc,IAC9D,MAAM,IAAI,EAAgB,QAAQ,EAAK,+CAA+C;AAE1F;AAEA,SAAS,EAAmB,GAAc,GAAmC;CAC3E,IAAM,IAAa,EAAI;CACvB,IAAI,CAAC,GAAY;CAEjB,IAAM,IAAO,OAAO,KAAK,CAAU;CACnC,IAAI,EAAK,WAAW,GAAG,MAAM,IAAI,EAAgB,QAAQ,EAAK,+BAA+B;CAE7F,IAAM,IAAW,EACd,KAAK,MAAQ;EACZ,IAAM,IAAU,OAAO,CAAG;EAC1B,IAAI,CAAC,OAAO,UAAU,CAAO,KAAK,OAAO,CAAO,MAAM,GACpD,MAAM,IAAI,EAAgB,QAAQ,EAAK,+BAA+B,EAAI,EAAE;EAE9E,IAAI,IAAU,KAAK,KAAW,EAAI,SAChC,MAAM,IAAI,EACR,QAAQ,EAAK,+BAA+B,EAAQ,qBAAqB,EAAI,UAAU,EAAE,EAC3F;EAEF,IAAI,OAAO,EAAW,MAAa,YACjC,MAAM,IAAI,EAAgB,QAAQ,EAAK,2BAA2B,EAAQ,oBAAoB;EAEhG,OAAO;CACT,CAAC,CAAC,CACD,MAAM,GAAG,MAAM,IAAI,CAAC;CAEvB,KAAK,IAAI,IAAQ,GAAG,IAAQ,EAAS,QAAQ,KAC3C,IAAI,EAAS,OAAW,EAAS,IAAQ,KAAK,GAC5C,MAAM,IAAI,EAAgB,QAAQ,EAAK,mCAAmC,EAAS,IAAQ,KAAK,GAAG;CAIvG,IAAI,EAAS,GAAG,EAAE,MAAM,EAAI,UAAU,GACpC,MAAM,IAAI,EAAgB,QAAQ,EAAK,sCAAsC,EAAI,SAAS;AAE9F;AAEA,SAAS,EAAY,GAAc,GAAmC;CACpE,IAAI,CAAC,OAAO,UAAU,EAAI,OAAO,KAAK,EAAI,UAAU,GAClD,MAAM,IAAI,EAAgB,QAAQ,EAAK,qCAAqC;CAG9E,IAAI,OAAO,EAAI,WAAY,cAAc,OAAO,EAAI,OAAQ,YAC1D,MAAM,IAAI,EAAgB,QAAQ,EAAK,8BAA8B;CAIvE,AADA,EAAc,EAAI,OAAO,CAAI,GAC7B,EAAmB,GAAM,CAAG;AAC9B;AAEA,SAAgB,EAA2C,GAAY;CACrE,KAAK,IAAM,CAAC,GAAM,MAAQ,OAAO,QAAQ,CAAI,GAAG,EAAY,GAAM,CAAG;CAErE,OAAO;AACT"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { defineJobs } from './definitions.ts';
2
2
  export { PostmasterDisposedError, PostmasterError, PostmasterJobError } from './errors.ts';
3
3
  export { createPostmaster } from './postmaster.ts';
4
- export type { CreatePostmasterOptions, EnqueueOptions, EntryFilter, EntryStatus, FlushResult, InferJobPayload, JobContext, JobDefinition, JobDefinitions, JsonPrimitive, JsonValue, Postmaster, PostmasterEntry, PostmasterEvent, PostmasterStats, PostmasterStore, RemoveResult, RetryPolicy, RetryResult, StoredFailure, StoredJob, StoreTx, Validate, } from './types.ts';
4
+ export type { CreatePostmasterOptions, EnqueueOptions, EntryFilter, EntryStatus, FlushResult, InferJobPayload, JobContext, JobDefinition, JobDefinitions, JsonPrimitive, JsonValue, Postmaster, PostmasterEntry, PostmasterEvent, PostmasterStats, RemoveResult, RetryPolicy, RetryResult, StoredFailure, Validate, VersionMigrations, } from './types.ts';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,YAAY,EACV,uBAAuB,EACvB,cAAc,EACd,WAAW,EACX,WAAW,EACX,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EACb,cAAc,EACd,aAAa,EACb,SAAS,EACT,UAAU,EACV,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,WAAW,EACX,WAAW,EACX,aAAa,EACb,SAAS,EACT,OAAO,EACP,QAAQ,GACT,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,YAAY,EACV,uBAAuB,EACvB,cAAc,EACd,WAAW,EACX,WAAW,EACX,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EACb,cAAc,EACd,aAAa,EACb,SAAS,EACT,UAAU,EACV,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,WAAW,EACX,WAAW,EACX,aAAa,EACb,QAAQ,EACR,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./errors.cjs"),t=require("./_store-utils.cjs");let n=require("@vielzeug/vault"),r=require("@vielzeug/vault/indexeddb");function i(t,n){return n().catch(n=>{throw n instanceof e.PostmasterError?n:new e.PostmasterError(`${t} failed`,{cause:n})})}function a(e){let a={jobs:(0,n.table)(`id`,{indexes:[`availableAt`,`leaseExpiresAt`,`status`]})},o=(0,r.createIndexedDB)({name:e.name,schema:a}),s=e=>({async countByStatus(){let[t,n,r]=await Promise.all([e.query(`jobs`).equals(`status`,`queued`).count(),e.query(`jobs`).equals(`status`,`running`).count(),e.query(`jobs`).equals(`status`,`dead-letter`).count()]);return{deadLetter:r,queued:t,running:n}},async delete(t){await e.delete(`jobs`,t)},async findClaimable(n){let r=(await e.query(`jobs`).filter(e=>e.status===`queued`&&e.availableAt<=n||e.status===`running`&&(e.leaseExpiresAt??0)<=n).toArray()).sort((e,t)=>e.availableAt-t.availableAt||e.createdAt-t.createdAt)[0];return r?t.normalize(r):void 0},async findNextWake(t){let n=await e.getAll(`jobs`),r;for(let e of n){let n;e.status===`queued`&&e.availableAt>t?n=e.availableAt:e.status===`running`&&(e.leaseExpiresAt??0)>t&&(n=e.leaseExpiresAt),n!==void 0&&(r===void 0||n<r)&&(r=n)}return r},async get(n){let r=await e.get(`jobs`,n);return r?t.normalize(r):void 0},async put(n){await e.put(`jobs`,t.normalize(n))}});return{get disposalSignal(){return o.disposalSignal},dispose(){return i(`disposing IndexedDB store`,()=>o.dispose())},get disposed(){return o.disposed},list(e){return i(`listing jobs`,async()=>{let n=o.query(`jobs`);return(e?.status===void 0?await n.toArray():await n.equals(`status`,e.status).toArray()).sort((e,t)=>e.createdAt-t.createdAt).map(t.normalize)})},subscribe(e){return o.observe(`jobs`,()=>e(),{immediate:!1})},transact(e){return i(`executing transaction`,()=>o.batch([`jobs`],async t=>e(s(t))))},async[Symbol.asyncDispose](){await o.dispose()}}}exports.createIndexedDbPostmasterStore=a;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./errors.cjs"),t=require("./_store-utils.cjs");let n=require("@vielzeug/arsenal"),r=require("@vielzeug/vault"),i=require("@vielzeug/vault/indexeddb");var a=Symbol(`corrupt`),o=[`dead-letter`,`queued`,`running`];function s(e){return typeof e==`string`&&o.includes(e)}function c(e){return typeof e==`string`&&e.length>0}function l(e){return Number.isInteger(e)&&e>=0}function u(e){return Number.isSafeInteger(e)&&e>=0}function d(e,t=new Set){if(e===null||typeof e==`boolean`||typeof e==`string`)return!0;if(typeof e==`number`)return Number.isFinite(e);if(typeof e!=`object`||t.has(e))return!1;t.add(e);let r=Array.isArray(e)?e.every(e=>d(e,t)):(0,n.isPlainObject)(e)&&Object.values(e).every(e=>d(e,t));return t.delete(e),r}function f(e){return(0,n.isPlainObject)(e)&&typeof e.message==`string`&&c(e.name)&&u(e.occurredAt)}function p(t){if(!(0,n.isPlainObject)(t))throw new e.PostmasterError(`invalid stored job: expected a plain object`);let{id:r,name:i,key:a,status:o,version:p,payload:m,attempts:h,availableAt:g,createdAt:_,updatedAt:v,failure:y,leaseExpiresAt:b,ownerId:x}=t;if(!c(r))throw new e.PostmasterError(`invalid stored job: id must be a non-empty string`);if(!c(i))throw new e.PostmasterError(`invalid stored job: name must be a non-empty string`);if(!c(a))throw new e.PostmasterError(`invalid stored job: key must be a non-empty string`);if(!s(o))throw new e.PostmasterError(`invalid stored job: status must be one of dead-letter, queued, running`);if(!l(p)||p<1)throw new e.PostmasterError(`invalid stored job: version must be a positive integer`);if(!d(m))throw new e.PostmasterError(`invalid stored job: payload must be a JSON value`);if(!l(h))throw new e.PostmasterError(`invalid stored job: attempts must be a non-negative integer`);if(!u(g))throw new e.PostmasterError(`invalid stored job: availableAt must be a timestamp`);if(!u(_))throw new e.PostmasterError(`invalid stored job: createdAt must be a timestamp`);if(!u(v))throw new e.PostmasterError(`invalid stored job: updatedAt must be a timestamp`);if(y!==void 0&&!f(y))throw new e.PostmasterError(`invalid stored job: failure must be a StoredFailure`);if(o===`running`){if(h<1)throw new e.PostmasterError(`invalid stored job: running jobs must have an attempt`);if(!u(b))throw new e.PostmasterError(`invalid stored job: running jobs must have a lease expiry`);if(!c(x))throw new e.PostmasterError(`invalid stored job: running jobs must have an owner`);if(y!==void 0)throw new e.PostmasterError(`invalid stored job: running jobs must not have a failure`)}else if(b!==void 0||x!==void 0)throw new e.PostmasterError(`invalid stored job: only running jobs may have lease ownership`);return{attempts:h,availableAt:g,createdAt:_,id:r,key:a,name:i,payload:m,status:o,updatedAt:v,version:p,...y===void 0?{}:{failure:y},...b===void 0?{}:{leaseExpiresAt:b},...x===void 0?{}:{ownerId:x}}}function m(e){return!(a in e)}function h(t,n){return n().catch(n=>{throw n instanceof e.PostmasterError?n:new e.PostmasterError(`${t} failed`,{cause:n})})}function g(o){let l=new Set,d=e=>{try{return p(e)}catch(t){let r=(0,n.isPlainObject)(e)&&c(e.id)?e.id:void 0,i=t instanceof Error?t.message:String(t),d={...r===void 0?{}:{id:r},reason:i},f=`${r??``}\u0000${i}`;if(!l.has(f)){l.add(f);try{o.onCorruptRecord?.(d)}catch{}}return{[a]:d,id:r??``,...(0,n.isPlainObject)(e)&&u(e.availableAt)?{availableAt:e.availableAt}:{},...(0,n.isPlainObject)(e)&&u(e.leaseExpiresAt)?{leaseExpiresAt:e.leaseExpiresAt}:{},...(0,n.isPlainObject)(e)&&s(e.status)?{status:e.status}:{}}}},f={jobs:(0,r.table)(`id`,{indexes:[`availableAt`,`leaseExpiresAt`,`status`]})},g=(0,i.createIndexedDB)({codecs:{jobs:{decode:d,encode:t=>{if(!m(t))throw new e.PostmasterError(`cannot persist a corrupt stored job`);return p(t)}}},name:o.name,schema:f}),_=e=>({async countByStatus(){let t=(await e.getAll(`jobs`)).filter(m),n=0,r=0,i=0;for(let e of t)e.status===`queued`?n+=1:e.status===`running`?r+=1:e.status===`dead-letter`&&(i+=1);return{deadLetter:i,queued:n,running:r}},async delete(t){await e.delete(`jobs`,t)},async findClaimable(n){let r=(await e.getAll(`jobs`)).filter(m).filter(e=>e.status===`queued`&&e.availableAt<=n||e.status===`running`&&(e.leaseExpiresAt??0)<=n);return r.sort((e,t)=>e.availableAt-t.availableAt||e.createdAt-t.createdAt),r[0]?t.normalize(r[0]):void 0},async findNextWake(t){let n=(await e.getAll(`jobs`)).filter(m),r;for(let e of n){let n;e.status===`queued`&&e.availableAt>t?n=e.availableAt:e.status===`running`&&(e.leaseExpiresAt??0)>t&&(n=e.leaseExpiresAt),n!==void 0&&(r===void 0||n<r)&&(r=n)}return r},async get(n){let r=await e.get(`jobs`,n);return r&&m(r)?t.normalize(r):void 0},async put(n){await e.put(`jobs`,t.normalize(n))}});return{get disposalSignal(){return g.disposalSignal},dispose(){return h(`disposing IndexedDB store`,()=>g.dispose())},get disposed(){return g.disposed},list(e){return h(`listing jobs`,async()=>(e?.status===void 0?await g.getAll(`jobs`):await g.getAllByIndex(`jobs`,`status`,e.status)).filter(m).sort((e,t)=>e.createdAt-t.createdAt).map(t.normalize))},subscribe(e){return g.observe(`jobs`,()=>e(),{immediate:!1})},transact(e){return h(`executing transaction`,()=>g.batch([`jobs`],async t=>e(_(t))))},async[Symbol.asyncDispose](){await g.dispose()}}}exports.createIndexedDbPostmasterStore=g;
2
2
  //# sourceMappingURL=indexeddb.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"indexeddb.cjs","names":[],"sources":["../src/indexeddb.ts"],"sourcesContent":["import { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\nimport { normalize } from './_store-utils.ts';\nimport { PostmasterError } from './errors.ts';\nimport type { EntryFilter, PostmasterStats, PostmasterStore, StoredJob, StoreTx } from './types.ts';\n\ntype PostmasterSchema = {\n jobs: ReturnType<typeof table<StoredJob, 'id'>>;\n};\n\ntype IndexedDbPostmasterStoreOptions = {\n readonly name: string;\n};\n\nfunction wrap<T>(operation: string, fn: () => Promise<T>): Promise<T> {\n return fn().catch((cause: unknown) => {\n if (cause instanceof PostmasterError) throw cause;\n throw new PostmasterError(`${operation} failed`, { cause });\n });\n}\n\nexport function createIndexedDbPostmasterStore(options: IndexedDbPostmasterStoreOptions): PostmasterStore {\n const schema: PostmasterSchema = {\n jobs: table<StoredJob, 'id'>('id', {\n indexes: ['availableAt', 'leaseExpiresAt', 'status'],\n }),\n };\n const vault = createIndexedDB({ name: options.name, schema });\n\n const buildTx = (vaultTx: Parameters<Parameters<typeof vault.batch>[1]>[0]): StoreTx => ({\n async countByStatus(): Promise<PostmasterStats> {\n const [queued, running, deadLetter] = await Promise.all([\n vaultTx.query('jobs').equals('status', 'queued').count(),\n vaultTx.query('jobs').equals('status', 'running').count(),\n vaultTx.query('jobs').equals('status', 'dead-letter').count(),\n ]);\n return { deadLetter, queued, running };\n },\n async delete(id: string): Promise<void> {\n await vaultTx.delete('jobs', id);\n },\n async findClaimable(now: number): Promise<StoredJob | undefined> {\n const candidates = await vaultTx\n .query('jobs')\n .filter(\n (entry) =>\n (entry.status === 'queued' && entry.availableAt <= now) ||\n (entry.status === 'running' && (entry.leaseExpiresAt ?? 0) <= now),\n )\n .toArray();\n const entry = candidates.sort((a, b) => a.availableAt - b.availableAt || a.createdAt - b.createdAt)[0];\n return entry ? normalize(entry) : undefined;\n },\n async findNextWake(now: number): Promise<number | undefined> {\n const entries = await vaultTx.getAll('jobs');\n let next: number | undefined;\n for (const entry of entries) {\n let wake: number | undefined;\n if (entry.status === 'queued' && entry.availableAt > now) wake = entry.availableAt;\n else if (entry.status === 'running' && (entry.leaseExpiresAt ?? 0) > now) wake = entry.leaseExpiresAt;\n if (wake !== undefined && (next === undefined || wake < next)) next = wake;\n }\n return next;\n },\n async get(id: string): Promise<StoredJob | undefined> {\n const entry = await vaultTx.get('jobs', id);\n return entry ? normalize(entry) : undefined;\n },\n async put(entry: StoredJob): Promise<void> {\n await vaultTx.put('jobs', normalize(entry));\n },\n });\n\n return {\n get disposalSignal() {\n return vault.disposalSignal;\n },\n dispose(): Promise<void> {\n return wrap('disposing IndexedDB store', () => vault.dispose());\n },\n get disposed() {\n return vault.disposed;\n },\n list(filter?: EntryFilter): Promise<StoredJob[]> {\n return wrap('listing jobs', async () => {\n const query = vault.query('jobs');\n const entries =\n filter?.status === undefined ? await query.toArray() : await query.equals('status', filter.status).toArray();\n return entries.sort((a, b) => a.createdAt - b.createdAt).map(normalize);\n });\n },\n subscribe(listener: () => void): () => void {\n return vault.observe('jobs', () => listener(), { immediate: false });\n },\n transact<T>(fn: (tx: StoreTx) => Promise<T>): Promise<T> {\n return wrap('executing transaction', () => vault.batch(['jobs'], async (vaultTx) => fn(buildTx(vaultTx))));\n },\n async [Symbol.asyncDispose](): Promise<void> {\n await vault.dispose();\n },\n };\n}\n\nexport type { IndexedDbPostmasterStoreOptions };\n"],"mappings":"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"}
1
+ {"version":3,"file":"indexeddb.cjs","names":[],"sources":["../src/indexeddb.ts"],"sourcesContent":["import { isPlainObject } from '@vielzeug/arsenal';\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\nimport { normalize } from './_store-utils.ts';\nimport { PostmasterError } from './errors.ts';\nimport type {\n EntryFilter,\n EntryStatus,\n JsonValue,\n PostmasterStats,\n PostmasterStore,\n StoredFailure,\n StoredJob,\n StoreTx,\n} from './types.ts';\n\nconst CORRUPT = Symbol('corrupt');\n\ntype CorruptRecord = {\n readonly [CORRUPT]: CorruptStoredJob;\n readonly availableAt?: number;\n readonly id: string;\n readonly leaseExpiresAt?: number;\n readonly status?: EntryStatus;\n};\n\ntype DatabaseJob = CorruptRecord | StoredJob;\n\ntype PostmasterSchema = {\n jobs: ReturnType<typeof table<DatabaseJob, 'id'>>;\n};\n\nexport interface CorruptStoredJob {\n readonly id?: string;\n readonly reason: string;\n}\n\ntype IndexedDbPostmasterStoreOptions = {\n readonly name: string;\n readonly onCorruptRecord?: (record: CorruptStoredJob) => void;\n};\n\nconst ENTRY_STATUSES: readonly EntryStatus[] = ['dead-letter', 'queued', 'running'];\n\nfunction isEntryStatus(value: unknown): value is EntryStatus {\n return typeof value === 'string' && ENTRY_STATUSES.includes(value as EntryStatus);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction isNonNegativeInteger(value: unknown): value is number {\n return Number.isInteger(value) && (value as number) >= 0;\n}\n\nfunction isTimestamp(value: unknown): value is number {\n return Number.isSafeInteger(value) && (value as number) >= 0;\n}\n\nfunction isJsonValue(value: unknown, seen = new Set<object>()): value is JsonValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return true;\n if (typeof value === 'number') return Number.isFinite(value);\n if (typeof value !== 'object' || seen.has(value)) return false;\n\n seen.add(value);\n const valid = Array.isArray(value)\n ? value.every((item) => isJsonValue(item, seen))\n : isPlainObject(value) && Object.values(value).every((item) => isJsonValue(item, seen));\n seen.delete(value);\n return valid;\n}\n\nfunction isStoredFailure(value: unknown): value is StoredFailure {\n return (\n isPlainObject(value) &&\n typeof value.message === 'string' &&\n isNonEmptyString(value.name) &&\n isTimestamp(value.occurredAt)\n );\n}\n\n/**\n * Validates that persisted data conforms to the {@link StoredJob} shape before\n * it re-enters Postmaster's in-memory world. Replaces the prior no-op cast and\n * enforces Vault's required-codec boundary on durable records.\n */\nfunction parseStoredJob(value: unknown): StoredJob {\n if (!isPlainObject(value)) {\n throw new PostmasterError('invalid stored job: expected a plain object');\n }\n\n const {\n id,\n name,\n key,\n status,\n version,\n payload,\n attempts,\n availableAt,\n createdAt,\n updatedAt,\n failure,\n leaseExpiresAt,\n ownerId,\n } = value;\n\n if (!isNonEmptyString(id)) throw new PostmasterError('invalid stored job: id must be a non-empty string');\n if (!isNonEmptyString(name)) throw new PostmasterError('invalid stored job: name must be a non-empty string');\n if (!isNonEmptyString(key)) throw new PostmasterError('invalid stored job: key must be a non-empty string');\n if (!isEntryStatus(status)) {\n throw new PostmasterError('invalid stored job: status must be one of dead-letter, queued, running');\n }\n if (!isNonNegativeInteger(version) || version < 1) {\n throw new PostmasterError('invalid stored job: version must be a positive integer');\n }\n if (!isJsonValue(payload)) throw new PostmasterError('invalid stored job: payload must be a JSON value');\n if (!isNonNegativeInteger(attempts)) {\n throw new PostmasterError('invalid stored job: attempts must be a non-negative integer');\n }\n if (!isTimestamp(availableAt)) throw new PostmasterError('invalid stored job: availableAt must be a timestamp');\n if (!isTimestamp(createdAt)) throw new PostmasterError('invalid stored job: createdAt must be a timestamp');\n if (!isTimestamp(updatedAt)) throw new PostmasterError('invalid stored job: updatedAt must be a timestamp');\n if (failure !== undefined && !isStoredFailure(failure)) {\n throw new PostmasterError('invalid stored job: failure must be a StoredFailure');\n }\n\n if (status === 'running') {\n if (attempts < 1) throw new PostmasterError('invalid stored job: running jobs must have an attempt');\n if (!isTimestamp(leaseExpiresAt)) {\n throw new PostmasterError('invalid stored job: running jobs must have a lease expiry');\n }\n if (!isNonEmptyString(ownerId)) {\n throw new PostmasterError('invalid stored job: running jobs must have an owner');\n }\n if (failure !== undefined) throw new PostmasterError('invalid stored job: running jobs must not have a failure');\n } else if (leaseExpiresAt !== undefined || ownerId !== undefined) {\n throw new PostmasterError('invalid stored job: only running jobs may have lease ownership');\n }\n\n return {\n attempts,\n availableAt,\n createdAt,\n id,\n key,\n name,\n payload,\n status,\n updatedAt,\n version,\n ...(failure !== undefined ? { failure } : {}),\n ...(leaseExpiresAt !== undefined ? { leaseExpiresAt } : {}),\n ...(ownerId !== undefined ? { ownerId } : {}),\n };\n}\n\nfunction isValidRecord(record: DatabaseJob): record is StoredJob {\n return !(CORRUPT in record);\n}\n\nfunction wrap<T>(operation: string, fn: () => Promise<T>): Promise<T> {\n return fn().catch((cause: unknown) => {\n if (cause instanceof PostmasterError) throw cause;\n throw new PostmasterError(`${operation} failed`, { cause });\n });\n}\n\nexport function createIndexedDbPostmasterStore(options: IndexedDbPostmasterStoreOptions): PostmasterStore {\n const reported = new Set<string>();\n const decode = (value: unknown): DatabaseJob => {\n try {\n return parseStoredJob(value);\n } catch (cause) {\n const id = isPlainObject(value) && isNonEmptyString(value.id) ? value.id : undefined;\n const reason = cause instanceof Error ? cause.message : String(cause);\n const issue: CorruptStoredJob = { ...(id === undefined ? {} : { id }), reason };\n const signature = `${id ?? ''}\\u0000${reason}`;\n if (!reported.has(signature)) {\n reported.add(signature);\n try {\n options.onCorruptRecord?.(issue);\n } catch {}\n }\n return {\n [CORRUPT]: issue,\n id: id ?? '',\n ...(isPlainObject(value) && isTimestamp(value.availableAt) ? { availableAt: value.availableAt } : {}),\n ...(isPlainObject(value) && isTimestamp(value.leaseExpiresAt) ? { leaseExpiresAt: value.leaseExpiresAt } : {}),\n ...(isPlainObject(value) && isEntryStatus(value.status) ? { status: value.status } : {}),\n };\n }\n };\n const schema: PostmasterSchema = {\n jobs: table<DatabaseJob, 'id'>('id', {\n indexes: ['availableAt', 'leaseExpiresAt', 'status'],\n }),\n };\n const vault = createIndexedDB({\n codecs: {\n jobs: {\n decode,\n encode: (record) => {\n if (!isValidRecord(record)) throw new PostmasterError('cannot persist a corrupt stored job');\n return parseStoredJob(record);\n },\n },\n },\n name: options.name,\n schema,\n });\n\n const buildTx = (vaultTx: Parameters<Parameters<typeof vault.batch>[1]>[0]): StoreTx => ({\n async countByStatus(): Promise<PostmasterStats> {\n const entries = (await vaultTx.getAll('jobs')).filter(isValidRecord);\n let queued = 0;\n let running = 0;\n let deadLetter = 0;\n for (const entry of entries) {\n if (entry.status === 'queued') queued += 1;\n else if (entry.status === 'running') running += 1;\n else if (entry.status === 'dead-letter') deadLetter += 1;\n }\n return { deadLetter, queued, running };\n },\n async delete(id: string): Promise<void> {\n await vaultTx.delete('jobs', id);\n },\n async findClaimable(now: number): Promise<StoredJob | undefined> {\n const entries = (await vaultTx.getAll('jobs')).filter(isValidRecord);\n const candidates = entries.filter(\n (entry) =>\n (entry.status === 'queued' && entry.availableAt <= now) ||\n (entry.status === 'running' && (entry.leaseExpiresAt ?? 0) <= now),\n );\n candidates.sort((a, b) => a.availableAt - b.availableAt || a.createdAt - b.createdAt);\n return candidates[0] ? normalize(candidates[0]) : undefined;\n },\n async findNextWake(now: number): Promise<number | undefined> {\n const entries = (await vaultTx.getAll('jobs')).filter(isValidRecord);\n let next: number | undefined;\n for (const entry of entries) {\n let wake: number | undefined;\n if (entry.status === 'queued' && entry.availableAt > now) wake = entry.availableAt;\n else if (entry.status === 'running' && (entry.leaseExpiresAt ?? 0) > now) wake = entry.leaseExpiresAt;\n if (wake !== undefined && (next === undefined || wake < next)) next = wake;\n }\n return next;\n },\n async get(id: string): Promise<StoredJob | undefined> {\n const entry = await vaultTx.get('jobs', id);\n return entry && isValidRecord(entry) ? normalize(entry) : undefined;\n },\n async put(entry: StoredJob): Promise<void> {\n await vaultTx.put('jobs', normalize(entry));\n },\n });\n\n return {\n get disposalSignal() {\n return vault.disposalSignal;\n },\n dispose(): Promise<void> {\n return wrap('disposing IndexedDB store', () => vault.dispose());\n },\n get disposed() {\n return vault.disposed;\n },\n list(filter?: EntryFilter): Promise<StoredJob[]> {\n return wrap('listing jobs', async () => {\n const entries =\n filter?.status === undefined\n ? await vault.getAll('jobs')\n : await vault.getAllByIndex('jobs', 'status', filter.status);\n return entries\n .filter(isValidRecord)\n .sort((a, b) => a.createdAt - b.createdAt)\n .map(normalize);\n });\n },\n subscribe(listener: () => void): () => void {\n return vault.observe('jobs', () => listener(), { immediate: false });\n },\n transact<T>(fn: (tx: StoreTx) => Promise<T>): Promise<T> {\n return wrap('executing transaction', () => vault.batch(['jobs'], async (vaultTx) => fn(buildTx(vaultTx))));\n },\n async [Symbol.asyncDispose](): Promise<void> {\n await vault.dispose();\n },\n };\n}\n\nexport type { IndexedDbPostmasterStoreOptions };\n"],"mappings":"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"}
@@ -1,6 +1,11 @@
1
1
  import type { PostmasterStore } from './types.ts';
2
+ export interface CorruptStoredJob {
3
+ readonly id?: string;
4
+ readonly reason: string;
5
+ }
2
6
  type IndexedDbPostmasterStoreOptions = {
3
7
  readonly name: string;
8
+ readonly onCorruptRecord?: (record: CorruptStoredJob) => void;
4
9
  };
5
10
  export declare function createIndexedDbPostmasterStore(options: IndexedDbPostmasterStoreOptions): PostmasterStore;
6
11
  export type { IndexedDbPostmasterStoreOptions };
@@ -1 +1 @@
1
- {"version":3,"file":"indexeddb.d.ts","sourceRoot":"","sources":["../src/indexeddb.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgC,eAAe,EAAsB,MAAM,YAAY,CAAC;AAMpG,KAAK,+BAA+B,GAAG;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AASF,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,+BAA+B,GAAG,eAAe,CAgFxG;AAED,YAAY,EAAE,+BAA+B,EAAE,CAAC"}
1
+ {"version":3,"file":"indexeddb.d.ts","sourceRoot":"","sources":["../src/indexeddb.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAKV,eAAe,EAIhB,MAAM,YAAY,CAAC;AAkBpB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,KAAK,+BAA+B,GAAG;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC/D,CAAC;AAiIF,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,+BAA+B,GAAG,eAAe,CA0HxG;AAED,YAAY,EAAE,+BAA+B,EAAE,CAAC"}
package/dist/indexeddb.js CHANGED
@@ -1,43 +1,137 @@
1
1
  import { PostmasterError as e } from "./errors.js";
2
2
  import { normalize as t } from "./_store-utils.js";
3
- import { table as n } from "@vielzeug/vault";
4
- import { createIndexedDB as r } from "@vielzeug/vault/indexeddb";
3
+ import { isPlainObject as n } from "@vielzeug/arsenal";
4
+ import { table as r } from "@vielzeug/vault";
5
+ import { createIndexedDB as i } from "@vielzeug/vault/indexeddb";
5
6
  //#region src/indexeddb.ts
6
- function i(t, n) {
7
+ var a = Symbol("corrupt"), o = [
8
+ "dead-letter",
9
+ "queued",
10
+ "running"
11
+ ];
12
+ function s(e) {
13
+ return typeof e == "string" && o.includes(e);
14
+ }
15
+ function c(e) {
16
+ return typeof e == "string" && e.length > 0;
17
+ }
18
+ function l(e) {
19
+ return Number.isInteger(e) && e >= 0;
20
+ }
21
+ function u(e) {
22
+ return Number.isSafeInteger(e) && e >= 0;
23
+ }
24
+ function d(e, t = /* @__PURE__ */ new Set()) {
25
+ if (e === null || typeof e == "boolean" || typeof e == "string") return !0;
26
+ if (typeof e == "number") return Number.isFinite(e);
27
+ if (typeof e != "object" || t.has(e)) return !1;
28
+ t.add(e);
29
+ let r = Array.isArray(e) ? e.every((e) => d(e, t)) : n(e) && Object.values(e).every((e) => d(e, t));
30
+ return t.delete(e), r;
31
+ }
32
+ function f(e) {
33
+ return n(e) && typeof e.message == "string" && c(e.name) && u(e.occurredAt);
34
+ }
35
+ function p(t) {
36
+ if (!n(t)) throw new e("invalid stored job: expected a plain object");
37
+ let { id: r, name: i, key: a, status: o, version: p, payload: m, attempts: h, availableAt: g, createdAt: _, updatedAt: v, failure: y, leaseExpiresAt: b, ownerId: x } = t;
38
+ if (!c(r)) throw new e("invalid stored job: id must be a non-empty string");
39
+ if (!c(i)) throw new e("invalid stored job: name must be a non-empty string");
40
+ if (!c(a)) throw new e("invalid stored job: key must be a non-empty string");
41
+ if (!s(o)) throw new e("invalid stored job: status must be one of dead-letter, queued, running");
42
+ if (!l(p) || p < 1) throw new e("invalid stored job: version must be a positive integer");
43
+ if (!d(m)) throw new e("invalid stored job: payload must be a JSON value");
44
+ if (!l(h)) throw new e("invalid stored job: attempts must be a non-negative integer");
45
+ if (!u(g)) throw new e("invalid stored job: availableAt must be a timestamp");
46
+ if (!u(_)) throw new e("invalid stored job: createdAt must be a timestamp");
47
+ if (!u(v)) throw new e("invalid stored job: updatedAt must be a timestamp");
48
+ if (y !== void 0 && !f(y)) throw new e("invalid stored job: failure must be a StoredFailure");
49
+ if (o === "running") {
50
+ if (h < 1) throw new e("invalid stored job: running jobs must have an attempt");
51
+ if (!u(b)) throw new e("invalid stored job: running jobs must have a lease expiry");
52
+ if (!c(x)) throw new e("invalid stored job: running jobs must have an owner");
53
+ if (y !== void 0) throw new e("invalid stored job: running jobs must not have a failure");
54
+ } else if (b !== void 0 || x !== void 0) throw new e("invalid stored job: only running jobs may have lease ownership");
55
+ return {
56
+ attempts: h,
57
+ availableAt: g,
58
+ createdAt: _,
59
+ id: r,
60
+ key: a,
61
+ name: i,
62
+ payload: m,
63
+ status: o,
64
+ updatedAt: v,
65
+ version: p,
66
+ ...y === void 0 ? {} : { failure: y },
67
+ ...b === void 0 ? {} : { leaseExpiresAt: b },
68
+ ...x === void 0 ? {} : { ownerId: x }
69
+ };
70
+ }
71
+ function m(e) {
72
+ return !(a in e);
73
+ }
74
+ function h(t, n) {
7
75
  return n().catch((n) => {
8
76
  throw n instanceof e ? n : new e(`${t} failed`, { cause: n });
9
77
  });
10
78
  }
11
- function a(e) {
12
- let a = { jobs: n("id", { indexes: [
79
+ function g(o) {
80
+ let l = /* @__PURE__ */ new Set(), d = (e) => {
81
+ try {
82
+ return p(e);
83
+ } catch (t) {
84
+ let r = n(e) && c(e.id) ? e.id : void 0, i = t instanceof Error ? t.message : String(t), d = {
85
+ ...r === void 0 ? {} : { id: r },
86
+ reason: i
87
+ }, f = `${r ?? ""}\u0000${i}`;
88
+ if (!l.has(f)) {
89
+ l.add(f);
90
+ try {
91
+ o.onCorruptRecord?.(d);
92
+ } catch {}
93
+ }
94
+ return {
95
+ [a]: d,
96
+ id: r ?? "",
97
+ ...n(e) && u(e.availableAt) ? { availableAt: e.availableAt } : {},
98
+ ...n(e) && u(e.leaseExpiresAt) ? { leaseExpiresAt: e.leaseExpiresAt } : {},
99
+ ...n(e) && s(e.status) ? { status: e.status } : {}
100
+ };
101
+ }
102
+ }, f = { jobs: r("id", { indexes: [
13
103
  "availableAt",
14
104
  "leaseExpiresAt",
15
105
  "status"
16
- ] }) }, o = r({
17
- name: e.name,
18
- schema: a
19
- }), s = (e) => ({
106
+ ] }) }, g = i({
107
+ codecs: { jobs: {
108
+ decode: d,
109
+ encode: (t) => {
110
+ if (!m(t)) throw new e("cannot persist a corrupt stored job");
111
+ return p(t);
112
+ }
113
+ } },
114
+ name: o.name,
115
+ schema: f
116
+ }), _ = (e) => ({
20
117
  async countByStatus() {
21
- let [t, n, r] = await Promise.all([
22
- e.query("jobs").equals("status", "queued").count(),
23
- e.query("jobs").equals("status", "running").count(),
24
- e.query("jobs").equals("status", "dead-letter").count()
25
- ]);
118
+ let t = (await e.getAll("jobs")).filter(m), n = 0, r = 0, i = 0;
119
+ for (let e of t) e.status === "queued" ? n += 1 : e.status === "running" ? r += 1 : e.status === "dead-letter" && (i += 1);
26
120
  return {
27
- deadLetter: r,
28
- queued: t,
29
- running: n
121
+ deadLetter: i,
122
+ queued: n,
123
+ running: r
30
124
  };
31
125
  },
32
126
  async delete(t) {
33
127
  await e.delete("jobs", t);
34
128
  },
35
129
  async findClaimable(n) {
36
- let r = (await e.query("jobs").filter((e) => e.status === "queued" && e.availableAt <= n || e.status === "running" && (e.leaseExpiresAt ?? 0) <= n).toArray()).sort((e, t) => e.availableAt - t.availableAt || e.createdAt - t.createdAt)[0];
37
- return r ? t(r) : void 0;
130
+ let r = (await e.getAll("jobs")).filter(m).filter((e) => e.status === "queued" && e.availableAt <= n || e.status === "running" && (e.leaseExpiresAt ?? 0) <= n);
131
+ return r.sort((e, t) => e.availableAt - t.availableAt || e.createdAt - t.createdAt), r[0] ? t(r[0]) : void 0;
38
132
  },
39
133
  async findNextWake(t) {
40
- let n = await e.getAll("jobs"), r;
134
+ let n = (await e.getAll("jobs")).filter(m), r;
41
135
  for (let e of n) {
42
136
  let n;
43
137
  e.status === "queued" && e.availableAt > t ? n = e.availableAt : e.status === "running" && (e.leaseExpiresAt ?? 0) > t && (n = e.leaseExpiresAt), n !== void 0 && (r === void 0 || n < r) && (r = n);
@@ -46,7 +140,7 @@ function a(e) {
46
140
  },
47
141
  async get(n) {
48
142
  let r = await e.get("jobs", n);
49
- return r ? t(r) : void 0;
143
+ return r && m(r) ? t(r) : void 0;
50
144
  },
51
145
  async put(n) {
52
146
  await e.put("jobs", t(n));
@@ -54,32 +148,29 @@ function a(e) {
54
148
  });
55
149
  return {
56
150
  get disposalSignal() {
57
- return o.disposalSignal;
151
+ return g.disposalSignal;
58
152
  },
59
153
  dispose() {
60
- return i("disposing IndexedDB store", () => o.dispose());
154
+ return h("disposing IndexedDB store", () => g.dispose());
61
155
  },
62
156
  get disposed() {
63
- return o.disposed;
157
+ return g.disposed;
64
158
  },
65
159
  list(e) {
66
- return i("listing jobs", async () => {
67
- let n = o.query("jobs");
68
- return (e?.status === void 0 ? await n.toArray() : await n.equals("status", e.status).toArray()).sort((e, t) => e.createdAt - t.createdAt).map(t);
69
- });
160
+ return h("listing jobs", async () => (e?.status === void 0 ? await g.getAll("jobs") : await g.getAllByIndex("jobs", "status", e.status)).filter(m).sort((e, t) => e.createdAt - t.createdAt).map(t));
70
161
  },
71
162
  subscribe(e) {
72
- return o.observe("jobs", () => e(), { immediate: !1 });
163
+ return g.observe("jobs", () => e(), { immediate: !1 });
73
164
  },
74
165
  transact(e) {
75
- return i("executing transaction", () => o.batch(["jobs"], async (t) => e(s(t))));
166
+ return h("executing transaction", () => g.batch(["jobs"], async (t) => e(_(t))));
76
167
  },
77
168
  async [Symbol.asyncDispose]() {
78
- await o.dispose();
169
+ await g.dispose();
79
170
  }
80
171
  };
81
172
  }
82
173
  //#endregion
83
- export { a as createIndexedDbPostmasterStore };
174
+ export { g as createIndexedDbPostmasterStore };
84
175
 
85
176
  //# sourceMappingURL=indexeddb.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"indexeddb.js","names":[],"sources":["../src/indexeddb.ts"],"sourcesContent":["import { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\nimport { normalize } from './_store-utils.ts';\nimport { PostmasterError } from './errors.ts';\nimport type { EntryFilter, PostmasterStats, PostmasterStore, StoredJob, StoreTx } from './types.ts';\n\ntype PostmasterSchema = {\n jobs: ReturnType<typeof table<StoredJob, 'id'>>;\n};\n\ntype IndexedDbPostmasterStoreOptions = {\n readonly name: string;\n};\n\nfunction wrap<T>(operation: string, fn: () => Promise<T>): Promise<T> {\n return fn().catch((cause: unknown) => {\n if (cause instanceof PostmasterError) throw cause;\n throw new PostmasterError(`${operation} failed`, { cause });\n });\n}\n\nexport function createIndexedDbPostmasterStore(options: IndexedDbPostmasterStoreOptions): PostmasterStore {\n const schema: PostmasterSchema = {\n jobs: table<StoredJob, 'id'>('id', {\n indexes: ['availableAt', 'leaseExpiresAt', 'status'],\n }),\n };\n const vault = createIndexedDB({ name: options.name, schema });\n\n const buildTx = (vaultTx: Parameters<Parameters<typeof vault.batch>[1]>[0]): StoreTx => ({\n async countByStatus(): Promise<PostmasterStats> {\n const [queued, running, deadLetter] = await Promise.all([\n vaultTx.query('jobs').equals('status', 'queued').count(),\n vaultTx.query('jobs').equals('status', 'running').count(),\n vaultTx.query('jobs').equals('status', 'dead-letter').count(),\n ]);\n return { deadLetter, queued, running };\n },\n async delete(id: string): Promise<void> {\n await vaultTx.delete('jobs', id);\n },\n async findClaimable(now: number): Promise<StoredJob | undefined> {\n const candidates = await vaultTx\n .query('jobs')\n .filter(\n (entry) =>\n (entry.status === 'queued' && entry.availableAt <= now) ||\n (entry.status === 'running' && (entry.leaseExpiresAt ?? 0) <= now),\n )\n .toArray();\n const entry = candidates.sort((a, b) => a.availableAt - b.availableAt || a.createdAt - b.createdAt)[0];\n return entry ? normalize(entry) : undefined;\n },\n async findNextWake(now: number): Promise<number | undefined> {\n const entries = await vaultTx.getAll('jobs');\n let next: number | undefined;\n for (const entry of entries) {\n let wake: number | undefined;\n if (entry.status === 'queued' && entry.availableAt > now) wake = entry.availableAt;\n else if (entry.status === 'running' && (entry.leaseExpiresAt ?? 0) > now) wake = entry.leaseExpiresAt;\n if (wake !== undefined && (next === undefined || wake < next)) next = wake;\n }\n return next;\n },\n async get(id: string): Promise<StoredJob | undefined> {\n const entry = await vaultTx.get('jobs', id);\n return entry ? normalize(entry) : undefined;\n },\n async put(entry: StoredJob): Promise<void> {\n await vaultTx.put('jobs', normalize(entry));\n },\n });\n\n return {\n get disposalSignal() {\n return vault.disposalSignal;\n },\n dispose(): Promise<void> {\n return wrap('disposing IndexedDB store', () => vault.dispose());\n },\n get disposed() {\n return vault.disposed;\n },\n list(filter?: EntryFilter): Promise<StoredJob[]> {\n return wrap('listing jobs', async () => {\n const query = vault.query('jobs');\n const entries =\n filter?.status === undefined ? await query.toArray() : await query.equals('status', filter.status).toArray();\n return entries.sort((a, b) => a.createdAt - b.createdAt).map(normalize);\n });\n },\n subscribe(listener: () => void): () => void {\n return vault.observe('jobs', () => listener(), { immediate: false });\n },\n transact<T>(fn: (tx: StoreTx) => Promise<T>): Promise<T> {\n return wrap('executing transaction', () => vault.batch(['jobs'], async (vaultTx) => fn(buildTx(vaultTx))));\n },\n async [Symbol.asyncDispose](): Promise<void> {\n await vault.dispose();\n },\n };\n}\n\nexport type { IndexedDbPostmasterStoreOptions };\n"],"mappings":";;;;;AAcA,SAAS,EAAQ,GAAmB,GAAkC;CACpE,OAAO,EAAG,CAAC,CAAC,OAAO,MAAmB;EAEpC,MADI,aAAiB,IAAuB,IACtC,IAAI,EAAgB,GAAG,EAAU,UAAU,EAAE,SAAM,CAAC;CAC5D,CAAC;AACH;AAEA,SAAgB,EAA+B,GAA2D;CACxG,IAAM,IAA2B,EAC/B,MAAM,EAAuB,MAAM,EACjC,SAAS;EAAC;EAAe;EAAkB;CAAQ,EACrD,CAAC,EACH,GACM,IAAQ,EAAgB;EAAE,MAAM,EAAQ;EAAM;CAAO,CAAC,GAEtD,KAAW,OAAwE;EACvF,MAAM,gBAA0C;GAC9C,IAAM,CAAC,GAAQ,GAAS,KAAc,MAAM,QAAQ,IAAI;IACtD,EAAQ,MAAM,MAAM,CAAC,CAAC,OAAO,UAAU,QAAQ,CAAC,CAAC,MAAM;IACvD,EAAQ,MAAM,MAAM,CAAC,CAAC,OAAO,UAAU,SAAS,CAAC,CAAC,MAAM;IACxD,EAAQ,MAAM,MAAM,CAAC,CAAC,OAAO,UAAU,aAAa,CAAC,CAAC,MAAM;GAC9D,CAAC;GACD,OAAO;IAAE;IAAY;IAAQ;GAAQ;EACvC;EACA,MAAM,OAAO,GAA2B;GACtC,MAAM,EAAQ,OAAO,QAAQ,CAAE;EACjC;EACA,MAAM,cAAc,GAA6C;GAS/D,IAAM,KAAQ,MARW,EACtB,MAAM,MAAM,CAAC,CACb,QACE,MACE,EAAM,WAAW,YAAY,EAAM,eAAe,KAClD,EAAM,WAAW,cAAc,EAAM,kBAAkB,MAAM,CAClE,CAAC,CACA,QAAQ,EAAA,CACc,MAAM,GAAG,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;GACpG,OAAO,IAAQ,EAAU,CAAK,IAAI,KAAA;EACpC;EACA,MAAM,aAAa,GAA0C;GAC3D,IAAM,IAAU,MAAM,EAAQ,OAAO,MAAM,GACvC;GACJ,KAAK,IAAM,KAAS,GAAS;IAC3B,IAAI;IAGJ,AAFI,EAAM,WAAW,YAAY,EAAM,cAAc,IAAK,IAAO,EAAM,cAC9D,EAAM,WAAW,cAAc,EAAM,kBAAkB,KAAK,MAAK,IAAO,EAAM,iBACnF,MAAS,KAAA,MAAc,MAAS,KAAA,KAAa,IAAO,OAAO,IAAO;GACxE;GACA,OAAO;EACT;EACA,MAAM,IAAI,GAA4C;GACpD,IAAM,IAAQ,MAAM,EAAQ,IAAI,QAAQ,CAAE;GAC1C,OAAO,IAAQ,EAAU,CAAK,IAAI,KAAA;EACpC;EACA,MAAM,IAAI,GAAiC;GACzC,MAAM,EAAQ,IAAI,QAAQ,EAAU,CAAK,CAAC;EAC5C;CACF;CAEA,OAAO;EACL,IAAI,iBAAiB;GACnB,OAAO,EAAM;EACf;EACA,UAAyB;GACvB,OAAO,EAAK,mCAAmC,EAAM,QAAQ,CAAC;EAChE;EACA,IAAI,WAAW;GACb,OAAO,EAAM;EACf;EACA,KAAK,GAA4C;GAC/C,OAAO,EAAK,gBAAgB,YAAY;IACtC,IAAM,IAAQ,EAAM,MAAM,MAAM;IAGhC,QADE,GAAQ,WAAW,KAAA,IAAY,MAAM,EAAM,QAAQ,IAAI,MAAM,EAAM,OAAO,UAAU,EAAO,MAAM,CAAC,CAAC,QAAQ,EAAA,CAC9F,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC,IAAI,CAAS;GACxE,CAAC;EACH;EACA,UAAU,GAAkC;GAC1C,OAAO,EAAM,QAAQ,cAAc,EAAS,GAAG,EAAE,WAAW,GAAM,CAAC;EACrE;EACA,SAAY,GAA6C;GACvD,OAAO,EAAK,+BAA+B,EAAM,MAAM,CAAC,MAAM,GAAG,OAAO,MAAY,EAAG,EAAQ,CAAO,CAAC,CAAC,CAAC;EAC3G;EACA,OAAO,OAAO,gBAA+B;GAC3C,MAAM,EAAM,QAAQ;EACtB;CACF;AACF"}
1
+ {"version":3,"file":"indexeddb.js","names":[],"sources":["../src/indexeddb.ts"],"sourcesContent":["import { isPlainObject } from '@vielzeug/arsenal';\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\nimport { normalize } from './_store-utils.ts';\nimport { PostmasterError } from './errors.ts';\nimport type {\n EntryFilter,\n EntryStatus,\n JsonValue,\n PostmasterStats,\n PostmasterStore,\n StoredFailure,\n StoredJob,\n StoreTx,\n} from './types.ts';\n\nconst CORRUPT = Symbol('corrupt');\n\ntype CorruptRecord = {\n readonly [CORRUPT]: CorruptStoredJob;\n readonly availableAt?: number;\n readonly id: string;\n readonly leaseExpiresAt?: number;\n readonly status?: EntryStatus;\n};\n\ntype DatabaseJob = CorruptRecord | StoredJob;\n\ntype PostmasterSchema = {\n jobs: ReturnType<typeof table<DatabaseJob, 'id'>>;\n};\n\nexport interface CorruptStoredJob {\n readonly id?: string;\n readonly reason: string;\n}\n\ntype IndexedDbPostmasterStoreOptions = {\n readonly name: string;\n readonly onCorruptRecord?: (record: CorruptStoredJob) => void;\n};\n\nconst ENTRY_STATUSES: readonly EntryStatus[] = ['dead-letter', 'queued', 'running'];\n\nfunction isEntryStatus(value: unknown): value is EntryStatus {\n return typeof value === 'string' && ENTRY_STATUSES.includes(value as EntryStatus);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction isNonNegativeInteger(value: unknown): value is number {\n return Number.isInteger(value) && (value as number) >= 0;\n}\n\nfunction isTimestamp(value: unknown): value is number {\n return Number.isSafeInteger(value) && (value as number) >= 0;\n}\n\nfunction isJsonValue(value: unknown, seen = new Set<object>()): value is JsonValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return true;\n if (typeof value === 'number') return Number.isFinite(value);\n if (typeof value !== 'object' || seen.has(value)) return false;\n\n seen.add(value);\n const valid = Array.isArray(value)\n ? value.every((item) => isJsonValue(item, seen))\n : isPlainObject(value) && Object.values(value).every((item) => isJsonValue(item, seen));\n seen.delete(value);\n return valid;\n}\n\nfunction isStoredFailure(value: unknown): value is StoredFailure {\n return (\n isPlainObject(value) &&\n typeof value.message === 'string' &&\n isNonEmptyString(value.name) &&\n isTimestamp(value.occurredAt)\n );\n}\n\n/**\n * Validates that persisted data conforms to the {@link StoredJob} shape before\n * it re-enters Postmaster's in-memory world. Replaces the prior no-op cast and\n * enforces Vault's required-codec boundary on durable records.\n */\nfunction parseStoredJob(value: unknown): StoredJob {\n if (!isPlainObject(value)) {\n throw new PostmasterError('invalid stored job: expected a plain object');\n }\n\n const {\n id,\n name,\n key,\n status,\n version,\n payload,\n attempts,\n availableAt,\n createdAt,\n updatedAt,\n failure,\n leaseExpiresAt,\n ownerId,\n } = value;\n\n if (!isNonEmptyString(id)) throw new PostmasterError('invalid stored job: id must be a non-empty string');\n if (!isNonEmptyString(name)) throw new PostmasterError('invalid stored job: name must be a non-empty string');\n if (!isNonEmptyString(key)) throw new PostmasterError('invalid stored job: key must be a non-empty string');\n if (!isEntryStatus(status)) {\n throw new PostmasterError('invalid stored job: status must be one of dead-letter, queued, running');\n }\n if (!isNonNegativeInteger(version) || version < 1) {\n throw new PostmasterError('invalid stored job: version must be a positive integer');\n }\n if (!isJsonValue(payload)) throw new PostmasterError('invalid stored job: payload must be a JSON value');\n if (!isNonNegativeInteger(attempts)) {\n throw new PostmasterError('invalid stored job: attempts must be a non-negative integer');\n }\n if (!isTimestamp(availableAt)) throw new PostmasterError('invalid stored job: availableAt must be a timestamp');\n if (!isTimestamp(createdAt)) throw new PostmasterError('invalid stored job: createdAt must be a timestamp');\n if (!isTimestamp(updatedAt)) throw new PostmasterError('invalid stored job: updatedAt must be a timestamp');\n if (failure !== undefined && !isStoredFailure(failure)) {\n throw new PostmasterError('invalid stored job: failure must be a StoredFailure');\n }\n\n if (status === 'running') {\n if (attempts < 1) throw new PostmasterError('invalid stored job: running jobs must have an attempt');\n if (!isTimestamp(leaseExpiresAt)) {\n throw new PostmasterError('invalid stored job: running jobs must have a lease expiry');\n }\n if (!isNonEmptyString(ownerId)) {\n throw new PostmasterError('invalid stored job: running jobs must have an owner');\n }\n if (failure !== undefined) throw new PostmasterError('invalid stored job: running jobs must not have a failure');\n } else if (leaseExpiresAt !== undefined || ownerId !== undefined) {\n throw new PostmasterError('invalid stored job: only running jobs may have lease ownership');\n }\n\n return {\n attempts,\n availableAt,\n createdAt,\n id,\n key,\n name,\n payload,\n status,\n updatedAt,\n version,\n ...(failure !== undefined ? { failure } : {}),\n ...(leaseExpiresAt !== undefined ? { leaseExpiresAt } : {}),\n ...(ownerId !== undefined ? { ownerId } : {}),\n };\n}\n\nfunction isValidRecord(record: DatabaseJob): record is StoredJob {\n return !(CORRUPT in record);\n}\n\nfunction wrap<T>(operation: string, fn: () => Promise<T>): Promise<T> {\n return fn().catch((cause: unknown) => {\n if (cause instanceof PostmasterError) throw cause;\n throw new PostmasterError(`${operation} failed`, { cause });\n });\n}\n\nexport function createIndexedDbPostmasterStore(options: IndexedDbPostmasterStoreOptions): PostmasterStore {\n const reported = new Set<string>();\n const decode = (value: unknown): DatabaseJob => {\n try {\n return parseStoredJob(value);\n } catch (cause) {\n const id = isPlainObject(value) && isNonEmptyString(value.id) ? value.id : undefined;\n const reason = cause instanceof Error ? cause.message : String(cause);\n const issue: CorruptStoredJob = { ...(id === undefined ? {} : { id }), reason };\n const signature = `${id ?? ''}\\u0000${reason}`;\n if (!reported.has(signature)) {\n reported.add(signature);\n try {\n options.onCorruptRecord?.(issue);\n } catch {}\n }\n return {\n [CORRUPT]: issue,\n id: id ?? '',\n ...(isPlainObject(value) && isTimestamp(value.availableAt) ? { availableAt: value.availableAt } : {}),\n ...(isPlainObject(value) && isTimestamp(value.leaseExpiresAt) ? { leaseExpiresAt: value.leaseExpiresAt } : {}),\n ...(isPlainObject(value) && isEntryStatus(value.status) ? { status: value.status } : {}),\n };\n }\n };\n const schema: PostmasterSchema = {\n jobs: table<DatabaseJob, 'id'>('id', {\n indexes: ['availableAt', 'leaseExpiresAt', 'status'],\n }),\n };\n const vault = createIndexedDB({\n codecs: {\n jobs: {\n decode,\n encode: (record) => {\n if (!isValidRecord(record)) throw new PostmasterError('cannot persist a corrupt stored job');\n return parseStoredJob(record);\n },\n },\n },\n name: options.name,\n schema,\n });\n\n const buildTx = (vaultTx: Parameters<Parameters<typeof vault.batch>[1]>[0]): StoreTx => ({\n async countByStatus(): Promise<PostmasterStats> {\n const entries = (await vaultTx.getAll('jobs')).filter(isValidRecord);\n let queued = 0;\n let running = 0;\n let deadLetter = 0;\n for (const entry of entries) {\n if (entry.status === 'queued') queued += 1;\n else if (entry.status === 'running') running += 1;\n else if (entry.status === 'dead-letter') deadLetter += 1;\n }\n return { deadLetter, queued, running };\n },\n async delete(id: string): Promise<void> {\n await vaultTx.delete('jobs', id);\n },\n async findClaimable(now: number): Promise<StoredJob | undefined> {\n const entries = (await vaultTx.getAll('jobs')).filter(isValidRecord);\n const candidates = entries.filter(\n (entry) =>\n (entry.status === 'queued' && entry.availableAt <= now) ||\n (entry.status === 'running' && (entry.leaseExpiresAt ?? 0) <= now),\n );\n candidates.sort((a, b) => a.availableAt - b.availableAt || a.createdAt - b.createdAt);\n return candidates[0] ? normalize(candidates[0]) : undefined;\n },\n async findNextWake(now: number): Promise<number | undefined> {\n const entries = (await vaultTx.getAll('jobs')).filter(isValidRecord);\n let next: number | undefined;\n for (const entry of entries) {\n let wake: number | undefined;\n if (entry.status === 'queued' && entry.availableAt > now) wake = entry.availableAt;\n else if (entry.status === 'running' && (entry.leaseExpiresAt ?? 0) > now) wake = entry.leaseExpiresAt;\n if (wake !== undefined && (next === undefined || wake < next)) next = wake;\n }\n return next;\n },\n async get(id: string): Promise<StoredJob | undefined> {\n const entry = await vaultTx.get('jobs', id);\n return entry && isValidRecord(entry) ? normalize(entry) : undefined;\n },\n async put(entry: StoredJob): Promise<void> {\n await vaultTx.put('jobs', normalize(entry));\n },\n });\n\n return {\n get disposalSignal() {\n return vault.disposalSignal;\n },\n dispose(): Promise<void> {\n return wrap('disposing IndexedDB store', () => vault.dispose());\n },\n get disposed() {\n return vault.disposed;\n },\n list(filter?: EntryFilter): Promise<StoredJob[]> {\n return wrap('listing jobs', async () => {\n const entries =\n filter?.status === undefined\n ? await vault.getAll('jobs')\n : await vault.getAllByIndex('jobs', 'status', filter.status);\n return entries\n .filter(isValidRecord)\n .sort((a, b) => a.createdAt - b.createdAt)\n .map(normalize);\n });\n },\n subscribe(listener: () => void): () => void {\n return vault.observe('jobs', () => listener(), { immediate: false });\n },\n transact<T>(fn: (tx: StoreTx) => Promise<T>): Promise<T> {\n return wrap('executing transaction', () => vault.batch(['jobs'], async (vaultTx) => fn(buildTx(vaultTx))));\n },\n async [Symbol.asyncDispose](): Promise<void> {\n await vault.dispose();\n },\n };\n}\n\nexport type { IndexedDbPostmasterStoreOptions };\n"],"mappings":"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"}