@lowdefy/client 6.0.0 → 7.0.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.
Files changed (51) hide show
  1. package/dist/Client.js +54 -17
  2. package/dist/Context.js +30 -12
  3. package/dist/HtmlOverlay.js +127 -0
  4. package/dist/IconProvider.js +24 -0
  5. package/dist/MountEvents.js +3 -3
  6. package/dist/adapters/createRouter.js +9 -3
  7. package/dist/adapters/url.js +1 -7
  8. package/dist/auth/createAuthMethods.js +1034 -41
  9. package/dist/block/Block.js +6 -3
  10. package/dist/block/CategorySwitch.js +6 -0
  11. package/dist/block/Container.js +3 -0
  12. package/dist/block/InputContainer.js +8 -2
  13. package/dist/block/List.js +3 -0
  14. package/dist/block/LoadingBlock.js +6 -12
  15. package/dist/block/LoadingContainer.js +6 -1
  16. package/dist/block/LoadingList.js +6 -1
  17. package/dist/block/loadingBlockMethods.js +26 -0
  18. package/dist/claimCallerReload.js +38 -0
  19. package/dist/createAntdIconConfig.js +91 -0
  20. package/dist/createCallAPI.js +6 -2
  21. package/dist/createCallRequest.js +6 -2
  22. package/dist/createHandleError.js +21 -11
  23. package/dist/createIcon.js +138 -36
  24. package/dist/createPageLifecycleManager.js +172 -0
  25. package/dist/formatIconTitle.js +26 -0
  26. package/dist/getActiveLocale.js +27 -0
  27. package/dist/getContextKey.js +29 -0
  28. package/dist/getMessageIcon.js +43 -0
  29. package/dist/hasOtherRoles.js +30 -0
  30. package/dist/index.js +1 -0
  31. package/dist/initLowdefyContext.js +103 -4
  32. package/dist/isOtherCaller.js +29 -0
  33. package/dist/recordCallerStamp.js +31 -0
  34. package/dist/renderIconNodes.js +46 -0
  35. package/dist/request.js +74 -6
  36. package/dist/setLowdefyUser.js +32 -0
  37. package/dist/setupLink.js +19 -13
  38. package/dist/shouldReloadForAuthError.js +39 -0
  39. package/dist/shouldReloadForBuild.js +44 -0
  40. package/dist/shouldReloadForCaller.js +50 -0
  41. package/dist/storage/clearUserStorage.js +36 -0
  42. package/dist/storage/clearUserStorageOnOwnerChange.js +41 -0
  43. package/dist/storage/createStorage.js +48 -0
  44. package/dist/storage/listUserStorageKeys.js +36 -0
  45. package/dist/storage/readStorageItem.js +31 -0
  46. package/dist/storage/userStorageKey.js +24 -0
  47. package/dist/storage/userStoragePrefix.js +25 -0
  48. package/dist/useDarkMode.js +17 -4
  49. package/dist/useLocale.js +18 -5
  50. package/dist/websocket/createWebSocketClient.js +239 -100
  51. package/package.json +11 -10
@@ -16,7 +16,7 @@
16
16
  import { ErrorBoundary } from '@lowdefy/block-utils';
17
17
  import CategorySwitch from './CategorySwitch.js';
18
18
  import MountEvents from '../MountEvents.js';
