quidproquo-web-react 0.1.1 → 0.1.2

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.
Files changed (31) hide show
  1. package/lib/commonjs/auth/hooks/useRefreshTokens.js +5 -3
  2. package/lib/commonjs/dateTime/DateTime.js +6 -14
  3. package/lib/commonjs/hooks/asmj/createQpqRuntimeDefinition.js +1 -1
  4. package/lib/commonjs/hooks/asmj/index.d.ts +1 -1
  5. package/lib/commonjs/hooks/asmj/useQpqRuntime.js +1 -0
  6. package/lib/commonjs/hooks/useAsyncEffect.js +3 -0
  7. package/lib/commonjs/hooks/useQpq.js +10 -3
  8. package/lib/commonjs/hooks/useRunEvery.js +6 -2
  9. package/lib/commonjs/hooks/useThrottledMemo.js +3 -0
  10. package/lib/commonjs/useFieldBinding/useFieldBinding.js +3 -3
  11. package/lib/commonjs/webSocketQueue/hooks/actionProcessor/getServiceRequestActionProcessor.js +1 -1
  12. package/lib/commonjs/webSocketQueue/hooks/useQpqWebsocketQueueRuntime.d.ts +1 -6
  13. package/lib/commonjs/websocket/WebSocketAuthSync/WebSocketAuthSync.js +3 -3
  14. package/lib/commonjs/websocket/WebSocketAuthSync/hooks/useWebsocketAuthSync.js +1 -1
  15. package/lib/esm/auth/hooks/useRefreshTokens.js +5 -3
  16. package/lib/esm/dateTime/DateTime.js +6 -14
  17. package/lib/esm/hooks/asmj/createQpqRuntimeDefinition.js +1 -1
  18. package/lib/esm/hooks/asmj/index.d.ts +1 -1
  19. package/lib/esm/hooks/asmj/index.js +1 -1
  20. package/lib/esm/hooks/asmj/useQpqRuntime.js +1 -0
  21. package/lib/esm/hooks/useAsyncEffect.js +3 -0
  22. package/lib/esm/hooks/useQpq.js +11 -4
  23. package/lib/esm/hooks/useRunEvery.js +6 -2
  24. package/lib/esm/hooks/useThrottledMemo.js +3 -0
  25. package/lib/esm/useFieldBinding/useFieldBinding.js +3 -3
  26. package/lib/esm/webSocketQueue/hooks/actionProcessor/getServiceRequestActionProcessor.js +1 -1
  27. package/lib/esm/webSocketQueue/hooks/useQpqWebsocketQueueRuntime.d.ts +1 -6
  28. package/lib/esm/webSocketQueue/hooks/useWebsocketQueueSendEvent.js +1 -1
  29. package/lib/esm/websocket/WebSocketAuthSync/WebSocketAuthSync.js +3 -3
  30. package/lib/esm/websocket/WebSocketAuthSync/hooks/useWebsocketAuthSync.js +1 -1
  31. package/package.json +7 -7
@@ -5,7 +5,9 @@ const react_1 = require("react");
5
5
  const useFastCallback_1 = require("../../hooks/useFastCallback");
6
6
  const useRefreshTokens = (authenticationInfo, refreshTokens) => {
7
7
  const stableRefreshTokens = (0, useFastCallback_1.useFastCallback)(refreshTokens);
8
- const refresh = () => {
8
+ // Stable identity so the effect below can list it as a dependency without
9
+ // re-running on every render.
10
+ const refresh = (0, useFastCallback_1.useFastCallback)(() => {
9
11
  if (authenticationInfo && authenticationInfo.refreshToken && authenticationInfo.expiresAt) {
10
12
  const now = new Date().toISOString();
11
13
  const timeToExpire = new Date(authenticationInfo.expiresAt).getTime() - new Date(now).getTime();
@@ -23,7 +25,7 @@ const useRefreshTokens = (authenticationInfo, refreshTokens) => {
23
25
  }
24
26
  }
25
27
  return null;
26
- };
28
+ });
27
29
  (0, react_1.useEffect)(() => {
28
30
  const timerId = refresh();
29
31
  return () => {
@@ -38,6 +40,6 @@ const useRefreshTokens = (authenticationInfo, refreshTokens) => {
38
40
  // reference would re-run this effect every render — and when the token is
39
41
  // expired/near-expiry the "refresh immediately" branch would then fire a
40
42
  // network refresh on every render, an unbounded loop.
41
- }, [authenticationInfo === null || authenticationInfo === void 0 ? void 0 : authenticationInfo.expiresAt, authenticationInfo === null || authenticationInfo === void 0 ? void 0 : authenticationInfo.refreshToken]);
43
+ }, [refresh, authenticationInfo === null || authenticationInfo === void 0 ? void 0 : authenticationInfo.expiresAt, authenticationInfo === null || authenticationInfo === void 0 ? void 0 : authenticationInfo.refreshToken]);
42
44
  };
