@owlmeans/server-job 0.1.18-rc.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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/agent-meta/manifest.json +16 -0
  4. package/agent-meta/skills/server-job/SKILL.md +140 -0
  5. package/build/actions/cancel.d.ts +15 -0
  6. package/build/actions/cancel.d.ts.map +1 -0
  7. package/build/actions/cancel.js +19 -0
  8. package/build/actions/cancel.js.map +1 -0
  9. package/build/actions/get.d.ts +10 -0
  10. package/build/actions/get.d.ts.map +1 -0
  11. package/build/actions/get.js +12 -0
  12. package/build/actions/get.js.map +1 -0
  13. package/build/actions/index.d.ts +5 -0
  14. package/build/actions/index.d.ts.map +1 -0
  15. package/build/actions/index.js +5 -0
  16. package/build/actions/index.js.map +1 -0
  17. package/build/actions/list.d.ts +13 -0
  18. package/build/actions/list.d.ts.map +1 -0
  19. package/build/actions/list.js +26 -0
  20. package/build/actions/list.js.map +1 -0
  21. package/build/actions/watch.d.ts +17 -0
  22. package/build/actions/watch.d.ts.map +1 -0
  23. package/build/actions/watch.js +57 -0
  24. package/build/actions/watch.js.map +1 -0
  25. package/build/consts.d.ts +26 -0
  26. package/build/consts.d.ts.map +1 -0
  27. package/build/consts.js +26 -0
  28. package/build/consts.js.map +1 -0
  29. package/build/entrypoints.d.ts +21 -0
  30. package/build/entrypoints.d.ts.map +1 -0
  31. package/build/entrypoints.js +46 -0
  32. package/build/entrypoints.js.map +1 -0
  33. package/build/helper.d.ts +14 -0
  34. package/build/helper.d.ts.map +1 -0
  35. package/build/helper.js +21 -0
  36. package/build/helper.js.map +1 -0
  37. package/build/index.d.ts +8 -0
  38. package/build/index.d.ts.map +1 -0
  39. package/build/index.js +7 -0
  40. package/build/index.js.map +1 -0
  41. package/build/schemas.d.ts +10 -0
  42. package/build/schemas.d.ts.map +1 -0
  43. package/build/schemas.js +17 -0
  44. package/build/schemas.js.map +1 -0
  45. package/build/types.d.ts +60 -0
  46. package/build/types.d.ts.map +1 -0
  47. package/build/types.js +2 -0
  48. package/build/types.js.map +1 -0
  49. package/build/utils/index.d.ts +3 -0
  50. package/build/utils/index.d.ts.map +1 -0
  51. package/build/utils/index.js +3 -0
  52. package/build/utils/index.js.map +1 -0
  53. package/build/utils/owner.d.ts +33 -0
  54. package/build/utils/owner.d.ts.map +1 -0
  55. package/build/utils/owner.js +41 -0
  56. package/build/utils/owner.js.map +1 -0
  57. package/build/utils/resource.d.ts +23 -0
  58. package/build/utils/resource.d.ts.map +1 -0
  59. package/build/utils/resource.js +28 -0
  60. package/build/utils/resource.js.map +1 -0
  61. package/package.json +46 -0
  62. package/src/actions/cancel.ts +26 -0
  63. package/src/actions/get.ts +19 -0
  64. package/src/actions/index.ts +4 -0
  65. package/src/actions/list.ts +36 -0
  66. package/src/actions/watch.ts +66 -0
  67. package/src/consts.ts +28 -0
  68. package/src/entrypoints.ts +56 -0
  69. package/src/helper.ts +28 -0
  70. package/src/index.ts +8 -0
  71. package/src/schemas.ts +19 -0
  72. package/src/types.ts +66 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/owner.ts +60 -0
  75. package/src/utils/resource.ts +40 -0
  76. package/tsconfig.json +16 -0
