@cosmicdrift/kumiko-renderer 0.403.0 → 0.404.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/dist/web/app/create-app.js +6 -2
- package/dist/web/app/session-list-state-store.js +1 -1
- package/dist/web/icons.d.ts +85 -85
- package/dist/web/primitives/field.d.ts +13 -13
- package/dist/web/sse/live-events.d.ts +3 -0
- package/dist/web/sse/live-events.js +36 -1
- package/package.json +15 -15
- package/src/changes.json +6 -0
|
@@ -3,7 +3,7 @@ import { isOpenToAllGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { rendererError, rendererWarn } from "../../dev-console.js";
|
|
6
|
-
import { AppFeaturesProvider, BasePathProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LeaveGuardDialog, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
|
|
6
|
+
import { AppFeaturesProvider, BasePathProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LeaveGuardDialog, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useBasePath, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
|
|
7
7
|
import { createLiveDispatcher } from "../../live/index.js";
|
|
8
8
|
import { lastSegment } from "../layout/nav-tree.js";
|
|
9
9
|
import { clearFileMetaCache } from "../primitives/file-thumbnail.js";
|
|
@@ -274,6 +274,11 @@ function AppSchemaBoundary({ app, screenQn, schemaUrl, navAdapter, onAppLoaded,
|
|
|
274
274
|
}
|
|
275
275
|
return (_jsx(BrowserNavBoot, { app: app, fallbackQn: fallbackQn, useNavApi: navAdapter, hasWorkspaces: (app.workspaces?.length ?? 0) > 0, ...(translate !== undefined && { translate }), ...(onRowClick !== undefined && { onRowClick }), ...(shell !== undefined && { shell }) }));
|
|
276
276
|
}
|
|
277
|
+
// createKumikoApp mounts BasePathProvider above the NavProvider that calls this hook.
|
|
278
|
+
function useBrowserNavApiWithBasePath(navOptions) {
|
|
279
|
+
const basePath = useBasePath();
|
|
280
|
+
return useBrowserNavApi({ ...navOptions, ...(basePath !== "" && { basePath }) });
|
|
281
|
+
}
|
|
277
282
|
export function createKumikoApp(options = {}) {
|
|
278
283
|
const rootId = options.rootId ?? "root";
|
|
279
284
|
const container = document.getElementById(rootId);
|
|
@@ -342,7 +347,6 @@ export function createKumikoApp(options = {}) {
|
|
|
342
347
|
}
|
|
343
348
|
const localeResolver = options.locale ?? createBrowserLocaleResolver();
|
|
344
349
|
const { basePath } = options;
|
|
345
|
-
const useBrowserNavApiWithBasePath = (navOptions) => useBrowserNavApi({ ...navOptions, ...(basePath !== undefined && { basePath }) });
|
|
346
350
|
const navAdapter = options.navAdapter ?? useBrowserNavApiWithBasePath;
|
|
347
351
|
const reporter = options.reportClientErrors === false ? undefined : createClientErrorReporter();
|
|
348
352
|
reporter?.installGlobalHandlers();
|
|
@@ -16,7 +16,7 @@ function openSessionStorage() {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
function prefixedKeys(storage) {
|
|
19
|
-
return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter((key) => key
|
|
19
|
+
return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter((key) => key?.startsWith(KEY_PREFIX) === true);
|
|
20
20
|
}
|
|
21
21
|
function removeAllRemembered(storage) {
|
|
22
22
|
for (const key of prefixedKeys(storage))
|
package/dist/web/icons.d.ts
CHANGED
|
@@ -2,91 +2,91 @@ import type { NavIconKey } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
export type { NavIconKey } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
4
4
|
export declare const NAV_ICONS: {
|
|
5
|
-
readonly dashboard: import("
|
|
6
|
-
readonly "layout-grid": import("
|
|
7
|
-
readonly "book-open": import("
|
|
8
|
-
readonly "clipboard-list": import("
|
|
9
|
-
readonly package: import("
|
|
10
|
-
readonly gauge: import("
|
|
11
|
-
readonly list: import("
|
|
12
|
-
readonly table: import("
|
|
13
|
-
readonly layers: import("
|
|
14
|
-
readonly building: import("
|
|
15
|
-
readonly calculator: import("
|
|
16
|
-
readonly wallet: import("
|
|
17
|
-
readonly coins: import("
|
|
18
|
-
readonly "credit-card": import("
|
|
19
|
-
readonly "piggy-bank": import("
|
|
20
|
-
readonly receipt: import("
|
|
21
|
-
readonly chart: import("
|
|
22
|
-
readonly "bar-chart": import("
|
|
23
|
-
readonly trending: import("
|
|
24
|
-
readonly sparkles: import("
|
|
25
|
-
readonly wand: import("
|
|
26
|
-
readonly calendar: import("
|
|
27
|
-
readonly file: import("
|
|
28
|
-
readonly folder: import("
|
|
29
|
-
readonly "folder-open": import("
|
|
30
|
-
readonly home: import("
|
|
31
|
-
readonly bell: import("
|
|
32
|
-
readonly shield: import("
|
|
33
|
-
readonly "shield-check": import("
|
|
34
|
-
readonly send: import("
|
|
35
|
-
readonly settings: import("
|
|
36
|
-
readonly users: import("
|
|
37
|
-
readonly user: import("
|
|
38
|
-
readonly search: import("
|
|
39
|
-
readonly tag: import("
|
|
40
|
-
readonly key: import("
|
|
41
|
-
readonly link: import("
|
|
42
|
-
readonly palette: import("
|
|
43
|
-
readonly share: import("
|
|
44
|
-
readonly server: import("
|
|
45
|
-
readonly mail: import("
|
|
46
|
-
readonly lock: import("
|
|
47
|
-
readonly hash: import("
|
|
48
|
-
readonly download: import("
|
|
49
|
-
readonly upload: import("
|
|
50
|
-
readonly rocket: import("
|
|
51
|
-
readonly plus: import("
|
|
52
|
-
readonly languages: import("
|
|
53
|
-
readonly trash: import("
|
|
54
|
-
readonly x: import("
|
|
55
|
-
readonly check: import("
|
|
56
|
-
readonly "arrow-left": import("
|
|
57
|
-
readonly "arrow-right": import("
|
|
58
|
-
readonly copy: import("
|
|
59
|
-
readonly pencil: import("
|
|
60
|
-
readonly eye: import("
|
|
61
|
-
readonly "eye-off": import("
|
|
62
|
-
readonly filter: import("
|
|
63
|
-
readonly refresh: import("
|
|
64
|
-
readonly "more-horizontal": import("
|
|
65
|
-
readonly "more-vertical": import("
|
|
66
|
-
readonly "external-link": import("
|
|
67
|
-
readonly "chevron-down": import("
|
|
68
|
-
readonly "chevron-left": import("
|
|
69
|
-
readonly "chevron-right": import("
|
|
70
|
-
readonly "chevron-up": import("
|
|
71
|
-
readonly save: import("
|
|
72
|
-
readonly undo: import("
|
|
73
|
-
readonly archive: import("
|
|
74
|
-
readonly star: import("
|
|
75
|
-
readonly flag: import("
|
|
76
|
-
readonly clock: import("
|
|
77
|
-
readonly "map-pin": import("
|
|
78
|
-
readonly phone: import("
|
|
79
|
-
readonly printer: import("
|
|
80
|
-
readonly "alert-triangle": import("
|
|
81
|
-
readonly info: import("
|
|
82
|
-
readonly "check-circle": import("
|
|
83
|
-
readonly "x-circle": import("
|
|
84
|
-
readonly loader: import("
|
|
85
|
-
readonly mic: import("
|
|
86
|
-
readonly "circle-stop": import("
|
|
87
|
-
readonly image: import("
|
|
88
|
-
readonly camera: import("
|
|
89
|
-
readonly headphones: import("
|
|
5
|
+
readonly dashboard: import("lucide-react").LucideIcon;
|
|
6
|
+
readonly "layout-grid": import("lucide-react").LucideIcon;
|
|
7
|
+
readonly "book-open": import("lucide-react").LucideIcon;
|
|
8
|
+
readonly "clipboard-list": import("lucide-react").LucideIcon;
|
|
9
|
+
readonly package: import("lucide-react").LucideIcon;
|
|
10
|
+
readonly gauge: import("lucide-react").LucideIcon;
|
|
11
|
+
readonly list: import("lucide-react").LucideIcon;
|
|
12
|
+
readonly table: import("lucide-react").LucideIcon;
|
|
13
|
+
readonly layers: import("lucide-react").LucideIcon;
|
|
14
|
+
readonly building: import("lucide-react").LucideIcon;
|
|
15
|
+
readonly calculator: import("lucide-react").LucideIcon;
|
|
16
|
+
readonly wallet: import("lucide-react").LucideIcon;
|
|
17
|
+
readonly coins: import("lucide-react").LucideIcon;
|
|
18
|
+
readonly "credit-card": import("lucide-react").LucideIcon;
|
|
19
|
+
readonly "piggy-bank": import("lucide-react").LucideIcon;
|
|
20
|
+
readonly receipt: import("lucide-react").LucideIcon;
|
|
21
|
+
readonly chart: import("lucide-react").LucideIcon;
|
|
22
|
+
readonly "bar-chart": import("lucide-react").LucideIcon;
|
|
23
|
+
readonly trending: import("lucide-react").LucideIcon;
|
|
24
|
+
readonly sparkles: import("lucide-react").LucideIcon;
|
|
25
|
+
readonly wand: import("lucide-react").LucideIcon;
|
|
26
|
+
readonly calendar: import("lucide-react").LucideIcon;
|
|
27
|
+
readonly file: import("lucide-react").LucideIcon;
|
|
28
|
+
readonly folder: import("lucide-react").LucideIcon;
|
|
29
|
+
readonly "folder-open": import("lucide-react").LucideIcon;
|
|
30
|
+
readonly home: import("lucide-react").LucideIcon;
|
|
31
|
+
readonly bell: import("lucide-react").LucideIcon;
|
|
32
|
+
readonly shield: import("lucide-react").LucideIcon;
|
|
33
|
+
readonly "shield-check": import("lucide-react").LucideIcon;
|
|
34
|
+
readonly send: import("lucide-react").LucideIcon;
|
|
35
|
+
readonly settings: import("lucide-react").LucideIcon;
|
|
36
|
+
readonly users: import("lucide-react").LucideIcon;
|
|
37
|
+
readonly user: import("lucide-react").LucideIcon;
|
|
38
|
+
readonly search: import("lucide-react").LucideIcon;
|
|
39
|
+
readonly tag: import("lucide-react").LucideIcon;
|
|
40
|
+
readonly key: import("lucide-react").LucideIcon;
|
|
41
|
+
readonly link: import("lucide-react").LucideIcon;
|
|
42
|
+
readonly palette: import("lucide-react").LucideIcon;
|
|
43
|
+
readonly share: import("lucide-react").LucideIcon;
|
|
44
|
+
readonly server: import("lucide-react").LucideIcon;
|
|
45
|
+
readonly mail: import("lucide-react").LucideIcon;
|
|
46
|
+
readonly lock: import("lucide-react").LucideIcon;
|
|
47
|
+
readonly hash: import("lucide-react").LucideIcon;
|
|
48
|
+
readonly download: import("lucide-react").LucideIcon;
|
|
49
|
+
readonly upload: import("lucide-react").LucideIcon;
|
|
50
|
+
readonly rocket: import("lucide-react").LucideIcon;
|
|
51
|
+
readonly plus: import("lucide-react").LucideIcon;
|
|
52
|
+
readonly languages: import("lucide-react").LucideIcon;
|
|
53
|
+
readonly trash: import("lucide-react").LucideIcon;
|
|
54
|
+
readonly x: import("lucide-react").LucideIcon;
|
|
55
|
+
readonly check: import("lucide-react").LucideIcon;
|
|
56
|
+
readonly "arrow-left": import("lucide-react").LucideIcon;
|
|
57
|
+
readonly "arrow-right": import("lucide-react").LucideIcon;
|
|
58
|
+
readonly copy: import("lucide-react").LucideIcon;
|
|
59
|
+
readonly pencil: import("lucide-react").LucideIcon;
|
|
60
|
+
readonly eye: import("lucide-react").LucideIcon;
|
|
61
|
+
readonly "eye-off": import("lucide-react").LucideIcon;
|
|
62
|
+
readonly filter: import("lucide-react").LucideIcon;
|
|
63
|
+
readonly refresh: import("lucide-react").LucideIcon;
|
|
64
|
+
readonly "more-horizontal": import("lucide-react").LucideIcon;
|
|
65
|
+
readonly "more-vertical": import("lucide-react").LucideIcon;
|
|
66
|
+
readonly "external-link": import("lucide-react").LucideIcon;
|
|
67
|
+
readonly "chevron-down": import("lucide-react").LucideIcon;
|
|
68
|
+
readonly "chevron-left": import("lucide-react").LucideIcon;
|
|
69
|
+
readonly "chevron-right": import("lucide-react").LucideIcon;
|
|
70
|
+
readonly "chevron-up": import("lucide-react").LucideIcon;
|
|
71
|
+
readonly save: import("lucide-react").LucideIcon;
|
|
72
|
+
readonly undo: import("lucide-react").LucideIcon;
|
|
73
|
+
readonly archive: import("lucide-react").LucideIcon;
|
|
74
|
+
readonly star: import("lucide-react").LucideIcon;
|
|
75
|
+
readonly flag: import("lucide-react").LucideIcon;
|
|
76
|
+
readonly clock: import("lucide-react").LucideIcon;
|
|
77
|
+
readonly "map-pin": import("lucide-react").LucideIcon;
|
|
78
|
+
readonly phone: import("lucide-react").LucideIcon;
|
|
79
|
+
readonly printer: import("lucide-react").LucideIcon;
|
|
80
|
+
readonly "alert-triangle": import("lucide-react").LucideIcon;
|
|
81
|
+
readonly info: import("lucide-react").LucideIcon;
|
|
82
|
+
readonly "check-circle": import("lucide-react").LucideIcon;
|
|
83
|
+
readonly "x-circle": import("lucide-react").LucideIcon;
|
|
84
|
+
readonly loader: import("lucide-react").LucideIcon;
|
|
85
|
+
readonly mic: import("lucide-react").LucideIcon;
|
|
86
|
+
readonly "circle-stop": import("lucide-react").LucideIcon;
|
|
87
|
+
readonly image: import("lucide-react").LucideIcon;
|
|
88
|
+
readonly camera: import("lucide-react").LucideIcon;
|
|
89
|
+
readonly headphones: import("lucide-react").LucideIcon;
|
|
90
90
|
};
|
|
91
91
|
export declare function Icon({ name, className, }: {
|
|
92
92
|
readonly name: NavIconKey;
|
|
@@ -7,19 +7,19 @@ import "../react-css-custom-properties.ts";
|
|
|
7
7
|
export declare function fieldLabelId(id: string): string;
|
|
8
8
|
export declare function DefaultField(props: FieldProps): ReactNode;
|
|
9
9
|
declare const FIELD_ICONS: {
|
|
10
|
-
readonly mail: import("
|
|
11
|
-
readonly lock: import("
|
|
12
|
-
readonly hash: import("
|
|
13
|
-
readonly search: import("
|
|
14
|
-
readonly user: import("
|
|
15
|
-
readonly phone: import("
|
|
16
|
-
readonly calendar: import("
|
|
17
|
-
readonly link: import("
|
|
18
|
-
readonly tag: import("
|
|
19
|
-
readonly building: import("
|
|
20
|
-
readonly globe: import("
|
|
21
|
-
readonly key: import("
|
|
22
|
-
readonly "map-pin": import("
|
|
10
|
+
readonly mail: import("lucide-react").LucideIcon;
|
|
11
|
+
readonly lock: import("lucide-react").LucideIcon;
|
|
12
|
+
readonly hash: import("lucide-react").LucideIcon;
|
|
13
|
+
readonly search: import("lucide-react").LucideIcon;
|
|
14
|
+
readonly user: import("lucide-react").LucideIcon;
|
|
15
|
+
readonly phone: import("lucide-react").LucideIcon;
|
|
16
|
+
readonly calendar: import("lucide-react").LucideIcon;
|
|
17
|
+
readonly link: import("lucide-react").LucideIcon;
|
|
18
|
+
readonly tag: import("lucide-react").LucideIcon;
|
|
19
|
+
readonly building: import("lucide-react").LucideIcon;
|
|
20
|
+
readonly globe: import("lucide-react").LucideIcon;
|
|
21
|
+
readonly key: import("lucide-react").LucideIcon;
|
|
22
|
+
readonly "map-pin": import("lucide-react").LucideIcon;
|
|
23
23
|
};
|
|
24
24
|
export declare function fieldIconFor(icon: string | undefined): (typeof FIELD_ICONS)[FieldIconKey] | undefined;
|
|
25
25
|
export declare const FIELD_ICON_CLASS = "pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground";
|
|
@@ -10,6 +10,8 @@ export type CreateEventSourceLiveEventsOptions = {
|
|
|
10
10
|
readonly onSessionEnded?: () => void;
|
|
11
11
|
/** Reconnect-timer seam; defaults to the global setTimeout/clearTimeout. */
|
|
12
12
|
readonly timers?: LiveEventTimers;
|
|
13
|
+
/** Stall-watchdog timer seam; defaults to the global setTimeout/clearTimeout. */
|
|
14
|
+
readonly stallTimers?: LiveEventTimers;
|
|
13
15
|
/** Jitter source in [0, 1); defaults to Math.random. */
|
|
14
16
|
readonly random?: () => number;
|
|
15
17
|
/** Clock for synthetic resync events; defaults to Date.now. */
|
|
@@ -21,6 +23,7 @@ export type LiveEventTimers = {
|
|
|
21
23
|
};
|
|
22
24
|
export declare const RECONNECT_BASE_DELAY_MS = 1000;
|
|
23
25
|
export declare const RECONNECT_MAX_DELAY_MS = 30000;
|
|
26
|
+
export declare const SSE_STALL_TIMEOUT_MS = 30000;
|
|
24
27
|
export declare function reconnectDelayMs(attempt: number, random: number): number;
|
|
25
28
|
/** Returns an EventSource-backed `LiveEventSubscriber`.
|
|
26
29
|
* Typically called once during app bootstrap and passed as a value to
|
|
@@ -3,6 +3,9 @@ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
|
3
3
|
import { isSessionEndedError, LIVE_RESYNC_EVENT_TYPE, } from "../../index.js";
|
|
4
4
|
export const RECONNECT_BASE_DELAY_MS = 1_000;
|
|
5
5
|
export const RECONNECT_MAX_DELAY_MS = 30_000;
|
|
6
|
+
// Two missed server heartbeats (15 s each, SSE_HEARTBEAT_INTERVAL_MS in framework sse-route.ts).
|
|
7
|
+
// A KEDA interceptor can leave the stream hanging without an `error` event (kedacore/http-add-on#1803).
|
|
8
|
+
export const SSE_STALL_TIMEOUT_MS = 30_000;
|
|
6
9
|
const defaultTimers = {
|
|
7
10
|
set: (callback, delayMs) => {
|
|
8
11
|
const handle = setTimeout(callback, delayMs);
|
|
@@ -67,6 +70,7 @@ async function isSessionEndedRejection(url) {
|
|
|
67
70
|
export function createEventSourceLiveEvents(options = {}) {
|
|
68
71
|
const url = options.url ?? "/api/sse";
|
|
69
72
|
const timers = options.timers ?? defaultTimers;
|
|
73
|
+
const stallTimers = options.stallTimers ?? defaultTimers;
|
|
70
74
|
const random = options.random ?? Math.random;
|
|
71
75
|
const now = options.now ?? Date.now;
|
|
72
76
|
const subscribers = new Set();
|
|
@@ -74,6 +78,25 @@ export function createEventSourceLiveEvents(options = {}) {
|
|
|
74
78
|
const wiredEntities = new Set();
|
|
75
79
|
let cancelReconnect;
|
|
76
80
|
let reconnectAttempt = 0;
|
|
81
|
+
let cancelStallWatchdog;
|
|
82
|
+
const disarmStallWatchdog = () => {
|
|
83
|
+
cancelStallWatchdog?.();
|
|
84
|
+
cancelStallWatchdog = undefined;
|
|
85
|
+
};
|
|
86
|
+
const armStallWatchdog = (connection) => {
|
|
87
|
+
// skip: a frame from a connection that is no longer the current one
|
|
88
|
+
if (source !== connection)
|
|
89
|
+
return;
|
|
90
|
+
disarmStallWatchdog();
|
|
91
|
+
cancelStallWatchdog = stallTimers.set(() => {
|
|
92
|
+
cancelStallWatchdog = undefined;
|
|
93
|
+
// skip: a late timer of a connection that was already replaced
|
|
94
|
+
if (source !== connection)
|
|
95
|
+
return;
|
|
96
|
+
connection.close();
|
|
97
|
+
scheduleReconnect();
|
|
98
|
+
}, SSE_STALL_TIMEOUT_MS);
|
|
99
|
+
};
|
|
77
100
|
// `subscribers` is a flat Set across all entities — the browser-side
|
|
78
101
|
// addEventListener(entityName, ...) gate below only filters which frames
|
|
79
102
|
// arrive at all, not which subscriber a given frame is for. Without this
|
|
@@ -119,6 +142,7 @@ export function createEventSourceLiveEvents(options = {}) {
|
|
|
119
142
|
return;
|
|
120
143
|
connection.addEventListener(entityName, (e) => {
|
|
121
144
|
const data = propertyOf(e, "data");
|
|
145
|
+
armStallWatchdog(connection);
|
|
122
146
|
if (typeof data === "string")
|
|
123
147
|
handleEvent(data, entityName);
|
|
124
148
|
});
|
|
@@ -142,14 +166,22 @@ export function createEventSourceLiveEvents(options = {}) {
|
|
|
142
166
|
sessionEndedSignalled = true;
|
|
143
167
|
onSessionEnded?.();
|
|
144
168
|
};
|
|
169
|
+
armStallWatchdog(connection);
|
|
170
|
+
connection.addEventListener("ping", () => armStallWatchdog(connection));
|
|
145
171
|
connection.addEventListener("open", () => {
|
|
172
|
+
armStallWatchdog(connection);
|
|
146
173
|
reconnectAttempt = 0;
|
|
147
174
|
emitResync();
|
|
148
175
|
});
|
|
149
176
|
connection.addEventListener(SESSION_ENDED_EVENT, (e) => {
|
|
150
177
|
const code = sessionEndedFrameCode(e);
|
|
151
|
-
if (code !== undefined && isSessionEndedError({ code, httpStatus: 401 }))
|
|
178
|
+
if (code !== undefined && isSessionEndedError({ code, httpStatus: 401 })) {
|
|
179
|
+
disarmStallWatchdog();
|
|
152
180
|
signalSessionEnded();
|
|
181
|
+
// skip: a reconnect after a session end would only be refused again
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
armStallWatchdog(connection);
|
|
153
185
|
});
|
|
154
186
|
connection.addEventListener("error", () => {
|
|
155
187
|
// skip: CONNECTING means a dropped stream the browser retries itself;
|
|
@@ -161,10 +193,12 @@ export function createEventSourceLiveEvents(options = {}) {
|
|
|
161
193
|
if (source !== connection)
|
|
162
194
|
return;
|
|
163
195
|
if (sessionEnded) {
|
|
196
|
+
disarmStallWatchdog();
|
|
164
197
|
signalSessionEnded();
|
|
165
198
|
// skip: the session ended, reconnecting would only be refused again
|
|
166
199
|
return;
|
|
167
200
|
}
|
|
201
|
+
disarmStallWatchdog();
|
|
168
202
|
scheduleReconnect();
|
|
169
203
|
});
|
|
170
204
|
});
|
|
@@ -196,6 +230,7 @@ export function createEventSourceLiveEvents(options = {}) {
|
|
|
196
230
|
cancelReconnect = undefined;
|
|
197
231
|
}
|
|
198
232
|
reconnectAttempt = 0;
|
|
233
|
+
disarmStallWatchdog();
|
|
199
234
|
// skip: already closed, nothing to close
|
|
200
235
|
if (source === undefined)
|
|
201
236
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.404.1",
|
|
4
4
|
"description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -48,21 +48,21 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
52
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
53
|
-
"@types/qrcode": "^1.5.
|
|
51
|
+
"@cosmicdrift/kumiko-framework": "0.404.1",
|
|
52
|
+
"@cosmicdrift/kumiko-types": "0.404.1",
|
|
53
|
+
"@types/qrcode": "^1.5.6",
|
|
54
54
|
"class-variance-authority": "^0.7.1",
|
|
55
55
|
"clsx": "^2.1.1",
|
|
56
56
|
"cmdk": "^1.1.1",
|
|
57
|
-
"lucide-react": "^1.
|
|
58
|
-
"pdfjs-dist": "6.
|
|
57
|
+
"lucide-react": "^1.65.0",
|
|
58
|
+
"pdfjs-dist": "6.4.299",
|
|
59
59
|
"qrcode": "^1.5.4",
|
|
60
60
|
"radix-ui": "^1.8.1",
|
|
61
|
-
"react": "^19.
|
|
62
|
-
"react-day-picker": "^10.0.
|
|
63
|
-
"react-dom": "^19.
|
|
64
|
-
"react-resizable-panels": "^4.
|
|
65
|
-
"tailwind-merge": "^3.
|
|
61
|
+
"react": "^19.3.1",
|
|
62
|
+
"react-day-picker": "^10.0.2",
|
|
63
|
+
"react-dom": "^19.3.1",
|
|
64
|
+
"react-resizable-panels": "^4.15.2",
|
|
65
|
+
"tailwind-merge": "^3.7.0",
|
|
66
66
|
"temporal-polyfill": "^0.3.2",
|
|
67
67
|
"zod": "^4.6.7"
|
|
68
68
|
},
|
|
@@ -79,13 +79,13 @@
|
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
83
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
82
|
+
"@cosmicdrift/kumiko-locale-de": "0.404.1",
|
|
83
|
+
"@cosmicdrift/kumiko-testing": "0.404.1",
|
|
84
84
|
"@tailwindcss/cli": "^4.3.3",
|
|
85
85
|
"@testing-library/react": "^16.3.3",
|
|
86
86
|
"@testing-library/user-event": "^14.6.8",
|
|
87
|
-
"@types/react": "^19.
|
|
88
|
-
"@types/react-dom": "^19.
|
|
87
|
+
"@types/react": "^19.3.0",
|
|
88
|
+
"@types/react-dom": "^19.3.0",
|
|
89
89
|
"jsdom": "^29.1.1",
|
|
90
90
|
"tailwindcss": "^4.3.3"
|
|
91
91
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.404.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "SSE client reconnects after a stalled stream",
|
|
6
|
+
"detail": "createEventSourceLiveEvents arms a 30 s watchdog (two missed server heartbeats) that is reset by open, ping and every entity or session-ended frame. On expiry it closes the connection and reconnects through the existing backoff path, which resyncs all listeners. The timer is cleared on close and error. The seam is the new stallTimers option."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.403.0",
|
|
4
10
|
"type": "improvement",
|