43
45
  exports.useRefreshTokens = useRefreshTokens;
@@ -27,20 +27,12 @@ const pickOptions = (options, keys) => {
27
27
  }, {});
28
28
  return Object.keys(picked).length > 0 ? picked : undefined;
29
29
  };
30
- const toDate = (props) => {
31
- if ('isoDateTime' in props) {
32
- return new Date(props.isoDateTime);
33
- }
34
- if ('unixTimestampMs' in props) {
35
- return new Date(props.unixTimestampMs);
36
- }
37
- return props.date;
38
- };
39
- const component = (props) => {
40
- const { locale, dateTimeFormatOptions, showTimeAgo = true, hideDate = false, hideTime = false, hideTimeAgo = false, isMultiline = true, placeholder = '—', className } = props;
30
+ const DateTimeComponent = (props) => {
31
+ const { locale, dateTimeFormatOptions, showTimeAgo = true, hideDate = false, hideTime = false, hideTimeAgo = false, isMultiline = true, placeholder = '', className, } = props;
41
32
  // Stable primitive key so a fresh props object doesn't churn the memo.
33
+ // Both the ISO string and the epoch-ms number are valid Date constructor inputs.
42
34
  const dateKey = 'isoDateTime' in props ? props.isoDateTime : 'unixTimestampMs' in props ? props.unixTimestampMs : props.date.getTime();
43
- const date = (0, react_1.useMemo)(() => toDate(props), [dateKey]);
35
+ const date = (0, react_1.useMemo)(() => new Date(dateKey), [dateKey]);
44
36
  const isValidDate = !Number.isNaN(date.getTime());
45
37
  const formattedDate = (0, react_1.useMemo)(() => { var _a; return (hideDate ? '' : date.toLocaleDateString(locale, (_a = pickOptions(dateTimeFormatOptions, DATE_OPTION_KEYS)) !== null && _a !== void 0 ? _a : DEFAULT_DATE_OPTIONS)); }, [date, locale, dateTimeFormatOptions, hideDate]);
46
38
  const formattedTime = (0, react_1.useMemo)(() => { var _a; return (hideTime ? '' : date.toLocaleTimeString(locale, (_a = pickOptions(dateTimeFormatOptions, TIME_OPTION_KEYS)) !== null && _a !== void 0 ? _a : DEFAULT_TIME_OPTIONS)); }, [date, locale, dateTimeFormatOptions, hideTime]);
@@ -52,6 +44,6 @@ const component = (props) => {
52
44
  if (!isValidDate) {
53
45
  return (0, jsx_runtime_1.jsx)("time", { className: className, children: placeholder });
54
46
  }
55
- return ((0, jsx_runtime_1.jsx)("time", { dateTime: date.toISOString(), title: date.toLocaleString(locale), className: className, children: isMultiline ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [formattedDateTime ? (0, jsx_runtime_1.jsx)("div", { children: formattedDateTime }) : null, displayTimeAgo ? (0, jsx_runtime_1.jsx)("div", { children: timeAgo }) : null] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [formattedDateTime, displayTimeAgo ? ` (${timeAgo})` : ''] })) }));
47
+ return ((0, jsx_runtime_1.jsx)("time", { className: className, dateTime: date.toISOString(), title: date.toLocaleString(locale), children: isMultiline ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [formattedDateTime ? (0, jsx_runtime_1.jsx)("div", { children: formattedDateTime }) : null, displayTimeAgo ? (0, jsx_runtime_1.jsx)("div", { children: timeAgo }) : null] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [formattedDateTime, displayTimeAgo ? ` (${timeAgo})` : ''] })) }));
56
48
  };
57
- exports.DateTime = (0, react_1.memo)(component);
49
+ exports.DateTime = (0, react_1.memo)(DateTimeComponent);
@@ -38,7 +38,7 @@ function useQpqRuntimeState(atom, name) {
38
38
  const info = atom(name);
39
39
  info.state = newState;
40
40
  setState(newState);
41
- }, [setState]);
41
+ }, [atom, name, setState]);
42
42
  const getState = (0, react_1.useCallback)(() => {
43
43
  const state = atom(name).state;
44
44
  return state;
@@ -1,5 +1,5 @@
1
1
  export * from './actionProcessor';
2
- export * from "./bubbleReducer";
2
+ export * from './bubbleReducer';
3
3
  export * from './createQpqRuntimeDefinition';
4
4
  export * from './QpqContextProvider';
5
5
  export * from './QpqMappedApi';
@@ -42,6 +42,7 @@ function useQpqRuntime(atom, mainStory, name, getActionProcessors = () => () =>
42
42
  if (mainStory) {
43
43
  resolver(mainStory)();
44
44
  }
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
46
  }, []);
46
47
  return [api, state, dispatch];
47
48
  }
