@ventually/ui 0.0.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,303 @@
1
+ import { FlowProducer, Queue, Scheduler, UnrecoverableError, Worker, } from "@ventually/core";
2
+ import { MemoryAdapter } from "@ventually/memory";
3
+ const QUEUE_CONFIG = {
4
+ email: {
5
+ description: "Transactional email delivery with retries and recurring heartbeats.",
6
+ concurrency: 3,
7
+ pollIntervalMs: 20,
8
+ attempts: 3,
9
+ removeOnCompleteCount: 30,
10
+ removeOnFailCount: 30,
11
+ recurringEnabled: true,
12
+ },
13
+ fetcher: {
14
+ description: "Fetches raw document content before downstream processing.",
15
+ concurrency: 1,
16
+ pollIntervalMs: 20,
17
+ attempts: 1,
18
+ removeOnCompleteCount: 0,
19
+ removeOnFailCount: 0,
20
+ recurringEnabled: false,
21
+ },
22
+ chunker: {
23
+ description: "Splits fetched content into smaller processing chunks.",
24
+ concurrency: 1,
25
+ pollIntervalMs: 20,
26
+ attempts: 1,
27
+ removeOnCompleteCount: 0,
28
+ removeOnFailCount: 0,
29
+ recurringEnabled: false,
30
+ },
31
+ embeddings: {
32
+ description: "Generates embeddings for prepared chunks.",
33
+ concurrency: 1,
34
+ pollIntervalMs: 20,
35
+ attempts: 1,
36
+ removeOnCompleteCount: 0,
37
+ removeOnFailCount: 0,
38
+ recurringEnabled: false,
39
+ },
40
+ vectorstore: {
41
+ description: "Persists vectors into the final storage layer.",
42
+ concurrency: 1,
43
+ pollIntervalMs: 20,
44
+ attempts: 1,
45
+ removeOnCompleteCount: 0,
46
+ removeOnFailCount: 0,
47
+ recurringEnabled: false,
48
+ },
49
+ };
50
+ export class EventuallyUIRuntime {
51
+ offsetMs = 0;
52
+ adapter = new MemoryAdapter();
53
+ events = [];
54
+ workers = new Map();
55
+ scheduleHandles = new Map();
56
+ emailQueue = new Queue("email", {
57
+ adapter: this.adapter,
58
+ now: () => this.now(),
59
+ defaultJobOptions: {
60
+ attempts: 3,
61
+ removeOnComplete: { count: 30 },
62
+ removeOnFail: { count: 30 },
63
+ },
64
+ });
65
+ scheduler = new Scheduler("email", {
66
+ adapter: this.adapter,
67
+ now: () => this.now(),
68
+ defaultJobOptions: {
69
+ removeOnComplete: { count: 30 },
70
+ removeOnFail: { count: 30 },
71
+ },
72
+ });
73
+ flowProducer = new FlowProducer({
74
+ adapter: this.adapter,
75
+ now: () => this.now(),
76
+ });
77
+ constructor() {
78
+ this.registerWorker("email", new Worker("email", async (job) => {
79
+ await job.updateProgress(25);
80
+ await this.sleep(50);
81
+ if (job.data.subject.toLowerCase().includes("fail")) {
82
+ throw new UnrecoverableError("Simulated delivery failure");
83
+ }
84
+ await job.updateProgress(100);
85
+ return {
86
+ messageId: `msg_${job.id.slice(0, 8)}`,
87
+ };
88
+ }, { adapter: this.adapter, concurrency: 3, pollInterval: 20, now: () => this.now() }));
89
+ this.registerWorker("fetcher", new Worker("fetcher", async (job) => {
90
+ await this.sleep(30);
91
+ return { body: `Fetched: ${job.data.value}` };
92
+ }, { adapter: this.adapter, pollInterval: 20, now: () => this.now() }));
93
+ this.registerWorker("chunker", new Worker("chunker", async (job) => {
94
+ await this.sleep(30);
95
+ return { chunks: job.data.body.split(/\s+/).filter(Boolean) };
96
+ }, { adapter: this.adapter, pollInterval: 20, now: () => this.now() }));
97
+ this.registerWorker("embeddings", new Worker("embeddings", async (job) => {
98
+ await this.sleep(40);
99
+ return { vectors: job.data.chunks.length || 4 };
100
+ }, { adapter: this.adapter, pollInterval: 20, now: () => this.now() }));
101
+ this.registerWorker("vectorstore", new Worker("vectorstore", async (job) => {
102
+ await this.sleep(40);
103
+ return { upserted: job.data.vectors || 4 };
104
+ }, { adapter: this.adapter, pollInterval: 20, now: () => this.now() }));
105
+ }
106
+ start() {
107
+ for (const worker of this.workers.values()) {
108
+ void worker.run();
109
+ }
110
+ }
111
+ snapshot() {
112
+ const queues = this.adapter.getAllQueuesSnapshot().map((queue) => this.toQueueSnapshot(queue));
113
+ return {
114
+ now: this.now(),
115
+ queues,
116
+ events: this.events.slice(-80).reverse(),
117
+ workers: [...this.workers.entries()].map(([queue, worker]) => ({
118
+ queue,
119
+ paused: worker.__paused ?? false,
120
+ })),
121
+ };
122
+ }
123
+ async enqueueEmail(kind) {
124
+ const subjectMap = {
125
+ welcome: "Welcome aboard",
126
+ digest: "Daily digest",
127
+ failure: "Fail this message",
128
+ };
129
+ await this.emailQueue.add(kind, {
130
+ to: "alice@example.com",
131
+ subject: subjectMap[kind],
132
+ html: `<p>${subjectMap[kind]}</p>`,
133
+ }, {
134
+ priority: kind === "welcome" ? 10 : 3,
135
+ delay: kind === "digest" ? "5s" : 0,
136
+ });
137
+ }
138
+ async enqueueFlow() {
139
+ await this.flowProducer.add({
140
+ name: "upsert-vectors",
141
+ queueName: "vectorstore",
142
+ data: { vectors: 0 },
143
+ children: [
144
+ {
145
+ name: "embed-chunks",
146
+ queueName: "embeddings",
147
+ data: { chunks: [] },
148
+ children: [
149
+ {
150
+ name: "chunk-document",
151
+ queueName: "chunker",
152
+ data: { body: "" },
153
+ children: [
154
+ {
155
+ name: "fetch-document",
156
+ queueName: "fetcher",
157
+ data: { value: "https://example.com/doc.pdf" },
158
+ },
159
+ ],
160
+ },
161
+ ],
162
+ },
163
+ ],
164
+ });
165
+ }
166
+ createRecurringEmail() {
167
+ const handle = this.scheduler.every("heartbeat", {
168
+ to: "ops@example.com",
169
+ subject: "Heartbeat",
170
+ html: "<p>System pulse</p>",
171
+ }, "15s", { jitter: "2s" });
172
+ this.scheduleHandles.set(handle.id, handle);
173
+ this.pushEvent({
174
+ queue: "email",
175
+ type: "scheduled",
176
+ jobId: handle.id,
177
+ name: "heartbeat",
178
+ detail: "Recurring email schedule started",
179
+ });
180
+ return handle.id;
181
+ }
182
+ cancelSchedule(id) {
183
+ this.scheduleHandles.get(id)?.cancel();
184
+ this.scheduleHandles.delete(id);
185
+ }
186
+ pauseWorker(queue) {
187
+ const worker = this.workers.get(queue);
188
+ if (!worker) {
189
+ return;
190
+ }
191
+ worker.pause();
192
+ worker.__paused = true;
193
+ }
194
+ resumeWorker(queue) {
195
+ const worker = this.workers.get(queue);
196
+ if (!worker) {
197
+ return;
198
+ }
199
+ worker.resume();
200
+ worker.__paused = false;
201
+ }
202
+ advanceTime(ms) {
203
+ this.offsetMs += ms;
204
+ this.adapter.advanceTime(ms);
205
+ }
206
+ toQueueSnapshot(queue) {
207
+ const config = QUEUE_CONFIG[queue.name] ?? {
208
+ description: "Queue configuration is not available.",
209
+ concurrency: 1,
210
+ pollIntervalMs: 20,
211
+ attempts: 1,
212
+ removeOnCompleteCount: 0,
213
+ removeOnFailCount: 0,
214
+ recurringEnabled: false,
215
+ };
216
+ return {
217
+ name: queue.name,
218
+ config,
219
+ counts: queue.counts,
220
+ jobs: queue.jobs.map((job) => ({
221
+ id: job.id,
222
+ name: job.name,
223
+ state: job.state,
224
+ attempt: job.attempt,
225
+ attempts: job.attempts,
226
+ createdAt: job.createdAt,
227
+ processedAt: job.processedAt,
228
+ finishedAt: job.finishedAt,
229
+ availableAt: job.availableAt,
230
+ failedReason: job.failedReason,
231
+ })),
232
+ };
233
+ }
234
+ registerWorker(queue, worker) {
235
+ worker.on("active", (job) => {
236
+ this.pushEvent({
237
+ queue,
238
+ type: "active",
239
+ jobId: job.id,
240
+ name: job.name,
241
+ detail: `attempt ${job.attempt}/${job.attempts}`,
242
+ });
243
+ });
244
+ worker.on("progress", (job, progress) => {
245
+ this.pushEvent({
246
+ queue,
247
+ type: "progress",
248
+ jobId: job.id,
249
+ name: job.name,
250
+ detail: JSON.stringify(progress),
251
+ });
252
+ });
253
+ worker.on("completed", (job, result) => {
254
+ this.pushEvent({
255
+ queue,
256
+ type: "completed",
257
+ jobId: job.id,
258
+ name: job.name,
259
+ detail: JSON.stringify(result),
260
+ });
261
+ });
262
+ worker.on("failed", (job, error) => {
263
+ this.pushEvent({
264
+ queue,
265
+ type: "failed",
266
+ jobId: job.id,
267
+ name: job.name,
268
+ detail: error.message,
269
+ });
270
+ });
271
+ worker.on("retrying", (job, error, nextRetryAt) => {
272
+ this.pushEvent({
273
+ queue,
274
+ type: "retrying",
275
+ jobId: job.id,
276
+ name: job.name,
277
+ detail: `${error.message} -> ${new Date(nextRetryAt).toLocaleTimeString()}`,
278
+ });
279
+ });
280
+ this.workers.set(queue, worker);
281
+ }
282
+ pushEvent(entry) {
283
+ this.events.push({
284
+ ...entry,
285
+ id: `${entry.queue}:${entry.jobId}:${this.now()}:${this.events.length}`,
286
+ at: this.now(),
287
+ });
288
+ if (this.events.length > 200) {
289
+ this.events.splice(0, this.events.length - 200);
290
+ }
291
+ }
292
+ now() {
293
+ return Date.now() + this.offsetMs;
294
+ }
295
+ async sleep(ms) {
296
+ await new Promise((resolve) => setTimeout(resolve, ms));
297
+ }
298
+ }
299
+ export function getEventuallyUIRuntime() {
300
+ globalThis.__eventuallyUIRuntime ??= new EventuallyUIRuntime();
301
+ globalThis.__eventuallyUIRuntime.start();
302
+ return globalThis.__eventuallyUIRuntime;
303
+ }
@@ -0,0 +1,71 @@
1
+ export type EventuallyUIJobState = "waiting" | "blocked" | "active" | "delayed" | "completed" | "failed";
2
+ export interface EventuallyUIJob {
3
+ id: string;
4
+ name: string;
5
+ state: EventuallyUIJobState;
6
+ attempt: number;
7
+ attempts: number;
8
+ createdAt: number;
9
+ processedAt: number | null;
10
+ finishedAt: number | null;
11
+ availableAt: number;
12
+ failedReason: string | null;
13
+ }
14
+ export interface EventuallyUIQueueSnapshot {
15
+ name: string;
16
+ config: {
17
+ description: string;
18
+ concurrency: number;
19
+ pollIntervalMs: number;
20
+ attempts: number;
21
+ removeOnCompleteCount: number;
22
+ removeOnFailCount: number;
23
+ recurringEnabled: boolean;
24
+ };
25
+ counts: Record<EventuallyUIJobState, number>;
26
+ jobs: EventuallyUIJob[];
27
+ }
28
+ export interface EventuallyUIEvent {
29
+ id: string;
30
+ queue: string;
31
+ type: string;
32
+ jobId: string;
33
+ name: string;
34
+ at: number;
35
+ detail: string;
36
+ }
37
+ export interface EventuallyUIWorker {
38
+ queue: string;
39
+ paused: boolean;
40
+ }
41
+ export interface EventuallyUISnapshot {
42
+ now: number;
43
+ queues: EventuallyUIQueueSnapshot[];
44
+ events: EventuallyUIEvent[];
45
+ workers: EventuallyUIWorker[];
46
+ }
47
+ export type EventuallyUIAction = {
48
+ action: "enqueue-job";
49
+ queue: string;
50
+ payload: unknown;
51
+ } | {
52
+ action: "enqueue-email";
53
+ kind: "welcome" | "digest" | "failure";
54
+ } | {
55
+ action: "enqueue-flow";
56
+ } | {
57
+ action: "schedule-heartbeat";
58
+ } | {
59
+ action: "cancel-schedule";
60
+ id: string;
61
+ } | {
62
+ action: "pause-worker";
63
+ queue: string;
64
+ } | {
65
+ action: "resume-worker";
66
+ queue: string;
67
+ } | {
68
+ action: "advance-time";
69
+ ms: number;
70
+ };
71
+ //# sourceMappingURL=shared-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared-types.d.ts","sourceRoot":"","sources":["../src/shared-types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,SAAS,GACT,SAAS,GACT,QAAQ,GACR,SAAS,GACT,WAAW,GACX,QAAQ,CAAC;AAEb,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,oBAAoB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QACN,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;QACpB,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,gBAAgB,EAAE,OAAO,CAAC;KAC3B,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC;IAC7C,IAAI,EAAE,eAAe,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,yBAAyB,EAAE,CAAC;IACpC,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,OAAO,EAAE,kBAAkB,EAAE,CAAC;CAC/B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1D;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,IAAI,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAA;CAAE,GACnE;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,GAC1B;IAAE,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAChC;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};