@auriclabs/jobs-infra 2.0.0 → 2.2.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @auriclabs/jobs-infra@2.0.0 build /home/runner/work/packages/packages/packages/jobs-infra
2
+ > @auriclabs/jobs-infra@2.2.0 build /home/runner/work/packages/packages/packages/jobs-infra
3
3
  > tsdown src/index.ts --format cjs,esm --dts --tsconfig tsconfig.build.json --no-hash
4
4
 
5
5
  [tsdown] Node.js v20.20.2 is deprecated. Support will be removed in the next minor release. Please upgrade to Node.js 22.18.0 or later.
@@ -7,19 +7,25 @@
7
7
  ℹ entry: src/index.ts
8
8
  ℹ tsconfig: tsconfig.build.json
9
9
  ℹ Build start
10
- ℹ [CJS] dist/index.cjs 1.67 kB │ gzip: 0.76 kB
11
- ℹ [CJS] 1 files, total: 1.67 kB
12
- ℹ [CJS] dist/index.d.cts.map 0.55 kB │ gzip: 0.27 kB
13
- ℹ [CJS] dist/index.d.cts 0.88 kB │ gzip: 0.39 kB
14
- ℹ [CJS] 2 files, total: 1.43 kB
15
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
10
+ ℹ [CJS] dist/index.cjs 5.47 kB │ gzip: 2.23 kB
11
+ ℹ [CJS] 1 files, total: 5.47 kB
12
+ ℹ [CJS] dist/index.d.cts.map 1.35 kB │ gzip: 0.51 kB
13
+ ℹ [CJS] dist/index.d.cts 5.09 kB │ gzip: 1.99 kB
14
+ ℹ [CJS] 2 files, total: 6.43 kB
15
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
16
+ - rolldown-plugin-dts:generate (69%)
17
+ - tsdown:deps (30%)
18
+ See https://rolldown.rs/options/checks#plugintimings for more details.
16
19
 
17
- ℹ [ESM] dist/index.mjs 1.58 kB │ gzip: 0.73 kB
18
- ℹ [ESM] dist/index.mjs.map 3.73 kB │ gzip: 1.35 kB
19
- ℹ [ESM] dist/index.d.mts.map 0.55 kB │ gzip: 0.27 kB
20
- ℹ [ESM] dist/index.d.mts 0.88 kB │ gzip: 0.38 kB
21
- ℹ [ESM] 4 files, total: 6.74 kB
22
- ✔ Build complete in 7174ms
23
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
20
+ ✔ Build complete in 6061ms
21
+ ℹ [ESM] dist/index.mjs  4.25 kB │ gzip: 1.77 kB
22
+ ℹ [ESM] dist/index.mjs.map 13.35 kB │ gzip: 4.78 kB
23
+ ℹ [ESM] dist/index.d.mts.map  1.35 kB │ gzip: 0.51 kB
24
+ ℹ [ESM] dist/index.d.mts  5.09 kB │ gzip: 1.99 kB
25
+ ℹ [ESM] 4 files, total: 24.03 kB
26
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
27
+ - rolldown-plugin-dts:generate (84%)
28
+ - tsdown:deps (16%)
29
+ See https://rolldown.rs/options/checks#plugintimings for more details.
24
30
 
25
- ✔ Build complete in 7175ms
31
+ ✔ Build complete in 6063ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @auriclabs/jobs-infra
2
2
 
3
+ ## 2.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - dd6d044: Take the `sst` components namespace as an explicit parameter in `createJobTable` and
8
+ `createJobsDashboard` instead of referencing the injected `sst` global.
9
+
10
+ `@auriclabs/jobs-infra` ships as ESM (`dist/index.mjs`). SST's config evaluator injects the `sst`
11
+ global into config source through an esbuild `onLoad` plugin whose filter is
12
+ `\.(js|ts|jsx|tsx)# @auriclabs/jobs-infra, which deliberately skips `.mjs`/`.cjs`. (`$app` / `$jsonStringify`/`$output`arrive via esbuild`Define`/`Inject`, which are extension-agnostic and keep working — only the `sst`namespace and`@pulumi/\*`provider aliases come from the skipped`onLoad`channel.) A bare`sst.aws.Dynamo`reference in the`.mjs`build therefore threw`ReferenceError:
13
+ sst is not
14
+ defined`at deploy time. Passing`sst`in as a parameter — as`@auriclabs/migrations`' `createTable(sst)`
15
+ already does — sidesteps the injection entirely.
16
+
17
+ Breaking:
18
+ - `createJobTable(name)` → `createJobTable(sst, name)`
19
+ - `createJobsDashboard(options)` now requires `options.sst`
20
+
21
+ `registerJobResources` is unchanged: it only uses `$jsonStringify` (an `Inject` global that works
22
+ in `.mjs`) plus caller-supplied resources.
23
+
24
+ ## 2.1.0
25
+
26
+ ### Minor Changes
27
+
28
+ - 2f09e53: Add typed job registry (defineJobs), in-process registry executor
29
+ (createRegistryExecutorHandler), continuation/time-budget helpers (continueJob, createTimeBudget)
30
+ with per-attempt continuation state, and retryJob (which resumes from the last attempt's
31
+ continuation state). Fix prepareNextJobAttempt to allow retrying failed jobs and to reject
32
+ concurrent attempts on running jobs.
33
+
34
+ Add a jobs dashboard modeled on the migrations dashboard: a bundled Vite/React UI (ui/), a
35
+ dashboard API (createJobsDashboardApiHandler — list/summary/detail/retry/cancel; no job creation),
36
+ list/summary read methods on the job service, and an `auric-jobs-dashboard` local CLI that serves
37
+ the same UI against a deployed table via SSO.
38
+
39
+ jobs-infra: add 'in-process' executor resource variant that subscribes a consumer-provided handler
40
+ with QUEUE_URL_LIST wired, wire QUEUE_URL_LIST on the lambda executor so scheduledAt re-enqueue
41
+ works there too, and add createJobsDashboard (ApiGatewayV2 + StaticSite + optional CloudFront
42
+ basic-auth) for deploying the dashboard.
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [2f09e53]
47
+ - @auriclabs/jobs@0.3.0
48
+
3
49
  ## 2.0.0
4
50
 
5
51
  ### Patch Changes
package/README.md CHANGED
@@ -91,6 +91,16 @@ interface LambdaJobResource {
91
91
  fns: FunctionWithName[]; // From @auriclabs/sst-utils
92
92
  }
93
93
 
94
+ // In-process jobs: SQS → consumer runs registered handlers directly
95
+ interface InProcessJobResource {
96
+ id: string;
97
+ executor: 'in-process';
98
+ queue: sst.aws.Queue;
99
+ handler: string; // wraps createRegistryExecutorHandler(...) from @auriclabs/jobs
100
+ link?: unknown[]; // extra linkables the handlers depend on
101
+ environment?: Record<string, string>;
102
+ }
103
+
94
104
  // Worker jobs: SQS → custom processing (no executor subscription)
95
105
  interface WorkerJobResource {
96
106
  id: string;
@@ -98,7 +108,36 @@ interface WorkerJobResource {
98
108
  queue: sst.aws.Queue;
99
109
  }
100
110
 
101
- type JobResource = LambdaJobResource | WorkerJobResource;
111
+ type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;
112
+ ```
113
+
114
+ Example `'in-process'` executor wiring:
115
+
116
+ ```typescript
117
+ registerJobResources({
118
+ table,
119
+ resources: [
120
+ {
121
+ id: 'worker',
122
+ executor: 'in-process',
123
+ queue: workerJobQueue,
124
+ handler: 'services/job/handlers/registry-executor.handler',
125
+ link: [bucket],
126
+ },
127
+ ],
128
+ handlerPaths: { ... },
129
+ });
130
+ ```
131
+
132
+ ```typescript
133
+ // services/job/handlers/registry-executor.ts
134
+ import { createRegistryExecutorHandler, initJobs } from '@auriclabs/jobs';
135
+ import { Resource } from 'sst';
136
+
137
+ initJobs({ tableName: Resource.JobTable.name });
138
+ export const handler = createRegistryExecutorHandler({
139
+ syncItems: async (payload) => { /* ... */ },
140
+ });
102
141
  ```
103
142
 
104
143
  ### What `registerJobResources` sets up
@@ -114,6 +153,37 @@ type JobResource = LambdaJobResource | WorkerJobResource;
114
153
  - Sets `LAMBDA_FUNCTION_LIST` env var (maps function names to ARNs)
115
154
  - Batch config: 10 items, 3 second window, partial responses enabled
116
155
 