@@ -31,6 +31,9 @@ const useAsyncEffect = (effect, deps) => {
31
31
  }
32
32
  });
33
33
  };
34
+ // The dependency list is supplied by the caller, so it can't be statically
35
+ // verified here — this hook is a passthrough wrapper around useEffect.
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
37
  }, deps);
35
38
  };
36
39
  exports.useAsyncEffect = useAsyncEffect;
@@ -15,6 +15,7 @@ const quidproquo_core_1 = require("quidproquo-core");
15
15
  const react_1 = require("react");
16
16
  const actionProcessor_1 = require("../actionProcessor");
17
17
  const QpqContextProvider_1 = require("./asmj/QpqContextProvider");
18
+ const useEffectCallback_1 = require("./useEffectCallback");
18
19
  function* withVersionCheck(story, versionRef, capturedVersion) {
19
20
  let nextValue = undefined;
20
21
  while (true) {
@@ -41,18 +42,24 @@ function useQpq(getActionProcessors = () => __awaiter(this, void 0, void 0, func
41
42
  const contextGetActionProcessors = (0, actionProcessor_1.useActionProcessors)();
42
43
  const versionRef = (0, react_1.useRef)(0);
43
44
  (0, react_1.useEffect)(() => {
45
+ const capturedVersionRef = versionRef;
44
46
  return () => {
45
- versionRef.current++;
47
+ capturedVersionRef.current++;
46
48
  };
47
49
  }, []);
50
+ // Stable identities so the runtime memo can list them as dependencies
51
+ // without being recreated every render — the runtime always calls the
52
+ // latest processors through these wrappers.
53
+ const stableContextGetActionProcessors = (0, useEffectCallback_1.useEffectCallback)(contextGetActionProcessors);
54
+ const stableGetActionProcessors = (0, useEffectCallback_1.useEffectCallback)(getActionProcessors);
48
55
  const resolveStory = (0, react_1.useMemo)(() => (0, quidproquo_core_1.createRuntime)([(0, quidproquo_core_1.defineModule)('UI')], {
49
56
  depth: 0,
50
57
  context: qpqContextValues,
51
58
  }, (qpqConfig, dynamicModuleLoader) => __awaiter(this, void 0, void 0, function* () {
52
- return (Object.assign(Object.assign(Object.assign({}, (yield (0, quidproquo_actionprocessor_web_1.getWebActionProcessors)(qpqConfig, dynamicModuleLoader))), (yield contextGetActionProcessors(qpqConfig, dynamicModuleLoader))), (yield getActionProcessors(qpqConfig, dynamicModuleLoader))));
59
+ return (Object.assign(Object.assign(Object.assign({}, (yield (0, quidproquo_actionprocessor_web_1.getWebActionProcessors)(qpqConfig, dynamicModuleLoader))), (yield stableContextGetActionProcessors(qpqConfig, dynamicModuleLoader))), (yield stableGetActionProcessors(qpqConfig, dynamicModuleLoader))));
53
60
  }), () => new Date().toISOString(), logger, `frontend::${'uuid'}`, quidproquo_core_1.QpqRuntimeType.UI, (_runtime) => __awaiter(this, void 0, void 0, function* () {
54
61
  // noop
55
- })), [qpqContextValues]);
62
+ })), [qpqContextValues, stableContextGetActionProcessors, stableGetActionProcessors]);
56
63
  const qpq = (0, react_1.useCallback)(function getStoryExecutor(story) {
57
64
  return (...args) => __awaiter(this, void 0, void 0, function* () {
58
65
  const capturedVersion = versionRef.current;
@@ -2,12 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useRunEvery = useRunEvery;
4
4
  const react_1 = require("react");
5
+ const useEffectCallback_1 = require("./useEffectCallback");
5
6
  function useRunEvery(func, intervalSeconds) {
6
7
  const [value, setValue] = (0, react_1.useState)(func);
7
8
  const intervalRef = (0, react_1.useRef)(null);
9
+ // Stable identity so a caller passing a fresh arrow each render doesn't
10
+ // tear down and recreate the interval.
11
+ const stableFunc = (0, useEffectCallback_1.useEffectCallback)(func);
8
12
  (0, react_1.useEffect)(() => {
9
13
  intervalRef.current = window.setInterval(() => {
10
- const newValue = func();
14
+ const newValue = stableFunc();
11
15
  setValue(newValue);
12
16
  }, intervalSeconds * 1000);
13
17
  return () => {
@@ -16,6 +20,6 @@ function useRunEvery(func, intervalSeconds) {
16
20
  intervalRef.current = null;
17
21
  }
18
22
  };
19
- }, [intervalSeconds]);
23
+ }, [intervalSeconds, stableFunc]);
20
24
  return value;
21
25
  }
@@ -26,6 +26,9 @@ function useThrottledMemo(factory, deps, delaySeconds = 1) {
26
26
  timeoutRef.current = null;
27
27
  }
28
28
  };
29
+ // The dependency list is supplied by the caller, so it can't be statically
30
+ // verified here — this hook is a throttled wrapper around useMemo semantics.
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
32
  }, [...deps, delaySeconds]);
