cabloy 5.1.172 → 5.1.174

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 (97) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/package.json +2 -2
  4. package/repo-docs/frontend/component-guide.md +33 -0
  5. package/repo-docs/frontend/model-resource-cookbook.md +0 -8
  6. package/repo-docs/frontend/model-resource-usage-guide.md +0 -4
  7. package/repo-e2e/specs/cabloy-basic.spec.ts +158 -0
  8. package/repo-e2e/specs/controller-boundary.spec.ts +261 -0
  9. package/test-results/.last-run.json +1 -1
  10. package/vona/packages-utils/lint/package.json +1 -1
  11. package/vona/packages-utils/lint/src/oxc/lint.ts +1 -1
  12. package/vona/packages-utils/lint/src/oxc/lintVue.ts +1 -1
  13. package/vona/pnpm-lock.yaml +238 -46
  14. package/vona/pnpm-workspace.yaml +1 -1
  15. package/zova/package.original.json +1 -1
  16. package/zova/packages-cli/cli/package.json +2 -2
  17. package/zova/packages-cli/cli-set-front/package.json +3 -3
  18. package/zova/packages-utils/babel-plugin-zova-behavior/package.json +2 -1
  19. package/zova/packages-utils/babel-plugin-zova-behavior/src/index.ts +21 -8
  20. package/zova/packages-utils/babel-plugin-zova-behavior/test/behaviorSlots.test.ts +111 -0
  21. package/zova/packages-utils/zova-jsx/package.json +2 -2
  22. package/zova/packages-utils/zova-vite/package.json +2 -2
  23. package/zova/packages-zova/zova/package.json +3 -3
  24. package/zova/packages-zova/zova-core/package.json +1 -1
  25. package/zova/packages-zova/zova-core/src/bean/beanBase.ts +1 -1
  26. package/zova/packages-zova/zova-core/src/bean/beanContainer.ts +95 -39
  27. package/zova/packages-zova/zova-core/src/bean/resource/error/type.ts +9 -0
  28. package/zova/packages-zova/zova-core/src/bean/type.ts +18 -0
  29. package/zova/packages-zova/zova-core/src/components/component.ts +2 -1
  30. package/zova/packages-zova/zova-core/src/composables/useController.ts +155 -25
  31. package/zova/packages-zova/zova-core/src/core/component/error.ts +62 -5
  32. package/zova/packages-zova/zova-core/src/core/context/component.ts +37 -2
  33. package/zova/packages-zova/zova-core/src/core/context/hooks.ts +13 -2
  34. package/zova/packages-zova/zova-core/src/core/context/meta.ts +1 -0
  35. package/zova/packages-zova/zova-core/src/core/context/state.ts +161 -0
  36. package/zova/packages-zova/zova-core/src/core/sys/config.ts +32 -7
  37. package/zova/packages-zova/zova-core/src/types/interface/monkey.ts +15 -0
  38. package/zova/packages-zova/zova-core/src/types/utils/powerPartial.ts +1 -1
  39. package/zova/pnpm-lock.yaml +40 -25
  40. package/zova/src/front/config/config/config.ts +11 -0
  41. package/zova/src/front/config/config/render.tsx +79 -0
  42. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +0 -1
  43. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/component/controllerBoundaryProbe.ts +31 -0
  44. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/component/controllerBoundaryProbeInline.ts +34 -0
  45. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/component/controllerBoundaryProbeOverride.ts +34 -0
  46. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/index.ts +58 -1
  47. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/controllerBoundary.ts +23 -0
  48. package/zova/src/suite/a-demo/modules/demo-basic/src/component/controllerBoundaryProbe/controller.tsx +36 -0
  49. package/zova/src/suite/a-demo/modules/demo-basic/src/component/controllerBoundaryProbeInline/controller.tsx +34 -0
  50. package/zova/src/suite/a-demo/modules/demo-basic/src/component/controllerBoundaryProbeOverride/controller.tsx +46 -0
  51. package/zova/src/suite/a-demo/modules/demo-basic/src/page/controllerBoundary/controller.tsx +185 -0
  52. package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +6 -0
  53. package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
  54. package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
  55. package/zova/src/suite/a-training/modules/training-student/src/model/student.ts +0 -3
  56. package/zova/src/suite/cabloy-basic/modules/basic-button/package.json +58 -0
  57. package/zova/src/suite/cabloy-basic/modules/basic-button/src/.metadata/component/button.ts +31 -0
  58. package/zova/src/suite/cabloy-basic/modules/basic-button/src/.metadata/index.ts +122 -0
  59. package/zova/src/suite/cabloy-basic/modules/basic-button/src/.metadata/this.ts +2 -0
  60. package/zova/src/suite/cabloy-basic/modules/basic-button/src/bean/behavior.perform.tsx +103 -0
  61. package/zova/src/suite/cabloy-basic/modules/basic-button/src/component/button/controller.tsx +39 -0
  62. package/zova/src/suite/cabloy-basic/modules/basic-button/src/index.ts +1 -0
  63. package/zova/src/suite/cabloy-basic/modules/basic-button/tsconfig.build.json +13 -0
  64. package/zova/src/suite/cabloy-basic/modules/basic-button/tsconfig.json +5 -0
  65. package/zova/src/suite/cabloy-basic/modules/basic-details/package.json +2 -1
  66. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionDelete.tsx +4 -4
  67. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +5 -1
  68. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +5 -1
  69. package/zova/src/suite/cabloy-basic/modules/basic-table/package.json +2 -1
  70. package/zova/src/suite/cabloy-basic/modules/basic-table/src/bean/tableCell.actionDelete.tsx +4 -4
  71. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionRefresh/controller.tsx +10 -8
  72. package/zova/src/suite/cabloy-basic/package.json +1 -0
  73. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/package.json +1 -1
  74. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/model/layoutprofile.ts +0 -3
  75. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  76. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/page/resource.ts +6 -1
  77. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +0 -1
  78. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resource/controller.tsx +5 -1
  79. package/zova/src/suite-vendor/a-cabloy/package.json +3 -3
  80. package/zova/src/suite-vendor/a-zova/modules/a-bean/cli/controller/metadata/generateFilePage.ts +3 -1
  81. package/zova/src/suite-vendor/a-zova/modules/a-bean/package.json +1 -1
  82. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  83. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useMutation.ts +1 -11
  84. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +1 -17
  85. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +2 -1
  86. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +2 -7
  87. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/mutation.types.ts +14 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryEnsureLoaded.test.ts +56 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  90. package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkey.ts +8 -0
  91. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  92. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +85 -1
  93. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +25 -1
  94. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/ssr.ts +15 -1
  95. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  96. package/zova/src/suite-vendor/a-zova/package.json +6 -6
  97. /package/vona/patches/{zova-core@5.1.89.patch → zova-core@5.1.90.patch} +0 -0
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  import type { IBeanRecord, IControllerData } from '../bean/type.ts';
11
11
  import type { Constructable } from '../decorator/index.ts';
