@hyperfixation/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/activity.d.ts +73 -0
- package/dist/activity.js +84 -0
- package/dist/define-app.d.ts +171 -0
- package/dist/define-app.js +283 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +19 -0
- package/dist/labels.d.ts +41 -0
- package/dist/labels.js +62 -0
- package/dist/outcomes.d.ts +35 -0
- package/dist/outcomes.js +50 -0
- package/dist/pages.d.ts +11 -0
- package/dist/pages.js +1 -0
- package/dist/pause.d.ts +41 -0
- package/dist/pause.js +52 -0
- package/dist/records.d.ts +67 -0
- package/dist/records.js +111 -0
- package/dist/registry.d.ts +46 -0
- package/dist/registry.js +75 -0
- package/dist/resolution.d.ts +57 -0
- package/dist/resolution.js +221 -0
- package/dist/resolvers.d.ts +23 -0
- package/dist/resolvers.js +9 -0
- package/dist/schedules.d.ts +40 -0
- package/dist/schedules.js +32 -0
- package/dist/scorers.d.ts +17 -0
- package/dist/scorers.js +3 -0
- package/dist/scores.d.ts +80 -0
- package/dist/scores.js +113 -0
- package/dist/sources.d.ts +13 -0
- package/dist/sources.js +3 -0
- package/dist/specs.d.ts +12 -0
- package/dist/specs.js +7 -0
- package/dist/status-route.d.ts +29 -0
- package/dist/status-route.js +61 -0
- package/dist/status-token.d.ts +19 -0
- package/dist/status-token.js +40 -0
- package/dist/status.d.ts +45 -0
- package/dist/status.js +99 -0
- package/dist/step-client.d.ts +8 -0
- package/dist/step-client.js +8 -0
- package/dist/tasks.d.ts +96 -0
- package/dist/tasks.js +151 -0
- package/dist/workspace-views.d.ts +143 -0
- package/dist/workspace-views.js +0 -0
- package/dist/workspace.d.ts +91 -0
- package/dist/workspace.js +136 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Graham Lutz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { StepContext } from "@hyperfixation/workflows";
|
|
2
|
+
import type { ClientBase, Pool } from "pg";
|
|
3
|
+
export declare const ACTIVITY_LIST_OPERATION = "activity.list";
|
|
4
|
+
/** `<noun>.<verb>`, the shape `action.uncertain` and `approval.<decision>` already write. */
|
|
5
|
+
export declare const ACTIVITY_KIND_PATTERN: RegExp;
|
|
6
|
+
export declare class InvalidActivityKind extends Error {
|
|
7
|
+
readonly kind: string;
|
|
8
|
+
constructor(kind: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `DO NOTHING` on the partial `(run_id, key)` index: a step re-run under a second attempt writes
|
|
12
|
+
* the same key and adds nothing, which is what makes every step-side write in this package
|
|
13
|
+
* replay-safe. A web-side write passes `key` null, no conflict can fire, and the row is added.
|
|
14
|
+
*/
|
|
15
|
+
export declare const INSERT_ACTIVITY_STATEMENT: string;
|
|
16
|
+
export declare const EXISTING_ACTIVITY_STATEMENT = "SELECT id FROM hf_activity WHERE run_id = $1 AND key = $2";
|
|
17
|
+
/** The whole row, as any writer in this package supplies it. */
|
|
18
|
+
export interface ActivityWrite {
|
|
19
|
+
kind: string;
|
|
20
|
+
recordType?: string | null;
|
|
21
|
+
recordId?: string | number | null;
|
|
22
|
+
actorId?: string | null;
|
|
23
|
+
body?: string | null;
|
|
24
|
+
meta?: unknown;
|
|
25
|
+
runId?: string | null;
|
|
26
|
+
key?: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface ActivityRecorded {
|
|
29
|
+
id: number;
|
|
30
|
+
/** False when a replay found the row its own key had already written. */
|
|
31
|
+
created: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** What a flow records: the run and the key come from the step, not from the caller. */
|
|
34
|
+
export interface ActivityRecordOptions {
|
|
35
|
+
kind: string;
|
|
36
|
+
recordType?: string;
|
|
37
|
+
recordId?: string | number;
|
|
38
|
+
actorId?: string;
|
|
39
|
+
body?: string;
|
|
40
|
+
meta?: unknown;
|
|
41
|
+
/** Distinguishes two rows written by one step; defaults to the step's key and the kind. */
|
|
42
|
+
key?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ActivityListOptions {
|
|
45
|
+
recordType: string;
|
|
46
|
+
recordId: string | number;
|
|
47
|
+
}
|
|
48
|
+
export interface ActivityRow {
|
|
49
|
+
id: number;
|
|
50
|
+
recordType: string | null;
|
|
51
|
+
recordId: string | null;
|
|
52
|
+
kind: string;
|
|
53
|
+
actorId: string | null;
|
|
54
|
+
body: string | null;
|
|
55
|
+
meta: unknown;
|
|
56
|
+
/** Null for a web-side write; C6's timeline groups those under "manual". */
|
|
57
|
+
runId: string | null;
|
|
58
|
+
at: Date;
|
|
59
|
+
}
|
|
60
|
+
export declare function assertActivityKind(kind: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* The one `hf_activity` writer in this package — tasks, labels, outcomes, scores and
|
|
63
|
+
* `records.archive()` all go through it, on whatever client their own transaction already holds.
|
|
64
|
+
*/
|
|
65
|
+
export declare function insertActivity(queryable: Pool | ClientBase, write: ActivityWrite): Promise<ActivityRecorded>;
|
|
66
|
+
/**
|
|
67
|
+
* A flow's own timeline entry, written inside `ctx.tx` so it commits with whatever else the step
|
|
68
|
+
* wrote. The default key is the step's, which makes one row per step per kind: a step that wants
|
|
69
|
+
* two rows of one kind names them itself.
|
|
70
|
+
*/
|
|
71
|
+
export declare function recordActivity(ctx: StepContext, options: ActivityRecordOptions): Promise<ActivityRecorded>;
|
|
72
|
+
/** A control-plane read: `run_id` travels with the row so the timeline can group by it. */
|
|
73
|
+
export declare function listActivity(pool: Pool, options: ActivityListOptions): Promise<ActivityRow[]>;
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { assertNotInWorkflow } from "@hyperfixation/db";
|
|
2
|
+
import { stepClient } from "./step-client.js";
|
|
3
|
+
export const ACTIVITY_LIST_OPERATION = "activity.list";
|
|
4
|
+
/** `<noun>.<verb>`, the shape `action.uncertain` and `approval.<decision>` already write. */
|
|
5
|
+
export const ACTIVITY_KIND_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
6
|
+
export class InvalidActivityKind extends Error {
|
|
7
|
+
kind;
|
|
8
|
+
constructor(kind) {
|
|
9
|
+
super(`InvalidActivityKind: ${JSON.stringify(kind)} is not a <noun>.<verb> activity kind, ` +
|
|
10
|
+
"as 'task.created' and 'action.uncertain' are");
|
|
11
|
+
this.name = "InvalidActivityKind";
|
|
12
|
+
this.kind = kind;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `DO NOTHING` on the partial `(run_id, key)` index: a step re-run under a second attempt writes
|
|
17
|
+
* the same key and adds nothing, which is what makes every step-side write in this package
|
|
18
|
+
* replay-safe. A web-side write passes `key` null, no conflict can fire, and the row is added.
|
|
19
|
+
*/
|
|
20
|
+
export const INSERT_ACTIVITY_STATEMENT = "INSERT INTO hf_activity (record_type, record_id, kind, actor_id, body, meta, run_id, key) " +
|
|
21
|
+
"VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8) " +
|
|
22
|
+
"ON CONFLICT (run_id, key) WHERE key IS NOT NULL DO NOTHING RETURNING id";
|
|
23
|
+
export const EXISTING_ACTIVITY_STATEMENT = "SELECT id FROM hf_activity WHERE run_id = $1 AND key = $2";
|
|
24
|
+
const LIST_ACTIVITY_STATEMENT = "SELECT id, record_type, record_id, kind, actor_id, body, meta, run_id, at FROM hf_activity " +
|
|
25
|
+
"WHERE record_type = $1 AND record_id = $2 ORDER BY at, id";
|
|
26
|
+
export function assertActivityKind(kind) {
|
|
27
|
+
if (!ACTIVITY_KIND_PATTERN.test(kind))
|
|
28
|
+
throw new InvalidActivityKind(kind);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The one `hf_activity` writer in this package — tasks, labels, outcomes, scores and
|
|
32
|
+
* `records.archive()` all go through it, on whatever client their own transaction already holds.
|
|
33
|
+
*/
|
|
34
|
+
export async function insertActivity(queryable, write) {
|
|
35
|
+
assertActivityKind(write.kind);
|
|
36
|
+
const runId = write.runId ?? null;
|
|
37
|
+
const key = write.key ?? null;
|
|
38
|
+
const inserted = await queryable.query(INSERT_ACTIVITY_STATEMENT, [
|
|
39
|
+
write.recordType ?? null,
|
|
40
|
+
write.recordId === undefined || write.recordId === null ? null : String(write.recordId),
|
|
41
|
+
write.kind,
|
|
42
|
+
write.actorId ?? null,
|
|
43
|
+
write.body ?? null,
|
|
44
|
+
write.meta === undefined ? null : JSON.stringify(write.meta),
|
|
45
|
+
runId,
|
|
46
|
+
key,
|
|
47
|
+
]);
|
|
48
|
+
if (inserted.rows[0] !== undefined)
|
|
49
|
+
return { id: Number(inserted.rows[0].id), created: true };
|
|
50
|
+
const found = await queryable.query(EXISTING_ACTIVITY_STATEMENT, [runId, key]);
|
|
51
|
+
return { id: Number(found.rows[0].id), created: false };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A flow's own timeline entry, written inside `ctx.tx` so it commits with whatever else the step
|
|
55
|
+
* wrote. The default key is the step's, which makes one row per step per kind: a step that wants
|
|
56
|
+
* two rows of one kind names them itself.
|
|
57
|
+
*/
|
|
58
|
+
export async function recordActivity(ctx, options) {
|
|
59
|
+
assertActivityKind(options.kind);
|
|
60
|
+
return ctx.tx((db) => insertActivity(stepClient(db), {
|
|
61
|
+
...options,
|
|
62
|
+
runId: ctx.runId,
|
|
63
|
+
key: options.key ?? `${ctx.key}:${options.kind}`,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
/** A control-plane read: `run_id` travels with the row so the timeline can group by it. */
|
|
67
|
+
export async function listActivity(pool, options) {
|
|
68
|
+
assertNotInWorkflow(ACTIVITY_LIST_OPERATION);
|
|
69
|
+
const { rows } = await pool.query(LIST_ACTIVITY_STATEMENT, [
|
|
70
|
+
options.recordType,
|
|
71
|
+
String(options.recordId),
|
|
72
|
+
]);
|
|
73
|
+
return rows.map((row) => ({
|
|
74
|
+
id: Number(row.id),
|
|
75
|
+
recordType: row.record_type,
|
|
76
|
+
recordId: row.record_id,
|
|
77
|
+
kind: row.kind,
|
|
78
|
+
actorId: row.actor_id,
|
|
79
|
+
body: row.body,
|
|
80
|
+
meta: row.meta,
|
|
81
|
+
runId: row.run_id,
|
|
82
|
+
at: row.at,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { DBOSClient } from "@dbos-inc/dbos-sdk";
|
|
2
|
+
import type { StepDatabase } from "@hyperfixation/db";
|
|
3
|
+
import { type ActionChannel, type ApprovalDraftSchema, type DecideOptions, type DecideResult, type Flow, type ReconcileOptions, type ReconcileReport, type RunsStartOptions, type StartedRun, type StepContext } from "@hyperfixation/workflows";
|
|
4
|
+
import type { Pool } from "pg";
|
|
5
|
+
import { type ActivityListOptions, type ActivityRecordOptions, type ActivityRecorded, type ActivityRow } from "./activity.js";
|
|
6
|
+
import { type LabelAddOptions, type LabelListOptions, type LabelRow } from "./labels.js";
|
|
7
|
+
import { type OutcomeListOptions, type OutcomeRecordOptions, type OutcomeRow } from "./outcomes.js";
|
|
8
|
+
import type { PageDefinition } from "./pages.js";
|
|
9
|
+
import { type PauseOptions, type PauseResult, type ResumeResult } from "./pause.js";
|
|
10
|
+
import { type ArchiveOptions, type ArchiveResult, type RecordDefinition } from "./records.js";
|
|
11
|
+
import { type Registry } from "./registry.js";
|
|
12
|
+
import { type ResolveBatchResult } from "./resolution.js";
|
|
13
|
+
import type { ResolverDefinition } from "./resolvers.js";
|
|
14
|
+
import { type AnySchedule, type ScheduleFired } from "./schedules.js";
|
|
15
|
+
import type { ScorerDefinition } from "./scorers.js";
|
|
16
|
+
import { type ScoreWritten, type StepWriteScoreOptions } from "./scores.js";
|
|
17
|
+
import type { SourceDefinition } from "./sources.js";
|
|
18
|
+
import type { SpecDefinition } from "./specs.js";
|
|
19
|
+
import { type StatusReport } from "./status.js";
|
|
20
|
+
import { type TaskCloseOptions, type TaskClosed, type TaskCreateManualOptions, type TaskCreateOptions, type TaskCreated, type TaskListOptions, type TaskRow } from "./tasks.js";
|
|
21
|
+
import { type AppWorkspace } from "./workspace.js";
|
|
22
|
+
/**
|
|
23
|
+
* A flow of any shape. `Flow<never, unknown>` is the bottom of the family: its input is
|
|
24
|
+
* contravariant, so every `Flow<I, O>` is one of these.
|
|
25
|
+
*/
|
|
26
|
+
export type AnyFlow = Flow<never, unknown>;
|
|
27
|
+
export interface ApprovalTypeDefinition {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
/** The Zod schema an edited draft is parsed against; a type without one refuses every edit. */
|
|
30
|
+
readonly schema?: ApprovalDraftSchema;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The pool and client every control-plane operation runs on. In the worker this is the control
|
|
34
|
+
* pool `startWorker()` built and the worker's own `DBOSClient`; in the web it is the web's
|
|
35
|
+
* ordinary pool and `getClient()`. Neither is fenced, and neither has to be: a control-plane
|
|
36
|
+
* operation's fence is a predicate, and `assertNotInWorkflow()` is what keeps it out of a run.
|
|
37
|
+
*/
|
|
38
|
+
export interface ControlPlane {
|
|
39
|
+
pool: Pool;
|
|
40
|
+
client: DBOSClient;
|
|
41
|
+
}
|
|
42
|
+
export declare class AppNotAttached extends Error {
|
|
43
|
+
readonly operation: string;
|
|
44
|
+
constructor(operation: string);
|
|
45
|
+
}
|
|
46
|
+
export declare class NoApplicationVersion extends Error {
|
|
47
|
+
constructor(operation: string);
|
|
48
|
+
}
|
|
49
|
+
export interface DefineAppOptions {
|
|
50
|
+
name: string;
|
|
51
|
+
/** Defaults to `HF_BUILD_SHA`, the one source of a version in every process. */
|
|
52
|
+
applicationVersion?: string;
|
|
53
|
+
flows?: readonly AnyFlow[];
|
|
54
|
+
sources?: readonly SourceDefinition[];
|
|
55
|
+
resolvers?: readonly ResolverDefinition[];
|
|
56
|
+
scorers?: readonly ScorerDefinition[];
|
|
57
|
+
specs?: readonly SpecDefinition[];
|
|
58
|
+
approvalTypes?: readonly ApprovalTypeDefinition[];
|
|
59
|
+
channels?: readonly ActionChannel[];
|
|
60
|
+
/** A bare `RecordTable` is still one of these: everything the workspace adds is optional. */
|
|
61
|
+
records?: readonly RecordDefinition[];
|
|
62
|
+
pages?: readonly PageDefinition[];
|
|
63
|
+
schedules?: readonly AnySchedule[];
|
|
64
|
+
}
|
|
65
|
+
export interface AppRecords {
|
|
66
|
+
/** Registered record types, keyed by the `record_type` machinery rows carry. */
|
|
67
|
+
readonly types: Registry<RecordDefinition>;
|
|
68
|
+
archive(options: ArchiveOptions): Promise<ArchiveResult>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The write helpers split by where they run, not by what they write: `record` takes the step's
|
|
72
|
+
* context and writes inside `ctx.tx`, `list` is a control-plane read. There is no one entry point
|
|
73
|
+
* that sniffs the handle — a flow and the web reach different functions on purpose.
|
|
74
|
+
*/
|
|
75
|
+
export interface AppActivity {
|
|
76
|
+
record(ctx: StepContext, options: ActivityRecordOptions): Promise<ActivityRecorded>;
|
|
77
|
+
list(options: ActivityListOptions): Promise<ActivityRow[]>;
|
|
78
|
+
}
|
|
79
|
+
export interface AppTasks {
|
|
80
|
+
/** A flow's follow-up, keyed by the step so a replay finds its own task. */
|
|
81
|
+
create(ctx: StepContext, options: TaskCreateOptions): Promise<TaskCreated>;
|
|
82
|
+
createManual(options: TaskCreateManualOptions): Promise<TaskCreated>;
|
|
83
|
+
complete(options: TaskCloseOptions): Promise<TaskClosed>;
|
|
84
|
+
cancel(options: TaskCloseOptions): Promise<TaskClosed>;
|
|
85
|
+
list(options?: TaskListOptions): Promise<TaskRow[]>;
|
|
86
|
+
}
|
|
87
|
+
export interface AppLabels {
|
|
88
|
+
add(options: LabelAddOptions): Promise<{
|
|
89
|
+
id: number;
|
|
90
|
+
}>;
|
|
91
|
+
list(options: LabelListOptions): Promise<LabelRow[]>;
|
|
92
|
+
}
|
|
93
|
+
export interface AppOutcomes {
|
|
94
|
+
record(options: OutcomeRecordOptions): Promise<{
|
|
95
|
+
id: number;
|
|
96
|
+
}>;
|
|
97
|
+
list(options: OutcomeListOptions): Promise<OutcomeRow[]>;
|
|
98
|
+
}
|
|
99
|
+
export interface AppScores {
|
|
100
|
+
/** The `hf_score` row, the record's mixin columns and the timeline entry, in one transaction. */
|
|
101
|
+
write(ctx: StepContext, options: StepWriteScoreOptions): Promise<ScoreWritten>;
|
|
102
|
+
}
|
|
103
|
+
export interface AppResolutionBatchOptions {
|
|
104
|
+
resolver: string;
|
|
105
|
+
source: string;
|
|
106
|
+
limit?: number | undefined;
|
|
107
|
+
maxAttempts?: number | undefined;
|
|
108
|
+
}
|
|
109
|
+
export interface AppResolution {
|
|
110
|
+
/**
|
|
111
|
+
* Step-side, so it takes the open `ctx.tx` and not the control plane: resolution is a write
|
|
112
|
+
* inside a run's transaction, and the table comes from the resolver's registered record type.
|
|
113
|
+
*/
|
|
114
|
+
batch(tx: StepDatabase, options: AppResolutionBatchOptions): Promise<ResolveBatchResult>;
|
|
115
|
+
}
|
|
116
|
+
export interface AppSchedules extends Registry<AnySchedule> {
|
|
117
|
+
/** Starts the schedule's flow now, unless the app is paused. */
|
|
118
|
+
fire(name: string): Promise<ScheduleFired>;
|
|
119
|
+
due(now: Date, lastFired: ReadonlyMap<string, Date>): string[];
|
|
120
|
+
}
|
|
121
|
+
export interface App {
|
|
122
|
+
readonly name: string;
|
|
123
|
+
readonly applicationVersion: string | undefined;
|
|
124
|
+
readonly flows: Registry<AnyFlow>;
|
|
125
|
+
readonly sources: Registry<SourceDefinition>;
|
|
126
|
+
readonly resolvers: Registry<ResolverDefinition>;
|
|
127
|
+
readonly scorers: Registry<ScorerDefinition>;
|
|
128
|
+
readonly specs: Registry<SpecDefinition>;
|
|
129
|
+
readonly approvalTypes: Registry<ApprovalTypeDefinition>;
|
|
130
|
+
readonly channels: Registry<ActionChannel>;
|
|
131
|
+
readonly records: AppRecords;
|
|
132
|
+
readonly activity: AppActivity;
|
|
133
|
+
readonly tasks: AppTasks;
|
|
134
|
+
readonly labels: AppLabels;
|
|
135
|
+
readonly outcomes: AppOutcomes;
|
|
136
|
+
readonly scores: AppScores;
|
|
137
|
+
readonly resolution: AppResolution;
|
|
138
|
+
/** Keyed by `path`, not by a name: the path is what a workspace link points at. */
|
|
139
|
+
readonly pages: Registry<PageDefinition>;
|
|
140
|
+
readonly schedules: AppSchedules;
|
|
141
|
+
/** Paths and nav items for the workspace the template renders. */
|
|
142
|
+
readonly workspace: AppWorkspace;
|
|
143
|
+
/** Hands the app the handles every control-plane operation below runs on. */
|
|
144
|
+
attach(controlPlane: ControlPlane): void;
|
|
145
|
+
/** For a process that is tearing its pool down; the next call refuses rather than using it. */
|
|
146
|
+
detach(): void;
|
|
147
|
+
controlPlane(operation?: string): ControlPlane;
|
|
148
|
+
runs: {
|
|
149
|
+
start<I>(flow: Flow<I, unknown>, input: I, options?: RunsStartOptions): Promise<StartedRun>;
|
|
150
|
+
};
|
|
151
|
+
approvals: {
|
|
152
|
+
decide(options: DecideOptions): Promise<DecideResult>;
|
|
153
|
+
};
|
|
154
|
+
reconcile(options?: Partial<ReconcileOptions>): Promise<ReconcileReport>;
|
|
155
|
+
pause(options?: PauseOptions): Promise<PauseResult>;
|
|
156
|
+
resume(options?: PauseOptions): Promise<ResumeResult>;
|
|
157
|
+
status(): Promise<StatusReport>;
|
|
158
|
+
/** A `fetch` handler: the template's `app/api/status/[[...route]]/route.ts` is one line. */
|
|
159
|
+
statusHandler(request: Request): Promise<Response>;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The app object, and the thing that finally owns the pool and the `DBOSClient` that
|
|
163
|
+
* `reconcile()`, the bump path and `decide()` have taken as bare parameters until now. Every
|
|
164
|
+
* control-plane operation on it is the same function those chunks shipped, with the handles
|
|
165
|
+
* closed over — there is no second implementation of any of them here.
|
|
166
|
+
*
|
|
167
|
+
* Registration is module-level and the handles are not: `src/hyperfixation.ts` is imported by
|
|
168
|
+
* the web and by `worker.ts` alike, and only one of those has a control pool at import time.
|
|
169
|
+
* So `attach()` is a separate call, made once the process knows which shape it is.
|
|
170
|
+
*/
|
|
171
|
+
export declare function defineApp(options: DefineAppOptions): App;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { reconcile, runsStart, decide as decideApprovals, } from "@hyperfixation/workflows";
|
|
2
|
+
import { listActivity, recordActivity, } from "./activity.js";
|
|
3
|
+
import { addLabel, listLabels, } from "./labels.js";
|
|
4
|
+
import { listOutcomes, recordOutcome, } from "./outcomes.js";
|
|
5
|
+
import { pauseApp, resumeApp } from "./pause.js";
|
|
6
|
+
import { archiveRecord, assertRecordStages, } from "./records.js";
|
|
7
|
+
import { createRegistry, UnknownRegistration } from "./registry.js";
|
|
8
|
+
import { resolveBatch } from "./resolution.js";
|
|
9
|
+
import { fireSchedule, schedulesDue, } from "./schedules.js";
|
|
10
|
+
import { writeStepScore } from "./scores.js";
|
|
11
|
+
import { createStatusHandler, STATUS_TOKEN_ACTOR } from "./status-route.js";
|
|
12
|
+
import { appStatus } from "./status.js";
|
|
13
|
+
import { cancelTask, completeTask, createManualTask, createTask, listTasks, } from "./tasks.js";
|
|
14
|
+
import { workspaceNav, workspaceRoute, } from "./workspace.js";
|
|
15
|
+
import { workspaceBoard, workspaceHome, workspaceInbox, workspaceRecord, } from "./workspace-views.js";
|
|
16
|
+
export class AppNotAttached extends Error {
|
|
17
|
+
operation;
|
|
18
|
+
constructor(operation) {
|
|
19
|
+
super(`AppNotAttached: ${operation} needs the app's control plane; call app.attach({ pool, client }) ` +
|
|
20
|
+
"with startWorker()'s control pool in the worker, or the web's pool and getClient() in the web");
|
|
21
|
+
this.name = "AppNotAttached";
|
|
22
|
+
this.operation = operation;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class NoApplicationVersion extends Error {
|
|
26
|
+
constructor(operation) {
|
|
27
|
+
super(`NoApplicationVersion: ${operation} needs the version this deploy runs; defineApp() reads ` +
|
|
28
|
+
"HF_BUILD_SHA, which is unset here");
|
|
29
|
+
this.name = "NoApplicationVersion";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The app object, and the thing that finally owns the pool and the `DBOSClient` that
|
|
34
|
+
* `reconcile()`, the bump path and `decide()` have taken as bare parameters until now. Every
|
|
35
|
+
* control-plane operation on it is the same function those chunks shipped, with the handles
|
|
36
|
+
* closed over — there is no second implementation of any of them here.
|
|
37
|
+
*
|
|
38
|
+
* Registration is module-level and the handles are not: `src/hyperfixation.ts` is imported by
|
|
39
|
+
* the web and by `worker.ts` alike, and only one of those has a control pool at import time.
|
|
40
|
+
* So `attach()` is a separate call, made once the process knows which shape it is.
|
|
41
|
+
*/
|
|
42
|
+
export function defineApp(options) {
|
|
43
|
+
const applicationVersion = options.applicationVersion ?? process.env.HF_BUILD_SHA;
|
|
44
|
+
const flows = createRegistry("flow");
|
|
45
|
+
const sources = createRegistry("source");
|
|
46
|
+
const resolvers = createRegistry("resolver");
|
|
47
|
+
const scorers = createRegistry("scorer");
|
|
48
|
+
const specs = createRegistry("spec");
|
|
49
|
+
const approvalTypes = createRegistry("approval type");
|
|
50
|
+
const channels = createRegistry("channel");
|
|
51
|
+
const recordTypes = createRegistry("record type", (entry) => entry.recordType);
|
|
52
|
+
const pages = createRegistry("page", (entry) => entry.path);
|
|
53
|
+
// `Object.assign` rather than a spread: the registry's `size` is a getter, and a spread would
|
|
54
|
+
// copy today's count instead of it.
|
|
55
|
+
const schedules = Object.assign(createRegistry("schedule"), {
|
|
56
|
+
async fire(name) {
|
|
57
|
+
const { pool } = controlPlane("schedules.fire");
|
|
58
|
+
return fireSchedule(pool, schedules.require(name), (flow, input) => app.runs.start(flow, input));
|
|
59
|
+
},
|
|
60
|
+
due(now, lastFired) {
|
|
61
|
+
return schedulesDue(schedules.all(), now, lastFired);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
for (const flow of options.flows ?? [])
|
|
65
|
+
flows.register(flow);
|
|
66
|
+
for (const source of options.sources ?? [])
|
|
67
|
+
sources.register(source);
|
|
68
|
+
for (const resolver of options.resolvers ?? [])
|
|
69
|
+
resolvers.register(resolver);
|
|
70
|
+
for (const spec of options.specs ?? [])
|
|
71
|
+
specs.register(spec);
|
|
72
|
+
for (const scorer of options.scorers ?? [])
|
|
73
|
+
scorers.register(scorer);
|
|
74
|
+
for (const type of options.approvalTypes ?? [])
|
|
75
|
+
approvalTypes.register(type);
|
|
76
|
+
for (const channel of options.channels ?? [])
|
|
77
|
+
channels.register(channel);
|
|
78
|
+
for (const record of options.records ?? []) {
|
|
79
|
+
assertRecordStages(record);
|
|
80
|
+
recordTypes.register(record);
|
|
81
|
+
}
|
|
82
|
+
for (const page of options.pages ?? [])
|
|
83
|
+
pages.register(page);
|
|
84
|
+
for (const schedule of options.schedules ?? [])
|
|
85
|
+
schedules.register(schedule);
|
|
86
|
+
// Cross-registry, so after every registration: a scorer whose spec is unregistered would
|
|
87
|
+
// write `spec_version` rows nothing can explain, and a schedule whose flow is unregistered
|
|
88
|
+
// would refuse at its first firing rather than at boot.
|
|
89
|
+
for (const scorer of scorers.all()) {
|
|
90
|
+
if (!specs.has(scorer.spec.name)) {
|
|
91
|
+
throw new UnknownRegistration("spec", scorer.spec.name, specs.names());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const schedule of schedules.all()) {
|
|
95
|
+
if (!flows.has(schedule.flow.name)) {
|
|
96
|
+
throw new UnknownRegistration("flow", schedule.flow.name, flows.names());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let attached;
|
|
100
|
+
const controlPlane = (operation = "this operation") => {
|
|
101
|
+
if (attached === undefined)
|
|
102
|
+
throw new AppNotAttached(operation);
|
|
103
|
+
return attached;
|
|
104
|
+
};
|
|
105
|
+
const versionFor = (operation) => {
|
|
106
|
+
if (applicationVersion === undefined)
|
|
107
|
+
throw new NoApplicationVersion(operation);
|
|
108
|
+
return applicationVersion;
|
|
109
|
+
};
|
|
110
|
+
const workspaceRegistries = { records: recordTypes, pages };
|
|
111
|
+
const viewDeps = (operation) => ({
|
|
112
|
+
pool: controlPlane(operation).pool,
|
|
113
|
+
records: recordTypes,
|
|
114
|
+
hasSchema: (type) => approvalTypes.get(type)?.schema !== undefined,
|
|
115
|
+
});
|
|
116
|
+
const app = {
|
|
117
|
+
name: options.name,
|
|
118
|
+
applicationVersion,
|
|
119
|
+
flows,
|
|
120
|
+
sources,
|
|
121
|
+
resolvers,
|
|
122
|
+
scorers,
|
|
123
|
+
specs,
|
|
124
|
+
approvalTypes,
|
|
125
|
+
channels,
|
|
126
|
+
pages,
|
|
127
|
+
schedules,
|
|
128
|
+
workspace: {
|
|
129
|
+
route: (path) => workspaceRoute(workspaceRegistries, path),
|
|
130
|
+
nav: () => workspaceNav(workspaceRegistries),
|
|
131
|
+
async inbox(inboxOptions) {
|
|
132
|
+
return workspaceInbox(viewDeps("workspace.inbox"), inboxOptions);
|
|
133
|
+
},
|
|
134
|
+
async home(homeOptions) {
|
|
135
|
+
return workspaceHome(viewDeps("workspace.home"), homeOptions);
|
|
136
|
+
},
|
|
137
|
+
async board(recordType, boardOptions) {
|
|
138
|
+
return workspaceBoard(viewDeps("workspace.board"), recordType, boardOptions);
|
|
139
|
+
},
|
|
140
|
+
async record(recordType, id) {
|
|
141
|
+
return workspaceRecord(viewDeps("workspace.record"), recordType, id);
|
|
142
|
+
},
|
|
143
|
+
// The one workspace write, and no second implementation of it: the web's decision is
|
|
144
|
+
// `decide()`'s, with the `via` the session already fixes.
|
|
145
|
+
decide: (decideOptions) => app.approvals.decide({ ...decideOptions, via: "web" }),
|
|
146
|
+
},
|
|
147
|
+
records: {
|
|
148
|
+
types: recordTypes,
|
|
149
|
+
async archive(archiveOptions) {
|
|
150
|
+
const { pool, client } = controlPlane("records.archive");
|
|
151
|
+
return archiveRecord(pool, client, recordTypes, archiveOptions);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
activity: {
|
|
155
|
+
// No `controlPlane()`: a step-side write runs on the step pool `ctx.tx` already holds.
|
|
156
|
+
record: (ctx, recordOptions) => recordActivity(ctx, recordOptions),
|
|
157
|
+
async list(listOptions) {
|
|
158
|
+
const { pool } = controlPlane("activity.list");
|
|
159
|
+
return listActivity(pool, listOptions);
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
tasks: {
|
|
163
|
+
create: (ctx, createOptions) => createTask(ctx, recordTypes, createOptions),
|
|
164
|
+
async createManual(createOptions) {
|
|
165
|
+
const { pool } = controlPlane("tasks.createManual");
|
|
166
|
+
return createManualTask(pool, recordTypes, createOptions);
|
|
167
|
+
},
|
|
168
|
+
async complete(closeOptions) {
|
|
169
|
+
const { pool } = controlPlane("tasks.complete");
|
|
170
|
+
return completeTask(pool, closeOptions);
|
|
171
|
+
},
|
|
172
|
+
async cancel(closeOptions) {
|
|
173
|
+
const { pool } = controlPlane("tasks.cancel");
|
|
174
|
+
return cancelTask(pool, closeOptions);
|
|
175
|
+
},
|
|
176
|
+
async list(listOptions = {}) {
|
|
177
|
+
const { pool } = controlPlane("tasks.list");
|
|
178
|
+
return listTasks(pool, listOptions);
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
labels: {
|
|
182
|
+
async add(addOptions) {
|
|
183
|
+
const { pool } = controlPlane("labels.add");
|
|
184
|
+
return addLabel(pool, recordTypes, addOptions);
|
|
185
|
+
},
|
|
186
|
+
async list(listOptions) {
|
|
187
|
+
const { pool } = controlPlane("labels.list");
|
|
188
|
+
return listLabels(pool, listOptions);
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
outcomes: {
|
|
192
|
+
async record(outcomeOptions) {
|
|
193
|
+
const { pool } = controlPlane("outcomes.record");
|
|
194
|
+
return recordOutcome(pool, recordTypes, outcomeOptions);
|
|
195
|
+
},
|
|
196
|
+
async list(listOptions) {
|
|
197
|
+
const { pool } = controlPlane("outcomes.list");
|
|
198
|
+
return listOutcomes(pool, listOptions);
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
scores: {
|
|
202
|
+
write: (ctx, writeOptions) => writeStepScore(ctx, recordTypes, writeOptions),
|
|
203
|
+
},
|
|
204
|
+
resolution: {
|
|
205
|
+
batch(tx, batchOptions) {
|
|
206
|
+
const resolver = resolvers.require(batchOptions.resolver);
|
|
207
|
+
return resolveBatch(tx, {
|
|
208
|
+
resolver,
|
|
209
|
+
table: recordTypes.require(resolver.recordType).table,
|
|
210
|
+
source: batchOptions.source,
|
|
211
|
+
limit: batchOptions.limit,
|
|
212
|
+
maxAttempts: batchOptions.maxAttempts,
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
attach(next) {
|
|
217
|
+
attached = next;
|
|
218
|
+
},
|
|
219
|
+
detach() {
|
|
220
|
+
attached = undefined;
|
|
221
|
+
},
|
|
222
|
+
controlPlane,
|
|
223
|
+
runs: {
|
|
224
|
+
async start(flow, input, startOptions) {
|
|
225
|
+
const { pool, client } = controlPlane("runs.start");
|
|
226
|
+
// A flow the app never registered has no queue on this worker and would strand its run
|
|
227
|
+
// at the first bump, where the registry is the only source of a queue name.
|
|
228
|
+
if (!flows.has(flow.name)) {
|
|
229
|
+
throw new UnknownRegistration("flow", flow.name, flows.names());
|
|
230
|
+
}
|
|
231
|
+
return runsStart(pool, client, flow, input, startOptions);
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
approvals: {
|
|
235
|
+
async decide(decideOptions) {
|
|
236
|
+
const { pool, client } = controlPlane("approvals.decide");
|
|
237
|
+
return decideApprovals(pool, client, {
|
|
238
|
+
schemaFor: (type) => approvalTypes.get(type)?.schema,
|
|
239
|
+
...decideOptions,
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
async reconcile(reconcileOptions = {}) {
|
|
244
|
+
const { pool, client } = controlPlane("reconcile");
|
|
245
|
+
return reconcile(pool, client, {
|
|
246
|
+
applicationVersion: reconcileOptions.applicationVersion ?? versionFor("reconcile"),
|
|
247
|
+
...(reconcileOptions.lockTimeout === undefined
|
|
248
|
+
? {}
|
|
249
|
+
: { lockTimeout: reconcileOptions.lockTimeout }),
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
async pause(pauseOptions = {}) {
|
|
253
|
+
const { pool, client } = controlPlane("app.pause");
|
|
254
|
+
return pauseApp(pool, client, pauseOptions);
|
|
255
|
+
},
|
|
256
|
+
async resume(resumeOptions = {}) {
|
|
257
|
+
const { pool, client } = controlPlane("app.resume");
|
|
258
|
+
return resumeApp(pool, client, {
|
|
259
|
+
...resumeOptions,
|
|
260
|
+
applicationVersion: versionFor("app.resume"),
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
async status() {
|
|
264
|
+
const { pool } = controlPlane("app.status");
|
|
265
|
+
return appStatus(pool, { app: options.name, applicationVersion: applicationVersion ?? null });
|
|
266
|
+
},
|
|
267
|
+
async statusHandler(request) {
|
|
268
|
+
const { pool } = controlPlane("app.statusHandler");
|
|
269
|
+
return createStatusHandler({
|
|
270
|
+
pool,
|
|
271
|
+
app: options.name,
|
|
272
|
+
applicationVersion: applicationVersion ?? null,
|
|
273
|
+
handlers: {
|
|
274
|
+
status: () => app.status(),
|
|
275
|
+
// The actor is the write token, not a session: nothing else authenticated the call.
|
|
276
|
+
pause: () => app.pause({ userId: STATUS_TOKEN_ACTOR }),
|
|
277
|
+
resume: () => app.resume({ userId: STATUS_TOKEN_ACTOR }),
|
|
278
|
+
},
|
|
279
|
+
})(request);
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
return app;
|
|
283
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { defineApp, AppNotAttached, NoApplicationVersion, type AnyFlow, type App, type AppActivity, type AppLabels, type AppOutcomes, type AppRecords, type AppResolution, type AppResolutionBatchOptions, type AppSchedules, type AppScores, type AppTasks, type ApprovalTypeDefinition, type ControlPlane, type DefineAppOptions, } from "./define-app.js";
|
|
2
|
+
export { createRegistry, DuplicateRegistration, InvalidDefinition, UnknownRegistration, type Registry, } from "./registry.js";
|
|
3
|
+
export { defineSource, type SourceDefinition, type SourceRow } from "./sources.js";
|
|
4
|
+
export { defineResolver, type ResolverDefinition, type ResolverFuzzy } from "./resolvers.js";
|
|
5
|
+
export { bigramDice, fuzzyCandidateStatement, resolveBatch, DEFAULT_RESOLVE_LIMIT, DEFAULT_RESOLVE_MAX_ATTEMPTS, FUZZY_CANDIDATE_LIMIT, type ResolveBatchOptions, type ResolveBatchResult, } from "./resolution.js";
|
|
6
|
+
export { defineSpec, type SpecDefinition } from "./specs.js";
|
|
7
|
+
export { defineScorer, type Scored, type ScorerDefinition } from "./scorers.js";
|
|
8
|
+
export type { PageDefinition } from "./pages.js";
|
|
9
|
+
export { defineSchedule, fireSchedule, schedulesDue, type AnySchedule, type ScheduleDefinition, type ScheduleFired, } from "./schedules.js";
|
|
10
|
+
export { latestScores, writeScore, writeStepScore, EXISTING_SCORE_STATEMENT, LATEST_SCORES_STATEMENT, WRITE_SCORE_STATEMENT, type LatestScoreRow, type LatestScoresOptions, type ScoreWritten, type StepWriteScoreOptions, type WriteScoreOptions, } from "./scores.js";
|
|
11
|
+
export { assertActivityKind, insertActivity, listActivity, recordActivity, InvalidActivityKind, ACTIVITY_KIND_PATTERN, ACTIVITY_LIST_OPERATION, EXISTING_ACTIVITY_STATEMENT, INSERT_ACTIVITY_STATEMENT, type ActivityListOptions, type ActivityRecordOptions, type ActivityRecorded, type ActivityRow, type ActivityWrite, } from "./activity.js";
|
|
12
|
+
export { cancelOpenTasksForRecord, cancelTask, completeTask, createManualTask, createTask, flowOriginRef, listTasks, TASK_CANCEL_OPERATION, TASK_COMPLETE_OPERATION, TASK_CREATE_MANUAL_OPERATION, TASK_LIST_OPERATION, type TaskCloseOptions, type TaskClosed, type TaskCreateManualOptions, type TaskCreateOptions, type TaskCreated, type TaskListOptions, type TaskRow, type TaskTarget, } from "./tasks.js";
|
|
13
|
+
export { addLabel, listLabels, LABEL_ADD_OPERATION, LABEL_LIST_OPERATION, type LabelAddOptions, type LabelListOptions, type LabelRow, } from "./labels.js";
|
|
14
|
+
export { listOutcomes, recordOutcome, OUTCOME_LIST_OPERATION, OUTCOME_RECORD_OPERATION, type OutcomeListOptions, type OutcomeRecordOptions, type OutcomeRow, } from "./outcomes.js";
|
|
15
|
+
export { pauseApp, resumeApp, PAUSED_MARKER, PAUSE_OPERATION, RESUMED_MARKER, RESUME_OPERATION, type PauseOptions, type PauseResult, type ResumeOptions, type ResumeResult, } from "./pause.js";
|
|
16
|
+
export { archiveRecord, archiveDecisionKey, assertRecordStages, displayColumnOf, ARCHIVED_MARKER, ARCHIVE_OPERATION, DEFAULT_DISPLAY_COLUMN, type ArchiveOptions, type ArchiveResult, type RecordDefinition, type StageDefinition, } from "./records.js";
|
|
17
|
+
export { appStatus, CORE_VERSION, type PeriodStatus, type QueueStatus, type StatusOptions, type StatusReport, } from "./status.js";
|
|
18
|
+
export { createStatusHandler, statusRouteOf, STATUS_TOKEN_ACTOR, type StatusHandlerOptions, type StatusRoute, type StatusRouteHandlers, } from "./status-route.js";
|
|
19
|
+
export { bearerToken, hashStatusToken, statusTokenMatches, STATUS_TOKEN_DIGEST, } from "./status-token.js";
|
|
20
|
+
/**
|
|
21
|
+
* The workspace's types only — its functions are `@hyperfixation/core/workspace`. They are here
|
|
22
|
+
* because `App.workspace` names them.
|
|
23
|
+
*/
|
|
24
|
+
export type { AppWorkspace, DraftField, WorkspaceNavItem, WorkspaceRegistries, WorkspaceRoute, } from "./workspace.js";
|
|
25
|
+
export { workspaceBoard, workspaceHome, workspaceInbox, workspaceRecord, DEFAULT_BOARD_LIMIT, WORKSPACE_BOARD_OPERATION, WORKSPACE_HOME_OPERATION, WORKSPACE_INBOX_OPERATION, WORKSPACE_RECORD_OPERATION, type BoardCard, type BoardColumn, type BoardOptions, type BoardView, type HomeOptions, type HomeView, type InboxItem, type InboxOptions, type InboxView, type RecordView, type ReviewQueueCount, type TimelineGroup, type WorkspaceDecideOptions, type WorkspaceViewDeps, type WorkspaceViews, } from "./workspace-views.js";
|