156
+ 3. **In-process executor subscriptions** for each `InProcessJobResource` (`executor: 'in-process'`)
157
+ - Subscribes queue to the consumer-provided handler
158
+ - Links table, queue, and any extra `link` entries
159
+ - Sets `QUEUE_URL_LIST` env var (needed for scheduledAt re-enqueue and continuations)
160
+ - Same batch config as the Lambda executor
161
+
162
+ ### `createJobsDashboard(options)`
163
+
164
+ Deploys the jobs dashboard that ships inside `@auriclabs/jobs`: an
165
+ `ApiGatewayV2` routing `$default` to your dashboard API handler, and a
166
+ `StaticSite` serving the package's `ui/` bundle with the API URL injected
167
+ at deploy time. Optional CloudFront basic-auth gates the static site
168
+ (discovery prevention only — the API itself is unauthenticated, so deploy
169
+ on dev/demo stages only).
170
+
171
+ ```typescript
172
+ import { createJobsDashboard } from '@auriclabs/jobs-infra';
173
+
174
+ if (['dev', 'demo'].includes($app.stage)) {
175
+ createJobsDashboard({
176
+ apiHandler: 'services/job/dashboard.handler', // wraps createJobsDashboardApiHandler
177
+ table,
178
+ basicAuth: { username: 'ops', password: secret.value },
179
+ });
180
+ }
181
+ ```
182
+
183
+ Options: `apiHandler`, `table`, `link?` (extra linkables for the API fn),
184
+ `domain?`, `uiPath?` (override the resolved @auriclabs/jobs ui dir),
185
+ `basicAuth?` (`{ username, password, realm? }`). Returns `{ api, site }`.
186
+
117
187
  ## Full Example
118
188
 