30
33
  return value;
31
34
  }
@@ -12,9 +12,9 @@ const useFieldBinding = (computedAtom, someSetter, valueExtractor) => {
12
12
  someSetter(newValue);
13
13
  };
14
14
  (0, react_1.useEffect)(() => {
15
- if (computedValue !== value) {
16
- setValue(computedValue);
17
- }
15
+ // React bails out when the value is unchanged, so no equality guard is
16
+ // needed — and local edits (which also change `value`) don't re-run this.
17
+ setValue(computedValue);
18
18
  }, [computedValue]);
19
19
  return [value, handleChange];
20
20
  };
@@ -13,7 +13,7 @@ exports.getServiceRequestActionProcessor = void 0;
13
13
  const quidproquo_core_1 = require("quidproquo-core");
14
14
  const quidproquo_webserver_1 = require("quidproquo-webserver");
15
15
  const getServiceRequestActionProcessor = (sendEventRef) => ({
16
- [quidproquo_webserver_1.ServiceActionType.Request]: (_a) => __awaiter(void 0, [_a], void 0, function* ({ serviceName, method, payload, }) {
16
+ [quidproquo_webserver_1.ServiceActionType.Request]: (_a) => __awaiter(void 0, [_a], void 0, function* ({ serviceName, method, payload }) {
17
17
  var _b;
18
18
  const response = yield ((_b = sendEventRef.current) === null || _b === void 0 ? void 0 : _b.call(sendEventRef, { type: `qpq/serviceRequest/${serviceName}/${method}`, payload }));
19
19
  if (response && !response.success) {
@@ -3,9 +3,4 @@ import { AnyWebSocketQueueEventMessageWithCorrelation } from 'quidproquo-webserv
3
3
  import { QpqRuntimeDefinition } from '../../hooks/asmj/createQpqRuntimeDefinition';
4
4
  import { QpqApi, QpqMappedApi } from '../../hooks/asmj/QpqMappedApi';
5
5
  import { ActionProcessorListResolverFactory } from '../../hooks/asmj/useQpqRuntime';
6
- export declare function useQpqWebsocketQueueRuntime<TState, TAction, TApi extends QpqApi>(atom: QpqRuntimeDefinition<TState, TAction, TApi>, mainStory?: Story<any, any>, name?: string, getActionProcessors?: ActionProcessorListResolverFactory<TState>): [
7
- QpqMappedApi<TApi>,
8
- TState,
9
- (action: any) => void,
10
- (event: Omit<AnyWebSocketQueueEventMessageWithCorrelation, 'correlationId'>) => Promise<any>
11
- ];
6
+ export declare function useQpqWebsocketQueueRuntime<TState, TAction, TApi extends QpqApi>(atom: QpqRuntimeDefinition<TState, TAction, TApi>, mainStory?: Story<any, any>, name?: string, getActionProcessors?: ActionProcessorListResolverFactory<TState>): [QpqMappedApi<TApi>, TState, (action: any) => void, (event: Omit<AnyWebSocketQueueEventMessageWithCorrelation, 'correlationId'>) => Promise<any>];
@@ -5,10 +5,10 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const hooks_1 = require("./hooks");
7
7
  const WebSocketAuthContext_1 = require("./WebSocketAuthContext");
8
- const component = ({ children, accessToken }) => {
8
+ const WebSocketAuthSyncComponent = ({ children, accessToken }) => {
9
9
  const isAuthenticated = (0, hooks_1.useWebsocketAuthSync)(accessToken);
10
10
  // Keep the active websocket alive
11
11
  (0, hooks_1.useWebsocketPingPong)();
12
- return ((0, jsx_runtime_1.jsx)(WebSocketAuthContext_1.WebSocketAuthContext.Provider, { value: isAuthenticated, children: children }));
12
+ return (0, jsx_runtime_1.jsx)(WebSocketAuthContext_1.WebSocketAuthContext.Provider, { value: isAuthenticated, children: children });
13
13
  };
14
- exports.WebSocketAuthSync = (0, react_1.memo)(component);
14
+ exports.WebSocketAuthSync = (0, react_1.memo)(WebSocketAuthSyncComponent);
@@ -34,7 +34,7 @@ const useWebsocketAuthSync = (accessToken) => {
34
34
  (0, hooks_2.useSubscribeToWebsocket)(quidproquo_web_1.WebsocketServiceEvent.OPEN, updateAuthTokens);
35
35
  (0, hooks_2.useSubscribeToWebsocket)(quidproquo_web_1.WebsocketServiceEvent.CLOSE, () => setIsAuthenticated(false));
36
36
  // Sync the tokens when they change
37
- (0, react_1.useEffect)(updateAuthTokens, [accessToken, websocketApi, isAuthenticated]);
37
+ (0, react_1.useEffect)(updateAuthTokens, [updateAuthTokens, accessToken, websocketApi, isAuthenticated]);
38
38
  return isAuthenticated;
39
39
  };
40
40
  exports.useWebsocketAuthSync = useWebsocketAuthSync;
@@ -2,7 +2,9 @@ import { useEffect } from 'react';
2
2
  import { useFastCallback } from '../../hooks/useFastCallback';
3
3
  export const useRefreshTokens = (authenticationInfo, refreshTokens) => {
4
4
  const stableRefreshTokens = useFastCallback(refreshTokens);
5
- const refresh = () => {
5
+ // Stable identity so the effect below can list it as a dependency without
6
+ // re-running on every render.
7
+ const refresh = useFastCallback(() => {
6
8
  if (authenticationInfo && authenticationInfo.refreshToken && authenticationInfo.expiresAt) {
7
9
  const now = new Date().toISOString();
8
10
  const timeToExpire = new Date(authenticationInfo.expiresAt).getTime() - new Date(now).getTime();
@@ -20,7 +22,7 @@ export const useRefreshTokens = (authenticationInfo, refreshTokens) => {
20
22
  }
21
23
  }
22
24
  return null;
23
- };
25
+ });
24
26
  useEffect(() => {
25
27
  const timerId = refresh();
26
28
  return () => {
@@ -35,5 +37,5 @@ export const useRefreshTokens = (authenticationInfo, refreshTokens) => {
35
37
  // reference would re-run this effect every render — and when the token is
36
38
  // expired/near-expiry the "refresh immediately" branch would then fire a
37
39
  // network refresh on every render, an unbounded loop.
38
- }, [authenticationInfo?.expiresAt, authenticationInfo?.refreshToken]);
40
+ }, [refresh, authenticationInfo?.expiresAt, authenticationInfo?.refreshToken]);
39
41
  };
@@ -24,20 +24,12 @@ const pickOptions = (options, keys) => {
24
24
  }, {});
25
25
  return Object.keys(picked).length > 0 ? picked : undefined;
26
26
  };
27
- const toDate = (props) => {
28
- if ('isoDateTime' in props) {
29
- return new Date(props.isoDateTime);
30
- }
31
- if ('unixTimestampMs' in props) {
32
- return new Date(props.unixTimestampMs);
33
- }
34
- return props.date;
35
- };
36
- const component = (props) => {
37
- const { locale, dateTimeFormatOptions, showTimeAgo = true, hideDate = false, hideTime = false, hideTimeAgo = false, isMultiline = true, placeholder = '—', className } = props;
27
+ const DateTimeComponent = (props) => {
28
+ const { locale, dateTimeFormatOptions, showTimeAgo = true, hideDate = false, hideTime = false, hideTimeAgo = false, isMultiline = true, placeholder = '', className, } = props;
38
29
  // Stable primitive key so a fresh props object doesn't churn the memo.
30
+ // Both the ISO string and the epoch-ms number are valid Date constructor inputs.
39
31
  const dateKey = 'isoDateTime' in props ? props.isoDateTime : 'unixTimestampMs' in props ? props.unixTimestampMs : props.date.getTime();
40
- const date = useMemo(() => toDate(props), [dateKey]);
32
+ const date = useMemo(() => new Date(dateKey), [dateKey]);
41
33
  const isValidDate = !Number.isNaN(date.getTime());
42
34
  const formattedDate = useMemo(() => (hideDate ? '' : date.toLocaleDateString(locale, pickOptions(dateTimeFormatOptions, DATE_OPTION_KEYS) ?? DEFAULT_DATE_OPTIONS)), [date, locale, dateTimeFormatOptions, hideDate]);
43
35
  const formattedTime = useMemo(() => (hideTime ? '' : date.toLocaleTimeString(locale, pickOptions(dateTimeFormatOptions, TIME_OPTION_KEYS) ?? DEFAULT_TIME_OPTIONS)), [date, locale, dateTimeFormatOptions, hideTime]);
@@ -49,6 +41,6 @@ const component = (props) => {
49
41
  if (!isValidDate) {
50
42
  return _jsx("time", { className: className, children: placeholder });
51
43
  }
52
- return (_jsx("time", { dateTime: date.toISOString(), title: date.toLocaleString(locale), className: className, children: isMultiline ? (_jsxs(_Fragment, { children: [formattedDateTime ? _jsx("div", { children: formattedDateTime }) : null, displayTimeAgo ? _jsx("div", { children: timeAgo }) : null] })) : (_jsxs(_Fragment, { children: [formattedDateTime, displayTimeAgo ? ` (${timeAgo})` : ''] })) }));
44
+ return (_jsx("time", { className: className, dateTime: date.toISOString(), title: date.toLocaleString(locale), children: isMultiline ? (_jsxs(_Fragment, { children: [formattedDateTime ? _jsx("div", { children: formattedDateTime }) : null, displayTimeAgo ? _jsx("div", { children: timeAgo }) : null] })) : (_jsxs(_Fragment, { children: [formattedDateTime, displayTimeAgo ? ` (${timeAgo})` : ''] })) }));
53
45
  };
54
- export const DateTime = memo(component);
46
+ export const DateTime = memo(DateTimeComponent);
@@ -32,7 +32,7 @@ export function useQpqRuntimeState(atom, name) {
32
32
  const info = atom(name);
33
33
  info.state = newState;
34
34
  setState(newState);
35
- }, [setState]);
35
+ }, [atom, name, setState]);
36
36
  const getState = useCallback(() => {
37
37
  const state = atom(name).state;
38
38
  return state;
@@ -1,5 +1,5 @@
1
1
  export * from './actionProcessor';
2
- export * from "./bubbleReducer";
2
+ export * from './bubbleReducer';
3
3
  export * from './createQpqRuntimeDefinition';
4
4
  export * from './QpqContextProvider';
5
5
  export * from './QpqMappedApi';
@@ -1,5 +1,5 @@
1
1
  export * from './actionProcessor';
2
- export * from "./bubbleReducer";
2
+ export * from './bubbleReducer';
3
3
  export * from './createQpqRuntimeDefinition';
4
4
  export * from './QpqContextProvider';
5
5
  export * from './QpqMappedApi';
@@ -31,6 +31,7 @@ export function useQpqRuntime(atom, mainStory, name, getActionProcessors = () =>
31
31
  if (mainStory) {
32
32
  resolver(mainStory)();
33
33
  }
34
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
35
  }, []);
35
36
  return [api, state, dispatch];
36
37
  }
@@ -19,5 +19,8 @@ export const useAsyncEffect = (effect, deps) => {
19
19
  }
20
20
  });
21
21
  };
22
+ // The dependency list is supplied by the caller, so it can't be statically
23
+ // verified here — this hook is a passthrough wrapper around useEffect.
24
+ // eslint-disable-next-line react-hooks/exhaustive-deps
22
25
  }, deps);
