@checkstack/queue-frontend 0.1.0 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,95 @@
1
1
  # @checkstack/queue-frontend
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [4eed42d]
8
+ - @checkstack/frontend-api@0.3.0
9
+ - @checkstack/ui@0.2.2
10
+
11
+ ## 0.2.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 180be38: # Queue Lag Warning
16
+
17
+ Added a queue lag warning system that displays alerts when pending jobs exceed configurable thresholds.
18
+
19
+ ## Features
20
+
21
+ - **Backend Stats API**: New `getStats`, `getLagStatus`, and `updateLagThresholds` RPC endpoints
22
+ - **Signal-based Updates**: `QUEUE_LAG_CHANGED` signal for real-time frontend updates
23
+ - **Aggregated Stats**: `QueueManager.getAggregatedStats()` sums stats across all queues
24
+ - **Configurable Thresholds**: Warning (default 100) and Critical (default 500) thresholds stored in config
25
+ - **Dashboard Integration**: Queue lag alert displayed on main Dashboard (access-gated)
26
+ - **Queue Settings Page**: Lag alert and Performance Tuning guidance card with concurrency tips
27
+
28
+ ## UI Changes
29
+
30
+ - Queue lag alert banner appears on Dashboard and Queue Settings when pending jobs exceed thresholds
31
+ - New "Performance Tuning" card with concurrency settings guidance and bottleneck indicators
32
+
33
+ - 7a23261: ## TanStack Query Integration
34
+
35
+ Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
36
+
37
+ ### New Features
38
+
39
+ - **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
40
+ - **Automatic request deduplication**: Multiple components requesting the same data share a single network request
41
+ - **Built-in caching**: Configurable stale time and cache duration per query
42
+ - **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
43
+ - **Background refetching**: Stale data is automatically refreshed when components mount
44
+
45
+ ### Contract Changes
46
+
47
+ All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
48
+
49
+ ```typescript
50
+ const getItems = proc()
51
+ .meta({ operationType: "query", access: [access.read] })
52
+ .output(z.array(itemSchema))
53
+ .query();
54
+
55
+ const createItem = proc()
56
+ .meta({ operationType: "mutation", access: [access.manage] })
57
+ .input(createItemSchema)
58
+ .output(itemSchema)
59
+ .mutation();
60
+ ```
61
+
62
+ ### Migration
63
+
64
+ ```typescript
65
+ // Before (forPlugin pattern)
66
+ const api = useApi(myPluginApiRef);
67
+ const [items, setItems] = useState<Item[]>([]);
68
+ useEffect(() => {
69
+ api.getItems().then(setItems);
70
+ }, [api]);
71
+
72
+ // After (usePluginClient pattern)
73
+ const client = usePluginClient(MyPluginApi);
74
+ const { data: items, isLoading } = client.getItems.useQuery({});
75
+ ```
76
+
77
+ ### Bug Fixes
78
+
79
+ - Fixed `rpc.test.ts` test setup for middleware type inference
80
+ - Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
81
+ - Fixed null→undefined warnings in notification and queue frontends
82
+
83
+ ### Patch Changes
84
+
85
+ - Updated dependencies [180be38]
86
+ - Updated dependencies [7a23261]
87
+ - @checkstack/queue-common@0.2.0
88
+ - @checkstack/frontend-api@0.2.0
89
+ - @checkstack/common@0.3.0
90
+ - @checkstack/ui@0.2.1
91
+ - @checkstack/signal-frontend@0.0.7
92
+
3
93
  ## 0.1.0
4
94
 