12
+ import type { IControllerLoadEvent } from '../types/interface/monkey.ts';
12
13
 
13
14
  import {
14
15
  BeanControllerIdentifier,
@@ -16,6 +17,7 @@ import {
16
17
  BeanStyleIdentifier,
17
18
  SymbolControllerRefDisable,
18
19
  } from '../bean/type.ts';
20
+ import { normalizeError } from '../core/component/error.ts';
19
21
  import { ZovaContext } from '../core/context/index.ts';
20
22
  import { sys } from '../core/sys/sys.ts';
21
23
 
@@ -81,6 +83,7 @@ async function _useController(
81
83
  ) {
82
84
  // ctx
83
85
  const ctx = new ZovaContext(getCurrentInstance()!);
86
+ setLoadBoundaryOptions(controllerBeanFullName);
84
87
  // ctx: monkey
85
88
  if (ctx.app) {
86
89
  ctx.app.meta.module._monkeyModuleSync(true, 'appContextInitialize', undefined, ctx);
@@ -97,24 +100,31 @@ async function _useController(
97
100
  ctx,
98
101
  );
99
102
  }
100
- if (process.env.CLIENT) {
101
- // dispose
102
- onBeforeUnmount(() => {
103
- if (ctx.disposed) return;
104
- // undefined better than null
105
- setControllerRef(ctx, false);
106
- if (ctx.bean !== ctx.app.bean) {
107
- ctx.bean.dispose();
108
- }
109
- });
110
- onUnmounted(() => {
111
- ctx.dispose();
112
- });
103
+
104
+ let retryPromise: Promise<void> | undefined;
105
+ let loadBeans = new Set<object>();
106
+
107
+ function setLoadBoundaryOptions(controllerBeanFullName: Constructable | string) {
108
+ const boundary = (controllerBeanFullName as any).$componentOptions?.boundary;
109
+ ctx.meta.state.setLoadRenderMode(boundary?.renderMode);
110
+ ctx.meta.state.setLoadRetryEnabled(boundary?.retry);
113
111
  }
114
112
 
115
- async function __load() {
113
+ function isAttemptActive(attemptId: number) {
114
+ return ctx.meta.state.isLoadAttemptActive(attemptId);
115
+ }
116
+
117
+ function getLoadLoadingDelay() {
118
+ const delay =
119
+ (controllerBeanFullName as any).$componentOptions?.boundary?.loading?.delay ??
120
+ sys.config.boundary.loading?.delay;
121
+ const delayNumber = Number(delay);
122
+ return Number.isFinite(delayNumber) && delayNumber > 0 ? delayNumber : 0;
123
+ }
124
+
125
+ async function __loadBeans(attemptId: number) {
116
126
  // controller
117
- if (ctx.disposed) return;
127
+ if (!isAttemptActive(attemptId)) return false;
118
128
  await ctx.bean._newBeanInner(
119
129
  true,
120
130
  BeanControllerIdentifier,
@@ -122,9 +132,17 @@ async function _useController(
122
132
  controllerBeanFullName,
123
133
  true,
124
134
  false,
135
+ controller => {
136
+ loadBeans.add(controller as object);
137
+ ctx.meta.state.setLoadBeanAttempt(controller as object, attemptId);
138
+ if (!isAttemptActive(attemptId)) return;
139
+ ctx.meta.state.setLoadController(controller);
140
+ setLoadBoundaryOptions((controller as any).constructor);
141
+ },
125
142
  );
143
+ if (!isAttemptActive(attemptId)) return false;
144
+ // style
126
145
  if (styleBeanFullName) {
127
- if (ctx.disposed) return;
128
146
  await ctx.bean._newBeanInner(
129
147
  true,
130
148
  BeanStyleIdentifier,
@@ -132,10 +150,15 @@ async function _useController(
132
150
  styleBeanFullName,
133
151
  true,
134
152
  false,
153
+ style => {
154
+ loadBeans.add(style as object);
155
+ ctx.meta.state.setLoadBeanAttempt(style as object, attemptId);
156
+ },
135
157
  );
136
158
  }
159
+ if (!isAttemptActive(attemptId)) return false;
160
+ // render
137
161
  if (renderBeanFullName) {
138
- if (ctx.disposed) return;
139
162
  await ctx.bean._newBeanInner(
140
163
  true,
141
164
  BeanRenderIdentifier,
@@ -143,14 +166,23 @@ async function _useController(
143
166
  renderBeanFullName,
144
167
  true,
145
168
  false,
169
+ render => {
170
+ loadBeans.add(render as object);
171
+ ctx.meta.state.setLoadBeanAttempt(render as object, attemptId);
172
+ },
146
173
  );
147
174
  }
148
- // must touch inited on server/client, force router.use effect
149
- if (ctx.disposed) return;
150
- ctx.meta.state.inited.touch();
175
+ return isAttemptActive(attemptId);
176
+ }
177
+
178
+ async function __load(attemptId: number) {
179
+ if (!(await __loadBeans(attemptId))) return;
180
+ if (!isAttemptActive(attemptId)) return;
181
+ ctx.meta.state.setLoadReady();
151
182
  if (process.env.CLIENT) {
152
183
  ctx.util.instanceScope(() => {
153
184
  queuePostFlushCb(() => {
185
+ if (!isAttemptActive(attemptId)) return;
154
186
  setControllerRef(ctx, true);
155
187
  ctx.meta.hooks.invokeHook('mounted');
156
188
  });
@@ -158,15 +190,113 @@ async function _useController(
158
190
  }
159
191
  }
160
192
 
161
- // load
162
- ctx.meta.hooks.onCreated(async () => {
163
- if (ctx.disposed) return;
193
+ async function runLoadAttempt(
194
+ attemptId: number,
195
+ beforeLoad?: () => void | Promise<void>,
196
+ ): Promise<void> {
164
197
  try {
165
- return await __load();
198
+ await beforeLoad?.();
199
+ if (!isAttemptActive(attemptId)) return;
200
+ await __load(attemptId);
166
201
  } catch (err) {
202
+ if (!isAttemptActive(attemptId)) return;
203
+ const result = ctx.app?.meta.error.handleLoadError(err, ctx, ctx.instance as any) ?? {
204
+ err: normalizeError(err),
205
+ disposition: 'fallback' as const,
206
+ };
207
+ if (!isAttemptActive(attemptId)) return;
208
+ if (result.disposition === 'handled') {
209
+ ctx.meta.state.setLoadHandled();
210
+ return;
211
+ }
212
+ ctx.app?.meta.module._monkeyModuleSync(true, 'controllerLoad', undefined, {
213
+ phase: 'fallback',
214
+ ctx,
215
+ controllerBeanFullName,
216
+ controllerBeanName:
217
+ typeof controllerBeanFullName === 'string'
218
+ ? controllerBeanFullName
219
+ : controllerBeanFullName.name,
220
+ error: result.err,
221
+ controllerRecorded: !!ctx.meta.state.loadController,
222
+ });
223
+ if (!isAttemptActive(attemptId)) return;
224
+ ctx.meta.state.setLoadError(result.err);
225
+ }
226
+ }
227
+
228
+ function retry() {
229
+ if (!ctx.meta.state.isLoadRetryAvailable) return Promise.resolve();
230
+ if (retryPromise) return retryPromise;
231
+ const promise = (async () => {
232
+ const attemptId = ctx.meta.state.beginLoadAttempt(getLoadLoadingDelay());
233
+ const previousLoadBeans = loadBeans;
234
+ loadBeans = new Set();
235
+ ctx.meta.hooks.clearMounted();
236
+ setControllerRef(ctx, false);
237
+ await runLoadAttempt(attemptId, () => {
238
+ ctx.bean.retryReset(previousLoadBeans);
239
+ });
240
+ })();
241
+ retryPromise = promise;
242
+ void promise.finally(() => {
243
+ if (retryPromise === promise) {
244
+ retryPromise = undefined;
245
+ }
246
+ });
247
+ return promise;
248
+ }
249
+
250
+ ctx.meta.state.setLoadRetry(retry);
251
+ if (process.env.CLIENT) {
252
+ ctx.meta.hooks.onHydrated(() => {
253
+ if (!ctx.disposed) {
254
+ ctx.meta.state.setLoadRetryReady();
255
+ }
256
+ });
257
+ // dispose
258
+ onBeforeUnmount(() => {
167
259
  if (ctx.disposed) return;
168
- throw err;
260
+ // undefined better than null
261
+ setControllerRef(ctx, false);
262
+ if (ctx.bean !== ctx.app.bean) {
263
+ ctx.bean.dispose();
264
+ }
265
+ });
266
+ onUnmounted(() => {
267
+ ctx.dispose();
268
+ });
269
+ }
270
+
271
+ // load
272
+ ctx.meta.hooks.onCreated(async () => {
273
+ if (ctx.disposed) return;
274
+ const loadEvent: IControllerLoadEvent = {
275
+ phase: 'prepare',
276
+ ctx,
277
+ controllerBeanFullName,
278
+ controllerBeanName:
279
+ typeof controllerBeanFullName === 'string'
280
+ ? controllerBeanFullName
281
+ : controllerBeanFullName.name,
282
+ };
283
+ ctx.app?.meta.module._monkeyModuleSync(true, 'controllerLoad', undefined, loadEvent);
284
+ if (loadEvent.replayError) {
285
+ const attemptId = ctx.meta.state.beginLoadAttempt();
286
+ if (loadEvent.replayControllerRecorded) {
287
+ try {
288
+ await __loadBeans(attemptId);
289
+ } catch {
290
+ // The server's fallback decision is authoritative during initial hydration.
291
+ }
292
+ }
293
+ if (!ctx.disposed) {
294
+ ctx.meta.state.setLoadError(loadEvent.replayError);
295
+ }
296
+ return;
169
297
  }
298
+ const attemptId = ctx.meta.state.beginLoadAttempt(getLoadLoadingDelay());
299
+ await runLoadAttempt(attemptId);
170
300
  });
171
301
  if (process.env.SERVER) {
172
302
  onServerPrefetch(() => {
@@ -1,7 +1,12 @@
1
1
  import type { ComponentPublicInstance } from 'vue';
2
2
 
3
3
  import type { IErrorObject } from '../../bean/resource/error/errorObject.ts';
4
- import type { IErrorInstanceInfo, IModuleError } from '../../bean/resource/error/type.ts';
4
+ import type {
5
+ IErrorHandlerLoad,
6
+ IErrorInstanceInfo,
7
+ IModuleError,
8
+ } from '../../bean/resource/error/type.ts';
9
+ import type { ZovaContext } from '../context/context.ts';
5
10
 
6
11
  import { ErrorClass } from '../../bean/resource/error/errorClass.ts';
7
12
  import { SymbolErrorInstanceInfo } from '../../bean/resource/error/type.ts';
@@ -42,6 +47,22 @@ export class AppError extends ErrorClass {
42
47
  };
43
48
  }
44
49
 
50
+ public handleLoadError(
51
+ originalError: unknown,
52
+ ctx: ZovaContext,
53
+ instance?: ComponentPublicInstance | null,
54
+ ) {
55
+ const err = normalizeError(originalError);
56
+ const load: IErrorHandlerLoad = {
57
+ kind: 'controller-load',
58
+ ctx,
59
+ originalError,
60
+ disposition: 'fallback',
61
+ };
62
+ this._handleError(err, instance, 'useController:load', load);
63
+ return { err, disposition: load.disposition };
64
+ }
65
+
45
66
  private _handleUnhandledError(error: Error, infoDefault: string) {
46
67
  if (error instanceof Error) {
47
68
  const errorInfo: IErrorInstanceInfo | undefined = error[SymbolErrorInstanceInfo];
@@ -61,6 +82,7 @@ export class AppError extends ErrorClass {
61
82
  err: Error,
62
83
  instance: ComponentPublicInstance | null | undefined,
63
84
  info: string | undefined,
85
+ load?: IErrorHandlerLoad,
64
86
  ) {
65
87
  if (!this.app) {
66
88
  // means destroyed
@@ -69,7 +91,7 @@ export class AppError extends ErrorClass {
69
91
  }
70
92
  const err2 = this.app.meta.event.emitSync(
71
93
  'app:errorHandler',
72
- { err: err as Error, instance, info },
94
+ { err: err as Error, instance, info, load },
73
95
  data => {
74
96
  return data.err;
75
97
  },
@@ -77,10 +99,45 @@ export class AppError extends ErrorClass {
77
99
  // only log error in client
78
100
  if (process.env.CLIENT) {
79
101
  if (!err2 || !(err2 instanceof Error)) return err2;
80
- if (!info || !['useMutationData'].includes(info)) {
81
- console.error(err2);
82
- }
102
+ console.error(err2);
83
103
  }
84
104
  return err2;
85
105
  }
86
106
  }
107
+
108
+ export function normalizeError(error: unknown) {
109
+ if (error instanceof Error) return error;
110
+ const normalized = new Error(getErrorMessage(error));
111
+ if (error && (typeof error === 'object' || typeof error === 'function')) {
112
+ for (const key of ['code', 'status', 'pagePath', 'url']) {
113
+ const descriptor = Object.getOwnPropertyDescriptor(error, key);
114
+ if (!descriptor || !('value' in descriptor)) continue;
115
+ const value = descriptor.value;
116
+ if (
117
+ (key === 'code' || key === 'status') &&
118
+ (typeof value === 'number' || typeof value === 'string')
119
+ ) {
120
+ (normalized as any)[key] = value;
121
+ }
122
+ if ((key === 'pagePath' || key === 'url') && typeof value === 'string') {
123
+ (normalized as any)[key] = value;
124
+ }
125
+ }
126
+ }
127
+ return normalized;
128
+ }
129
+
130
+ function getErrorMessage(error: unknown) {
131
+ if (
132
+ error === null ||
133
+ error === undefined ||
134
+ typeof error === 'string' ||
135
+ typeof error === 'number' ||
136
+ typeof error === 'boolean' ||
137
+ typeof error === 'bigint' ||
138
+ typeof error === 'symbol'
139
+ ) {
140
+ return String(error);
141
+ }
142
+ return 'Unknown error';
143
+ }
@@ -21,8 +21,12 @@ export class CtxComponent extends BeanSimple {
21
21
  const self = this;
22
22
  instance[renderMethod] = function (this, ...args) {
23
23
  if (instance.isUnmounted) return;
24
- if (!self.ctx.meta.state.inited.state) {
25
- return self._bean_render_original.call(this, ...args);
24
+ const loadStatus = self.ctx.meta.state.loadStatus;
25
+ if (loadStatus === 'loading') {
26
+ return self.ctx.meta.state.loadLoadingVisible ? self._renderLoading() : null;
27
+ }
28
+ if (loadStatus === 'error') {
29
+ return self._renderError(self.ctx.meta.state.loadError);
26
30
  }
27
31
  const render = self._getRender();
28
32
  if (!render) {
@@ -77,6 +81,30 @@ export class CtxComponent extends BeanSimple {
77
81
  this._ssrRenderReset = false;
78
82
  }
79
83
 
84
+ private _renderLoading() {
85
+ const renderMode = this.ctx.meta.state.loadRenderMode;
86
+ const controller = this.ctx.meta.state.loadController as any;
87
+ if (controller?.renderLoading) {
88
+ return controller.renderLoading(renderMode);
89
+ }
90
+ const renderLoading = this.sys.config.boundary.renderLoading;
91
+ return renderLoading?.(this.ctx, renderMode) ?? null;
92
+ }
93
+
94
+ private _renderError(error: unknown) {
95
+ const renderMode = this.ctx.meta.state.loadRenderMode;
96
+ const retry = this.ctx.meta.state.loadRetry;
97
+ const controller = this.ctx.meta.state.loadController as any;
98
+ if (controller?.renderError) {
99
+ return controller.renderError(error, renderMode, retry);
100
+ }
101
+ const renderError = this.sys.config.boundary.renderError;
102
+ if (renderError) {
103
+ return renderError(this.ctx, error, renderMode, retry);
104
+ }
105
+ return _renderErrorDefault(error);
106
+ }
107
+
80
108
  private _getRender(): any {
81
109
  const render = this.bean._getBeanSyncOnly<BeanControllerBase>(BeanControllerIdentifier);
82
110
  if (!render) return;
@@ -85,3 +113,10 @@ export class CtxComponent extends BeanSimple {
85
113
  return this.bean._getBeanSyncOnly(BeanRenderIdentifier);
86
114
  }
87
115
  }
116
+
117
+ function _renderErrorDefault(error: unknown) {
118
+ if (error instanceof Error) {
119
+ return error.message;
120
+ }
121
+ return String(error);
122
+ }
@@ -1,6 +1,6 @@
1
1
  import { BeanSimple } from '../../bean/beanSimple.ts';
2
2
 
3
- export type ZovaHookType = 'created' | 'mounted';
3
+ export type ZovaHookType = 'created' | 'mounted' | 'hydrated';
4
4
 
5
5
  export const SymbolHooksFns = Symbol('SymbolHooksFns');
6
6
  export const SymbolHooksState = Symbol('SymbolHooksState');
@@ -18,10 +18,21 @@ export class CtxHooks extends BeanSimple {
18
18
  this._onHook('created', fn);
19
19
  }
20
20
 
21
- onMounted(fn: any) {
21
+ onMounted(fn: any, owner?: object) {
22
+ if (owner && !this.ctx.meta.state.isLoadBeanActive(owner)) return;
22
23
  this._onHook('mounted', fn);
23
24
  }
24
25
 
26
+ /** @internal */
27
+ public onHydrated(fn: any) {
28
+ this._onHook('hydrated', fn);
29
+ }
30
+
31
+ clearMounted() {
32
+ if (this[SymbolHooksState].mounted) return;
33
+ this[SymbolHooksFns].mounted = undefined;
34
+ }
35
+
25
36
  private _onHook(type: ZovaHookType, fn: any) {
26
37
  if (this[SymbolHooksState][type]) {
27
38
  this.ctx.util.instanceScope(fn);
@@ -23,6 +23,7 @@ export class CtxMeta extends BeanSimple {
23
23
 
24
24
  /** @internal */
25
25
  public dispose() {
26
+ this.state.dispose();
26
27
  this.component.dispose();
27
28
  this.hooks.dispose();
28
29
  }
@@ -1,17 +1,42 @@
1
+ import type {
2
+ TypeComponentBoundaryRenderMode,
3
+ TypeComponentBoundaryRetry,
4
+ } from '../../bean/type.ts';
5
+
1
6
  import { BeanSimple } from '../../bean/beanSimple.ts';
2
7
  import { StateLock } from '../../utils/stateLock.ts';
3
8
 
9
+ export type CtxLoadStatus = 'loading' | 'ready' | 'error';
10
+
4
11
  let __id: number = 0;
5
12
 
6
13
  export class CtxState extends BeanSimple {
7
14
  private _id: number;
8
15
  private _inited: StateLock;
16
+ private _loadStatus: CtxLoadStatus = 'loading';
17
+ private _loadRenderMode: TypeComponentBoundaryRenderMode = 'block';
18
+ private _loadRetryEnabled = false;
19
+ private _loadRetryReady = false;
20
+ private _loadRetry?: TypeComponentBoundaryRetry;
21
+ private _loadAttemptId = 0;
22
+ private _loadBeanAttempts = new WeakMap<object, number>();
23
+ private _loadError: unknown;
24
+ private _loadController: unknown;
25
+ /** @internal SSR hydration protocol key. */
26
+ public controllerLoadKey?: string;
27
+ private _loadLoadingVisible = false;
28
+ private _loadLoadingTimer: ReturnType<typeof setTimeout> | undefined;
9
29
 
10
30
  protected __init__() {
11
31
  this._id = ++__id;
12
32
  this._inited = StateLock.create();
13
33
  }
14
34
 
35
+ /** @internal */
36
+ public dispose() {
37
+ this._clearLoadLoadingTimer();
38
+ }
39
+
15
40
  get id() {
16
41
  return this._id;
17
42
  }
@@ -19,4 +44,140 @@ export class CtxState extends BeanSimple {
19
44
  get inited() {
20
45
  return this._inited;
21
46
  }
47
+
48
+ get loadStatus() {
49
+ return this._loadStatus;
50
+ }
51
+
52
+ get loadRenderMode() {
53
+ return this._loadRenderMode;
54
+ }
55
+
56
+ get loadRetry() {
57
+ if (!this.isLoadRetryAvailable) return undefined;
58
+ return this._loadRetry;
59
+ }
60
+
61
+ get isLoadRetryAvailable() {
62
+ return (
63
+ this._loadRetryEnabled &&
64
+ this._loadRetryReady &&
65
+ this._loadStatus === 'error' &&
66
+ !this.ctx.disposed &&
67
+ !!this._loadRetry
68
+ );
69
+ }
70
+
71
+ get loadAttemptId() {
72
+ return this._loadAttemptId;
73
+ }
74
+
75
+ get loadError() {
76
+ return this._loadError;
77
+ }
78
+
79
+ get loadController() {
80
+ return this._loadController;
81
+ }
82
+
83
+ get loadLoadingVisible() {
84
+ return this._loadLoadingVisible;
85
+ }
86
+
87
+ setLoadController(controller: unknown) {
88
+ this._loadController = controller;
89
+ }
90
+
91
+ setLoadRenderMode(renderMode: unknown) {
92
+ this._loadRenderMode = renderMode === 'inline' ? 'inline' : 'block';
93
+ }
94
+
95
+ setLoadRetryEnabled(retryEnabled: unknown) {
96
+ this._loadRetryEnabled = retryEnabled === true;
97
+ }
98
+
99
+ setLoadRetry(retry: TypeComponentBoundaryRetry) {
100
+ this._loadRetry = retry;
101
+ }
102
+
103
+ setLoadRetryReady() {
104
+ this._loadRetryReady = true;
105
+ }
106
+
107
+ beginLoadAttempt(delay?: number) {
108
+ this._clearLoadLoadingTimer();
109
+ this._loadAttemptId++;
110
+ this._loadStatus = 'loading';
111
+ this._loadError = undefined;
112
+ this._loadController = undefined;
113
+ this._loadLoadingVisible = false;
114
+ if (delay !== undefined) {
115
+ this.startLoadLoading(delay);
116
+ }
117
+ return this._loadAttemptId;
118
+ }
119
+
120
+ isLoadAttemptActive(attemptId: number) {
121
+ return !this.ctx.disposed && this._loadAttemptId === attemptId;
122
+ }
123
+
124
+ setLoadBeanAttempt(bean: object, attemptId: number) {
125
+ this._loadBeanAttempts.set(bean, attemptId);
126
+ }
127
+
128
+ getLoadBeanAttempt(bean: object) {
129
+ return this._loadBeanAttempts.get(bean);
130
+ }
131
+
132
+ isLoadBeanActive(bean: object) {
133
+ const attemptId = this._loadBeanAttempts.get(bean);
134
+ return attemptId === undefined || this.isLoadAttemptActive(attemptId);
135
+ }
136
+
137
+ startLoadLoading(delay: number) {
138
+ this._clearLoadLoadingTimer();
139
+ if (delay === 0) {
140
+ this._loadLoadingVisible = true;
141
+ return;
142
+ }
143
+ this._loadLoadingTimer = setTimeout(() => {
144
+ this._loadLoadingTimer = undefined;
145
+ if (this.ctx.disposed || this._loadStatus !== 'loading') return;
146
+ this._loadLoadingVisible = true;
147
+ }, delay);
148
+ }
149
+
150
+ /**
151
+ * Resolves the Controller-load terminal lock consumed by the custom SSR hydration runtime.
152
+ * A settled load is not necessarily a successful load: error and handled control-flow paths
153
+ * must also allow the rendered boundary subtree to hydrate.
154
+ */
155
+ settleLoad() {
156
+ this._inited.touch();
157
+ }
158
+
159
+ setLoadReady() {
160
+ this._clearLoadLoadingTimer();
161
+ this._loadError = undefined;
162
+ this._loadStatus = 'ready';
163
+ this.settleLoad();
164
+ }
165
+
166
+ setLoadError(error: unknown) {
167
+ this._clearLoadLoadingTimer();
168
+ this._loadError = error;
169
+ this._loadStatus = 'error';
170
+ this.settleLoad();
171
+ }
172
+
173
+ setLoadHandled() {
174
+ this._clearLoadLoadingTimer();
175
+ this.settleLoad();
176
+ }
177
+
178
+ private _clearLoadLoadingTimer() {
179
+ if (this._loadLoadingTimer === undefined) return;
180
+ clearTimeout(this._loadLoadingTimer);
181
+ this._loadLoadingTimer = undefined;
182
+ }
22
183
  }
@@ -1,5 +1,6 @@
1
1
  import type { ZovaConfigMeta } from '@cabloy/module-info';
2
2
  import type ms from 'ms';
3
+ import type { VNodeChild } from 'vue';
3
4
 
4
5
  import type {
5
6
  ILayoutRecord,
@@ -8,9 +9,14 @@ import type {
8
9
  } from '../../bean/resource/component/type.ts';
9
10
  import type { ILocaleRecord } from '../../bean/resource/locale/type.ts';
10
11
  import type { ZovaConfigRoutes } from '../../bean/resource/page/type.ts';
11
- import type { IBeanScopeConfig } from '../../bean/type.ts';
12
+ import type {
13
+ IBeanScopeConfig,
14
+ TypeComponentBoundaryRenderMode,
15
+ TypeComponentBoundaryRetry,
16
+ } from '../../bean/type.ts';
12
17
  import type { ZovaConfigEnv } from '../../types/utils/env.ts';
13
18
  import type { PowerPartial } from '../../types/utils/powerPartial.ts';
19
+ import type { ZovaContext } from '../context/context.ts';
14
20
  import type { ConfigLogger } from '../logger/types.ts';
15
21
 
16
22
  export function configDefault(env: ZovaConfigEnv): PowerPartial<ZovaConfig> {
@@ -20,10 +26,33 @@ export function configDefault(env: ZovaConfigEnv): PowerPartial<ZovaConfig> {
20
26
  mode: env.META_MODE,
21
27
  appMode: env.META_APP_MODE,
22
28
  },
29
+ boundary: {},
23
30
  };
24
31
  return config;
25
32
  }
26
33
 
34
+ export interface ZovaConfigApp {
35
+ name: string;
36
+ title: string;
37
+ description: string;
38
+ version: string;
39
+ }
40
+
41
+ export interface ZovaConfigBoundaryLoading {
42
+ delay?: number;
43
+ }
44
+
45
+ export interface ZovaConfigBoundary {
46
+ loading?: ZovaConfigBoundaryLoading;
47
+ renderLoading?: (ctx: ZovaContext, renderMode: TypeComponentBoundaryRenderMode) => VNodeChild;
48
+ renderError?: (
49
+ ctx: ZovaContext,
50
+ error: unknown,
51
+ renderMode: TypeComponentBoundaryRenderMode,
52
+ retry?: TypeComponentBoundaryRetry,
53
+ ) => VNodeChild;
54
+ }
55
+
27
56
  export interface ZovaConfigSsrResponseCachePolicy {
28
57
  expires?: number | ms.StringValue;
29
58
  }
@@ -35,12 +64,8 @@ export interface ZovaConfigSsrProfile {
35
64
 
36
65
  export interface ZovaConfig {
37
66
  meta: ZovaConfigMeta;
38
- app: {
39
- name: string;
40
- title: string;
41
- description: string;
42
- version: string;
43
- };
67
+ app: ZovaConfigApp;
68
+ boundary: ZovaConfigBoundary;
44
69
  api: {
45
70
  baseURL: string;
46
71
  prefix: string;