23
26
  };
@@ -3,6 +3,7 @@ import { createRuntime, defineModule, QpqRuntimeType, } from 'quidproquo-core';
3
3
  import { useCallback, useEffect, useMemo, useRef } from 'react';
4
4
  import { useActionProcessors } from '../actionProcessor';
5
5
  import { useQpqContextValues } from './asmj/QpqContextProvider';
6
+ import { useEffectCallback } from './useEffectCallback';
6
7
  function* withVersionCheck(story, versionRef, capturedVersion) {
7
8
  let nextValue = undefined;
8
9
  while (true) {
@@ -29,20 +30,26 @@ export function useQpq(getActionProcessors = async () => ({})) {
29
30
  const contextGetActionProcessors = useActionProcessors();
30
31
  const versionRef = useRef(0);
31
32
  useEffect(() => {
33
+ const capturedVersionRef = versionRef;
32
34
  return () => {
33
- versionRef.current++;
35
+ capturedVersionRef.current++;
34
36
  };
35
37
  }, []);
38
+ // Stable identities so the runtime memo can list them as dependencies
39
+ // without being recreated every render — the runtime always calls the
40
+ // latest processors through these wrappers.
41
+ const stableContextGetActionProcessors = useEffectCallback(contextGetActionProcessors);
42
+ const stableGetActionProcessors = useEffectCallback(getActionProcessors);
36
43
  const resolveStory = useMemo(() => createRuntime([defineModule('UI')], {
37
44
  depth: 0,
38
45
  context: qpqContextValues,
39
46
  }, async (qpqConfig, dynamicModuleLoader) => ({
40
47
  ...(await getWebActionProcessors(qpqConfig, dynamicModuleLoader)),
41
- ...(await contextGetActionProcessors(qpqConfig, dynamicModuleLoader)),
42
- ...(await getActionProcessors(qpqConfig, dynamicModuleLoader)),
48
+ ...(await stableContextGetActionProcessors(qpqConfig, dynamicModuleLoader)),
49
+ ...(await stableGetActionProcessors(qpqConfig, dynamicModuleLoader)),
43
50
  }), () => new Date().toISOString(), logger, `frontend::${'uuid'}`, QpqRuntimeType.UI, async (_runtime) => {
44
51
  // noop
45
- }), [qpqContextValues]);
52
+ }), [qpqContextValues, stableContextGetActionProcessors, stableGetActionProcessors]);
46
53
  const qpq = useCallback(function getStoryExecutor(story) {
47
54
  return async (...args) => {
48
55
  const capturedVersion = versionRef.current;
@@ -1,10 +1,14 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
+ import { useEffectCallback } from './useEffectCallback';
2
3
  export function useRunEvery(func, intervalSeconds) {
3
4
  const [value, setValue] = useState(func);
4
5
  const intervalRef = useRef(null);
6
+ // Stable identity so a caller passing a fresh arrow each render doesn't
7
+ // tear down and recreate the interval.
8
+ const stableFunc = useEffectCallback(func);
5
9
  useEffect(() => {
6
10
  intervalRef.current = window.setInterval(() => {
7
- const newValue = func();
11
+ const newValue = stableFunc();
8
12
  setValue(newValue);
9
13
  }, intervalSeconds * 1000);
10
14
  return () => {
@@ -13,6 +17,6 @@ export function useRunEvery(func, intervalSeconds) {
13
17
  intervalRef.current = null;
14
18
  }
15
19
  };
16
- }, [intervalSeconds]);
20
+ }, [intervalSeconds, stableFunc]);
17
21
  return value;
18
22
  }
@@ -23,6 +23,9 @@ export function useThrottledMemo(factory, deps, delaySeconds = 1) {
23
23
  timeoutRef.current = null;
24
24
  }
25
25
  };