119
189
  ```typescript
package/dist/index.cjs CHANGED
@@ -1,6 +1,31 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let node_module = require("node:module");
25
+ let node_path = require("node:path");
26
+ node_path = __toESM(node_path);
2
27
  //#region src/job-table.ts
3
- function createJobTable(name) {
28
+ function createJobTable(sst, name) {
4
29
  return new sst.aws.Dynamo(name, {
5
30
  fields: {
6
31
  pk: "string",
@@ -46,7 +71,26 @@ function registerJobResources(config) {
46
71
  resource.queue,
47
72
  ...resource.fns
48
73
  ],
49
- environment: { LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])) }
74
+ environment: {
75
+ LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),
76
+ QUEUE_URL_LIST
77
+ }
78
+ }, { batch: {
79
+ size: 10,
80
+ window: "3 seconds",
81
+ partialResponses: true
82
+ } });
83
+ if (resource.executor === "in-process") resource.queue.subscribe({
84
+ handler: resource.handler,
85
+ link: [
86
+ table,
87
+ resource.queue,
88
+ ...resource.link ?? []
89
+ ],
90
+ environment: {
91
+ ...resource.environment,
92
+ QUEUE_URL_LIST
93
+ }
50
94
  }, { batch: {
51
95
  size: 10,
52
96
  window: "3 seconds",
@@ -55,5 +99,69 @@ function registerJobResources(config) {
55
99
  });
56
100
  }
57
101
  //#endregion
102
+ //#region src/dashboard.ts
103
+ function createJobsDashboard(options) {
104
+ const { sst } = options;
105
+ const api = new sst.aws.ApiGatewayV2("JobsDashboardApi", { cors: true });
106
+ const link = [options.table, ...options.link ?? []];
107
+ api.route("$default", {
108
+ handler: options.apiHandler,
109
+ link
110
+ });
111
+ const uiPath = options.uiPath ?? resolveJobsUiPath();
112
+ const uiRelative = node_path.default.relative(process.cwd(), uiPath);
113
+ const basicAuthInjection = options.basicAuth ? buildBasicAuthInjection(options.basicAuth) : void 0;
114
+ return {
115
+ api,
116
+ site: new sst.aws.StaticSite("JobsDashboard", {
117
+ path: uiRelative,
118
+ build: {
119
+ command: [
120
+ "rm -rf _deploy",
121
+ "cp -r dist _deploy",
122
+ `sed -i.bak 's|<head>|<head><script>globalThis.__JOBS_API_URL__="'$VITE_API_URL'"<\/script>|' _deploy/index.html`,
123
+ "rm -f _deploy/index.html.bak"
124
+ ].join(" && "),
125
+ output: "_deploy"
126
+ },
127
+ dev: {
128
+ command: "npx vite dev",
129
+ url: "http://localhost:3101"
130
+ },
131
+ environment: { VITE_API_URL: api.url },
132
+ domain: options.domain,
133
+ ...basicAuthInjection && { edge: { viewerRequest: { injection: basicAuthInjection } } }
134
+ })
135
+ };
136
+ }
137
+ /**
138
+ * Resolve the `@auriclabs/jobs` package's `ui/` directory. jobs-infra is a
139
+ * separate package, so the UI ships with `@auriclabs/jobs` — resolve it
140
+ * through the consumer's node_modules (the workspace link covers local dev).
141
+ */
142
+ function resolveJobsUiPath() {
143
+ const pkgJsonPath = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href).resolve("@auriclabs/jobs/package.json");
144
+ const pkgRoot = node_path.default.dirname(pkgJsonPath);
145
+ return node_path.default.join(pkgRoot, "ui");
146
+ }
147
+ function buildBasicAuthInjection(auth) {
148
+ const realm = (auth.realm ?? "Jobs Dashboard").replace(/"/g, "\\\"");
149
+ return $output([auth.username, auth.password]).apply(([username, password]) => {
150
+ return [
151
+ "var __auth = event.request.headers.authorization && event.request.headers.authorization.value;",
152
+ `if (__auth !== "Basic ${Buffer.from(`${username}:${password}`).toString("base64")}") {`,
153
+ " return {",
154
+ " statusCode: 401,",
155
+ " statusDescription: \"Unauthorized\",",
156
+ " headers: {",
157
+ ` "www-authenticate": { value: 'Basic realm="${realm}"' }`,
158
+ " }",
159
+ " };",
160
+ "}"
161
+ ].join("\n");
162
+ });
163
+ }
164
+ //#endregion
58
165
  exports.createJobTable = createJobTable;
166
+ exports.createJobsDashboard = createJobsDashboard;
59
167
  exports.registerJobResources = registerJobResources;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,27 @@
1
1
  import { FunctionWithName } from "@auriclabs/sst-utils";
2
2
 
3
3
  //#region src/job-table.d.ts
4
- declare function createJobTable(name: string): sst.aws.Dynamo;
4
+ /**
5
+ * The subset of the SST components namespace that {@link createJobTable} needs
6
+ * at runtime.
7
+ *
8
+ * `@auriclabs/jobs-infra` ships as ESM (`dist/index.mjs`). SST's config
9
+ * evaluator injects the `sst` global into config source via an esbuild
10
+ * `onLoad` plugin whose filter is `\.(js|ts|jsx|tsx)$` — it deliberately skips
11
+ * `.mjs`/`.cjs`. (The `$app` / `$jsonStringify` / `$output` globals arrive via
12
+ * esbuild `Define`/`Inject`, which are extension-agnostic, so those keep
13
+ * working; only the `sst` namespace and the `@pulumi/*` provider aliases come
14
+ * from the skipped `onLoad` channel.) A bare `sst.aws.Dynamo` reference in this
15
+ * `.mjs` file therefore throws `ReferenceError: sst is not defined` at deploy
16
+ * time. Taking `sst` as a parameter — exactly as `@auriclabs/migrations`'
17
+ * `createTable(sst)` does — sidesteps the injection entirely.
18
+ */
19
+ interface JobTableSstProvider {
20
+ aws: {
21
+ Dynamo: typeof sst.aws.Dynamo;
22
+ };
23
+ }
24
+ declare function createJobTable(sst: JobTableSstProvider, name: string): sst.aws.Dynamo;
5
25
  //#endregion
6
26
  //#region src/job-resources.d.ts
7
27
  interface LambdaJobResource {
@@ -10,12 +30,22 @@ interface LambdaJobResource {
10
30
  queue: sst.aws.Queue;
11
31
  fns: FunctionWithName[];
12
32
  }
33
+ interface InProcessJobResource {
34
+ id: string;
35
+ executor: 'in-process';
36
+ queue: sst.aws.Queue;
37
+ /** Path to a handler that wraps createRegistryExecutorHandler(...) from @auriclabs/jobs. */
38
+ handler: string;
39
+ /** Extra linkables the in-process handlers depend on. */
40
+ link?: unknown[];
41
+ environment?: Record<string, string>;
42
+ }
13
43
  interface WorkerJobResource {
14
44
  id: string;
15
45
  executor?: never;
16
46
  queue: sst.aws.Queue;
17
47
  }
18
- type JobResource = LambdaJobResource | WorkerJobResource;
48
+ type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;
19
49
  interface RegisterJobResourcesConfig {
20
50
  table: sst.aws.Dynamo;
21
51
  resources: JobResource[];
@@ -26,5 +56,72 @@ interface RegisterJobResourcesConfig {
26
56
  }
27
57
  declare function registerJobResources(config: RegisterJobResourcesConfig): void;
28
58
  //#endregion
29
- export { JobResource, LambdaJobResource, RegisterJobResourcesConfig, WorkerJobResource, createJobTable, registerJobResources };
59
+ //#region src/dashboard.d.ts
60
+ interface JobsDashboardBasicAuthConfig {
61
+ /** Plaintext username — typically wired from an SST secret. */
62
+ username: $util.Input<string>;
63
+ /** Plaintext password — typically wired from an SST secret. */
64
+ password: $util.Input<string>;
65
+ /** Realm shown in browser auth dialog. Defaults to "Jobs Dashboard". */
66
+ realm?: string;
67
+ }
68
+ /**
69
+ * The subset of the SST components namespace that {@link createJobsDashboard}
70
+ * needs at runtime.
71
+ *
72
+ * Passed in rather than referenced as the `sst` global for the same reason as
73
+ * {@link JobTableSstProvider}: this package ships as `.mjs`, and SST's config
74
+ * evaluator skips `.mjs`/`.cjs` when injecting the `sst` global (its esbuild
75
+ * `onLoad` plugin filters on `\.(js|ts|jsx|tsx)$`). See the note on
76
+ * {@link JobTableSstProvider} for the full mechanism.
77
+ */
78
+ interface JobsDashboardSstProvider {
79
+ aws: {
80
+ ApiGatewayV2: typeof sst.aws.ApiGatewayV2;
81
+ StaticSite: typeof sst.aws.StaticSite;
82
+ };
83
+ }
84
+ interface JobsDashboardOptions {
85
+ /**
86
+ * SST components namespace — pass the injected `sst` global from your
87
+ * `sst.config.ts` (a `.ts` file, which SST *does* inject `sst` into).
88
+ * See {@link JobsDashboardSstProvider}.
89
+ */
90
+ sst: JobsDashboardSstProvider;
91
+ /**
92
+ * Handler path for the dashboard API Lambda. The handler should call
93
+ * `initJobs({ tableName: Resource.<JobTable>.name })` and export
94
+ * `createJobsDashboardApiHandler()` from `@auriclabs/jobs`.
95
+ */
96
+ apiHandler: string;
97
+ /** Job table — linked into the API function. */
98
+ table: sst.aws.Dynamo;
99
+ /** Extra linkables for the API function (beyond the table). */
100
+ link?: unknown[];
101
+ domain?: sst.aws.StaticSiteArgs['domain'];
102
+ /**
103
+ * Override the path to the `@auriclabs/jobs` package's `ui/` directory.
104
+ * Defaults to resolving the installed package via `require.resolve`.
105
+ */
106
+ uiPath?: string;
107
+ /**
108
+ * Optional HTTP basic auth gate on the static site. When set, injects a
109
+ * basic-auth check into the StaticSite's CloudFront viewer-request
110
+ * function — requests without the matching `Authorization: Basic <base64>`
111
+ * header get a 401 before any routing logic runs. Credentials are inlined
112
+ * into the function source at deploy time via Pulumi apply.
113
+ *
114
+ * Note: this only gates the static UI. The API gateway URL remains
115
+ * directly callable — browsers don't auto-send basic auth credentials
116
+ * cross-origin, so the dashboard's fetch() can't carry them. Treat this
117
+ * as discovery-prevention for the dashboard, not API protection.
118
+ */
119
+ basicAuth?: JobsDashboardBasicAuthConfig;
120
+ }
121
+ declare function createJobsDashboard(options: JobsDashboardOptions): {
122
+ api: sst.aws.ApiGatewayV2;
123
+ site: sst.aws.StaticSite;
124
+ };
125
+ //#endregion
126
+ export { InProcessJobResource, JobResource, JobTableSstProvider, JobsDashboardBasicAuthConfig, JobsDashboardOptions, JobsDashboardSstProvider, LambdaJobResource, RegisterJobResourcesConfig, WorkerJobResource, createJobTable, createJobsDashboard, registerJobResources };
30
127
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts"],"mappings":";;;iBAAgB,cAAA,CAAe,IAAA,WAAY,GAAA,CAAA,GAAA,CAAA,MAAA;;;UCE1B,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EACf,GAAA,EAAK,gBAAA;AAAA;AAAA,UAGU,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;AAAA;AAAA,KAGL,WAAA,GAAc,iBAAA,GAAoB,iBAAA;AAAA,UAE7B,0BAAA;EACf,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EACf,SAAA,EAAW,WAAA;EACX,YAAA;IACE,MAAA;IACA,QAAA;EAAA;AAAA;AAAA,iBAIY,oBAAA,CAAqB,MAAA,EAAQ,0BAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts","../src/dashboard.ts"],"mappings":";;;;;;AAeA;;;;;;;;;;;AAMA;UANiB,mBAAA;EACf,GAAA;IACE,MAAA,SAAe,GAAA,CAAI,GAAA,CAAI,MAAA;EAAA;AAAA;AAAA,iBAIX,cAAA,CAAe,GAAA,EAAK,mBAAA,EAAqB,IAAA,WAAY,GAAA,CAAA,GAAA,CAAA,MAAA;;;UCnBpD,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EACf,GAAA,EAAK,gBAAA;AAAA;AAAA,UAGU,oBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EDKU;ECHzB,OAAA;EDG+B;ECD/B,IAAA;EACA,WAAA,GAAc,MAAA;AAAA;AAAA,UAGC,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;AAAA;AAAA,KAGL,WAAA,GAAc,iBAAA,GAAoB,oBAAA,GAAuB,iBAAA;AAAA,UAEpD,0BAAA;EACf,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EACf,SAAA,EAAW,WAAA;EACX,YAAA;IACE,MAAA;IACA,QAAA;EAAA;AAAA;AAAA,iBAIY,oBAAA,CAAqB,MAAA,EAAQ,0BAAA;;;UClC5B,4BAAA;;EAEf,QAAA,EAAU,KAAA,CAAM,KAAA;EFUD;EERf,QAAA,EAAU,KAAA,CAAM,KAAA;;EAEhB,KAAA;AAAA;;;;;;;AFYF;;;;UECiB,wBAAA;EACf,GAAA;IACE,YAAA,SAAqB,GAAA,CAAI,GAAA,CAAI,YAAA;IAC7B,UAAA,SAAmB,GAAA,CAAI,GAAA,CAAI,UAAA;EAAA;AAAA;AAAA,UAId,oBAAA;EFRoD;;;;ACnBrE;ECiCE,GAAA,EAAK,wBAAA;;;;;;EAML,UAAA;EDpCW;ECsCX,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EDrCf;ECuCA,IAAA;EACA,MAAA,GAAS,GAAA,CAAI,GAAA,CAAI,cAAA;EDxCI;AAGvB;;;EC0CE,MAAA;EDzCA;;;;;;;;;;;;ECsDA,SAAA,GAAY,4BAAA;AAAA;AAAA,iBAGE,mBAAA,CAAoB,OAAA,EAAS,oBAAA"}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,27 @@
1
1
  import { FunctionWithName } from "@auriclabs/sst-utils";
2
2
 
3
3
  //#region src/job-table.d.ts
4
- declare function createJobTable(name: string): sst.aws.Dynamo;
4
+ /**
5
+ * The subset of the SST components namespace that {@link createJobTable} needs
6
+ * at runtime.
7
+ *
8
+ * `@auriclabs/jobs-infra` ships as ESM (`dist/index.mjs`). SST's config
9
+ * evaluator injects the `sst` global into config source via an esbuild
10
+ * `onLoad` plugin whose filter is `\.(js|ts|jsx|tsx)$` — it deliberately skips
11
+ * `.mjs`/`.cjs`. (The `$app` / `$jsonStringify` / `$output` globals arrive via
12
+ * esbuild `Define`/`Inject`, which are extension-agnostic, so those keep
13
+ * working; only the `sst` namespace and the `@pulumi/*` provider aliases come
14
+ * from the skipped `onLoad` channel.) A bare `sst.aws.Dynamo` reference in this
15
+ * `.mjs` file therefore throws `ReferenceError: sst is not defined` at deploy
16
+ * time. Taking `sst` as a parameter — exactly as `@auriclabs/migrations`'
17
+ * `createTable(sst)` does — sidesteps the injection entirely.
18
+ */
19
+ interface JobTableSstProvider {
20
+ aws: {
21
+ Dynamo: typeof sst.aws.Dynamo;
22
+ };
23
+ }
24
+ declare function createJobTable(sst: JobTableSstProvider, name: string): sst.aws.Dynamo;
5
25
  //#endregion
6
26
  //#region src/job-resources.d.ts
7
27
  interface LambdaJobResource {
@@ -10,12 +30,22 @@ interface LambdaJobResource {
10
30
  queue: sst.aws.Queue;
11
31
  fns: FunctionWithName[];
12
32
  }
33
+ interface InProcessJobResource {
34
+ id: string;
35
+ executor: 'in-process';
36
+ queue: sst.aws.Queue;
37
+ /** Path to a handler that wraps createRegistryExecutorHandler(...) from @auriclabs/jobs. */
38
+ handler: string;
39
+ /** Extra linkables the in-process handlers depend on. */
40
+ link?: unknown[];
41
+ environment?: Record<string, string>;
42
+ }
13
43
  interface WorkerJobResource {
14
44
  id: string;
15
45
  executor?: never;
16
46
  queue: sst.aws.Queue;
17
47
  }
18
- type JobResource = LambdaJobResource | WorkerJobResource;
48
+ type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;
19
49
  interface RegisterJobResourcesConfig {
20
50
  table: sst.aws.Dynamo;
21
51
  resources: JobResource[];
@@ -26,5 +56,72 @@ interface RegisterJobResourcesConfig {
26
56
  }
27
57
  declare function registerJobResources(config: RegisterJobResourcesConfig): void;
28
58
  //#endregion
29
- export { JobResource, LambdaJobResource, RegisterJobResourcesConfig, WorkerJobResource, createJobTable, registerJobResources };
59
+ //#region src/dashboard.d.ts
60
+ interface JobsDashboardBasicAuthConfig {
61
+ /** Plaintext username — typically wired from an SST secret. */
62
+ username: $util.Input<string>;
63
+ /** Plaintext password — typically wired from an SST secret. */
64
+ password: $util.Input<string>;
65
+ /** Realm shown in browser auth dialog. Defaults to "Jobs Dashboard". */
66
+ realm?: string;
67
+ }
68
+ /**
69
+ * The subset of the SST components namespace that {@link createJobsDashboard}
70
+ * needs at runtime.
71
+ *
72
+ * Passed in rather than referenced as the `sst` global for the same reason as
73
+ * {@link JobTableSstProvider}: this package ships as `.mjs`, and SST's config
74
+ * evaluator skips `.mjs`/`.cjs` when injecting the `sst` global (its esbuild
75
+ * `onLoad` plugin filters on `\.(js|ts|jsx|tsx)$`). See the note on
76
+ * {@link JobTableSstProvider} for the full mechanism.
77
+ */
78
+ interface JobsDashboardSstProvider {
79
+ aws: {
80
+ ApiGatewayV2: typeof sst.aws.ApiGatewayV2;
81
+ StaticSite: typeof sst.aws.StaticSite;
82
+ };
83
+ }
84
+ interface JobsDashboardOptions {
85
+ /**
86
+ * SST components namespace — pass the injected `sst` global from your
87
+ * `sst.config.ts` (a `.ts` file, which SST *does* inject `sst` into).
88
+ * See {@link JobsDashboardSstProvider}.
89
+ */
90
+ sst: JobsDashboardSstProvider;
91
+ /**
92
+ * Handler path for the dashboard API Lambda. The handler should call
93
+ * `initJobs({ tableName: Resource.<JobTable>.name })` and export
94
+ * `createJobsDashboardApiHandler()` from `@auriclabs/jobs`.
95
+ */
96
+ apiHandler: string;
97
+ /** Job table — linked into the API function. */
98
+ table: sst.aws.Dynamo;
99
+ /** Extra linkables for the API function (beyond the table). */
100
+ link?: unknown[];
101
+ domain?: sst.aws.StaticSiteArgs['domain'];
102
+ /**
103
+ * Override the path to the `@auriclabs/jobs` package's `ui/` directory.
104
+ * Defaults to resolving the installed package via `require.resolve`.
105
+ */
106
+ uiPath?: string;
107
+ /**
108
+ * Optional HTTP basic auth gate on the static site. When set, injects a
109
+ * basic-auth check into the StaticSite's CloudFront viewer-request
110
+ * function — requests without the matching `Authorization: Basic <base64>`
111
+ * header get a 401 before any routing logic runs. Credentials are inlined
112
+ * into the function source at deploy time via Pulumi apply.
113
+ *
114
+ * Note: this only gates the static UI. The API gateway URL remains
115
+ * directly callable — browsers don't auto-send basic auth credentials
116
+ * cross-origin, so the dashboard's fetch() can't carry them. Treat this
117
+ * as discovery-prevention for the dashboard, not API protection.
118
+ */
119
+ basicAuth?: JobsDashboardBasicAuthConfig;
120
+ }
121
+ declare function createJobsDashboard(options: JobsDashboardOptions): {
122
+ api: sst.aws.ApiGatewayV2;
123
+ site: sst.aws.StaticSite;
124
+ };
125
+ //#endregion
126
+ export { InProcessJobResource, JobResource, JobTableSstProvider, JobsDashboardBasicAuthConfig, JobsDashboardOptions, JobsDashboardSstProvider, LambdaJobResource, RegisterJobResourcesConfig, WorkerJobResource, createJobTable, createJobsDashboard, registerJobResources };
30
127
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts"],"mappings":";;;iBAAgB,cAAA,CAAe,IAAA,WAAY,GAAA,CAAA,GAAA,CAAA,MAAA;;;UCE1B,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EACf,GAAA,EAAK,gBAAA;AAAA;AAAA,UAGU,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;AAAA;AAAA,KAGL,WAAA,GAAc,iBAAA,GAAoB,iBAAA;AAAA,UAE7B,0BAAA;EACf,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EACf,SAAA,EAAW,WAAA;EACX,YAAA;IACE,MAAA;IACA,QAAA;EAAA;AAAA;AAAA,iBAIY,oBAAA,CAAqB,MAAA,EAAQ,0BAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts","../src/dashboard.ts"],"mappings":";;;;;;AAeA;;;;;;;;;;;AAMA;UANiB,mBAAA;EACf,GAAA;IACE,MAAA,SAAe,GAAA,CAAI,GAAA,CAAI,MAAA;EAAA;AAAA;AAAA,iBAIX,cAAA,CAAe,GAAA,EAAK,mBAAA,EAAqB,IAAA,WAAY,GAAA,CAAA,GAAA,CAAA,MAAA;;;UCnBpD,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EACf,GAAA,EAAK,gBAAA;AAAA;AAAA,UAGU,oBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;EDKU;ECHzB,OAAA;EDG+B;ECD/B,IAAA;EACA,WAAA,GAAc,MAAA;AAAA;AAAA,UAGC,iBAAA;EACf,EAAA;EACA,QAAA;EACA,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,KAAA;AAAA;AAAA,KAGL,WAAA,GAAc,iBAAA,GAAoB,oBAAA,GAAuB,iBAAA;AAAA,UAEpD,0BAAA;EACf,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EACf,SAAA,EAAW,WAAA;EACX,YAAA;IACE,MAAA;IACA,QAAA;EAAA;AAAA;AAAA,iBAIY,oBAAA,CAAqB,MAAA,EAAQ,0BAAA;;;UClC5B,4BAAA;;EAEf,QAAA,EAAU,KAAA,CAAM,KAAA;EFUD;EERf,QAAA,EAAU,KAAA,CAAM,KAAA;;EAEhB,KAAA;AAAA;;;;;;;AFYF;;;;UECiB,wBAAA;EACf,GAAA;IACE,YAAA,SAAqB,GAAA,CAAI,GAAA,CAAI,YAAA;IAC7B,UAAA,SAAmB,GAAA,CAAI,GAAA,CAAI,UAAA;EAAA;AAAA;AAAA,UAId,oBAAA;EFRoD;;;;ACnBrE;ECiCE,GAAA,EAAK,wBAAA;;;;;;EAML,UAAA;EDpCW;ECsCX,KAAA,EAAO,GAAA,CAAI,GAAA,CAAI,MAAA;EDrCf;ECuCA,IAAA;EACA,MAAA,GAAS,GAAA,CAAI,GAAA,CAAI,cAAA;EDxCI;AAGvB;;;EC0CE,MAAA;EDzCA;;;;;;;;;;;;ECsDA,SAAA,GAAY,4BAAA;AAAA;AAAA,iBAGE,mBAAA,CAAoB,OAAA,EAAS,oBAAA"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,7 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
1
3
  //#region src/job-table.ts
2
- function createJobTable(name) {
4
+ function createJobTable(sst, name) {
3
5
  return new sst.aws.Dynamo(name, {
4
6
  fields: {
5
7
  pk: "string",
@@ -45,15 +47,97 @@ function registerJobResources(config) {
45
47
  resource.queue,
46
48
  ...resource.fns
47
49
  ],
48
- environment: { LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])) }
50
+ environment: {
51
+ LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),
52
+ QUEUE_URL_LIST
53
+ }
49
54
  }, { batch: {
50
55
  size: 10,
51
56
  window: "3 seconds",
52
57
  partialResponses: true
53
58
  } });
59
+ if (resource.executor === "in-process") resource.queue.subscribe({
60
+ handler: resource.handler,
61
+ link: [
62
+ table,
63
+ resource.queue,
64
+ ...resource.link ?? []
65
+ ],
66
+ environment: {
67
+ ...resource.environment,
68
+ QUEUE_URL_LIST
69
+ }
70
+ }, { batch: {
71
+ size: 10,
72
+ window: "3 seconds",
73
+ partialResponses: true
74
+ } });
75
+ });
76
+ }
77
+ //#endregion
78
+ //#region src/dashboard.ts
79
+ function createJobsDashboard(options) {
80
+ const { sst } = options;
81
+ const api = new sst.aws.ApiGatewayV2("JobsDashboardApi", { cors: true });
82
+ const link = [options.table, ...options.link ?? []];
83
+ api.route("$default", {
84
+ handler: options.apiHandler,
85
+ link
86
+ });
87
+ const uiPath = options.uiPath ?? resolveJobsUiPath();
88
+ const uiRelative = path.relative(process.cwd(), uiPath);
89
+ const basicAuthInjection = options.basicAuth ? buildBasicAuthInjection(options.basicAuth) : void 0;
90
+ return {
91
+ api,
92
+ site: new sst.aws.StaticSite("JobsDashboard", {
93
+ path: uiRelative,
94
+ build: {
95
+ command: [
96
+ "rm -rf _deploy",
97
+ "cp -r dist _deploy",
98
+ `sed -i.bak 's|<head>|<head><script>globalThis.__JOBS_API_URL__="'$VITE_API_URL'"<\/script>|' _deploy/index.html`,
99
+ "rm -f _deploy/index.html.bak"
100
+ ].join(" && "),
101
+ output: "_deploy"
102
+ },
103
+ dev: {
104
+ command: "npx vite dev",
105
+ url: "http://localhost:3101"
106
+ },
107
+ environment: { VITE_API_URL: api.url },
108
+ domain: options.domain,
109
+ ...basicAuthInjection && { edge: { viewerRequest: { injection: basicAuthInjection } } }
110
+ })
111
+ };
112
+ }
113
+ /**
114
+ * Resolve the `@auriclabs/jobs` package's `ui/` directory. jobs-infra is a
115
+ * separate package, so the UI ships with `@auriclabs/jobs` — resolve it
116
+ * through the consumer's node_modules (the workspace link covers local dev).
117
+ */
118
+ function resolveJobsUiPath() {
119
+ const pkgJsonPath = createRequire(import.meta.url).resolve("@auriclabs/jobs/package.json");
120
+ const pkgRoot = path.dirname(pkgJsonPath);
121
+ return path.join(pkgRoot, "ui");
122
+ }
123
+ function buildBasicAuthInjection(auth) {
124
+ const realm = (auth.realm ?? "Jobs Dashboard").replace(/"/g, "\\\"");
125
+ return $output([auth.username, auth.password]).apply(([username, password]) => {
126
+ return [
127
+ "var __auth = event.request.headers.authorization && event.request.headers.authorization.value;",
128
+ `if (__auth !== "Basic ${Buffer.from(`${username}:${password}`).toString("base64")}") {`,
129
+ " return {",
130
+ " statusCode: 401,",
131
+ " statusDescription: \"Unauthorized\",",
132
+ " headers: {",
133
+ ` "www-authenticate": { value: 'Basic realm="${realm}"' }`,
134
+ " }",
135
+ " };",
136
+ "}"
137
+ ].join("\n");
54
138
  });
55
139
  }
56
140
  //#endregion
57
- export { createJobTable, registerJobResources };
141
+ export { createJobTable, createJobsDashboard, registerJobResources };
58
142
 
59
143
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts"],"sourcesContent":["export function createJobTable(name: string) {\n return new sst.aws.Dynamo(name, {\n fields: {\n pk: 'string',\n sk: 'string',\n numberIndexPk: 'string',\n numberIndexSk: 'number',\n gsi1pk: 'string',\n gsi1sk: 'string',\n },\n primaryIndex: {\n hashKey: 'pk',\n rangeKey: 'sk',\n },\n globalIndexes: {\n numberIndex: { hashKey: 'numberIndexPk', rangeKey: 'numberIndexSk' },\n gsi1: { hashKey: 'gsi1pk', rangeKey: 'gsi1sk' },\n },\n stream: 'new-and-old-images',\n });\n}\n","import { FunctionWithName } from '@auriclabs/sst-utils';\n\nexport interface LambdaJobResource {\n id: string;\n executor: 'lambda';\n queue: sst.aws.Queue;\n fns: FunctionWithName[];\n}\n\nexport interface WorkerJobResource {\n id: string;\n executor?: never;\n queue: sst.aws.Queue;\n}\n\nexport type JobResource = LambdaJobResource | WorkerJobResource;\n\nexport interface RegisterJobResourcesConfig {\n table: sst.aws.Dynamo;\n resources: JobResource[];\n handlerPaths: {\n stream: string;\n executor: string;\n };\n}\n\nexport function registerJobResources(config: RegisterJobResourcesConfig) {\n const { table, resources, handlerPaths } = config;\n const QUEUE_URL_LIST = $jsonStringify(resources.map(({ id, queue }) => [id, queue.url]));\n\n table.subscribe(\n 'JobTableStream',\n {\n handler: handlerPaths.stream,\n link: [table, ...resources.map(({ queue }) => queue)],\n environment: {\n QUEUE_URL_LIST,\n },\n },\n {\n filters: [\n {\n dynamodb: {\n NewImage: {\n __edb_e__: {\n S: ['job-attempt'],\n },\n },\n },\n },\n {\n dynamodb: {\n OldImage: {\n __edb_e__: {\n S: ['job-attempt'],\n },\n },\n },\n },\n ],\n },\n );\n\n resources.forEach((resource) => {\n if (!('executor' in resource)) {\n return;\n }\n\n if (resource.executor === 'lambda') {\n resource.queue.subscribe(\n {\n handler: handlerPaths.executor,\n link: [table, resource.queue, ...resource.fns],\n environment: {\n LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),\n },\n },\n {\n batch: {\n size: 10,\n window: '3 seconds',\n partialResponses: true,\n },\n },\n );\n }\n });\n}\n"],"mappings":";AAAA,SAAgB,eAAe,MAAc;AAC3C,QAAO,IAAI,IAAI,IAAI,OAAO,MAAM;EAC9B,QAAQ;GACN,IAAI;GACJ,IAAI;GACJ,eAAe;GACf,eAAe;GACf,QAAQ;GACR,QAAQ;GACT;EACD,cAAc;GACZ,SAAS;GACT,UAAU;GACX;EACD,eAAe;GACb,aAAa;IAAE,SAAS;IAAiB,UAAU;IAAiB;GACpE,MAAM;IAAE,SAAS;IAAU,UAAU;IAAU;GAChD;EACD,QAAQ;EACT,CAAC;;;;ACOJ,SAAgB,qBAAqB,QAAoC;CACvE,MAAM,EAAE,OAAO,WAAW,iBAAiB;CAC3C,MAAM,iBAAiB,eAAe,UAAU,KAAK,EAAE,IAAI,YAAY,CAAC,IAAI,MAAM,IAAI,CAAC,CAAC;AAExF,OAAM,UACJ,kBACA;EACE,SAAS,aAAa;EACtB,MAAM,CAAC,OAAO,GAAG,UAAU,KAAK,EAAE,YAAY,MAAM,CAAC;EACrD,aAAa,EACX,gBACD;EACF,EACD,EACE,SAAS,CACP,EACE,UAAU,EACR,UAAU,EACR,WAAW,EACT,GAAG,CAAC,cAAc,EACnB,EACF,EACF,EACF,EACD,EACE,UAAU,EACR,UAAU,EACR,WAAW,EACT,GAAG,CAAC,cAAc,EACnB,EACF,EACF,EACF,CACF,EACF,CACF;AAED,WAAU,SAAS,aAAa;AAC9B,MAAI,EAAE,cAAc,UAClB;AAGF,MAAI,SAAS,aAAa,SACxB,UAAS,MAAM,UACb;GACE,SAAS,aAAa;GACtB,MAAM;IAAC;IAAO,SAAS;IAAO,GAAG,SAAS;IAAI;GAC9C,aAAa,EACX,sBAAsB,eAAe,SAAS,IAAI,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,EAC/E;GACF,EACD,EACE,OAAO;GACL,MAAM;GACN,QAAQ;GACR,kBAAkB;GACnB,EACF,CACF;GAEH"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/job-table.ts","../src/job-resources.ts","../src/dashboard.ts"],"sourcesContent":["/**\n * The subset of the SST components namespace that {@link createJobTable} needs\n * at runtime.\n *\n * `@auriclabs/jobs-infra` ships as ESM (`dist/index.mjs`). SST's config\n * evaluator injects the `sst` global into config source via an esbuild\n * `onLoad` plugin whose filter is `\\.(js|ts|jsx|tsx)$` — it deliberately skips\n * `.mjs`/`.cjs`. (The `$app` / `$jsonStringify` / `$output` globals arrive via\n * esbuild `Define`/`Inject`, which are extension-agnostic, so those keep\n * working; only the `sst` namespace and the `@pulumi/*` provider aliases come\n * from the skipped `onLoad` channel.) A bare `sst.aws.Dynamo` reference in this\n * `.mjs` file therefore throws `ReferenceError: sst is not defined` at deploy\n * time. Taking `sst` as a parameter — exactly as `@auriclabs/migrations`'\n * `createTable(sst)` does — sidesteps the injection entirely.\n */\nexport interface JobTableSstProvider {\n aws: {\n Dynamo: typeof sst.aws.Dynamo;\n };\n}\n\nexport function createJobTable(sst: JobTableSstProvider, name: string) {\n return new sst.aws.Dynamo(name, {\n fields: {\n pk: 'string',\n sk: 'string',\n numberIndexPk: 'string',\n numberIndexSk: 'number',\n gsi1pk: 'string',\n gsi1sk: 'string',\n },\n primaryIndex: {\n hashKey: 'pk',\n rangeKey: 'sk',\n },\n globalIndexes: {\n numberIndex: { hashKey: 'numberIndexPk', rangeKey: 'numberIndexSk' },\n gsi1: { hashKey: 'gsi1pk', rangeKey: 'gsi1sk' },\n },\n stream: 'new-and-old-images',\n });\n}\n","import { FunctionWithName } from '@auriclabs/sst-utils';\n\nexport interface LambdaJobResource {\n id: string;\n executor: 'lambda';\n queue: sst.aws.Queue;\n fns: FunctionWithName[];\n}\n\nexport interface InProcessJobResource {\n id: string;\n executor: 'in-process';\n queue: sst.aws.Queue;\n /** Path to a handler that wraps createRegistryExecutorHandler(...) from @auriclabs/jobs. */\n handler: string;\n /** Extra linkables the in-process handlers depend on. */\n link?: unknown[];\n environment?: Record<string, string>;\n}\n\nexport interface WorkerJobResource {\n id: string;\n executor?: never;\n queue: sst.aws.Queue;\n}\n\nexport type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;\n\nexport interface RegisterJobResourcesConfig {\n table: sst.aws.Dynamo;\n resources: JobResource[];\n handlerPaths: {\n stream: string;\n executor: string;\n };\n}\n\nexport function registerJobResources(config: RegisterJobResourcesConfig) {\n const { table, resources, handlerPaths } = config;\n const QUEUE_URL_LIST = $jsonStringify(resources.map(({ id, queue }) => [id, queue.url]));\n\n table.subscribe(\n 'JobTableStream',\n {\n handler: handlerPaths.stream,\n link: [table, ...resources.map(({ queue }) => queue)],\n environment: {\n QUEUE_URL_LIST,\n },\n },\n {\n filters: [\n {\n dynamodb: {\n NewImage: {\n __edb_e__: {\n S: ['job-attempt'],\n },\n },\n },\n },\n {\n dynamodb: {\n OldImage: {\n __edb_e__: {\n S: ['job-attempt'],\n },\n },\n },\n },\n ],\n },\n );\n\n resources.forEach((resource) => {\n if (!('executor' in resource)) {\n return;\n }\n\n if (resource.executor === 'lambda') {\n resource.queue.subscribe(\n {\n handler: handlerPaths.executor,\n link: [table, resource.queue, ...resource.fns],\n environment: {\n LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),\n // needed for startJob's scheduledAt re-enqueue\n QUEUE_URL_LIST,\n },\n },\n {\n batch: {\n size: 10,\n window: '3 seconds',\n partialResponses: true,\n },\n },\n );\n }\n\n if (resource.executor === 'in-process') {\n resource.queue.subscribe(\n {\n handler: resource.handler,\n link: [table, resource.queue, ...(resource.link ?? [])],\n environment: {\n ...resource.environment,\n // in-process executors re-enqueue for scheduledAt deferrals and\n // continuations — must win over consumer-provided environment\n QUEUE_URL_LIST,\n },\n },\n {\n batch: {\n size: 10,\n window: '3 seconds',\n partialResponses: true,\n },\n },\n );\n }\n });\n}\n","import { createRequire } from 'node:module';\nimport path from 'node:path';\n\nexport interface JobsDashboardBasicAuthConfig {\n /** Plaintext username — typically wired from an SST secret. */\n username: $util.Input<string>;\n /** Plaintext password — typically wired from an SST secret. */\n password: $util.Input<string>;\n /** Realm shown in browser auth dialog. Defaults to \"Jobs Dashboard\". */\n realm?: string;\n}\n\n/**\n * The subset of the SST components namespace that {@link createJobsDashboard}\n * needs at runtime.\n *\n * Passed in rather than referenced as the `sst` global for the same reason as\n * {@link JobTableSstProvider}: this package ships as `.mjs`, and SST's config\n * evaluator skips `.mjs`/`.cjs` when injecting the `sst` global (its esbuild\n * `onLoad` plugin filters on `\\.(js|ts|jsx|tsx)$`). See the note on\n * {@link JobTableSstProvider} for the full mechanism.\n */\nexport interface JobsDashboardSstProvider {\n aws: {\n ApiGatewayV2: typeof sst.aws.ApiGatewayV2;\n StaticSite: typeof sst.aws.StaticSite;\n };\n}\n\nexport interface JobsDashboardOptions {\n /**\n * SST components namespace — pass the injected `sst` global from your\n * `sst.config.ts` (a `.ts` file, which SST *does* inject `sst` into).\n * See {@link JobsDashboardSstProvider}.\n */\n sst: JobsDashboardSstProvider;\n /**\n * Handler path for the dashboard API Lambda. The handler should call\n * `initJobs({ tableName: Resource.<JobTable>.name })` and export\n * `createJobsDashboardApiHandler()` from `@auriclabs/jobs`.\n */\n apiHandler: string;\n /** Job table — linked into the API function. */\n table: sst.aws.Dynamo;\n /** Extra linkables for the API function (beyond the table). */\n link?: unknown[];\n domain?: sst.aws.StaticSiteArgs['domain'];\n /**\n * Override the path to the `@auriclabs/jobs` package's `ui/` directory.\n * Defaults to resolving the installed package via `require.resolve`.\n */\n uiPath?: string;\n /**\n * Optional HTTP basic auth gate on the static site. When set, injects a\n * basic-auth check into the StaticSite's CloudFront viewer-request\n * function — requests without the matching `Authorization: Basic <base64>`\n * header get a 401 before any routing logic runs. Credentials are inlined\n * into the function source at deploy time via Pulumi apply.\n *\n * Note: this only gates the static UI. The API gateway URL remains\n * directly callable — browsers don't auto-send basic auth credentials\n * cross-origin, so the dashboard's fetch() can't carry them. Treat this\n * as discovery-prevention for the dashboard, not API protection.\n */\n basicAuth?: JobsDashboardBasicAuthConfig;\n}\n\nexport function createJobsDashboard(options: JobsDashboardOptions) {\n const { sst } = options;\n const api = new sst.aws.ApiGatewayV2('JobsDashboardApi', { cors: true });\n\n const link: unknown[] = [options.table, ...(options.link ?? [])];\n\n api.route('$default', {\n handler: options.apiHandler,\n link,\n });\n\n const uiPath = options.uiPath ?? resolveJobsUiPath();\n const uiRelative = path.relative(process.cwd(), uiPath);\n\n const basicAuthInjection = options.basicAuth\n ? buildBasicAuthInjection(options.basicAuth)\n : undefined;\n\n // Copy pre-built dist and inject the API URL at deploy time.\n // The build command copies ui/dist to a temp output dir and injects\n // a script tag that sets globalThis.__JOBS_API_URL__ before the app loads.\n const site = new sst.aws.StaticSite('JobsDashboard', {\n path: uiRelative,\n\n build: {\n command: [\n // _deploy persists in node_modules across deploys — a stale copy would\n // nest the new dist and keep serving the old bundle\n 'rm -rf _deploy',\n 'cp -r dist _deploy',\n `sed -i.bak 's|<head>|<head><script>globalThis.__JOBS_API_URL__=\"'$VITE_API_URL'\"</script>|' _deploy/index.html`,\n 'rm -f _deploy/index.html.bak',\n ].join(' && '),\n output: '_deploy',\n },\n dev: {\n command: 'npx vite dev',\n url: 'http://localhost:3101',\n },\n environment: {\n VITE_API_URL: api.url,\n },\n domain: options.domain,\n ...(basicAuthInjection && {\n edge: {\n viewerRequest: { injection: basicAuthInjection },\n },\n }),\n });\n\n return { api, site };\n}\n\n/**\n * Resolve the `@auriclabs/jobs` package's `ui/` directory. jobs-infra is a\n * separate package, so the UI ships with `@auriclabs/jobs` — resolve it\n * through the consumer's node_modules (the workspace link covers local dev).\n */\nfunction resolveJobsUiPath(): string {\n const require = createRequire(import.meta.url);\n const pkgJsonPath = require.resolve('@auriclabs/jobs/package.json');\n const pkgRoot = path.dirname(pkgJsonPath);\n return path.join(pkgRoot, 'ui');\n}\n\nfunction buildBasicAuthInjection(auth: JobsDashboardBasicAuthConfig) {\n const realm = (auth.realm ?? 'Jobs Dashboard').replace(/\"/g, '\\\\\"');\n\n // Inline the encoded credential into the CloudFront Function source at\n // deploy time. CFFs can't read SSM at runtime, so the credential lives\n // in the deployed function's code (same trust boundary as SST secret\n // state). Rotate by updating the upstream secret and redeploying.\n //\n // Returned as an injection string that SST splices into the start of\n // its existing `cloudfront-js-2.0` viewer-request handler — a 401\n // return short-circuits the rest of the routing logic.\n return $output([auth.username, auth.password]).apply(([username, password]) => {\n const encoded = Buffer.from(`${username}:${password}`).toString('base64');\n return [\n 'var __auth = event.request.headers.authorization && event.request.headers.authorization.value;',\n `if (__auth !== \"Basic ${encoded}\") {`,\n ' return {',\n ' statusCode: 401,',\n ' statusDescription: \"Unauthorized\",',\n ' headers: {',\n ` \"www-authenticate\": { value: 'Basic realm=\"${realm}\"' }`,\n ' }',\n ' };',\n '}',\n ].join('\\n');\n });\n}\n"],"mappings":";;;AAqBA,SAAgB,eAAe,KAA0B,MAAc;AACrE,QAAO,IAAI,IAAI,IAAI,OAAO,MAAM;EAC9B,QAAQ;GACN,IAAI;GACJ,IAAI;GACJ,eAAe;GACf,eAAe;GACf,QAAQ;GACR,QAAQ;GACT;EACD,cAAc;GACZ,SAAS;GACT,UAAU;GACX;EACD,eAAe;GACb,aAAa;IAAE,SAAS;IAAiB,UAAU;IAAiB;GACpE,MAAM;IAAE,SAAS;IAAU,UAAU;IAAU;GAChD;EACD,QAAQ;EACT,CAAC;;;;ACHJ,SAAgB,qBAAqB,QAAoC;CACvE,MAAM,EAAE,OAAO,WAAW,iBAAiB;CAC3C,MAAM,iBAAiB,eAAe,UAAU,KAAK,EAAE,IAAI,YAAY,CAAC,IAAI,MAAM,IAAI,CAAC,CAAC;AAExF,OAAM,UACJ,kBACA;EACE,SAAS,aAAa;EACtB,MAAM,CAAC,OAAO,GAAG,UAAU,KAAK,EAAE,YAAY,MAAM,CAAC;EACrD,aAAa,EACX,gBACD;EACF,EACD,EACE,SAAS,CACP,EACE,UAAU,EACR,UAAU,EACR,WAAW,EACT,GAAG,CAAC,cAAc,EACnB,EACF,EACF,EACF,EACD,EACE,UAAU,EACR,UAAU,EACR,WAAW,EACT,GAAG,CAAC,cAAc,EACnB,EACF,EACF,EACF,CACF,EACF,CACF;AAED,WAAU,SAAS,aAAa;AAC9B,MAAI,EAAE,cAAc,UAClB;AAGF,MAAI,SAAS,aAAa,SACxB,UAAS,MAAM,UACb;GACE,SAAS,aAAa;GACtB,MAAM;IAAC;IAAO,SAAS;IAAO,GAAG,SAAS;IAAI;GAC9C,aAAa;IACX,sBAAsB,eAAe,SAAS,IAAI,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAE9E;IACD;GACF,EACD,EACE,OAAO;GACL,MAAM;GACN,QAAQ;GACR,kBAAkB;GACnB,EACF,CACF;AAGH,MAAI,SAAS,aAAa,aACxB,UAAS,MAAM,UACb;GACE,SAAS,SAAS;GAClB,MAAM;IAAC;IAAO,SAAS;IAAO,GAAI,SAAS,QAAQ,EAAE;IAAE;GACvD,aAAa;IACX,GAAG,SAAS;IAGZ;IACD;GACF,EACD,EACE,OAAO;GACL,MAAM;GACN,QAAQ;GACR,kBAAkB;GACnB,EACF,CACF;GAEH;;;;ACtDJ,SAAgB,oBAAoB,SAA+B;CACjE,MAAM,EAAE,QAAQ;CAChB,MAAM,MAAM,IAAI,IAAI,IAAI,aAAa,oBAAoB,EAAE,MAAM,MAAM,CAAC;CAExE,MAAM,OAAkB,CAAC,QAAQ,OAAO,GAAI,QAAQ,QAAQ,EAAE,CAAE;AAEhE,KAAI,MAAM,YAAY;EACpB,SAAS,QAAQ;EACjB;EACD,CAAC;CAEF,MAAM,SAAS,QAAQ,UAAU,mBAAmB;CACpD,MAAM,aAAa,KAAK,SAAS,QAAQ,KAAK,EAAE,OAAO;CAEvD,MAAM,qBAAqB,QAAQ,YAC/B,wBAAwB,QAAQ,UAAU,GAC1C,KAAA;AAkCJ,QAAO;EAAE;EAAK,MA7BD,IAAI,IAAI,IAAI,WAAW,iBAAiB;GACnD,MAAM;GAEN,OAAO;IACL,SAAS;KAGP;KACA;KACA;KACA;KACD,CAAC,KAAK,OAAO;IACd,QAAQ;IACT;GACD,KAAK;IACH,SAAS;IACT,KAAK;IACN;GACD,aAAa,EACX,cAAc,IAAI,KACnB;GACD,QAAQ,QAAQ;GAChB,GAAI,sBAAsB,EACxB,MAAM,EACJ,eAAe,EAAE,WAAW,oBAAoB,EACjD,EACF;GACF,CAAC;EAEkB;;;;;;;AAQtB,SAAS,oBAA4B;CAEnC,MAAM,cADU,cAAc,OAAO,KAAK,IAAI,CAClB,QAAQ,+BAA+B;CACnE,MAAM,UAAU,KAAK,QAAQ,YAAY;AACzC,QAAO,KAAK,KAAK,SAAS,KAAK;;AAGjC,SAAS,wBAAwB,MAAoC;CACnE,MAAM,SAAS,KAAK,SAAS,kBAAkB,QAAQ,MAAM,OAAM;AAUnE,QAAO,QAAQ,CAAC,KAAK,UAAU,KAAK,SAAS,CAAC,CAAC,OAAO,CAAC,UAAU,cAAc;AAE7E,SAAO;GACL;GACA,yBAHc,OAAO,KAAK,GAAG,SAAS,GAAG,WAAW,CAAC,SAAS,SAAS,CAGtC;GACjC;GACA;GACA;GACA;GACA,oDAAoD,MAAM;GAC1D;GACA;GACA;GACD,CAAC,KAAK,KAAK;GACZ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auriclabs/jobs-infra",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "SST infrastructure helpers for job queue tables and resources",
5
5
  "prettier": "@auriclabs/prettier-config",
