@donkeylabs/cli 0.1.0 → 0.1.1

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.
@@ -3,6 +3,7 @@ import { defineConfig } from "@donkeylabs/server";
3
3
  export default defineConfig({
4
4
  plugins: ["./src/server/plugins/**/index.ts"],
5
5
  outDir: ".@donkeylabs/server",
6
+ entry: "./src/server/index.ts",
6
7
  adapter: "@donkeylabs/adapter-sveltekit",
7
8
  client: {
8
9
  output: "./src/lib/api.ts",
@@ -4,15 +4,16 @@
4
4
  "version": "0.0.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "bun --bun vite dev",
8
- "build": "vite build",
7
+ "dev": "bun run gen:types && bun run dev:watch & bun --bun vite dev",
8
+ "dev:watch": "bun --watch --no-clear-screen scripts/watch-server.ts",
9
+ "build": "bun run gen:types && vite build",
9
10
  "preview": "bun build/server/entry.js",
10
11
  "prepare": "svelte-kit sync || echo ''",
11
12
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12
- "gen:types": "donkeylabs generate"
13
+ "gen:types": "donkeylabs generate",
14
+ "cli": "donkeylabs"
13
15
  },
14
16
  "devDependencies": {
15
- "@donkeylabs/cli": "*",
16
17
  "@sveltejs/kit": "^2.49.1",
17
18
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
18
19
  "@tailwindcss/vite": "^4.1.18",
@@ -23,8 +24,9 @@
23
24
  "vite": "^7.2.6"
24
25
  },
25
26
  "dependencies": {
26
- "@donkeylabs/adapter-sveltekit": "*",
27
- "@donkeylabs/server": "*",
27
+ "@donkeylabs/cli": "0.1.1",
28
+ "@donkeylabs/adapter-sveltekit": "0.1.2",
29
+ "@donkeylabs/server": "0.3.1",
28
30
  "bits-ui": "^2.15.4",
29
31
  "clsx": "^2.1.1",
30
32
  "kysely": "^0.27.6",
@@ -0,0 +1,55 @@
1
+ // Watch server files and regenerate types on changes
2
+ import { watch } from "node:fs";
3
+ import { exec } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { join } from "node:path";
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ const serverDir = join(import.meta.dir, "..", "src", "server");
10
+ let isGenerating = false;
11
+ let pendingGenerate = false;
12
+
13
+ async function regenerate() {
14
+ if (isGenerating) {
15
+ pendingGenerate = true;
16
+ return;
17
+ }
18
+
19
+ isGenerating = true;
20
+ console.log("\x1b[36m[watch]\x1b[0m Server files changed, regenerating types...");
21
+
22
+ try {
23
+ await execAsync("bun run gen:types");
24
+ console.log("\x1b[32m[watch]\x1b[0m Types regenerated successfully");
25
+ } catch (e: any) {
26
+ console.error("\x1b[31m[watch]\x1b[0m Error regenerating types:", e.message);
27
+ } finally {
28
+ isGenerating = false;
29
+ if (pendingGenerate) {
30
+ pendingGenerate = false;
31
+ await regenerate();
32
+ }
33
+ }
34
+ }
35
+
36
+ // Debounce to avoid multiple rapid regenerations
37
+ let debounceTimer: Timer | null = null;
38
+
39
+ function debouncedRegenerate() {
40
+ if (debounceTimer) clearTimeout(debounceTimer);
41
+ debounceTimer = setTimeout(regenerate, 300);
42
+ }
43
+
44
+ // Watch server directory recursively
45
+ watch(serverDir, { recursive: true }, (eventType, filename) => {
46
+ if (!filename) return;
47
+ if (!filename.endsWith(".ts")) return;
48
+
49
+ debouncedRegenerate();
50
+ });
51
+
52
+ console.log("\x1b[36m[watch]\x1b[0m Watching src/server/ for changes...");
53
+
54
+ // Keep process alive
55
+ await new Promise(() => {});
@@ -1,134 +1,74 @@
1
- /**
2
- * Generated API client for the demo
3
- * Extends UnifiedApiClientBase to handle SSR direct calls and browser HTTP calls
4
- */
5
- import { UnifiedApiClientBase } from "@donkeylabs/adapter-sveltekit/client";
6
-
7
- // Route type definitions
8
- export interface CounterResponse {
9
- count: number;
10
- }
11
-
12
- export interface CacheGetResponse {
13
- value: any;
14
- exists: boolean;
15
- }
16
-
17
- export interface CacheKeysResponse {
18
- keys: string[];
19
- size: number;
20
- }
21
-
22
- export interface JobEnqueueResponse {
23
- jobId: string;
24
- }
25
-
26
- export interface JobStatsResponse {
27
- pending: number;
28
- running: number;
29
- completed: number;
30
- }
31
-
32
- export interface CronTask {
33
- id: string;
34
- name: string;
35
- expression: string;
36
- enabled: boolean;
37
- lastRun?: string;
38
- nextRun?: string;
39
- }
40
-
41
- export interface CronListResponse {
42
- tasks: CronTask[];
43
- }
44
-
45
- export interface RateLimitResult {
46
- allowed: boolean;
47
- remaining: number;
48
- limit: number;
49
- resetAt: string;
50
- retryAfter?: number;
51
- }
1
+ // Auto-generated by donkeylabs generate
2
+ // DO NOT EDIT MANUALLY
52
3
 
53
- export interface SSEClientsResponse {
54
- total: number;
55
- byChannel: number;
56
- }
4
+ import { UnifiedApiClientBase, type ClientOptions } from "@donkeylabs/adapter-sveltekit/client";
57
5
 
58
- /**
59
- * Typed API client for the demo server
60
- */
61
6
  export class ApiClient extends UnifiedApiClientBase {
62
- // Counter routes
63
- counter = {
64
- get: () => this.request<{}, CounterResponse>("api.counter.get", {}),
65
- increment: () => this.request<{}, CounterResponse>("api.counter.increment", {}),
66
- decrement: () => this.request<{}, CounterResponse>("api.counter.decrement", {}),
67
- reset: () => this.request<{}, CounterResponse>("api.counter.reset", {}),
68
- };
69
-
70
- // Cache routes
71
- cache = {
72
- set: (input: { key: string; value: any; ttl?: number }) =>
73
- this.request<typeof input, { success: boolean }>("api.cache.set", input),
74
- get: (input: { key: string }) =>
75
- this.request<typeof input, CacheGetResponse>("api.cache.get", input),
76
- delete: (input: { key: string }) =>
77
- this.request<typeof input, { success: boolean }>("api.cache.delete", input),
78
- keys: () =>
79
- this.request<{}, CacheKeysResponse>("api.cache.keys", {}),
80
- };
81
-
82
- // Jobs routes
83
- jobs = {
84
- enqueue: (input: { name?: string; data?: any; delay?: number }) =>
85
- this.request<typeof input, JobEnqueueResponse>("api.jobs.enqueue", input),
86
- stats: () =>
87
- this.request<{}, JobStatsResponse>("api.jobs.stats", {}),
88
- };
89
-
90
- // Cron routes
91
- cron = {
92
- list: () =>
93
- this.request<{}, CronListResponse>("api.cron.list", {}),
94
- };
95
-
96
- // Rate limiter routes
97
- ratelimit = {
98
- check: (input: { key?: string; limit?: number; window?: number }) =>
99
- this.request<typeof input, RateLimitResult>("api.ratelimit.check", input),
100
- reset: (input: { key?: string }) =>
101
- this.request<typeof input, { success: boolean }>("api.ratelimit.reset", input),
102
- };
103
-
104
- // Events routes
105
- events = {
106
- emit: (input: { event?: string; data?: any }) =>
107
- this.request<typeof input, { success: boolean }>("api.events.emit", input),
108
- };
109
-
110
- // SSE routes
111
- sseRoutes = {
112
- broadcast: (input: { channel?: string; event?: string; data: any }) =>
113
- this.request<typeof input, { success: boolean }>("api.sse.broadcast", input),
114
- clients: () =>
115
- this.request<{}, SSEClientsResponse>("api.sse.clients", {}),
7
+ constructor(options?: ClientOptions) {
8
+ super(options);
9
+ }
10
+
11
+ api = {
12
+ counter: {
13
+ get: (input: any) => this.request("api.counter.get", input),
14
+ increment: (input: any) => this.request("api.counter.increment", input),
15
+ decrement: (input: any) => this.request("api.counter.decrement", input),
16
+ reset: (input: any) => this.request("api.counter.reset", input)
17
+ },
18
+ cache: {
19
+ set: (input: any) => this.request("api.cache.set", input),
20
+ get: (input: any) => this.request("api.cache.get", input),
21
+ delete: (input: any) => this.request("api.cache.delete", input),
22
+ keys: (input: any) => this.request("api.cache.keys", input)
23
+ },
24
+ jobs: {
25
+ enqueue: (input: any) => this.request("api.jobs.enqueue", input),
26
+ stats: (input: any) => this.request("api.jobs.stats", input)
27
+ },
28
+ cron: {
29
+ list: (input: any) => this.request("api.cron.list", input)
30
+ },
31
+ ratelimit: {
32
+ check: (input: any) => this.request("api.ratelimit.check", input),
33
+ reset: (input: any) => this.request("api.ratelimit.reset", input)
34
+ },
35
+ events: {
36
+ emit: (input: any) => this.request("api.events.emit", input)
37
+ },
38
+ sse: {
39
+ broadcast: (input: any) => this.request("api.sse.broadcast", input),
40
+ clients: (input: any) => this.request("api.sse.clients", input)
41
+ }
116
42
  };
117
43
  }
118
44
 
119
45
  /**
120
46
  * Create an API client instance
121
47
  *
122
- * @example
123
- * // In +page.server.ts (SSR - direct calls)
124
- * const api = createApi({ locals });
125
- * const data = await api.counter.get();
48
+ * @param options.locals - Pass SvelteKit locals for SSR direct calls (no HTTP overhead)
49
+ * @param options.baseUrl - Override the base URL for HTTP calls
50
+ *
51
+ * @example SSR usage in +page.server.ts:
52
+ * ```ts
53
+ * export const load = async ({ locals }) => {
54
+ * const api = createApi({ locals });
55
+ * const data = await api.myRoute.get({}); // Direct call, no HTTP!
56
+ * return { data };
57
+ * };
58
+ * ```
126
59
  *
127
- * @example
128
- * // In +page.svelte (browser - HTTP calls)
129
- * const api = createApi();
130
- * const data = await api.counter.get();
60
+ * @example Browser usage in +page.svelte:
61
+ * ```svelte
62
+ * <script>
63
+ * import { createApi } from '$lib/api';
64
+ * const api = createApi(); // HTTP calls
65
+ * let data = $state(null);
66
+ * async function load() {
67
+ * data = await api.myRoute.get({});
68
+ * }
69
+ * </script>
70
+ * ```
131
71
  */
132
- export function createApi(options?: { locals?: any }) {
72
+ export function createApi(options?: ClientOptions) {
133
73
  return new ApiClient(options);
134
74
  }
@@ -8,9 +8,9 @@ export const load: PageServerLoad = async ({ locals }) => {
8
8
 
9
9
  try {
10
10
  // Direct service call through typed client
11
- const { count } = await api.counter.get();
11
+ const result = await api.api.counter.get({}) as { count: number };
12
12
  return {
13
- count,
13
+ count: result.count,
14
14
  loadedAt: new Date().toISOString(),
15
15
  isSSR: true,
16
16
  };
@@ -1,155 +1,24 @@
1
1
  // Server entry for @donkeylabs/adapter-sveltekit
2
- import { AppServer, createPlugin, createRouter } from "@donkeylabs/server";
2
+ import { AppServer, createRouter } from "@donkeylabs/server";
3
3
  import { Kysely } from "kysely";
4
4
  import { BunSqliteDialect } from "kysely-bun-sqlite";
5
5
  import { Database } from "bun:sqlite";
6
6
  import { z } from "zod";
7
+ import { demoPlugin } from "./plugins/demo";
7
8
 
8
9
  // Simple in-memory database
9
10
  const db = new Kysely<{}>({
10
11
  dialect: new BunSqliteDialect({ database: new Database(":memory:") }),
11
12
  });
12
13
 
13
- // Random event messages for SSE demo
14
- const eventMessages = [
15
- "User logged in",
16
- "New order placed",
17
- "Payment received",
18
- "Item shipped",
19
- "Review submitted",
20
- "Comment added",
21
- "File uploaded",
22
- "Task completed",
23
- "Alert triggered",
24
- "Sync finished",
25
- ];
26
-
27
- // Demo plugin with all core service integrations
28
- const demoPlugin = createPlugin.define({
29
- name: "demo",
30
- service: async (ctx) => {
31
- let counter = 0;
32
-
33
- return {
34
- // Counter
35
- getCounter: () => counter,
36
- increment: () => ++counter,
37
- decrement: () => --counter,
38
- reset: () => { counter = 0; return counter; },
39
-
40
- // Cache helpers
41
- cacheSet: async (key: string, value: any, ttl?: number) => {
42
- await ctx.core.cache.set(key, value, ttl);
43
- return { success: true };
44
- },
45
- cacheGet: async (key: string) => {
46
- const value = await ctx.core.cache.get(key);
47
- const exists = await ctx.core.cache.has(key);
48
- return { value, exists };
49
- },
50
- cacheDelete: async (key: string) => {
51
- await ctx.core.cache.delete(key);
52
- return { success: true };
53
- },
54
- cacheKeys: async () => {
55
- const keys = await ctx.core.cache.keys();
56
- return { keys, size: keys.length };
57
- },
58
-
59
- // Jobs helpers
60
- enqueueJob: async (name: string, data: any, delay?: number) => {
61
- let jobId: string;
62
- if (delay && delay > 0) {
63
- const runAt = new Date(Date.now() + delay);
64
- jobId = await ctx.core.jobs.schedule(name, data, runAt);
65
- } else {
66
- jobId = await ctx.core.jobs.enqueue(name, data);
67
- }
68
- return { jobId };
69
- },
70
- getJobStats: async () => {
71
- const pending = await ctx.core.jobs.getByName("demo-job", "pending");
72
- const running = await ctx.core.jobs.getByName("demo-job", "running");
73
- const completed = await ctx.core.jobs.getByName("demo-job", "completed");
74
- return {
75
- pending: pending.length,
76
- running: running.length,
77
- completed: completed.length,
78
- };
79
- },
80
-
81
- // Cron helpers
82
- getCronTasks: () => ctx.core.cron.list().map(t => ({
83
- id: t.id,
84
- name: t.name,
85
- expression: t.expression,
86
- enabled: t.enabled,
87
- lastRun: t.lastRun?.toISOString(),
88
- nextRun: t.nextRun?.toISOString(),
89
- })),
90
-
91
- // Rate limiter helpers
92
- checkRateLimit: async (key: string, limit: number, window: number) => {
93
- return ctx.core.rateLimiter.check(key, limit, window);
94
- },
95
- resetRateLimit: async (key: string) => {
96
- await ctx.core.rateLimiter.reset(key);
97
- return { success: true };
98
- },
99
-
100
- // Events helpers (internal pub/sub)
101
- emitEvent: async (event: string, data: any) => {
102
- await ctx.core.events.emit(event, data);
103
- return { success: true };
104
- },
105
-
106
- // SSE broadcast
107
- broadcast: (channel: string, event: string, data: any) => {
108
- ctx.core.sse.broadcast(channel, event, data);
109
- return { success: true };
110
- },
111
- getSSEClients: () => ({
112
- total: ctx.core.sse.getClients().length,
113
- byChannel: ctx.core.sse.getClientsByChannel("events").length,
114
- }),
115
- };
116
- },
117
- init: async (ctx) => {
118
- // Register job handler for demo
119
- ctx.core.jobs.register("demo-job", async (data) => {
120
- ctx.core.logger.info("Demo job executed", { data });
121
- // Broadcast job completion via SSE
122
- ctx.core.sse.broadcast("events", "job-completed", {
123
- id: Date.now(),
124
- message: `Job completed: ${data.message || "No message"}`,
125
- timestamp: new Date().toISOString(),
126
- });
127
- });
128
-
129
- // Schedule cron job to broadcast SSE events every 5 seconds
130
- ctx.core.cron.schedule("*/5 * * * * *", () => {
131
- const message = eventMessages[Math.floor(Math.random() * eventMessages.length)];
132
- ctx.core.sse.broadcast("events", "cron-event", {
133
- id: Date.now(),
134
- message,
135
- timestamp: new Date().toISOString(),
136
- source: "cron",
137
- });
138
- }, { name: "sse-broadcaster" });
14
+ // Create server
15
+ export const server = new AppServer({
16
+ db,
17
+ port: 0, // Port managed by adapter
18
+ });
139
19
 
140
- // Listen for internal events and broadcast to SSE
141
- ctx.core.events.on("demo.*", (data) => {
142
- ctx.core.sse.broadcast("events", "internal-event", {
143
- id: Date.now(),
144
- message: `Internal event: ${JSON.stringify(data)}`,
145
- timestamp: new Date().toISOString(),
146
- source: "events",
147
- });
148
- });
20
+ server.registerPlugin(demoPlugin);
149
21
 
150
- ctx.core.logger.info("Demo plugin initialized with all core services");
151
- },
152
- });
153
22
 
154
23
  // Create routes
155
24
  const api = createRouter("api");
@@ -202,7 +71,7 @@ api.route("jobs.enqueue").typed({
202
71
  data: z.any().default({}),
203
72
  delay: z.number().optional()
204
73
  }),
205
- handle: async (input, ctx) => ctx.plugins.demo.enqueueJob(input.name, input.data, input.delay),
74
+ handle: async (input, ctx) => ctx.plugins.demo.enqueueJob(input.name!, input.data, input.delay),
206
75
  });
207
76
 
208
77
  api.route("jobs.stats").typed({
@@ -221,12 +90,12 @@ api.route("ratelimit.check").typed({
221
90
  limit: z.number().default(5),
222
91
  window: z.number().default(60000)
223
92
  }),
224
- handle: async (input, ctx) => ctx.plugins.demo.checkRateLimit(input.key, input.limit, input.window),
93
+ handle: async (input, ctx) => ctx.plugins.demo.checkRateLimit(input.key!, input.limit!, input.window!),
225
94
  });
226
95
 
227
96
  api.route("ratelimit.reset").typed({
228
97
  input: z.object({ key: z.string().default("demo") }),
229
- handle: async (input, ctx) => ctx.plugins.demo.resetRateLimit(input.key),
98
+ handle: async (input, ctx) => ctx.plugins.demo.resetRateLimit(input.key!),
230
99
  });
231
100
 
232
101
  // Events routes (internal pub/sub)
@@ -235,7 +104,7 @@ api.route("events.emit").typed({
235
104
  event: z.string().default("demo.test"),
236
105
  data: z.any().default({ test: true })
237
106
  }),
238
- handle: async (input, ctx) => ctx.plugins.demo.emitEvent(input.event, input.data),
107
+ handle: async (input, ctx) => ctx.plugins.demo.emitEvent(input.event!, input.data),
239
108
  });
240
109
 
241
110
  // SSE routes
@@ -245,19 +114,24 @@ api.route("sse.broadcast").typed({
245
114
  event: z.string().default("manual"),
246
115
  data: z.any()
247
116
  }),
248
- handle: async (input, ctx) => ctx.plugins.demo.broadcast(input.channel, input.event, input.data),
117
+ handle: async (input, ctx) => ctx.plugins.demo.broadcast(input.channel!, input.event!, input.data),
249
118
  });
250
119
 
251
120
  api.route("sse.clients").typed({
252
121
  handle: async (_input, ctx) => ctx.plugins.demo.getSSEClients(),
253
122
  });
254
123
 
255
- // Create server
256
- export const server = new AppServer({
257
- db,
258
- port: 0, // Port managed by adapter
259
- });
260
124
 
261
125
  // Register plugin and routes
262
- server.registerPlugin(demoPlugin);
263
126
  server.use(api);
127
+
128
+ // Handle DONKEYLABS_GENERATE for type generation
129
+ if (process.env.DONKEYLABS_GENERATE === "1") {
130
+ // Extract routes and output as JSON for CLI
131
+ const routes = api.getRoutes().map((r) => ({
132
+ name: r.name,
133
+ handler: r.handler || "typed",
134
+ }));
135
+ console.log(JSON.stringify({ routes }));
136
+ process.exit(0);
137
+ }
@@ -0,0 +1,144 @@
1
+ // Demo plugin with all core service integrations
2
+ import { createPlugin } from "@donkeylabs/server";
3
+
4
+
5
+ // Random event messages for SSE demo
6
+ const eventMessages = [
7
+ "User logged in",
8
+ "New order placed",
9
+ "Payment received",
10
+ "Item shipped",
11
+ "Review submitted",
12
+ "Comment added",
13
+ "File uploaded",
14
+ "Task completed",
15
+ "Alert triggered",
16
+ "Sync finished",
17
+ ];
18
+
19
+
20
+ export const demoPlugin = createPlugin.define({
21
+ name: "demo",
22
+ service: async (ctx) => {
23
+ let counter = 0;
24
+
25
+ return {
26
+ // Counter
27
+ getCounter: () => counter,
28
+ increment: () => ++counter,
29
+ decrement: () => --counter,
30
+ reset: () => { counter = 0; return counter; },
31
+
32
+ // Cache helpers
33
+ cacheSet: async (key: string, value: any, ttl?: number) => {
34
+ await ctx.core.cache.set(key, value, ttl);
35
+ return { success: true };
36
+ },
37
+ cacheGet: async (key: string) => {
38
+ const value = await ctx.core.cache.get(key);
39
+ const exists = await ctx.core.cache.has(key);
40
+ return { value, exists };
41
+ },
42
+ cacheDelete: async (key: string) => {
43
+ await ctx.core.cache.delete(key);
44
+ return { success: true };
45
+ },
46
+ cacheKeys: async () => {
47
+ const keys = await ctx.core.cache.keys();
48
+ return { keys, size: keys.length };
49
+ },
50
+
51
+ // Jobs helpers
52
+ enqueueJob: async (name: string, data: any, delay?: number) => {
53
+ let jobId: string;
54
+ if (delay && delay > 0) {
55
+ const runAt = new Date(Date.now() + delay);
56
+ jobId = await ctx.core.jobs.schedule(name, data, runAt);
57
+ } else {
58
+ jobId = await ctx.core.jobs.enqueue(name, data);
59
+ }
60
+ return { jobId };
61
+ },
62
+ getJobStats: async () => {
63
+ const pending = await ctx.core.jobs.getByName("demo-job", "pending");
64
+ const running = await ctx.core.jobs.getByName("demo-job", "running");
65
+ const completed = await ctx.core.jobs.getByName("demo-job", "completed");
66
+ return {
67
+ pending: pending.length,
68
+ running: running.length,
69
+ completed: completed.length,
70
+ };
71
+ },
72
+
73
+ // Cron helpers
74
+ getCronTasks: () => ctx.core.cron.list().map(t => ({
75
+ id: t.id,
76
+ name: t.name,
77
+ expression: t.expression,
78
+ enabled: t.enabled,
79
+ lastRun: t.lastRun?.toISOString(),
80
+ nextRun: t.nextRun?.toISOString(),
81
+ })),
82
+
83
+ // Rate limiter helpers
84
+ checkRateLimit: async (key: string, limit: number, window: number) => {
85
+ return ctx.core.rateLimiter.check(key, limit, window);
86
+ },
87
+ resetRateLimit: async (key: string) => {
88
+ await ctx.core.rateLimiter.reset(key);
89
+ return { success: true };
90
+ },
91
+
92
+ // Events helpers (internal pub/sub)
93
+ emitEvent: async (event: string, data: any) => {
94
+ await ctx.core.events.emit(event, data);
95
+ return { success: true };
96
+ },
97
+
98
+ // SSE broadcast
99
+ broadcast: (channel: string, event: string, data: any) => {
100
+ ctx.core.sse.broadcast(channel, event, data);
101
+ return { success: true };
102
+ },
103
+ getSSEClients: () => ({
104
+ total: ctx.core.sse.getClients().length,
105
+ byChannel: ctx.core.sse.getClientsByChannel("events").length,
106
+ }),
107
+ };
108
+ },
109
+ init: async (ctx) => {
110
+ // Register job handler for demo
111
+ ctx.core.jobs.register("demo-job", async (data) => {
112
+ ctx.core.logger.info("Demo job executed", { data });
113
+ // Broadcast job completion via SSE
114
+ ctx.core.sse.broadcast("events", "job-completed", {
115
+ id: Date.now(),
116
+ message: `Job completed: ${data.message || "No message"}`,
117
+ timestamp: new Date().toISOString(),
118
+ });
119
+ });
120
+
121
+ // Schedule cron job to broadcast SSE events every 5 seconds
122
+ ctx.core.cron.schedule("*/5 * * * * *", () => {
123
+ const message = eventMessages[Math.floor(Math.random() * eventMessages.length)];
124
+ ctx.core.sse.broadcast("events", "cron-event", {
125
+ id: Date.now(),
126
+ message,
127
+ timestamp: new Date().toISOString(),
128
+ source: "cron",
129
+ });
130
+ }, { name: "sse-broadcaster" });
131
+
132
+ // Listen for internal events and broadcast to SSE
133
+ ctx.core.events.on("demo.*", (data) => {
134
+ ctx.core.sse.broadcast("events", "internal-event", {
135
+ id: Date.now(),
136
+ message: `Internal event: ${JSON.stringify(data)}`,
137
+ timestamp: new Date().toISOString(),
138
+ source: "events",
139
+ });
140
+ });
141
+
142
+ ctx.core.logger.info("Demo plugin initialized with all core services");
143
+ },
144
+ });