5
95
  ### Minor Changes
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@checkstack/queue-frontend",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.tsx"
10
+ }
11
+ },
7
12
  "scripts": {
8
13
  "build": "tsc",
9
14
  "dev": "tsc --watch",
@@ -15,6 +20,7 @@
15
20
  "@checkstack/common": "workspace:*",
16
21
  "@checkstack/frontend-api": "workspace:*",
17
22
  "@checkstack/queue-common": "workspace:*",
23
+ "@checkstack/signal-frontend": "workspace:*",
18
24
  "@checkstack/ui": "workspace:*",
19
25
  "ajv": "^8.17.1",
20
26
  "ajv-formats": "^3.0.1",
package/src/api.ts CHANGED
@@ -1,15 +1,8 @@
1
- import { createApiRef } from "@checkstack/frontend-api";
2
- import { QueueApi } from "@checkstack/queue-common";
3
- import type { InferClient } from "@checkstack/common";
4
-
5
1
  // Re-export types for convenience
6
2
  export type {
7
3
  QueuePluginDto,
8
4
  QueueConfigurationDto,
9
5
  UpdateQueueConfiguration,
10
6
  } from "@checkstack/queue-common";
11
-
12
- // QueueApiClient type inferred from the client definition
13
- export type QueueApiClient = InferClient<typeof QueueApi>;
14
-
15
- export const queueApiRef = createApiRef<QueueApiClient>("queue-api");
7
+ // Client definition is in @checkstack/queue-common - use with usePluginClient
8
+ export { QueueApi } from "@checkstack/queue-common";
@@ -0,0 +1,95 @@
1
+ import React from "react";
2
+ import {
3
+ useApi,
4
+ accessApiRef,
5
+ usePluginClient,
6
+ } from "@checkstack/frontend-api";
7
+ import { useSignal } from "@checkstack/signal-frontend";
8
+ import {
9
+ QueueApi,
10
+ QUEUE_LAG_CHANGED,
11
+ queueAccess,
12
+ type LagSeverity,
13
+ } from "@checkstack/queue-common";
14
+ import { Alert, AlertTitle, AlertDescription } from "@checkstack/ui";
15
+ import { AlertTriangle, AlertCircle } from "lucide-react";
16
+
17
+ interface QueueLagAlertProps {
18
+ /** Only show if user has queue settings access */
19
+ requireAccess?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Displays a warning alert when queue is lagging (high pending jobs).
24
+ * Uses signal for real-time updates + initial fetch via useQuery.
25
+ */
26
+ export const QueueLagAlert: React.FC<QueueLagAlertProps> = ({
27
+ requireAccess = true,
28
+ }) => {
29
+ const accessApi = useApi(accessApiRef);
30
+ const queueClient = usePluginClient(QueueApi);
31
+
32
+ // Check access if required
33
+ const { allowed, loading: accessLoading } = accessApi.useAccess(
34
+ queueAccess.settings.read
35
+ );
36
+
37
+ // Fetch lag status via useQuery
38
+ const { data: lagStatus, isLoading } = queueClient.getLagStatus.useQuery(
39
+ undefined,
40
+ {
41
+ enabled: !requireAccess || allowed,
42
+ staleTime: 30_000, // Cache for 30 seconds
43
+ }
44
+ );
45
+
46
+ // State for real-time updates via signal
47
+ const [signalData, setSignalData] = React.useState<
48
+ | {
49
+ pending: number;
50
+ severity: LagSeverity;
51
+ }
52
+ | undefined
53
+ >();
54
+
55
+ // Listen for real-time lag updates
56
+ useSignal(QUEUE_LAG_CHANGED, (payload) => {
57
+ setSignalData({ pending: payload.pending, severity: payload.severity });
58
+ });
59
+
60
+ // Use signal data if available, otherwise use query data
61
+ const pending = signalData?.pending ?? lagStatus?.pending ?? 0;
62
+ const severity = signalData?.severity ?? lagStatus?.severity ?? "none";
63
+
64
+ // Don't render if loading, no access, or no lag
65
+ if (isLoading || accessLoading) {
66
+ return;
67
+ }
68
+
69
+ if (requireAccess && !allowed) {
70
+ return;
71
+ }
72
+
73
+ if (severity === "none") {
74
+ return;
75
+ }
76
+
77
+ const variant = severity === "critical" ? "error" : "warning";
78
+ const Icon = severity === "critical" ? AlertCircle : AlertTriangle;
79
+ const title =
80
+ severity === "critical" ? "Queue backlog critical" : "Queue building up";
81
+ const description =
82
+ severity === "critical"
83
+ ? `${pending} jobs pending. Consider scaling or reducing load.`
84
+ : `${pending} jobs pending. Some jobs may be delayed.`;
85
+
86
+ return (
87
+ <Alert variant={variant} className="mb-4">
88
+ <Icon className="h-5 w-5" />
89
+ <div>
90
+ <AlertTitle>{title}</AlertTitle>
91
+ <AlertDescription>{description}</AlertDescription>
92
+ </div>
93
+ </Alert>
94
+ );
95
+ };
package/src/index.tsx CHANGED
@@ -1,31 +1,18 @@
1
1
  import {
2
- rpcApiRef,
3
- ApiRef,
4
2
  UserMenuItemsSlot,
5
3
  createSlotExtension,
6
4
  createFrontendPlugin,
7
5
  } from "@checkstack/frontend-api";
8
- import { queueApiRef, type QueueApiClient } from "./api";
9
6
  import { QueueConfigPage } from "./pages/QueueConfigPage";
10
7
  import { QueueUserMenuItems } from "./components/UserMenuItems";
11
8
  import {
12
9
  queueRoutes,
13
- QueueApi,
14
10
  pluginMetadata,
15
11
  queueAccess,
16
12
  } from "@checkstack/queue-common";
17
13
 
18
14
  export const queuePlugin = createFrontendPlugin({
19
15
  metadata: pluginMetadata,
20
- apis: [
21
- {
22
- ref: queueApiRef,
23
- factory: (deps: { get: <T>(ref: ApiRef<T>) => T }): QueueApiClient => {
24
- const rpcApi = deps.get(rpcApiRef);
25
- return rpcApi.forPlugin(QueueApi);
26
- },
27
- },
28
- ],
29
16
  routes: [
30
17
  {
31
18
  route: queueRoutes.routes.config,
@@ -42,3 +29,4 @@ export const queuePlugin = createFrontendPlugin({
42
29
  });
43
30
 
44
31
  export * from "./api";
32
+ export { QueueLagAlert } from "./components/QueueLagAlert";
@@ -1,11 +1,15 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import {
3
- useApi,
4
3
  wrapInSuspense,
5
4
  accessApiRef,
5
+ useApi,
6
+ usePluginClient,
6
7
  } from "@checkstack/frontend-api";
7
- import { queueApiRef } from "../api";
8
- import { QueuePluginDto, queueAccess } from "@checkstack/queue-common";
8
+ import {
9
+ QueuePluginDto,
10
+ queueAccess,
11
+ QueueApi,
12
+ } from "@checkstack/queue-common";
9
13
  import {
10
14
  Button,
11
15
  Alert,
@@ -20,54 +24,55 @@ import {
20
24
  CardTitle,
21
25
  useToast,
22
26
  } from "@checkstack/ui";
23
- import { AlertTriangle, Save } from "lucide-react";
27
+ import { AlertTriangle, Save, Info, Gauge, Activity } from "lucide-react";
28
+ import { QueueLagAlert } from "../components/QueueLagAlert";
24
29
 
25
30
  const QueueConfigPageContent = () => {
26
- const api = useApi(queueApiRef);
31
+ const queueClient = usePluginClient(QueueApi);
27
32
  const accessApi = useApi(accessApiRef);
28
33
  const toast = useToast();
29
- const { allowed: canRead, loading: accessLoading } =
30
- accessApi.useAccess(queueAccess.settings.read);
34
+ const { allowed: canRead, loading: accessLoading } = accessApi.useAccess(
35
+ queueAccess.settings.read
36
+ );
31
37
  const { allowed: canUpdate } = accessApi.useAccess(
32
38
  queueAccess.settings.manage
33
39
  );
34
40
 
35
- const [plugins, setPlugins] = useState<QueuePluginDto[]>([]);
41
+ // Fetch plugins and configuration
42
+ const { data: pluginsList } = queueClient.getPlugins.useQuery();
43
+ const { data: configuration, refetch: refetchConfig } =
44
+ queueClient.getConfiguration.useQuery();
45
+ const updateConfigMutation = queueClient.updateConfiguration.useMutation();
46
+
36
47
  const [selectedPluginId, setSelectedPluginId] = useState<string>("");
37
48
  const [config, setConfig] = useState<Record<string, unknown>>({});
38
- const [isSaving, setIsSaving] = useState(false);
39
49
 
50
+ // Sync state with fetched data
40
51
  useEffect(() => {
41
- const fetchData = async () => {
42
- const [pluginsList, configuration] = await Promise.all([
43
- api.getPlugins(),
44
- api.getConfiguration(),
45
- ]);
46
- setPlugins(pluginsList);
52
+ if (configuration) {
47
53
  setSelectedPluginId(configuration.pluginId);
48
54
  setConfig(configuration.config);
49
- };
50
- fetchData();
51
- }, [api]);
55
+ }
56
+ }, [configuration]);
52
57
 
53
58
  const handleSave = async () => {
54
59
  if (!selectedPluginId) return;
55
- setIsSaving(true);
56
60
  try {
57
- await api.updateConfiguration({
61
+ await updateConfigMutation.mutateAsync({
58
62
  pluginId: selectedPluginId,
59
63
  config,
60
64
  });
61
65
  toast.success("Configuration saved successfully!");
66
+ refetchConfig();
62
67
  } catch (error) {
63
68
  const message = error instanceof Error ? error.message : String(error);
64
69
  toast.error(`Failed to save configuration: ${message}`);
65
- } finally {
66
- setIsSaving(false);
67
70
  }
68
71
  };
69
72
 
70
73
  const isMemoryQueue = selectedPluginId === "memory";
74
+ const plugins: QueuePluginDto[] = pluginsList ?? [];
75
+ const isSaving = updateConfigMutation.isPending;
71
76
 
72
77
  return (
73
78
  <PageLayout
@@ -77,50 +82,115 @@ const QueueConfigPageContent = () => {
77
82
  allowed={canRead}
78
83
  maxWidth="3xl"
79
84
  >
80
- <Card>
81
- <CardHeader>
82
- <CardTitle>Queue Configuration</CardTitle>
83
- <p className="text-sm text-muted-foreground">
84
- Select and configure the queue plugin
85
- </p>
86
- </CardHeader>
87
- <CardContent className="space-y-6">
88
- {isMemoryQueue && (
89
- <Alert variant="warning">
90
- <AlertTriangle className="h-5 w-5" />
91
- <div>
92
- <AlertTitle>In-Memory Queue Warning</AlertTitle>
93
- <AlertDescription>
94
- The in-memory queue is suitable for development and
95
- single-instance deployments only. It will not scale across
96
- multiple instances and jobs will be lost on restart. For
97
- production environments with multiple instances, consider
98
- using a persistent queue implementation.
99
- </AlertDescription>
85
+ <QueueLagAlert requireAccess={false} />
86
+ <div className="space-y-6">
87
+ <Card>
88
+ <CardHeader>
89
+ <CardTitle>Queue Configuration</CardTitle>
90
+ <p className="text-sm text-muted-foreground">
91
+ Select and configure the queue plugin
92
+ </p>
93
+ </CardHeader>
94
+ <CardContent className="space-y-6">
95
+ {isMemoryQueue && (
96
+ <Alert variant="warning">
97
+ <AlertTriangle className="h-5 w-5" />
98
+ <div>
99
+ <AlertTitle>In-Memory Queue Warning</AlertTitle>
100
+ <AlertDescription>
101
+ The in-memory queue is suitable for development and
102
+ single-instance deployments only. It will not scale across
103
+ multiple instances and jobs will be lost on restart. For
104
+ production environments with multiple instances, consider
105
+ using a persistent queue implementation.
106
+ </AlertDescription>
107
+ </div>
108
+ </Alert>
109
+ )}
110
+
111
+ <PluginConfigForm
112
+ label="Queue Plugin"
113
+ plugins={plugins}
114
+ selectedPluginId={selectedPluginId}
115
+ onPluginChange={(value) => {
116
+ setSelectedPluginId(value);
117
+ setConfig({});
118
+ }}
119
+ config={config}
120
+ onConfigChange={setConfig}
121
+ disabled={!canUpdate}
122
+ />
123
+ </CardContent>
124
+ <CardFooter className="flex justify-end gap-2">
125
+ <Button onClick={handleSave} disabled={!canUpdate || isSaving}>
126
+ <Save className="mr-2 h-4 w-4" />
127
+ {isSaving ? "Saving..." : "Save Configuration"}
128
+ </Button>
129
+ </CardFooter>
130
+ </Card>
131
+
132
+ {/* Performance Guidance */}
133
+ <Card>
134
+ <CardHeader>
135
+ <CardTitle className="flex items-center gap-2">
136
+ <Info className="h-5 w-5" />
137
+ Performance Tuning
138
+ </CardTitle>
139
+ </CardHeader>
140
+ <CardContent className="space-y-4">
141
+ <div className="grid gap-4 md:grid-cols-2">
142
+ <div className="space-y-2">
143
+ <h4 className="flex items-center gap-2 font-medium">
144
+ <Gauge className="h-4 w-4" />
145
+ Concurrency Settings
146
+ </h4>
147
+ <ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
148
+ <li>
149
+ <strong>Default (10)</strong>: Conservative, safe for most
150
+ workloads
151
+ </li>
152
+ <li>
153
+ <strong>Moderate (25-50)</strong>: Good for I/O-bound health
154
+ checks
155
+ </li>
156
+ <li>
157
+ <strong>Aggressive (100)</strong>: Maximum, monitor resource
158
+ usage
159
+ </li>
160
+ </ul>
161
+ <p className="text-xs text-muted-foreground mt-2">
162
+ Formula: throughput ≈ concurrency / avg_job_duration
163
+ </p>
100
164
  </div>
101
- </Alert>
102
- )}
103
165
 
104
- <PluginConfigForm
105
- label="Queue Plugin"
106
- plugins={plugins}
107
- selectedPluginId={selectedPluginId}
108
- onPluginChange={(value) => {
109
- setSelectedPluginId(value);
110
- setConfig({});
111
- }}
112
- config={config}
113
- onConfigChange={setConfig}
114
- disabled={!canUpdate}
115
- />
116
- </CardContent>
117
- <CardFooter className="flex justify-end gap-2">
118
- <Button onClick={handleSave} disabled={!canUpdate || isSaving}>
119
- <Save className="mr-2 h-4 w-4" />
120
- {isSaving ? "Saving..." : "Save Configuration"}
121
- </Button>
122
- </CardFooter>
123
- </Card>
166
+ <div className="space-y-2">
167
+ <h4 className="flex items-center gap-2 font-medium">
168
+ <Activity className="h-4 w-4" />
169
+ Bottleneck Indicators
170
+ </h4>
171
+ <ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
172
+ <li>
173
+ <strong>Jobs queueing</strong>: Increase concurrency or
174
+ scale horizontally
175
+ </li>
176
+ <li>
177
+ <strong>High CPU (&gt;70%)</strong>: Scale horizontally,
178
+ don't increase concurrency
179
+ </li>
180
+ <li>
181
+ <strong>DB connection errors</strong>: Reduce concurrency or
182
+ increase pool
183
+ </li>
184
+ <li>
185
+ <strong>Rate limit errors</strong>: Reduce concurrency for
186
+ external checks
187
+ </li>
188
+ </ul>
189
+ </div>
190
+ </div>
191
+ </CardContent>
192
+ </Card>
193
+ </div>
124
194
  </PageLayout>
125
195
  );
126
196
  };