6
6
  "main": "dist/index.cjs",
@@ -20,14 +20,21 @@
20
20
  "dependencies": {},
21
21
  "devDependencies": {
22
22
  "sst": "^4.3.7",
23
- "@auriclabs/sst-utils": "1.1.7",
24
- "@auriclabs/sst-types": "0.1.0"
23
+ "@auriclabs/jobs": "0.3.0",
24
+ "@auriclabs/sst-types": "0.1.0",
25
+ "@auriclabs/sst-utils": "1.1.14"
25
26
  },
26
27
  "peerDependencies": {
28
+ "@auriclabs/jobs": ">=0.3.0",
27
29
  "@auriclabs/sst-types": "^0.1.0",
28
30
  "@auriclabs/sst-utils": "^1.2.0",
29
31
  "sst": "^4.3.7"
30
32
  },
33
+ "peerDependenciesMeta": {
34
+ "@auriclabs/jobs": {
35
+ "optional": true
36
+ }
37
+ },
31
38
  "publishConfig": {
32
39
  "registry": "https://registry.npmjs.org/"
33
40
  },
@@ -0,0 +1,159 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+
4
+ export interface JobsDashboardBasicAuthConfig {
5
+ /** Plaintext username — typically wired from an SST secret. */
6
+ username: $util.Input<string>;
7
+ /** Plaintext password — typically wired from an SST secret. */
8
+ password: $util.Input<string>;
9
+ /** Realm shown in browser auth dialog. Defaults to "Jobs Dashboard". */
10
+ realm?: string;
11
+ }
12
+
13
+ /**
14
+ * The subset of the SST components namespace that {@link createJobsDashboard}
15
+ * needs at runtime.
16
+ *
17
+ * Passed in rather than referenced as the `sst` global for the same reason as
18
+ * {@link JobTableSstProvider}: this package ships as `.mjs`, and SST's config
19
+ * evaluator skips `.mjs`/`.cjs` when injecting the `sst` global (its esbuild
20
+ * `onLoad` plugin filters on `\.(js|ts|jsx|tsx)$`). See the note on
21
+ * {@link JobTableSstProvider} for the full mechanism.
22
+ */
23
+ export interface JobsDashboardSstProvider {
24
+ aws: {
25
+ ApiGatewayV2: typeof sst.aws.ApiGatewayV2;
26
+ StaticSite: typeof sst.aws.StaticSite;
27
+ };
28
+ }
29
+
30
+ export interface JobsDashboardOptions {
31
+ /**
32
+ * SST components namespace — pass the injected `sst` global from your
33
+ * `sst.config.ts` (a `.ts` file, which SST *does* inject `sst` into).
34
+ * See {@link JobsDashboardSstProvider}.
35
+ */
36
+ sst: JobsDashboardSstProvider;
37
+ /**
38
+ * Handler path for the dashboard API Lambda. The handler should call
39
+ * `initJobs({ tableName: Resource.<JobTable>.name })` and export
40
+ * `createJobsDashboardApiHandler()` from `@auriclabs/jobs`.
41
+ */
42
+ apiHandler: string;
43
+ /** Job table — linked into the API function. */
44
+ table: sst.aws.Dynamo;
45
+ /** Extra linkables for the API function (beyond the table). */
46
+ link?: unknown[];
47
+ domain?: sst.aws.StaticSiteArgs['domain'];
48
+ /**
49
+ * Override the path to the `@auriclabs/jobs` package's `ui/` directory.
50
+ * Defaults to resolving the installed package via `require.resolve`.
51
+ */
52
+ uiPath?: string;
53
+ /**
54
+ * Optional HTTP basic auth gate on the static site. When set, injects a
55
+ * basic-auth check into the StaticSite's CloudFront viewer-request
56
+ * function — requests without the matching `Authorization: Basic <base64>`
57
+ * header get a 401 before any routing logic runs. Credentials are inlined
58
+ * into the function source at deploy time via Pulumi apply.
59
+ *
60
+ * Note: this only gates the static UI. The API gateway URL remains
61
+ * directly callable — browsers don't auto-send basic auth credentials
62
+ * cross-origin, so the dashboard's fetch() can't carry them. Treat this
63
+ * as discovery-prevention for the dashboard, not API protection.
64
+ */
65
+ basicAuth?: JobsDashboardBasicAuthConfig;
66
+ }
67
+
68
+ export function createJobsDashboard(options: JobsDashboardOptions) {
69
+ const { sst } = options;
70
+ const api = new sst.aws.ApiGatewayV2('JobsDashboardApi', { cors: true });
71
+
72
+ const link: unknown[] = [options.table, ...(options.link ?? [])];
73
+
74
+ api.route('$default', {
75
+ handler: options.apiHandler,
76
+ link,
77
+ });
78
+
79
+ const uiPath = options.uiPath ?? resolveJobsUiPath();
80
+ const uiRelative = path.relative(process.cwd(), uiPath);
81
+
82
+ const basicAuthInjection = options.basicAuth
83
+ ? buildBasicAuthInjection(options.basicAuth)
84
+ : undefined;
85
+
86
+ // Copy pre-built dist and inject the API URL at deploy time.
87
+ // The build command copies ui/dist to a temp output dir and injects
88
+ // a script tag that sets globalThis.__JOBS_API_URL__ before the app loads.
89
+ const site = new sst.aws.StaticSite('JobsDashboard', {
90
+ path: uiRelative,
91
+
92
+ build: {
93
+ command: [
94
+ // _deploy persists in node_modules across deploys — a stale copy would
95
+ // nest the new dist and keep serving the old bundle
96
+ 'rm -rf _deploy',
97
+ 'cp -r dist _deploy',
98
+ `sed -i.bak 's|<head>|<head><script>globalThis.__JOBS_API_URL__="'$VITE_API_URL'"</script>|' _deploy/index.html`,
99
+ 'rm -f _deploy/index.html.bak',
100
+ ].join(' && '),
101
+ output: '_deploy',
102
+ },
103
+ dev: {
104
+ command: 'npx vite dev',
105
+ url: 'http://localhost:3101',
106
+ },
107
+ environment: {
108
+ VITE_API_URL: api.url,
109
+ },
110
+ domain: options.domain,
111
+ ...(basicAuthInjection && {
112
+ edge: {
113
+ viewerRequest: { injection: basicAuthInjection },
114
+ },
115
+ }),
116
+ });
117
+
118
+ return { api, site };
119
+ }
120
+
121
+ /**
122
+ * Resolve the `@auriclabs/jobs` package's `ui/` directory. jobs-infra is a
123
+ * separate package, so the UI ships with `@auriclabs/jobs` — resolve it
124
+ * through the consumer's node_modules (the workspace link covers local dev).
125
+ */
126
+ function resolveJobsUiPath(): string {
127
+ const require = createRequire(import.meta.url);
128
+ const pkgJsonPath = require.resolve('@auriclabs/jobs/package.json');
129
+ const pkgRoot = path.dirname(pkgJsonPath);
130
+ return path.join(pkgRoot, 'ui');
131
+ }
132
+
133
+ function buildBasicAuthInjection(auth: JobsDashboardBasicAuthConfig) {
134
+ const realm = (auth.realm ?? 'Jobs Dashboard').replace(/"/g, '\\"');
135
+
136
+ // Inline the encoded credential into the CloudFront Function source at
137
+ // deploy time. CFFs can't read SSM at runtime, so the credential lives
138
+ // in the deployed function's code (same trust boundary as SST secret
139
+ // state). Rotate by updating the upstream secret and redeploying.
140
+ //
141
+ // Returned as an injection string that SST splices into the start of
142
+ // its existing `cloudfront-js-2.0` viewer-request handler — a 401
143
+ // return short-circuits the rest of the routing logic.
144
+ return $output([auth.username, auth.password]).apply(([username, password]) => {
145
+ const encoded = Buffer.from(`${username}:${password}`).toString('base64');
146
+ return [
147
+ 'var __auth = event.request.headers.authorization && event.request.headers.authorization.value;',
148
+ `if (__auth !== "Basic ${encoded}") {`,
149
+ ' return {',
150
+ ' statusCode: 401,',
151
+ ' statusDescription: "Unauthorized",',
152
+ ' headers: {',
153
+ ` "www-authenticate": { value: 'Basic realm="${realm}"' }`,
154
+ ' }',
155
+ ' };',
156
+ '}',
157
+ ].join('\n');
158
+ });
159
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './job-table';
2
2
  export * from './job-resources';
3
+ export * from './dashboard';
@@ -7,13 +7,24 @@ export interface LambdaJobResource {
7
7
  fns: FunctionWithName[];
8
8
  }
9
9
 
10
+ export interface InProcessJobResource {
11
+ id: string;
12
+ executor: 'in-process';
13
+ queue: sst.aws.Queue;
14
+ /** Path to a handler that wraps createRegistryExecutorHandler(...) from @auriclabs/jobs. */
15
+ handler: string;
16
+ /** Extra linkables the in-process handlers depend on. */
17
+ link?: unknown[];
18
+ environment?: Record<string, string>;
19
+ }
20
+
10
21
  export interface WorkerJobResource {
11
22
  id: string;
12
23
  executor?: never;
13
24
  queue: sst.aws.Queue;
14
25
  }
15
26
 
16
- export type JobResource = LambdaJobResource | WorkerJobResource;
27
+ export type JobResource = LambdaJobResource | InProcessJobResource | WorkerJobResource;
17
28
 
18
29
  export interface RegisterJobResourcesConfig {
19
30
  table: sst.aws.Dynamo;
@@ -73,6 +84,30 @@ export function registerJobResources(config: RegisterJobResourcesConfig) {
73
84
  link: [table, resource.queue, ...resource.fns],
74
85
  environment: {
75
86
  LAMBDA_FUNCTION_LIST: $jsonStringify(resource.fns.map((f) => [f.name, f.arn])),
87
+ // needed for startJob's scheduledAt re-enqueue
88
+ QUEUE_URL_LIST,
89
+ },
90
+ },
91
+ {
92
+ batch: {
93
+ size: 10,
94
+ window: '3 seconds',
95
+ partialResponses: true,
96
+ },
97
+ },
98
+ );
99
+ }
100
+
101
+ if (resource.executor === 'in-process') {
102
+ resource.queue.subscribe(
103
+ {
104
+ handler: resource.handler,
105
+ link: [table, resource.queue, ...(resource.link ?? [])],
106
+ environment: {
107
+ ...resource.environment,
108
+ // in-process executors re-enqueue for scheduledAt deferrals and
109
+ // continuations — must win over consumer-provided environment
110
+ QUEUE_URL_LIST,
76
111
  },
77
112
  },
78
113
  {
package/src/job-table.ts CHANGED
@@ -1,4 +1,25 @@
1
- export function createJobTable(name: string) {
1
+ /**
2
+ * The subset of the SST components namespace that {@link createJobTable} needs
3
+ * at runtime.
4
+ *
5
+ * `@auriclabs/jobs-infra` ships as ESM (`dist/index.mjs`). SST's config
6
+ * evaluator injects the `sst` global into config source via an esbuild
7
+ * `onLoad` plugin whose filter is `\.(js|ts|jsx|tsx)$` — it deliberately skips
8
+ * `.mjs`/`.cjs`. (The `$app` / `$jsonStringify` / `$output` globals arrive via
9
+ * esbuild `Define`/`Inject`, which are extension-agnostic, so those keep
10
+ * working; only the `sst` namespace and the `@pulumi/*` provider aliases come
11
+ * from the skipped `onLoad` channel.) A bare `sst.aws.Dynamo` reference in this
12
+ * `.mjs` file therefore throws `ReferenceError: sst is not defined` at deploy
13
+ * time. Taking `sst` as a parameter — exactly as `@auriclabs/migrations`'
14
+ * `createTable(sst)` does — sidesteps the injection entirely.
15
+ */
16
+ export interface JobTableSstProvider {
17
+ aws: {
18
+ Dynamo: typeof sst.aws.Dynamo;
19
+ };
20
+ }
21
+
22
+ export function createJobTable(sst: JobTableSstProvider, name: string) {
2
23
  return new sst.aws.Dynamo(name, {
3
24
  fields: {
4
25
  pk: 'string',