@@ -0,0 +1,46 @@
1
+ import { entrypoint, filter, guard, query } from '@owlmeans/entrypoint';
2
+ import { backend, route, RouteMethod, socket } from '@owlmeans/route';
3
+ import { DEFAULT_GUARD } from '@owlmeans/auth-common';
4
+ import { DEFAULT_JOB_PATH } from './consts.js';
5
+ import { JobListQuerySchema } from './schemas.js';
6
+ /**
7
+ * The aliases one job group answers under.
8
+ *
9
+ * The shape — `<root>` and `<root>:<verb>` — is the contract `@owlmeans/client-job` addresses the
10
+ * same group by, so a group renamed here is renamed there by passing the same root.
11
+ */
12
+ export const jobEntrypointAliases = (root) => ({
13
+ base: root,
14
+ list: `${root}:list`,
15
+ get: `${root}:get`,
16
+ cancel: `${root}:cancel`,
17
+ watch: `${root}:watch`,
18
+ });
19
+ /**
20
+ * Declare the list/get/cancel/watch entrypoints of one job group.
21
+ *
22
+ * It belongs in the SHARED package of a target app — the one both the API and the browser import —
23
+ * so that the server elevates and the client calls the very declarations, and neither side ever
24
+ * writes a path. Declaring a second group is the same call with another root.
25
+ *
26
+ * The guard rides on the base alone: guards are inherited, so stating it once is what keeps the
27
+ * four from drifting apart.
28
+ */
29
+ export const declareJobEntrypoints = (root, opts) => {
30
+ const aliases = jobEntrypointAliases(root);
31
+ const base = {
32
+ ...(opts?.parent != null ? { parent: opts.parent } : {}),
33
+ ...(opts?.service != null ? { service: opts.service } : {}),
34
+ };
35
+ const guarded = opts?.guard === null ? undefined : guard(opts?.guard ?? DEFAULT_GUARD);
36
+ return [
37
+ entrypoint(route(aliases.base, opts?.path ?? DEFAULT_JOB_PATH, backend(base)), guarded),
38
+ entrypoint(route(aliases.list, '/', backend(aliases.base)), filter(query(JobListQuerySchema))),
39
+ // Static before parametric, so `/watch` is not swallowed by `/:id`. The router picks the
40
+ // static branch on its own; the order here is for whoever reads the declaration.
41
+ entrypoint(route(aliases.watch, '/watch', socket(aliases.base))),
42
+ entrypoint(route(aliases.get, '/:id', backend(aliases.base))),
43
+ entrypoint(route(aliases.cancel, '/:id', backend(aliases.base, RouteMethod.DELETE))),
44
+ ];
45
+ };
46
+ //# sourceMappingURL=entrypoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entrypoints.js","sourceRoot":"","sources":["../src/entrypoints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAEvE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAErE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAGjD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,IAAY,EAAwB,EAAE,CAAC,CAAC;IAC3E,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,GAAG,IAAI,OAAO;IACpB,GAAG,EAAE,GAAG,IAAI,MAAM;IAClB,MAAM,EAAE,GAAG,IAAI,SAAS;IACxB,KAAK,EAAE,GAAG,IAAI,QAAQ;CACvB,CAAC,CAAA;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,IAAY,EAAE,IAA2B,EACrB,EAAE;IACtB,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC1C,MAAM,IAAI,GAA0B;QAClC,GAAG,CAAC,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5D,CAAA;IACD,MAAM,OAAO,GAAG,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,aAAa,CAAC,CAAA;IAEtF,OAAO;QACL,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,gBAAgB,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC;QACvF,UAAU,CACR,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAC/C,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAClC;QACD,yFAAyF;QACzF,iFAAiF;QACjF,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7D,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;KACrF,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,14 @@
1
+ import type { ServerEntrypoint } from '@owlmeans/server-entrypoint';
2
+ import type { CommonEntrypoint } from '@owlmeans/entrypoint';
3
+ import type { JobHandlerOptions } from './types.js';
4
+ /**
5
+ * Attach this package's handlers to a group declared by {@link declareJobEntrypoints}.
6
+ *
7
+ * The whole server half of "a long job reports progress to the user's screen" is this call plus
8
+ * the queue driver an app already wires — nothing is subclassed, and an app that wants one
9
+ * handler of its own elevates that alias itself afterwards, since `elevate` replaces in place.
10
+ *
11
+ * @throws {SyntaxError} when the array carries no group under that root.
12
+ */
13
+ export declare const serveJobEntrypoints: <R>(entrypoints: (CommonEntrypoint | ServerEntrypoint<R>)[], root: string, opts?: JobHandlerOptions) => ServerEntrypoint<R>[];
14
+ //# sourceMappingURL=helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAG5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,eACtB,CAAC,gBAAgB,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,MAAM,SAAS,iBAAiB,KAC9F,gBAAgB,CAAC,CAAC,CAAC,EASrB,CAAA"}
@@ -0,0 +1,21 @@
1
+ import { elevate } from '@owlmeans/server-entrypoint';
2
+ import { jobEntrypointAliases } from './entrypoints.js';
3
+ import { cancelJob, getJob, listJobs, watchJobs } from './actions/index.js';
4
+ /**
5
+ * Attach this package's handlers to a group declared by {@link declareJobEntrypoints}.
6
+ *
7
+ * The whole server half of "a long job reports progress to the user's screen" is this call plus
8
+ * the queue driver an app already wires — nothing is subclassed, and an app that wants one
9
+ * handler of its own elevates that alias itself afterwards, since `elevate` replaces in place.
10
+ *
11
+ * @throws {SyntaxError} when the array carries no group under that root.
12
+ */
13
+ export const serveJobEntrypoints = (entrypoints, root, opts) => {
14
+ const aliases = jobEntrypointAliases(root);
15
+ elevate(entrypoints, aliases.base);
16
+ elevate(entrypoints, aliases.list, listJobs(opts));
17
+ elevate(entrypoints, aliases.watch, watchJobs(opts));
18
+ elevate(entrypoints, aliases.get, getJob(opts));
19
+ return elevate(entrypoints, aliases.cancel, cancelJob(opts));
20
+ };
21
+ //# sourceMappingURL=helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAA;AAGrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAG3E;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,WAAuD,EAAE,IAAY,EAAE,IAAwB,EACxE,EAAE;IACzB,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAE1C,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;IAClD,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;IACpD,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAE/C,OAAO,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;AAC9D,CAAC,CAAA"}
@@ -0,0 +1,8 @@
1
+ export type * from './types.js';
2
+ export * from './consts.js';
3
+ export * from './schemas.js';
4
+ export * from './entrypoints.js';
5
+ export * from './helper.js';
6
+ export * from './actions/index.js';
7
+ export * from './utils/index.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from './consts.js';
2
+ export * from './schemas.js';
3
+ export * from './entrypoints.js';
4
+ export * from './helper.js';
5
+ export * from './actions/index.js';
6
+ export * from './utils/index.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA"}
@@ -0,0 +1,10 @@
1
+ import type { JSONSchemaType } from 'ajv';
2
+ import type { JobListQuery } from './types.js';
3
+ /**
4
+ * Every field is optional: a list screen opens before it has a filter, and a caller that knows
5
+ * nothing about jobs must still get the page it is allowed to see. `state` and `name` are left as
6
+ * plain strings rather than enums so that a driver reporting a state this line does not know
7
+ * narrows the list instead of failing the request.
8
+ */
9
+ export declare const JobListQuerySchema: JSONSchemaType<JobListQuery>;
10
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,KAAK,CAAA;AACzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,YAAY,CAS3D,CAAA"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Every field is optional: a list screen opens before it has a filter, and a caller that knows
3
+ * nothing about jobs must still get the page it is allowed to see. `state` and `name` are left as
4
+ * plain strings rather than enums so that a driver reporting a state this line does not know
5
+ * narrows the list instead of failing the request.
6
+ */
7
+ export const JobListQuerySchema = {
8
+ type: 'object',
9
+ properties: {
10
+ state: { type: 'string', maxLength: 32, nullable: true },
11
+ name: { type: 'string', maxLength: 256, nullable: true },
12
+ page: { type: 'integer', minimum: 0, nullable: true },
13
+ size: { type: 'integer', minimum: 1, maximum: 200, nullable: true },
14
+ },
15
+ additionalProperties: true,
16
+ };
17
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAiC;IAC9D,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;QACxD,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE;QACxD,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;QACrD,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE;KACpE;IACD,oBAAoB,EAAE,IAAI;CAC3B,CAAA"}
@@ -0,0 +1,60 @@
1
+ import type { BasicConfig, BasicContext } from '@owlmeans/context';
2
+ import type { AbstractRequest } from '@owlmeans/entrypoint';
3
+ import type { QueueAppend, QueueConfig } from '@owlmeans/queue';
4
+ import type { ApiServerAppend } from '@owlmeans/server-api';
5
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context';
6
+ export interface Config extends ServerConfig {
7
+ queue?: QueueConfig;
8
+ }
9
+ export interface Context<C extends Config = Config> extends ServerContext<C>, ApiServerAppend, QueueAppend {
10
+ }
11
+ /** The alias every entrypoint of one job group answers under, derived from the group's root. */
12
+ export interface JobEntrypointAliases {
13
+ base: string;
14
+ list: string;
15
+ get: string;
16
+ cancel: string;
17
+ watch: string;
18
+ }
19
+ export interface JobEntrypointOptions {
20
+ /** The path segment the group answers under. Defaults to `/jobs`. */
21
+ path?: string;
22
+ /** The entrypoint the group hangs under — an app's API base. Top level when omitted. */
23
+ parent?: string;
24
+ /** The service route the group answers on, when it is not this app's own. */
25
+ service?: string;
26
+ /**
27
+ * The guard the group's base carries, and every entrypoint under it inherits.
28
+ *
29
+ * `DEFAULT_GUARD` unless told otherwise, because ownership is derived from the authenticated
30
+ * subject and an unguarded declaration has no subject to derive it from. Pass `null` only for a
31
+ * group that is scoped some other way — its handlers then answer `AuthorizationError`.
32
+ */
33
+ guard?: string | null;
34
+ }
35
+ /** The list entrypoint's query, as it travels on the wire. */
36
+ export interface JobListQuery {
37
+ state?: string;
38
+ name?: string;
39
+ page?: number;
40
+ size?: number;
41
+ }
42
+ /**
43
+ * A request that may read the queue unscoped.
44
+ *
45
+ * A predicate rather than a permission name: which permission, gate or role means "operator" is
46
+ * the application's decision, and hardcoding one here would make every deployment that names it
47
+ * differently patch this package.
48
+ */
49
+ export interface JobAdminCheck {
50
+ (req: AbstractRequest, ctx: BasicContext<BasicConfig>): boolean | Promise<boolean>;
51
+ }
52
+ export interface JobHandlerOptions {
53
+ /** Which declared queue these handlers read. The context's sole queue when omitted. */
54
+ queue?: string;
55
+ /** The field inside `JobRecord.data` that names the owner. Defaults to `owner`. */
56
+ ownerField?: string;
57
+ /** The escape hatch — see {@link JobAdminCheck}. Nothing is unscoped without one. */
58
+ admin?: JobAdminCheck;
59
+ }
60
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE3E,MAAM,WAAW,MAAO,SAAQ,YAAY;IAC1C,KAAK,CAAC,EAAE,WAAW,CAAA;CACpB;AAED,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC,EAC1E,eAAe,EAAE,WAAW;CAAI;AAElC,gGAAgG;AAChG,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,WAAW,CAAC,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACnF;AAED,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qFAAqF;IACrF,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ export * from './owner.js';
2
+ export * from './resource.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,3 @@
1
+ export * from './owner.js';
2
+ export * from './resource.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,33 @@
1
+ import type { BasicConfig, BasicContext } from '@owlmeans/context';
2
+ import type { AbstractRequest } from '@owlmeans/entrypoint';
3
+ import type { Criteria } from '@owlmeans/resource';
4
+ import type { JobRecord } from '@owlmeans/queue';
5
+ import type { JobHandlerOptions } from '../types.js';
6
+ /**
7
+ * The subject a job is attributed to.
8
+ *
9
+ * Profile first, user second — the same pair `@owlmeans/server-socket` addresses a connection by,
10
+ * so a job enqueued for one profile of a multi-profile account is not visible to the others.
11
+ */
12
+ export declare const jobOwnerOf: (req: AbstractRequest) => string | undefined;
13
+ /**
14
+ * @throws {AuthorizationError} when the request carries no authenticated subject.
15
+ */
16
+ export declare const requireJobOwner: (req: AbstractRequest) => string;
17
+ export declare const ownerFieldOf: (opts?: JobHandlerOptions) => string;
18
+ /** What one record says its owner is — the payload's own field, never a broker one. */
19
+ export declare const ownerOf: (record: JobRecord, opts?: JobHandlerOptions) => unknown;
20
+ /**
21
+ * Who this request reads the queue as: the owner every record must name, or `undefined` for a
22
+ * request that reads it unscoped.
23
+ *
24
+ * `undefined` is reachable only through {@link JobHandlerOptions.admin}. Absent an admin check
25
+ * every read is narrowed to the caller's own jobs and an anonymous request is refused, so a group
26
+ * wired with no options at all is scoped rather than open.
27
+ *
28
+ * @throws {AuthorizationError}
29
+ */
30
+ export declare const jobViewer: <C extends BasicConfig, T extends BasicContext<C>>(req: AbstractRequest, ctx: T, opts?: JobHandlerOptions) => Promise<string | undefined>;
31
+ /** The criteria one viewer reads the queue through; nothing to add when they read it unscoped. */
32
+ export declare const jobScope: (viewer: string | undefined, opts?: JobHandlerOptions) => Criteria<JobRecord>;
33
+ //# sourceMappingURL=owner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner.d.ts","sourceRoot":"","sources":["../../src/utils/owner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,QAAS,eAAe,KAAG,MAAM,GAAG,SAClB,CAAA;AAEzC;;GAEG;AACH,eAAO,MAAM,eAAe,QAAS,eAAe,KAAG,MAOtD,CAAA;AAED,eAAO,MAAM,YAAY,UAAW,iBAAiB,KAAG,MACf,CAAA;AAEzC,uFAAuF;AACvF,eAAO,MAAM,OAAO,WAAY,SAAS,SAAS,iBAAiB,KAAG,OACM,CAAA;AAE5E;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,GAAU,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,OACzE,eAAe,OAAO,CAAC,SAAS,iBAAiB,KACrD,OAAO,CAAC,MAAM,GAAG,SAAS,CAM5B,CAAA;AAED,kGAAkG;AAClG,eAAO,MAAM,QAAQ,WACX,MAAM,GAAG,SAAS,SAAS,iBAAiB,KACnD,QAAQ,CAAC,SAAS,CAAqE,CAAA"}
@@ -0,0 +1,41 @@
1
+ import { AuthorizationError } from '@owlmeans/auth';
2
+ import { DEFAULT_OWNER_FIELD } from '../consts.js';
3
+ /**
4
+ * The subject a job is attributed to.
5
+ *
6
+ * Profile first, user second — the same pair `@owlmeans/server-socket` addresses a connection by,
7
+ * so a job enqueued for one profile of a multi-profile account is not visible to the others.
8
+ */
9
+ export const jobOwnerOf = (req) => req.auth?.profileId ?? req.auth?.userId;
10
+ /**
11
+ * @throws {AuthorizationError} when the request carries no authenticated subject.
12
+ */
13
+ export const requireJobOwner = (req) => {
14
+ const owner = jobOwnerOf(req);
15
+ if (owner == null || owner === '') {
16
+ throw new AuthorizationError();
17
+ }
18
+ return owner;
19
+ };
20
+ export const ownerFieldOf = (opts) => opts?.ownerField ?? DEFAULT_OWNER_FIELD;
21
+ /** What one record says its owner is — the payload's own field, never a broker one. */
22
+ export const ownerOf = (record, opts) => record.data?.[ownerFieldOf(opts)];
23
+ /**
24
+ * Who this request reads the queue as: the owner every record must name, or `undefined` for a
25
+ * request that reads it unscoped.
26
+ *
27
+ * `undefined` is reachable only through {@link JobHandlerOptions.admin}. Absent an admin check
28
+ * every read is narrowed to the caller's own jobs and an anonymous request is refused, so a group
29
+ * wired with no options at all is scoped rather than open.
30
+ *
31
+ * @throws {AuthorizationError}
32
+ */
33
+ export const jobViewer = async (req, ctx, opts) => {
34
+ if (opts?.admin != null && await opts.admin(req, ctx)) {
35
+ return undefined;
36
+ }
37
+ return requireJobOwner(req);
38
+ };
39
+ /** The criteria one viewer reads the queue through; nothing to add when they read it unscoped. */
40
+ export const jobScope = (viewer, opts) => viewer == null ? {} : { [`data.${ownerFieldOf(opts)}`]: viewer };
41
+ //# sourceMappingURL=owner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner.js","sourceRoot":"","sources":["../../src/utils/owner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAGnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGlD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAoB,EAAsB,EAAE,CACrE,GAAG,CAAC,IAAI,EAAE,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAA;AAEzC;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,GAAoB,EAAU,EAAE;IAC9D,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;IAC7B,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,kBAAkB,EAAE,CAAA;IAChC,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,IAAwB,EAAU,EAAE,CAC/D,IAAI,EAAE,UAAU,IAAI,mBAAmB,CAAA;AAEzC,uFAAuF;AACvF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAiB,EAAE,IAAwB,EAAW,EAAE,CAC7E,MAAM,CAAC,IAA4C,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAA;AAE5E;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC5B,GAAoB,EAAE,GAAM,EAAE,IAAwB,EACzB,EAAE;IAC/B,IAAI,IAAI,EAAE,KAAK,IAAI,IAAI,IAAI,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAgC,CAAC,EAAE,CAAC;QACnF,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,OAAO,eAAe,CAAC,GAAG,CAAC,CAAA;AAC7B,CAAC,CAAA;AAED,kGAAkG;AAClG,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,MAA0B,EAAE,IAAwB,EAC/B,EAAE,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAA"}
@@ -0,0 +1,23 @@
1
+ import type { BasicConfig, BasicContext } from '@owlmeans/context';
2
+ import type { JobRecord, QueueResource } from '@owlmeans/queue';
3
+ import type { JobHandlerOptions } from '../types.js';
4
+ /**
5
+ * The queue these handlers read.
6
+ *
7
+ * Named by the declaration or, with nothing named, the context's sole declared queue — which
8
+ * `ctx.jobs()` refuses to guess once a second queue exists, so an app that grows one is told to
9
+ * name it rather than quietly served the wrong backlog.
10
+ */
11
+ export declare const jobsOf: <D = unknown, R = unknown>(ctx: BasicContext<BasicConfig>, opts?: JobHandlerOptions) => QueueResource<D, R>;
12
+ /** Does this viewer own that record? An unscoped viewer (`undefined`) owns everything. */
13
+ export declare const owns: (record: JobRecord, viewer: string | undefined, opts?: JobHandlerOptions) => boolean;
14
+ /**
15
+ * One job, as this viewer is allowed to see it.
16
+ *
17
+ * A job that exists but belongs to someone else answers exactly as an absent one does: telling an
18
+ * unauthorized caller apart from a wrong id is what turns an id space into an enumeration oracle.
19
+ *
20
+ * @throws {UnknownJob}
21
+ */
22
+ export declare const readOwnedJob: <D = unknown, R = unknown>(resource: QueueResource<D, R>, id: string, viewer: string | undefined, opts?: JobHandlerOptions) => Promise<JobRecord<D, R>>;
23
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/utils/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAE/D,OAAO,KAAK,EAAW,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAG7D;;;;;;GAMG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,OACxC,YAAY,CAAC,WAAW,CAAC,SAAS,iBAAiB,KACvD,aAAa,CAAC,CAAC,EAAE,CAAC,CAAwD,CAAA;AAE7E,0FAA0F;AAC1F,eAAO,MAAM,IAAI,WACP,SAAS,UAAU,MAAM,GAAG,SAAS,SAAS,iBAAiB,KACtE,OAA6D,CAAA;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAU,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,YAC/C,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,MAAM,UAAU,MAAM,GAAG,SAAS,SAAS,iBAAiB,KAC9F,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAOzB,CAAA"}
@@ -0,0 +1,28 @@
1
+ import { UnknownJob } from '@owlmeans/queue';
2
+ import { ownerOf } from './owner.js';
3
+ /**
4
+ * The queue these handlers read.
5
+ *
6
+ * Named by the declaration or, with nothing named, the context's sole declared queue — which
7
+ * `ctx.jobs()` refuses to guess once a second queue exists, so an app that grows one is told to
8
+ * name it rather than quietly served the wrong backlog.
9
+ */
10
+ export const jobsOf = (ctx, opts) => ctx.jobs(opts?.queue);
11
+ /** Does this viewer own that record? An unscoped viewer (`undefined`) owns everything. */
12
+ export const owns = (record, viewer, opts) => viewer == null || ownerOf(record, opts) === viewer;
13
+ /**
14
+ * One job, as this viewer is allowed to see it.
15
+ *
16
+ * A job that exists but belongs to someone else answers exactly as an absent one does: telling an
17
+ * unauthorized caller apart from a wrong id is what turns an id space into an enumeration oracle.
18
+ *
19
+ * @throws {UnknownJob}
20
+ */
21
+ export const readOwnedJob = async (resource, id, viewer, opts) => {
22
+ const record = await resource.load(id);
23
+ if (record == null || !owns(record, viewer, opts)) {
24
+ throw new UnknownJob(`${resource.queue}:${id}`);
25
+ }
26
+ return record;
27
+ };
28
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../../src/utils/resource.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE5C,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEpC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CACpB,GAA8B,EAAE,IAAwB,EACnC,EAAE,CAAE,GAA0B,CAAC,IAAI,CAAO,IAAI,EAAE,KAAK,CAAC,CAAA;AAE7E,0FAA0F;AAC1F,MAAM,CAAC,MAAM,IAAI,GAAG,CAClB,MAAiB,EAAE,MAA0B,EAAE,IAAwB,EAC9D,EAAE,CAAC,MAAM,IAAI,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,MAAM,CAAA;AAEhE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC/B,QAA6B,EAAE,EAAU,EAAE,MAA0B,EAAE,IAAwB,EACrE,EAAE;IAC5B,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACtC,IAAI,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC,CAAA;IACjD,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@owlmeans/server-job",
3
+ "version": "0.1.18-rc.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -b",
8
+ "dev": "sleep 354 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty"
10
+ },
11
+ "main": "build/index.js",
12
+ "module": "build/index.js",
13
+ "types": "build/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./build/index.js",
17
+ "require": "./build/index.js",
18
+ "default": "./build/index.js",
19
+ "module": "./build/index.js",
20
+ "types": "./build/index.d.ts"
21
+ }
22
+ },
23
+ "devDependencies": {
24
+ "@owlmeans/dep-config": "workspace:*",
25
+ "@types/node": "^26.1.0",
26
+ "nodemon": "^3.1.14",
27
+ "typescript": "^7.0.2"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "@owlmeans/auth": "^0.1.18-rc.9",
34
+ "@owlmeans/auth-common": "^0.1.18-rc.13",
35
+ "@owlmeans/context": "^0.1.18-rc.8",
36
+ "@owlmeans/entrypoint": "^0.1.18-rc.11",
37
+ "@owlmeans/queue": "^0.1.18-rc.9",
38
+ "@owlmeans/resource": "^0.1.18-rc.9",
39
+ "@owlmeans/route": "^0.1.18-rc.9",
40
+ "@owlmeans/server-api": "^0.1.18-rc.17",
41
+ "@owlmeans/server-context": "^0.1.18-rc.12",
42
+ "@owlmeans/server-entrypoint": "^0.1.18-rc.11",
43
+ "@owlmeans/server-socket": "^0.1.18-rc.18",
44
+ "@owlmeans/socket": "^0.1.18-rc.9"
45
+ }
46
+ }
@@ -0,0 +1,26 @@
1
+ import { handleParams } from '@owlmeans/server-api'
2
+ import type { AbstractResponse } from '@owlmeans/entrypoint'
3
+ import type { RefedEntrypointHandler } from '@owlmeans/server-entrypoint'
4
+ import type { JobHandlerOptions } from '../types.js'
5
+ import { jobViewer, jobsOf, readOwnedJob } from '../utils/index.js'
6
+
7
+ /**
8
+ * Cancel a job and answer with what was cancelled.
9
+ *
10
+ * Cancellation IS deletion in the queue contract — the job and its children leave the broker — so
11
+ * a job already finished cancels to its final record and one already gone answers `UnknownJob`.
12
+ * Nothing here interrupts a processor that is mid-run; a job holding a lock keeps it until the
13
+ * processor notices its own `signal`.
14
+ *
15
+ * @throws {UnknownJob}
16
+ */
17
+ export const cancelJob = (
18
+ opts?: JobHandlerOptions
19
+ ): RefedEntrypointHandler<AbstractResponse<any>> =>
20
+ handleParams<{ id: string }>(async ({ id }, ctx, req) => {
21
+ const resource = jobsOf(ctx, opts)
22
+ // Read first: `take` cannot tell whose job it removed, so ownership is settled before it.
23
+ await readOwnedJob(resource, id, await jobViewer(req, ctx, opts), opts)
24
+
25
+ return await resource.take(id)
26
+ })
@@ -0,0 +1,19 @@
1
+ import { handleParams } from '@owlmeans/server-api'
2
+ import type { AbstractResponse } from '@owlmeans/entrypoint'
3
+ import type { RefedEntrypointHandler } from '@owlmeans/server-entrypoint'
4
+ import type { JobHandlerOptions } from '../types.js'
5
+ import { jobViewer, jobsOf, readOwnedJob } from '../utils/index.js'
6
+
7
+ /**
8
+ * One job.
9
+ *
10
+ * @throws {UnknownJob} for an id that is absent AND for one that belongs to someone else.
11
+ */
12
+ export const getJob = (
13
+ opts?: JobHandlerOptions
14
+ ): RefedEntrypointHandler<AbstractResponse<any>> =>
15
+ handleParams<{ id: string }>(async ({ id }, ctx, req) => {
16
+ const resource = jobsOf(ctx, opts)
17
+
18
+ return await readOwnedJob(resource, id, await jobViewer(req, ctx, opts), opts)
19
+ })
@@ -0,0 +1,4 @@
1
+ export * from './list.js'
2
+ export * from './get.js'
3
+ export * from './cancel.js'
4
+ export * from './watch.js'
@@ -0,0 +1,36 @@
1
+ import { handleRequest } from '@owlmeans/server-api'
2
+ import type { AbstractResponse } from '@owlmeans/entrypoint'
3
+ import type { RefedEntrypointHandler } from '@owlmeans/server-entrypoint'
4
+ import type { Criteria } from '@owlmeans/resource'
5
+ import type { JobRecord } from '@owlmeans/queue'
6
+ import type { JobState } from '@owlmeans/queue'
7
+ import { DEFAULT_JOB_SORT } from '../consts.js'
8
+ import type { JobHandlerOptions, JobListQuery } from '../types.js'
9
+ import { jobScope, jobViewer, jobsOf } from '../utils/index.js'
10
+
11
+ /**
12
+ * The caller's jobs, newest first.
13
+ *
14
+ * Paging is opt-in: a `page` without a `size` is refused by the queue resource rather than
15
+ * silently windowed, because a broker has no default page size to count against. The state and
16
+ * name filters go through the same criteria language as every other resource, so a filter written
17
+ * for this list means the same thing applied to the store the browser holds.
18
+ */
19
+ export const listJobs = (
20
+ opts?: JobHandlerOptions
21
+ ): RefedEntrypointHandler<AbstractResponse<any>> => handleRequest(async (req, ctx) => {
22
+ const resource = jobsOf(ctx, opts)
23
+ const viewer = await jobViewer(req, ctx, opts)
24
+ const query = (req.query ?? {}) as JobListQuery
25
+
26
+ const where: Criteria<JobRecord> = {
27
+ ...jobScope(viewer, opts),
28
+ ...(query.state != null ? { state: query.state as JobState } : {}),
29
+ ...(query.name != null ? { name: query.name } : {}),
30
+ }
31
+
32
+ return await resource.list(where, {
33
+ sort: [{ field: DEFAULT_JOB_SORT, order: 'desc' }],
34
+ ...(query.size != null ? { size: query.size, page: query.page ?? 0 } : {}),
35
+ })
36
+ })
@@ -0,0 +1,66 @@
1
+ import { handleConnection } from '@owlmeans/server-socket'
2
+ import type { AbstractResponse } from '@owlmeans/entrypoint'
3
+ import type { RefedEntrypointHandler } from '@owlmeans/server-entrypoint'
4
+ import type { EventMessage } from '@owlmeans/socket'
5
+ import { MessageType } from '@owlmeans/socket'
6
+ import type { JobEvent } from '@owlmeans/queue'
7
+ import { JOB_EVENT } from '../consts.js'
8
+ import type { JobHandlerOptions } from '../types.js'
9
+ import { jobViewer, jobsOf, owns } from '../utils/index.js'
10
+
11
+ /**
12
+ * Push this caller's job lifecycle events down a socket.
13
+ *
14
+ * The frames are `JobEvent`s exactly as the queue publishes them, under the {@link JOB_EVENT}
15
+ * event name — no shape of this package's own, so a client applies them with the contract types.
16
+ *
17
+ * **A `JobEvent` carries no owner**, so each one is attributed by reading its job back, and the
18
+ * ids that answered are remembered for the life of the connection. A queue configured with
19
+ * `removeOnComplete` therefore loses its completion events here: the record they would be
20
+ * attributed by is gone by the time the event arrives, and an unattributable event is dropped
21
+ * rather than fanned out to everyone. Leave completed jobs in place on any queue that is watched.
22
+ */
23
+ export const watchJobs = (
24
+ opts?: JobHandlerOptions
25
+ ): RefedEntrypointHandler<AbstractResponse<any>> => handleConnection(async (conn, ctx, req) => {
26
+ const resource = jobsOf(ctx, opts)
27
+ const viewer = await jobViewer(req, ctx, opts)
28
+ const mine = new Set<string>()
29
+
30
+ const attributable = async (event: JobEvent): Promise<boolean> => {
31
+ if (viewer == null || mine.has(event.id)) {
32
+ return true
33
+ }
34
+ const record = await resource.load(event.id)
35
+ if (record == null || !owns(record, viewer, opts)) {
36
+ return false
37
+ }
38
+ mine.add(event.id)
39
+
40
+ return true
41
+ }
42
+
43
+ const unsubscribe = await resource.subscribe(async event => {
44
+ try {
45
+ if (await attributable(event)) {
46
+ await conn.notify(JOB_EVENT, event)
47
+ }
48
+ } catch (e) {
49
+ console.error('Job watch notify error:', e)
50
+ }
51
+ })
52
+
53
+ conn.listen(async message => {
54
+ if (typeof message !== 'object') {
55
+ return
56
+ }
57
+ const msg = message as EventMessage<void>
58
+ if (msg.type === MessageType.System && msg.event === 'close') {
59
+ try {
60
+ await unsubscribe()
61
+ } catch (e) {
62
+ console.error('Job watch unsubscribe error:', e)
63
+ }
64
+ }
65
+ })
66
+ })
package/src/consts.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The alias root a target app gets when its declaration names none, and the path segment the
3
+ * group answers under.
4
+ */
5
+ export const DEFAULT_JOB_ROOT = 'jobs'
6
+ export const DEFAULT_JOB_PATH = '/jobs'
7
+
8
+ /**
9
+ * Where the owner is recorded inside a job's payload.
10
+ *
11
+ * A `JobRecord` has no owner column — the broker keeps only what the contract declares — so the
12
+ * producer writes the subject into the job's own `data`, and every read here filters on
13
+ * `data.<field>`. Change it per declaration when the app's payloads already name the subject
14
+ * something else.
15
+ */
16
+ export const DEFAULT_OWNER_FIELD = 'owner'
17
+
18
+ /**
19
+ * The socket event a `JobEvent` frame is pushed under.
20
+ *
21
+ * This name, and the `<root>:<verb>` alias shape in `entrypoints.ts`, are the whole contract
22
+ * between this package and `@owlmeans/client-job` — the two halves cannot share a module without
23
+ * dragging fastify into a browser bundle, so they each state it and the skills pin it.
24
+ */
25
+ export const JOB_EVENT = 'job-event'
26
+
27
+ /** Newest first: a job list is read to see what is happening now. */
28
+ export const DEFAULT_JOB_SORT = 'createdAt'