26
+ // The dependency list is supplied by the caller, so it can't be statically
27
+ // verified here — this hook is a throttled wrapper around useMemo semantics.
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
29
  }, [...deps, delaySeconds]);
27
30
  return value;
28
31
  }
@@ -9,9 +9,9 @@ export const useFieldBinding = (computedAtom, someSetter, valueExtractor) => {
9
9
  someSetter(newValue);
10
10
  };
11
11
  useEffect(() => {
12
- if (computedValue !== value) {
13
- setValue(computedValue);
14
- }
12
+ // React bails out when the value is unchanged, so no equality guard is
13
+ // needed — and local edits (which also change `value`) don't re-run this.
14
+ setValue(computedValue);
15
15
  }, [computedValue]);
16
16
  return [value, handleChange];
17
17
  };
@@ -1,7 +1,7 @@
1
1
  import { actionResult, actionResultError } from 'quidproquo-core';
2
2
  import { ServiceActionType } from 'quidproquo-webserver';
3
3
  export const getServiceRequestActionProcessor = (sendEventRef) => ({
4
- [ServiceActionType.Request]: async ({ serviceName, method, payload, }) => {
4
+ [ServiceActionType.Request]: async ({ serviceName, method, payload }) => {
5
5
  const response = await sendEventRef.current?.({ type: `qpq/serviceRequest/${serviceName}/${method}`, payload });
6
6
  if (response && !response.success) {
7
7
  return actionResultError(response.error.errorType, response.error.errorText, response.error.errorStack);
@@ -3,9 +3,4 @@ import { AnyWebSocketQueueEventMessageWithCorrelation } from 'quidproquo-webserv
3
3
  import { QpqRuntimeDefinition } from '../../hooks/asmj/createQpqRuntimeDefinition';
4
4
  import { QpqApi, QpqMappedApi } from '../../hooks/asmj/QpqMappedApi';
5
5
  import { ActionProcessorListResolverFactory } from '../../hooks/asmj/useQpqRuntime';
6
- export declare function useQpqWebsocketQueueRuntime<TState, TAction, TApi extends QpqApi>(atom: QpqRuntimeDefinition<TState, TAction, TApi>, mainStory?: Story<any, any>, name?: string, getActionProcessors?: ActionProcessorListResolverFactory<TState>): [
7
- QpqMappedApi<TApi>,
8
- TState,
9
- (action: any) => void,
10
- (event: Omit<AnyWebSocketQueueEventMessageWithCorrelation, 'correlationId'>) => Promise<any>
11
- ];
6
+ export declare function useQpqWebsocketQueueRuntime<TState, TAction, TApi extends QpqApi>(atom: QpqRuntimeDefinition<TState, TAction, TApi>, mainStory?: Story<any, any>, name?: string, getActionProcessors?: ActionProcessorListResolverFactory<TState>): [QpqMappedApi<TApi>, TState, (action: any) => void, (event: Omit<AnyWebSocketQueueEventMessageWithCorrelation, 'correlationId'>) => Promise<any>];
@@ -1,5 +1,5 @@
1
1
  import { WebsocketServiceEvent } from 'quidproquo-web';
2
- import { WebSocketQueueServerMessageEventType } from 'quidproquo-webserver';
2
+ import { WebSocketQueueServerMessageEventType, } from 'quidproquo-webserver';
3
3
  import { useRef } from 'react';
4
4
  import { useFastCallback } from '../../hooks';
5
5
  import { useSubscribeToWebsocket, useWebsocketApi } from '../../websocket';
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { memo } from 'react';
3
3
  import { useWebsocketAuthSync, useWebsocketPingPong } from './hooks';
4
4
  import { WebSocketAuthContext } from './WebSocketAuthContext';
5
- const component = ({ children, accessToken }) => {
5
+ const WebSocketAuthSyncComponent = ({ children, accessToken }) => {
6
6
  const isAuthenticated = useWebsocketAuthSync(accessToken);
7
7
  // Keep the active websocket alive
8
8
  useWebsocketPingPong();
9
- return (_jsx(WebSocketAuthContext.Provider, { value: isAuthenticated, children: children }));
9
+ return _jsx(WebSocketAuthContext.Provider, { value: isAuthenticated, children: children });
10
10
  };
11
- export const WebSocketAuthSync = memo(component);
11
+ export const WebSocketAuthSync = memo(WebSocketAuthSyncComponent);
@@ -31,6 +31,6 @@ export const useWebsocketAuthSync = (accessToken) => {
31
31
  useSubscribeToWebsocket(WebsocketServiceEvent.OPEN, updateAuthTokens);
32
32
  useSubscribeToWebsocket(WebsocketServiceEvent.CLOSE, () => setIsAuthenticated(false));
33
33
  // Sync the tokens when they change
34
- useEffect(updateAuthTokens, [accessToken, websocketApi, isAuthenticated]);
34
+ useEffect(updateAuthTokens, [updateAuthTokens, accessToken, websocketApi, isAuthenticated]);
35
35
  return isAuthenticated;
36
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quidproquo-web-react",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -34,8 +34,8 @@
34
34
  "devDependencies": {
35
35
  "@testing-library/dom": "^10.4.1",
36
36
  "@testing-library/react": "^16.3.2",
37
- "quidproquo-core": "0.1.1",
38
- "quidproquo-tsconfig": "0.1.1",
37
+ "quidproquo-core": "0.1.2",
38
+ "quidproquo-tsconfig": "0.1.2",
39
39
  "typescript": "^5.8.2"
40
40
  },
41
41
  "peerDependencies": {
@@ -43,10 +43,10 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "jotai": "^2.12.1",
46
- "quidproquo-actionprocessor-node": "0.1.1",
47
- "quidproquo-actionprocessor-web": "0.1.1",
48
- "quidproquo-web": "0.1.1",
49
- "quidproquo-webserver": "0.1.1",
46
+ "quidproquo-actionprocessor-node": "0.1.2",
47
+ "quidproquo-actionprocessor-web": "0.1.2",
48
+ "quidproquo-web": "0.1.2",
49
+ "quidproquo-webserver": "0.1.2",
50
50
  "symbol-observable": "^4.0.0"
51
51
  }
52
52
  }