19
- const Block = ({ block, Blocks, context, lowdefy, parentLoading })=>{
19
+ const Block = ({ block, Blocks, context, lowdefy, parentLoading, renderHead })=>{
20
20
  const [updates, setUpdate] = useState(0);
21
21
  const loggedErrorsRef = useRef(new Set());
22
22
  context._internal.updaters[block.id] = ()=>setUpdate(updates + 1);
@@ -48,7 +48,9 @@ const Block = ({ block, Blocks, context, lowdefy, parentLoading })=>{
48
48
  configKey: block.eval?.configKey,
49
49
  onError: handleError,
50
50
  properties: block.eval?.properties
51
- }, /*#__PURE__*/ React.createElement(MountEvents, {
51
+ }, renderHead?.({
52
+ properties: block.eval.properties
53
+ }), /*#__PURE__*/ React.createElement(MountEvents, {
52
54
  context: context,
53
55
  triggerEvent: async ()=>{
54
56
  context._internal.lowdefy._internal.progress.dispatch({
@@ -76,7 +78,8 @@ const Block = ({ block, Blocks, context, lowdefy, parentLoading })=>{
76
78
  lowdefy._internal.progress.dispatch({
77
79
  type: 'done'
78
80
  });
79
- }
81
+ },
82
+ waitForMount: Object.hasOwn(block.Events.events, 'onMount')
80
83
  }, (eventLoading)=>/*#__PURE__*/ React.createElement(CategorySwitch, {
81
84
  block: block,
82
85
  Blocks: Blocks,
@@ -69,7 +69,9 @@ const CategorySwitch = ({ block, Blocks, context, loading, lowdefy })=>{
69
69
  layout: block.eval.layout
70
70
  }, /*#__PURE__*/ React.createElement(Component, {
71
71
  methods: Object.assign(block.methods, {
72
+ getAppStorageKey: lowdefy._internal.storage.appKey,
72
73
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
74
+ getUserStorageKey: lowdefy._internal.storage.userKey,
73
75
  registerEvent: block.registerEvent,
74
76
  registerMethod: block.registerMethod,
75
77
  setValue: block.setValue,
@@ -85,6 +87,7 @@ const CategorySwitch = ({ block, Blocks, context, loading, lowdefy })=>{
85
87
  loading: loading,
86
88
  menus: lowdefy.menus,
87
89
  pageId: lowdefy.pageId,
90
+ pathParams: context.pathParams,
88
91
  properties: block.eval.properties,
89
92
  required: block.eval.required,
90
93
  styles: block.eval.style ?? {},
@@ -108,7 +111,9 @@ const CategorySwitch = ({ block, Blocks, context, loading, lowdefy })=>{
108
111
  layout: block.eval.layout
109
112
  }, /*#__PURE__*/ React.createElement(Component, {
110
113
  methods: Object.assign(block.methods, {
114
+ getAppStorageKey: lowdefy._internal.storage.appKey,
111
115
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
116
+ getUserStorageKey: lowdefy._internal.storage.userKey,
112
117
  registerEvent: block.registerEvent,
113
118
  registerMethod: block.registerMethod,
114
119
  translate: lowdefy._internal.translate,
@@ -123,6 +128,7 @@ const CategorySwitch = ({ block, Blocks, context, loading, lowdefy })=>{
123
128
  loading: loading,
124
129
  menus: lowdefy.menus,
125
130
  pageId: lowdefy.pageId,
131
+ pathParams: context.pathParams,
126
132
  properties: block.eval.properties,
127
133
  required: block.eval.required,
128
134
  styles: block.eval.style ?? {},
@@ -51,7 +51,9 @@ const Container = ({ block, Blocks, Component, context, loading, lowdefy })=>{
51
51
  layout: block.eval.layout
52
52
  }, /*#__PURE__*/ React.createElement(Component, {
53
53
  methods: Object.assign(block.methods, {
54
+ getAppStorageKey: lowdefy._internal.storage.appKey,
54
55
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
56
+ getUserStorageKey: lowdefy._internal.storage.userKey,
55
57
  registerEvent: block.registerEvent,
56
58
  registerMethod: block.registerMethod,
57
59
  translate: lowdefy._internal.translate,
@@ -67,6 +69,7 @@ const Container = ({ block, Blocks, Component, context, loading, lowdefy })=>{
67
69
  loading: loading,
68
70
  menus: lowdefy.menus,
69
71
  pageId: lowdefy.pageId,
72
+ pathParams: context.pathParams,
70
73
  properties: block.eval.properties,
71
74
  required: block.eval.required,
72
75
  styles: block.eval.style ?? {},
@@ -24,7 +24,10 @@ const InputContainer = ({ block, Blocks, Component, context, loading, lowdefy })
24
24
  const slots = Blocks.subSlots[block.id][0].slots;
25
25
  Object.keys(slots).forEach((slotKey, i)=>{
26
26
  if (slots[slotKey].blocks.length === 0) return;
27
- content[slotKey] = (contentStyle)=>/*#__PURE__*/ React.createElement(Area, {
27
+ // A block may pass the loading its slot's blocks inherit (`{ loading }`), for slots that are
28
+ // its chrome rather than its content: the Table keeps its toolbar and bulk action blocks
29
+ // usable while its own rows load. By default they inherit the block's loading.
30
+ content[slotKey] = (contentStyle, { loading: slotLoading = loading } = {})=>/*#__PURE__*/ React.createElement(Area, {
28
31
  area: block.eval.slots[slotKey],
29
32
  areaKey: slotKey,
30
33
  style: {
@@ -41,7 +44,7 @@ const InputContainer = ({ block, Blocks, Component, context, loading, lowdefy })
41
44
  context: context,
42
45
  key: `co-${bl.blockId}-${k}`,
43
46
  lowdefy: lowdefy,
44
- parentLoading: loading
47
+ parentLoading: slotLoading
45
48
  })));
46
49
  });
47
50
  return /*#__PURE__*/ React.createElement(BlockLayout, {
@@ -51,7 +54,9 @@ const InputContainer = ({ block, Blocks, Component, context, loading, lowdefy })
51
54
  layout: block.eval.layout
52
55
  }, /*#__PURE__*/ React.createElement(Component, {
53
56
  methods: Object.assign(block.methods, {
57
+ getAppStorageKey: lowdefy._internal.storage.appKey,
54
58
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
59
+ getUserStorageKey: lowdefy._internal.storage.userKey,
55
60
  registerEvent: block.registerEvent,
56
61
  registerMethod: block.registerMethod,
57
62
  translate: lowdefy._internal.translate,
@@ -73,6 +78,7 @@ const InputContainer = ({ block, Blocks, Component, context, loading, lowdefy })
73
78
  loading: loading,
74
79
  menus: lowdefy.menus,
75
80
  pageId: lowdefy.pageId,
81
+ pathParams: context.pathParams,
76
82
  properties: block.eval.properties,
77
83
  required: block.eval.required,
78
84
  styles: block.eval.style ?? {},
@@ -56,7 +56,9 @@ const List = ({ block, Blocks, Component, context, loading, lowdefy })=>{
56
56
  layout: block.eval.layout
57
57
  }, /*#__PURE__*/ React.createElement(Component, {
58
58
  methods: Object.assign(block.methods, {
59
+ getAppStorageKey: lowdefy._internal.storage.appKey,
59
60
  getLocale: ()=>lowdefy.i18n?.active ?? lowdefy.i18n?.defaultLocale,
61
+ getUserStorageKey: lowdefy._internal.storage.userKey,
60
62
  moveItemDown: block.moveItemDown,
61
63
  moveItemUp: block.moveItemUp,
62
64
  pushItem: block.pushItem,
@@ -78,6 +80,7 @@ const List = ({ block, Blocks, Component, context, loading, lowdefy })=>{
78
80
  loading: loading,
79
81
  menus: lowdefy.menus,
80
82
  pageId: lowdefy.pageId,
83
+ pathParams: context.pathParams,
81
84
  properties: block.eval.properties,
82
85
  required: block.eval.required,
83
86
  styles: block.eval.style ?? {},
@@ -16,17 +16,7 @@
16
16
  import { BlockLayout } from '@lowdefy/layout';
17
17
  import LoadingContainer from './LoadingContainer.js';
18
18
  import LoadingList from './LoadingList.js';
19
- const blockMethods = {
20
- moveItemDown: ()=>{},
21
- moveItemUp: ()=>{},
22
- pushItem: ()=>{},
23
- registerEvent: ()=>{},
24
- registerMethod: ()=>{},
25
- removeItem: ()=>{},
26
- setValue: ()=>{},
27
- triggerEvent: ()=>{},
28
- unshiftItem: ()=>{}
29
- };
19
+ import loadingBlockMethods from './loadingBlockMethods.js';
30
20
  const LoadingBlock = ({ blockClass, blockId, blockLayout, blockProperties, blockStyle, context, lowdefy, skeleton })=>{
31
21
  let Component = lowdefy._internal.blockComponents[skeleton.type];
32
22
  useEffect(()=>{
@@ -74,7 +64,11 @@ const LoadingBlock = ({ blockClass, blockId, blockLayout, blockProperties, block
74
64
  components: lowdefy._internal.components,
75
65
  key: `s-${blockId}-${skeleton.id}`,
76
66
  menus: lowdefy.menus,
77
- methods: blockMethods,
67
+ methods: {
68
+ ...loadingBlockMethods,
69
+ getAppStorageKey: lowdefy._internal.storage.appKey,
70
+ getUserStorageKey: lowdefy._internal.storage.userKey
71
+ },
78
72
  pageId: lowdefy.pageId,
79
73
  properties: skeleton.properties ?? blockProperties
80
74
  }));
@@ -15,6 +15,7 @@
15
15
  */ import React from 'react';
16
16
  import { Area, BlockLayout } from '@lowdefy/layout';
17
17
  import LoadingBlock from './LoadingBlock.js';
18
+ import loadingBlockMethods from './loadingBlockMethods.js';
18
19
  const LoadingContainer = ({ blockClass, blockId, blockLayout, blockProperties, blockStyle, Component, context, lowdefy, skeleton })=>{
19
20
  const content = {};
20
21
  // eslint-disable-next-line prefer-destructuring
@@ -46,7 +47,11 @@ const LoadingContainer = ({ blockClass, blockId, blockLayout, blockProperties, b
46
47
  content: content,
47
48
  key: skeleton.id,
48
49
  menus: lowdefy.menus,
49
- methods: {},
50
+ methods: {
51
+ ...loadingBlockMethods,
52
+ getAppStorageKey: lowdefy._internal.storage.appKey,
53
+ getUserStorageKey: lowdefy._internal.storage.userKey
54
+ },
50
55
  pageId: lowdefy.pageId,
51
56
  properties: skeleton.properties ?? blockProperties
52
57
  }));
@@ -15,6 +15,7 @@
15
15
  */ import React from 'react';
16
16
  import { Area, BlockLayout } from '@lowdefy/layout';
17
17
  import LoadingBlock from './LoadingBlock.js';
18
+ import loadingBlockMethods from './loadingBlockMethods.js';
18
19
  const LoadingList = ({ blockClass, blockId, blockLayout, blockProperties, blockStyle, Component, context, lowdefy, skeleton })=>{
19
20
  const content = {};
20
21
  const contentList = [];
@@ -50,7 +51,11 @@ const LoadingList = ({ blockClass, blockId, blockLayout, blockProperties, blockS
50
51
  components: lowdefy._internal.components,
51
52
  list: contentList,
52
53
  menus: lowdefy.menus,
53
- methods: {},
54
+ methods: {
55
+ ...loadingBlockMethods,
56
+ getAppStorageKey: lowdefy._internal.storage.appKey,
57
+ getUserStorageKey: lowdefy._internal.storage.userKey
58
+ },
54
59
  pageId: lowdefy.pageId,
55
60
  properties: skeleton.properties ?? blockProperties
56
61
  }));
@@ -0,0 +1,26 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ const loadingBlockMethods = {
16
+ moveItemDown: ()=>{},
17
+ moveItemUp: ()=>{},
18
+ pushItem: ()=>{},
19
+ registerEvent: ()=>{},
20
+ registerMethod: ()=>{},
21
+ removeItem: ()=>{},
22
+ setValue: ()=>{},
23
+ triggerEvent: ()=>{},
24
+ unshiftItem: ()=>{}
25
+ };
26
+ export default loadingBlockMethods;
@@ -0,0 +1,38 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ // sessionStorage key holding when this tab last reloaded to follow its caller.
16
+ const RELOADED_FOR_CALLER_KEY = 'lowdefy.reloadedForCaller';
17
+ const RELOAD_INTERVAL_MS = 10000;
18
+ // True when this tab may reload because its caller changed (another person or
19
+ // organization, or the session ended), and records the reload. A tab reloads
20
+ // at most once per 10 seconds for its caller: if the freshly loaded page still
21
+ // disagrees with the server, reloading again would loop. Callers fail the call
22
+ // instead, so no data shows for the wrong caller either way.
23
+ function claimCallerReload({ window }) {
24
+ const now = Date.now();
25
+ try {
26
+ const last = Number(window.sessionStorage.getItem(RELOADED_FOR_CALLER_KEY));
27
+ if (Math.abs(now - last) < RELOAD_INTERVAL_MS) {
28
+ return false;
29
+ }
30
+ window.sessionStorage.setItem(RELOADED_FOR_CALLER_KEY, String(now));
31
+ } catch (error) {
32
+ // Browsers with storage disabled throw on sessionStorage access. Without
33
+ // the record the loop guard cannot work, so do not reload.
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ export default claimCallerReload;
@@ -0,0 +1,91 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import React from 'react';
16
+ import { ConfigProvider } from 'antd';
17
+ // Icon keys for a ConfigProvider around the page tree, so antd's own chrome
18
+ // draws with the app's icon set. Tag is left out: a context closeIcon makes
19
+ // every Tag closable (antd's useClosable), so the Tag block passes its own.
20
+ function createAntdIconConfig({ Icon }) {
21
+ function chromeIcon(name, properties) {
22
+ return /*#__PURE__*/ React.createElement(Icon, {
23
+ properties: {
24
+ name,
25
+ title: '',
26
+ ...properties
27
+ }
28
+ });
29
+ }
30
+ // antd renders a function menu.expandIcon as a component, so it is created
31
+ // once here to keep its identity, and it adds the expand-icon class itself.
32
+ function MenuExpandIcon({ isOpen }) {
33
+ const { getPrefixCls } = React.useContext(ConfigProvider.ConfigContext);
34
+ return /*#__PURE__*/ React.createElement(Icon, {
35
+ className: `${getPrefixCls('menu')}-submenu-expand-icon`,
36
+ properties: {
37
+ name: 'chevron-down',
38
+ rotate: isOpen ? 180 : 0,
39
+ title: ''
40
+ }
41
+ });
42
+ }
43
+ const closeIcon = chromeIcon('close');
44
+ return {
45
+ alert: {
46
+ closeIcon,
47
+ errorIcon: chromeIcon('error'),
48
+ infoIcon: chromeIcon('info'),
49
+ successIcon: chromeIcon('success'),
50
+ warningIcon: chromeIcon('warning')
51
+ },
52
+ breadcrumb: {
53
+ dropdownIcon: chromeIcon('chevron-down')
54
+ },
55
+ button: {
56
+ loadingIcon: chromeIcon('loading', {
57
+ spin: true
58
+ })
59
+ },
60
+ collapse: {
61
+ expandIcon: ({ isActive })=>chromeIcon('chevron-right', {
62
+ rotate: isActive ? 90 : 0
63
+ })
64
+ },
65
+ datePicker: {
66
+ suffixIcon: chromeIcon('calendar')
67
+ },
68
+ drawer: {
69
+ closeIcon
70
+ },
71
+ menu: {
72
+ expandIcon: MenuExpandIcon
73
+ },
74
+ modal: {
75
+ closeIcon
76
+ },
77
+ tabs: {
78
+ moreIcon: chromeIcon('more')
79
+ },
80
+ timePicker: {
81
+ suffixIcon: chromeIcon('clock')
82
+ },
83
+ tour: {
84
+ closeIcon
85
+ },
86
+ treeSelect: {
87
+ switcherIcon: chromeIcon('chevron-down')
88
+ }
89
+ };
90
+ }
91
+ export default createAntdIconConfig;
@@ -13,16 +13,20 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import request from './request.js';
16
- function createCallAPI({ basePath }) {
16
+ function createCallAPI(lowdefy) {
17
+ const { basePath, buildId } = lowdefy;
17
18
  function callAPI({ payload, endpointId, pageId, blockId }) {
18
19
  return request({
20
+ buildId,
19
21
  url: `${basePath}/api/endpoints/${endpointId}`,
20
22
  method: 'POST',
21
23
  body: {
22
24
  payload,
23
25
  pageId,
24
26
  blockId
25
- }
27
+ },
28
+ reloadOnAuthorizationError: false,
29
+ lowdefy
26
30
  });
27
31
  }
28
32
  return callAPI;
@@ -13,16 +13,20 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import request from './request.js';
16
- function createCallRequest({ basePath }) {
16
+ function createCallRequest(lowdefy) {
17
+ const { basePath, buildId } = lowdefy;
17
18
  function callRequest({ actionId, blockId, pageId, payload, requestId }) {
18
19
  return request({
20
+ buildId,
19
21
  url: `${basePath}/api/request/${pageId}/${requestId}`,
20
22
  method: 'POST',
21
23
  body: {
22
24
  actionId,
23
25
  blockId,
24
26
  payload
25
- }
27
+ },
28
+ reloadOnAuthorizationError: true,
29
+ lowdefy
26
30
  });
27
31
  }
28
32
  return callRequest;
@@ -12,26 +12,36 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import { UserError } from '@lowdefy/errors';
15
+ */ import { getDevError } from '@lowdefy/engine';
16
+ import { expectedErrorNames } from '@lowdefy/errors';
16
17
  import { serializer } from '@lowdefy/helpers';
17
18
  function createHandleError(lowdefy) {
18
19
  const loggedErrors = new Set();
19
20
  const logger = lowdefy._internal.logger;
20
21
  function logError(error) {
21
- if (!(error instanceof UserError)) {
22
- lowdefy._runtimeErrorCallback?.(error);
23
- }
22
+ lowdefy._runtimeErrorCallback?.(error);
24
23
  logger.error(error);
25
24
  }
26
25
  return async function handleError(error) {
27
- const errorKey = `${error.message}:${error.configKey || ''}`;
26
+ // The dev tools show the dev server's full error; every decision below stays on
27
+ // the error config sees. Keying on the shown message means two different dev
28
+ // failures of one action both display. Every server failure carries its own
29
+ // requestId, and prod gives them all one generic message, so the requestId in
30
+ // the key is what lets a later failure - and the ID support asks for - log at
31
+ // all; one failure reaching here twice still logs once.
32
+ const devError = getDevError(error);
33
+ const shown = devError ?? error;
34
+ const errorKey = `${shown.message}:${error.configKey ?? ''}:${error.requestId ?? ''}`;
28
35
  if (loggedErrors.has(errorKey)) {
29
36
  return;
30
37
  }
31
38
  loggedErrors.add(errorKey);
32
- // UserError is client-only — log to browser console, never send to server
33
- if (error instanceof UserError) {
34
- logError(error);
39
+ // An expected outcome - a UserError, or an auth gate's refusal the server
40
+ // already logged as one warning line - logs to the browser console only,
41
+ // never sent to the server or the error bar. Matched by name, not
42
+ // instanceof: plugins bundle their own @lowdefy/errors copy.
43
+ if (expectedErrorNames.has(error?.name)) {
44
+ logger.error(shown);
35
45
  return;
36
46
  }
37
47
  // Send known error types to server for logging with location resolution
@@ -41,7 +51,7 @@ function createHandleError(lowdefy) {
41
51
  // but never gets a source (location resolution is skipped for it), so
42
52
  // keying on source POSTed it back and had it logged twice.
43
53
  if (error.handled) {
44
- logError(error);
54
+ logError(shown);
45
55
  return;
46
56
  }
47
57
  // Client-originated errors — send to server for logging + location resolution
@@ -70,11 +80,11 @@ function createHandleError(lowdefy) {
70
80
  } catch {
71
81
  // Server logging failed - continue with local console
72
82
  }
73
- logError(error);
83
+ logError(shown);
74
84
  return;
75
85
  }
76
86
  // Other errors - just log locally
77
- logError(error);
87
+ logError(shown);
78
88
  };
79
89
  }
80
90
  export default createHandleError;