@ontemper/platform 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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @ontemper/platform — Temper Platform SDK (Restate-powered)
3
+ *
4
+ * Thin wrapper around Restate's TypeScript SDK providing:
5
+ * - ctx.step(name, fn) — journaled step execution (replayed on retry)
6
+ * - ctx.sleep(durationMs) — durable timer (survives process restarts)
7
+ * - ctx.get/set(key, value) — durable K/V state
8
+ * - Dual-write to Postgres for API/dashboard queryability
9
+ *
10
+ * The Restate journal is the source of truth for execution replay.
11
+ * Postgres writes are best-effort for dashboard visibility.
12
+ */
13
+ import * as restate from "@restatedev/restate-sdk";
14
+ interface CreateWorkflowOptions {
15
+ name?: string;
16
+ handler: (ctx: TemperContext, input: unknown) => Promise<unknown>;
17
+ }
18
+ /**
19
+ * Wrapper context that exposes safe operations only.
20
+ * Does NOT expose raw Restate context methods like genericCall() or objectClient()
21
+ * to enforce the sandbox trust model.
22
+ */
23
+ declare class TemperContext {
24
+ private ctx;
25
+ private executionId;
26
+ constructor(ctx: restate.WorkflowContext, executionId: string);
27
+ /** Run a named step with journaling. On replay, returns the cached result. */
28
+ step<T>(name: string, fn: () => Promise<T>): Promise<T>;
29
+ /** Durable sleep — survives process restarts. Restate manages the timer. */
30
+ sleep(durationMs: number): Promise<void>;
31
+ /** Get a durable state value. */
32
+ get<T>(key: string): Promise<T | null>;
33
+ /** Set a durable state value. */
34
+ set<T>(key: string, value: T): Promise<void>;
35
+ }
36
+ export declare function createWorkflow({ name, handler }: CreateWorkflowOptions): void;
37
+ export declare const temper: {
38
+ createWorkflow: typeof createWorkflow;
39
+ };
40
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @ontemper/platform — Temper Platform SDK (Restate-powered)
3
+ *
4
+ * Thin wrapper around Restate's TypeScript SDK providing:
5
+ * - ctx.step(name, fn) — journaled step execution (replayed on retry)
6
+ * - ctx.sleep(durationMs) — durable timer (survives process restarts)
7
+ * - ctx.get/set(key, value) — durable K/V state
8
+ * - Dual-write to Postgres for API/dashboard queryability
9
+ *
10
+ * The Restate journal is the source of truth for execution replay.
11
+ * Postgres writes are best-effort for dashboard visibility.
12
+ */
13
+ import * as restate from "@restatedev/restate-sdk";
14
+ import { endpoint } from "@restatedev/restate-sdk/fetch";
15
+ const POSTGRES_WRITE_URL = process.env.ORCHESTRATOR_URL;
16
+ /**
17
+ * Wrapper context that exposes safe operations only.
18
+ * Does NOT expose raw Restate context methods like genericCall() or objectClient()
19
+ * to enforce the sandbox trust model.
20
+ */
21
+ class TemperContext {
22
+ ctx;
23
+ executionId;
24
+ constructor(ctx, executionId) {
25
+ this.ctx = ctx;
26
+ this.executionId = executionId;
27
+ }
28
+ /** Run a named step with journaling. On replay, returns the cached result. */
29
+ async step(name, fn) {
30
+ return this.ctx.run(name, async () => {
31
+ const result = await fn();
32
+ // Dual-write step result to Postgres for dashboard
33
+ if (POSTGRES_WRITE_URL) {
34
+ await fetch(`${POSTGRES_WRITE_URL}/internal/step-result`, {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify({
38
+ execution_id: this.executionId,
39
+ step_name: name,
40
+ output: result,
41
+ status: "completed",
42
+ }),
43
+ }).catch(() => { }); // best-effort, Restate journal is source of truth
44
+ }
45
+ return result;
46
+ });
47
+ }
48
+ /** Durable sleep — survives process restarts. Restate manages the timer. */
49
+ async sleep(durationMs) {
50
+ await this.ctx.sleep(durationMs);
51
+ }
52
+ /** Get a durable state value. */
53
+ async get(key) {
54
+ return this.ctx.get(key);
55
+ }
56
+ /** Set a durable state value. */
57
+ async set(key, value) {
58
+ this.ctx.set(key, value);
59
+ }
60
+ }
61
+ function writeDualStatus(executionId, status, extra) {
62
+ if (!POSTGRES_WRITE_URL)
63
+ return;
64
+ return fetch(`${POSTGRES_WRITE_URL}/internal/execution-status`, {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
67
+ body: JSON.stringify({ execution_id: executionId, status, ...extra }),
68
+ }).catch(() => { }); // best-effort
69
+ }
70
+ export function createWorkflow({ name, handler }) {
71
+ // Restate service names must match ^([a-zA-Z]|_[a-zA-Z0-9])[a-zA-Z0-9._-]*$
72
+ // UUIDs start with digits, so prefix with "wf_" to make them valid.
73
+ const rawName = name || process.env.WORKFLOW_ID || "WorkflowRunner";
74
+ const workflowName = rawName.match(/^[a-zA-Z_]/) ? rawName : `wf_${rawName}`;
75
+ const workflow = restate.workflow({
76
+ name: workflowName,
77
+ handlers: {
78
+ run: async (ctx, input) => {
79
+ const executionId = ctx.key; // Restate workflow key = execution ID
80
+ const temperCtx = new TemperContext(ctx, executionId);
81
+ // Extract metadata from the Restate invocation envelope.
82
+ // Ingest sends: { workflow_id, input, trigger_type, org_id }
83
+ const envelope = input;
84
+ const workflowId = envelope?.workflow_id;
85
+ const orgId = envelope?.org_id;
86
+ const triggerType = envelope?.trigger_type;
87
+ // Unwrap the actual user payload from the ingest envelope
88
+ const userInput = envelope?.input ?? input;
89
+ // Dual-write: mark running (UPSERT — creates execution_runs row if ingest didn't)
90
+ await ctx.run("_mark_running", async () => {
91
+ await writeDualStatus(executionId, "running", {
92
+ workflow_id: workflowId,
93
+ org_id: orgId,
94
+ trigger_type: triggerType,
95
+ });
96
+ });
97
+ try {
98
+ const output = await handler(temperCtx, userInput);
99
+ // Dual-write: mark completed
100
+ await ctx.run("_mark_completed", async () => {
101
+ await writeDualStatus(executionId, "completed", { output });
102
+ });
103
+ return output;
104
+ }
105
+ catch (err) {
106
+ // Dual-write: mark failed
107
+ await ctx.run("_mark_failed", async () => {
108
+ await writeDualStatus(executionId, "failed", {
109
+ error: err instanceof Error ? err.message : String(err),
110
+ });
111
+ });
112
+ throw err;
113
+ }
114
+ },
115
+ },
116
+ });
117
+ // Start Bun HTTP server with Restate endpoint + health check
118
+ const port = Number(process.env.PORT);
119
+ if (!port)
120
+ throw new Error("PORT env var required");
121
+ const restateHandler = endpoint().bind(workflow).handler();
122
+ Bun.serve({
123
+ port,
124
+ hostname: "0.0.0.0", // Bind IPv4 — Restate connects via IPv4
125
+ async fetch(req) {
126
+ const url = new URL(req.url);
127
+ if (url.pathname === "/health" || url.pathname === "/healthcheck") {
128
+ return new Response(JSON.stringify({ status: "ok" }), {
129
+ headers: { "Content-Type": "application/json" },
130
+ });
131
+ }
132
+ return restateHandler.fetch(req);
133
+ },
134
+ });
135
+ }
136
+ // Customer-facing API (backward-compatible module shape)
137
+ export const temper = { createWorkflow };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@ontemper/platform",
3
+ "version": "0.1.0",
4
+ "description": "Temper platform SDK for durable workflow execution",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "tsc"
19
+ },
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "sideEffects": false,
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@restatedev/restate-sdk": "^1.4.0"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "^5.5.0"
32
+ }
33
+ }