@stackwright-pro/pulse 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/dist/index.mjs ADDED
@@ -0,0 +1,329 @@
1
+ // src/components/Pulse.tsx
2
+ import { useState as useState2, useEffect as useEffect2 } from "react";
3
+
4
+ // src/hooks/usePulse.ts
5
+ import { useState, useCallback, useEffect } from "react";
6
+ import { useQuery } from "@tanstack/react-query";
7
+ function usePulse(options) {
8
+ const {
9
+ fetcher,
10
+ interval = 5e3,
11
+ schema,
12
+ enabled = true,
13
+ refetchOnWindowFocus = true,
14
+ retryCount = 3
15
+ } = options;
16
+ const [lastSuccess, setLastSuccess] = useState(null);
17
+ const [errorCount, setErrorCount] = useState(0);
18
+ const validatedFetcher = useCallback(async () => {
19
+ const data2 = await fetcher();
20
+ if (schema) {
21
+ return schema.parse(data2);
22
+ }
23
+ return data2;
24
+ }, [fetcher, schema]);
25
+ const queryKey = ["pulse", fetcher.toString()];
26
+ const { data, isLoading, isFetching, isSuccess, isError, error, refetch } = useQuery({
27
+ queryKey,
28
+ queryFn: validatedFetcher,
29
+ refetchInterval: enabled ? Math.max(interval, 1e3) : false,
30
+ refetchOnWindowFocus,
31
+ retry: retryCount,
32
+ retryDelay: (attemptIndex) => Math.min(1e3 * 2 ** attemptIndex, 1e4),
33
+ staleTime: interval / 2,
34
+ throwOnError: false
35
+ });
36
+ useEffect(() => {
37
+ if (isSuccess && data !== void 0) {
38
+ setLastSuccess(/* @__PURE__ */ new Date());
39
+ setErrorCount(0);
40
+ }
41
+ }, [isSuccess, data]);
42
+ useEffect(() => {
43
+ if (isError) {
44
+ setErrorCount((c) => c + 1);
45
+ }
46
+ }, [isError]);
47
+ let state = "loading";
48
+ if (isLoading && data === void 0) {
49
+ state = "loading";
50
+ } else if (error && data === void 0) {
51
+ state = "error";
52
+ } else if (isFetching) {
53
+ state = "stale";
54
+ } else {
55
+ state = "live";
56
+ }
57
+ const meta = {
58
+ lastUpdated: lastSuccess ?? /* @__PURE__ */ new Date(0),
59
+ isStale: state === "stale" || state === "error",
60
+ isError: state === "error",
61
+ isLoading: isFetching && data === void 0,
62
+ errorCount,
63
+ refetch,
64
+ state
65
+ };
66
+ return {
67
+ data,
68
+ meta,
69
+ state,
70
+ error
71
+ };
72
+ }
73
+
74
+ // src/components/Pulse.tsx
75
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
76
+ function Pulse({
77
+ fetcher,
78
+ interval,
79
+ staleThreshold,
80
+ maxStaleAge,
81
+ schema,
82
+ enabled,
83
+ refetchOnWindowFocus,
84
+ retryCount,
85
+ children,
86
+ loadingState,
87
+ errorState,
88
+ emptyState,
89
+ showStaleDataOnError = true
90
+ }) {
91
+ const { data, meta, state } = usePulse({
92
+ fetcher,
93
+ interval,
94
+ staleThreshold,
95
+ maxStaleAge,
96
+ schema,
97
+ enabled,
98
+ refetchOnWindowFocus,
99
+ retryCount
100
+ });
101
+ if (state === "loading" && data === void 0) {
102
+ return /* @__PURE__ */ jsx(Fragment, { children: loadingState ?? /* @__PURE__ */ jsx(DefaultLoading, {}) });
103
+ }
104
+ if (state === "error") {
105
+ const errorUi = typeof errorState === "function" ? errorState(meta) : errorState ?? /* @__PURE__ */ jsx(DefaultError, { meta });
106
+ if (showStaleDataOnError && data !== void 0) {
107
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
108
+ errorUi,
109
+ children(data, meta)
110
+ ] });
111
+ }
112
+ return /* @__PURE__ */ jsx(Fragment, { children: errorUi });
113
+ }
114
+ if (data === null || data === void 0 || Array.isArray(data) && data.length === 0) {
115
+ return /* @__PURE__ */ jsx(Fragment, { children: emptyState ?? /* @__PURE__ */ jsx(DefaultEmpty, {}) });
116
+ }
117
+ return /* @__PURE__ */ jsx(Fragment, { children: children(data, meta) });
118
+ }
119
+ function useSecondsAgo(lastUpdated) {
120
+ const [secondsAgo, setSecondsAgo] = useState2(
121
+ () => Math.round((Date.now() - lastUpdated.getTime()) / 1e3)
122
+ );
123
+ useEffect2(() => {
124
+ const interval = setInterval(() => {
125
+ setSecondsAgo(Math.round((Date.now() - lastUpdated.getTime()) / 1e3));
126
+ }, 1e3);
127
+ return () => clearInterval(interval);
128
+ }, [lastUpdated]);
129
+ return secondsAgo;
130
+ }
131
+ function DefaultLoading() {
132
+ return /* @__PURE__ */ jsxs("div", { className: "pulse--loading", role: "status", "aria-live": "polite", "aria-atomic": "true", children: [
133
+ /* @__PURE__ */ jsx("span", { className: "pulse--spinner", "aria-hidden": "true" }),
134
+ /* @__PURE__ */ jsx("span", { className: "pulse--loading-text", children: "Loading..." })
135
+ ] });
136
+ }
137
+ function DefaultError({ meta }) {
138
+ const secondsAgo = useSecondsAgo(meta.lastUpdated);
139
+ return /* @__PURE__ */ jsxs("div", { className: "pulse--error", role: "alert", "aria-live": "assertive", "aria-atomic": "true", children: [
140
+ "\u26A0\uFE0F Connection lost. Last updated: ",
141
+ secondsAgo,
142
+ "s ago"
143
+ ] });
144
+ }
145
+ function DefaultEmpty() {
146
+ return /* @__PURE__ */ jsx("div", { className: "pulse--empty", children: "No data available" });
147
+ }
148
+
149
+ // src/components/PulseIndicator.tsx
150
+ import { useState as useState3, useEffect as useEffect3 } from "react";
151
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
152
+ function PulseIndicator({
153
+ meta,
154
+ showSeconds = true,
155
+ className,
156
+ labels = {}
157
+ }) {
158
+ const [secondsAgo, setSecondsAgo] = useState3(
159
+ () => Math.round((Date.now() - meta.lastUpdated.getTime()) / 1e3)
160
+ );
161
+ useEffect3(() => {
162
+ const interval = setInterval(() => {
163
+ setSecondsAgo(Math.round((Date.now() - meta.lastUpdated.getTime()) / 1e3));
164
+ }, 1e3);
165
+ return () => clearInterval(interval);
166
+ }, [meta.lastUpdated]);
167
+ let status;
168
+ let label;
169
+ let dotClass;
170
+ if (meta.isLoading) {
171
+ status = "syncing";
172
+ label = labels.syncing ?? "Syncing...";
173
+ dotClass = "dot--syncing";
174
+ } else if (meta.isError) {
175
+ status = "error";
176
+ label = labels.error ?? `Offline${showSeconds ? ` \u2022 ${formatSeconds(secondsAgo)}` : ""}`;
177
+ dotClass = "dot--error";
178
+ } else if (meta.isStale) {
179
+ status = "stale";
180
+ label = labels.stale ?? `Stale${showSeconds ? ` \u2022 ${formatSeconds(secondsAgo)}` : ""}`;
181
+ dotClass = "dot--stale";
182
+ } else {
183
+ status = "live";
184
+ label = showSeconds ? labels.live ?? `Live \u2022 ${formatSeconds(secondsAgo)}` : labels.live ?? "Live";
185
+ dotClass = "dot--live";
186
+ }
187
+ return /* @__PURE__ */ jsxs2(
188
+ "div",
189
+ {
190
+ className: `pulse-indicator ${status} ${className ?? ""}`,
191
+ "data-status": status,
192
+ role: "status",
193
+ "aria-live": "polite",
194
+ "aria-atomic": "true",
195
+ children: [
196
+ /* @__PURE__ */ jsx2("span", { className: `pulse-indicator__dot ${dotClass}`, "aria-hidden": "true" }),
197
+ /* @__PURE__ */ jsx2("span", { className: "pulse-indicator__label", children: label })
198
+ ]
199
+ }
200
+ );
201
+ }
202
+ function formatSeconds(seconds) {
203
+ if (seconds < 60) return `${seconds}s ago`;
204
+ const minutes = Math.floor(seconds / 60);
205
+ if (minutes < 60) return `${minutes}m ago`;
206
+ const hours = Math.floor(minutes / 60);
207
+ return `${hours}h ago`;
208
+ }
209
+
210
+ // src/components/PulseStates.tsx
211
+ import { useState as useState4, useEffect as useEffect4 } from "react";
212
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
213
+ function PulseLoadingState({
214
+ className,
215
+ text = "Loading..."
216
+ }) {
217
+ return /* @__PURE__ */ jsxs3("div", { className: `pulse--loading ${className ?? ""}`, role: "status", "aria-live": "polite", children: [
218
+ /* @__PURE__ */ jsx3("span", { className: "pulse--spinner", "aria-hidden": "true" }),
219
+ /* @__PURE__ */ jsx3("span", { className: "pulse--loading-text", children: text })
220
+ ] });
221
+ }
222
+ function useSecondsAgo2(lastUpdated) {
223
+ const [secondsAgo, setSecondsAgo] = useState4(
224
+ () => Math.round((Date.now() - lastUpdated.getTime()) / 1e3)
225
+ );
226
+ useEffect4(() => {
227
+ const interval = setInterval(() => {
228
+ setSecondsAgo(Math.round((Date.now() - lastUpdated.getTime()) / 1e3));
229
+ }, 1e3);
230
+ return () => clearInterval(interval);
231
+ }, [lastUpdated]);
232
+ return secondsAgo;
233
+ }
234
+ function PulseErrorState({
235
+ meta,
236
+ className,
237
+ customMessage
238
+ }) {
239
+ const secondsAgo = useSecondsAgo2(meta.lastUpdated);
240
+ return /* @__PURE__ */ jsxs3("div", { className: `pulse--error ${className ?? ""}`, role: "alert", children: [
241
+ "\u26A0\uFE0F ",
242
+ customMessage ?? `Connection lost. Last updated: ${secondsAgo}s ago`
243
+ ] });
244
+ }
245
+ function PulseEmptyState({
246
+ className,
247
+ text = "No data available"
248
+ }) {
249
+ return /* @__PURE__ */ jsx3("div", { className: `pulse--empty ${className ?? ""}`, children: text });
250
+ }
251
+ function PulseStaleState({
252
+ meta,
253
+ className,
254
+ customMessage
255
+ }) {
256
+ const secondsAgo = useSecondsAgo2(meta.lastUpdated);
257
+ return /* @__PURE__ */ jsxs3("div", { className: `pulse--stale ${className ?? ""}`, role: "status", children: [
258
+ "\u23F1\uFE0F ",
259
+ customMessage ?? `Data may be outdated. Last updated: ${secondsAgo}s ago`
260
+ ] });
261
+ }
262
+ function PulseSyncingState({
263
+ className,
264
+ text = "Syncing..."
265
+ }) {
266
+ return /* @__PURE__ */ jsxs3("div", { className: `pulse--syncing ${className ?? ""}`, role: "status", "aria-live": "polite", children: [
267
+ /* @__PURE__ */ jsx3("span", { className: "pulse--spinner", "aria-hidden": "true" }),
268
+ /* @__PURE__ */ jsx3("span", { className: "pulse--syncing-text", children: text })
269
+ ] });
270
+ }
271
+
272
+ // src/hooks/useStreaming.ts
273
+ function useStreaming(_options) {
274
+ return {
275
+ data: null,
276
+ isConnected: false,
277
+ isConnecting: false,
278
+ error: null
279
+ };
280
+ }
281
+
282
+ // src/validation/pulseValidator.ts
283
+ function createPulseValidator(schema) {
284
+ return {
285
+ /**
286
+ * Validates data, throwing on failure
287
+ */
288
+ parse: (data) => {
289
+ return schema.parse(data);
290
+ },
291
+ /**
292
+ * Safe parse - returns null on failure
293
+ */
294
+ safeParse: (data) => {
295
+ const result = schema.safeParse(data);
296
+ return result.success ? result.data : null;
297
+ }
298
+ };
299
+ }
300
+ var PulseValidationError = class extends Error {
301
+ constructor(message, issues) {
302
+ super(message);
303
+ this.issues = issues;
304
+ this.name = "PulseValidationError";
305
+ }
306
+ /**
307
+ * Get formatted error messages
308
+ */
309
+ getFormattedErrors() {
310
+ return this.issues.map((issue) => ({
311
+ path: issue.path.join("."),
312
+ message: issue.message,
313
+ code: issue.code
314
+ }));
315
+ }
316
+ };
317
+ export {
318
+ Pulse,
319
+ PulseEmptyState,
320
+ PulseErrorState,
321
+ PulseIndicator,
322
+ PulseLoadingState,
323
+ PulseStaleState,
324
+ PulseSyncingState,
325
+ PulseValidationError,
326
+ createPulseValidator,
327
+ usePulse,
328
+ useStreaming
329
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@stackwright-pro/pulse",
3
+ "version": "0.1.0",
4
+ "description": "Source-agnostic real-time data polling for Stackwright Pro",
5
+ "license": "PROPRIETARY",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "dependencies": {
20
+ "@tanstack/react-query": "^5.0.0",
21
+ "zod": "^4.3.6"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^18.0.0",
25
+ "react-dom": "^18.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/react": "^18.3.0",
29
+ "@types/react-dom": "^18.3.0",
30
+ "@testing-library/react": "^16.0.0",
31
+ "jsdom": "^25.0.0",
32
+ "tsup": "^8.5.0",
33
+ "typescript": "^5.8.3",
34
+ "vitest": "^4.0.18"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
38
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
39
+ "test": "vitest"
40
+ }
41
+ }