cabloy 5.1.207 → 5.1.209

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 +13 -0
  3. package/README.md +1 -0
  4. package/package.json +1 -1
  5. package/repo-agent-governance/managed-assets.json +9 -9
  6. package/repo-agent-governance/policies/repository.md +1 -1
  7. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +4 -2
  8. package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +13 -1
  9. package/repo-docs/backend/unit-testing.md +1 -1
  10. package/repo-docs/frontend/form-guide.md +17 -1
  11. package/repo-docs/frontend/model-architecture.md +4 -0
  12. package/repo-docs/frontend/model-state-guide.md +23 -2
  13. package/repo-docs/frontend/resource-entry-page-deep-dive.md +119 -0
  14. package/repo-docs/frontend/scripts.md +3 -3
  15. package/repo-docs/frontend/zova-form-under-the-hood.md +5 -0
  16. package/repo-docs/fullstack/parallel-worktree-environment.md +3 -3
  17. package/repo-docs/fullstack/quickstart.md +11 -1
  18. package/repo-docs/reference/repo-scripts.md +19 -5
  19. package/repo-e2e/specs/cabloy-basic.spec.ts +1577 -0
  20. package/vona/packages-vona/vona/package.json +1 -1
  21. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
  22. package/vona/src/suite/a-home/modules/home-user/src/dto/passport.ts +4 -0
  23. package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +172 -0
  24. package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +1 -0
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  26. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  27. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
  29. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  30. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  31. package/zova/packages-cli/cli/package.json +2 -2
  32. package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
  33. package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +9 -0
  34. package/zova/packages-cli/cli-set-front/package.json +1 -1
  35. package/zova/packages-zova/zova/package.json +2 -2
  36. package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +17 -17
  37. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +68 -52
  38. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +524 -343
  39. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +45 -11
  40. package/zova/src/suite/a-home/modules/home-passport/test/lib/persistenceIdentity.test.ts +229 -0
  41. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/package.json +9 -1
  42. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/.metadata/index.ts +81 -5
  43. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/bean/bean.draftCoordinator.ts +158 -0
  44. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx +11 -3
  45. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +438 -8
  46. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/config.ts +11 -0
  47. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/en-us.ts +9 -0
  48. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/zh-cn.ts +9 -0
  49. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/lib/draftSchema.ts +649 -0
  50. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/model/draft.ts +59 -0
  51. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/monkey.ts +11 -0
  52. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/draft.ts +49 -0
  53. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftDate.test.ts +262 -0
  54. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLifecycle.test.ts +1073 -0
  55. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLoader.mjs +13 -0
  56. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftModel.test.ts +572 -0
  57. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchema.test.ts +427 -0
  58. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchemaSafety.test.ts +200 -0
  59. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  60. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +6 -1
  61. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  62. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  63. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +86 -1
  64. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +88 -13
  65. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/schemaRefresh.ts +751 -0
  66. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefresh.test.ts +637 -0
  67. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefreshLifecycle.test.ts +994 -0
  68. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  69. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +331 -87
  70. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.query.ts +11 -6
  71. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +3 -1
  72. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +17 -1
  73. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/index.ts +1 -0
  74. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/modelPersistence.ts +204 -0
  75. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/persistenceQueue.ts +50 -0
  76. package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +5 -0
  77. package/zova/src/suite-vendor/a-zova/modules/a-model/src/service/storage.ts +47 -2
  78. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -0
  79. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelLoader.mjs +34 -0
  80. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelPersistence.test.ts +727 -0
  81. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceQueue.test.ts +137 -0
  82. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceTransition.test.ts +129 -0
  83. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  84. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/monkey.ts +7 -5
  85. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +2 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  87. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +14 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routeDiscard.ts +26 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-router/src/service/routerGuards.ts +6 -3
  90. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/index.ts +1 -0
  91. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/routeDiscard.ts +24 -0
  92. package/zova/src/suite-vendor/a-zova/modules/a-router/test/lib/routeDiscard.test.ts +105 -0
  93. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
  94. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +202 -30
  95. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabs.test.ts +524 -0
  96. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabsLoader.mjs +35 -0
  97. package/zova/src/suite-vendor/a-zova/package.json +6 -6
@@ -1,13 +1,24 @@
1
- import type { IDecoratorModelOptions, UseQueryOptions } from 'zova-module-a-model';
1
+ import type {
2
+ IDecoratorModelOptions,
3
+ ModelPersistenceToken,
4
+ ServiceStorage,
5
+ UseQueryOptions,
6
+ } from 'zova-module-a-model';
2
7
 
3
8
  import {
4
9
  RouteLocationNormalizedLoaded,
5
10
  RouteLocationNormalizedLoadedGeneric,
6
11
  } from '@cabloy/vue-router';
7
12
  import { mutate } from 'mutate-on-copy';
13
+ import { shallowReactive } from 'vue';
8
14
  import { deepEqual, deepExtend, TypeEventOff } from 'zova';
9
15
  import { BeanModelBase, Model } from 'zova-module-a-model';
10
- import { IPageMeta, IRouteViewRouteItem, IRouteViewRouteMeta } from 'zova-module-a-router';
16
+ import {
17
+ IPageMeta,
18
+ IRouteViewRouteItem,
19
+ IRouteViewRouteMeta,
20
+ TypeRouteDiscardReason,
21
+ } from 'zova-module-a-router';
11
22
 
12
23
  import {
13
24
  ModelTabsOptions,
@@ -19,6 +30,15 @@ import {
19
30
 
20
31
  export interface IModelOptionsTabs extends IDecoratorModelOptions, ModelTabsOptionsBase {}
21
32
 
33
+ interface TabsReadiness {
34
+ ctx: { disposed: boolean };
35
+ persistence: ServiceStorage;
36
+ token: ModelPersistenceToken;
37
+ ready: boolean;
38
+ promise?: Promise<void>;
39
+ work: (() => void)[];
40
+ }
41
+
22
42
  @Model<IModelOptionsTabs>({
23
43
  enableSelector: true,
24
44
  max: -1,
@@ -27,7 +47,29 @@ export interface IModelOptionsTabs extends IDecoratorModelOptions, ModelTabsOpti
27
47
  })
28
48
  export class ModelTabs extends BeanModelBase {
29
49
  tabsOptions: ModelTabsOptions;
30
- tabs: RouteTab[];
50
+ private _tabsState: RouteTab[];
51
+ private _tabsReadiness?: TabsReadiness;
52
+ private _tabsDisposed = false;
53
+ private _offPersistence?: () => void;
54
+
55
+ get tabs(): RouteTab[] {
56
+ if (this._tabsDisposed || this.ctx.disposed) return [];
57
+ if (!this.tabsOptions?.cache) return this._tabsState ?? [];
58
+ const readiness = this._getTabsReadiness();
59
+ if (!readiness.ready) {
60
+ this._startTabsRestore(readiness);
61
+ return [];
62
+ }
63
+ return this._tabsState;
64
+ }
65
+
66
+ set tabs(value: RouteTab[]) {
67
+ if (this._tabsDisposed || this.ctx.disposed) return;
68
+ // The pending array is only a projection, never a write/default for storage.
69
+ if (this.tabsOptions.cache && !this._getTabsReadiness().ready) return;
70
+ this._tabsState = value;
71
+ }
72
+
31
73
  componentKeyCurrent?: string;
32
74
  tabKeyCurrent?: string;
33
75
  tabCurrentIndex: number;
@@ -36,7 +78,9 @@ export class ModelTabs extends BeanModelBase {
36
78
  private _eventSsrHmrReload: TypeEventOff;
37
79
 
38
80
  protected async __init__(scene: string, options?: ModelTabsOptions) {
81
+ const ctx = this.ctx;
39
82
  await super.__init__(scene);
83
+ if (this._tabsDisposed || ctx.disposed) return;
40
84
  this.bean._setBean('$$modelTabs', this);
41
85
  // options
42
86
  this.tabsOptions = deepExtend({}, this.$onionOptions, options);
@@ -71,25 +115,30 @@ export class ModelTabs extends BeanModelBase {
71
115
  const queryOptionsTabs: UseQueryOptions<RouteTab[]> = {
72
116
  queryKey: ['tabs'],
73
117
  meta: {
74
- defaultData: this._getInitialTabs(),
118
+ // Memory query metadata is dehydrated; only DB state can retain a factory.
119
+ defaultData: this.tabsOptions.cache ? () => this._getInitialTabs() : this._getInitialTabs(),
75
120
  },
76
121
  };
77
122
  if (this.tabsOptions.cache) {
78
- this.tabs = this.$useStateLocalAsync(queryOptionsTabs);
123
+ this._tabsState = this.$useStateLocalAsync(queryOptionsTabs);
124
+ const persistence = this.$modelPersistence;
125
+ this._offPersistence = persistence.subscribe(() => {
126
+ if (!this._tabsDisposed && !this.ctx.disposed) {
127
+ this._startTabsRestore(this._getTabsReadiness());
128
+ }
129
+ });
130
+ // Keep initial ordering, but do not initialize a route from an obsolete
131
+ // restore if identity changes while the model itself is being loaded.
132
+ while (!this._tabsDisposed && !this.ctx.disposed) {
133
+ const readiness = this._getTabsReadiness();
134
+ await this._startTabsRestore(readiness);
135
+ if (this._isTabsReadinessCurrent(readiness)) break;
136
+ }
79
137
  } else {
80
- this.tabs = this.$useStateMem(queryOptionsTabs);
81
- }
82
- // load cache
83
- if (this.tabsOptions.cache) {
84
- // await this.$ensureStateLocalAsync(this.tabKeyCurrent);
85
- await this.$ensureStateLocalAsync(this.tabs);
86
- }
87
- // reset pageDirty
88
- this._resetAllPageDirty();
89
- // first route
90
- if (this.$currentRoute) {
91
- this.forwardRoute(this.$currentRoute);
138
+ this._tabsState = this.$useStateMem(queryOptionsTabs);
139
+ this._initializeTabs();
92
140
  }
141
+ if (this._tabsDisposed || this.ctx.disposed) return;
93
142
  // event
94
143
  if (process.env.CLIENT && this.sys.config.ssr.hmr) {
95
144
  this._eventSsrHmrReload = this.sys.meta.event.on('a-ssrhmr:reload', async (_data, next) => {
@@ -109,17 +158,90 @@ export class ModelTabs extends BeanModelBase {
109
158
  }
110
159
 
111
160
  protected __dispose__() {
161
+ this._tabsDisposed = true;
162
+ this._offPersistence?.();
163
+ this._tabsReadiness?.work.splice(0);
112
164
  if (this._eventSsrHmrReload) {
113
165
  this._eventSsrHmrReload();
166
+ this._eventSsrHmrReload = undefined!;
114
167
  }
115
168
  }
116
169
 
170
+ private _getTabsReadiness(): TabsReadiness {
171
+ const persistence = this.$modelPersistence;
172
+ let readiness = this._tabsReadiness;
173
+ if (!readiness || !persistence.current(readiness.token)) {
174
+ readiness?.work.splice(0);
175
+ readiness = shallowReactive({
176
+ ctx: this.ctx,
177
+ persistence,
178
+ token: persistence.capture(),
179
+ ready: false,
180
+ work: [],
181
+ });
182
+ this._tabsReadiness = readiness;
183
+ }
184
+ return readiness;
185
+ }
186
+
187
+ private _isTabsReadinessCurrent(readiness: TabsReadiness) {
188
+ return (
189
+ !this._tabsDisposed &&
190
+ !readiness.ctx.disposed &&
191
+ this._tabsReadiness === readiness &&
192
+ readiness.persistence.current(readiness.token)
193
+ );
194
+ }
195
+
196
+ private _startTabsRestore(readiness: TabsReadiness): Promise<void> {
197
+ if (!readiness.promise) {
198
+ // One actual backing read per epoch: repeated render/watch reads share it.
199
+ const state = this._tabsState;
200
+ readiness.promise = this.$ensureStateLocalAsync(state).then(() => {
201
+ if (!this._isTabsReadinessCurrent(readiness)) return;
202
+ readiness.ready = true;
203
+ this._initializeTabs();
204
+ for (const work of readiness.work.splice(0)) {
205
+ if (!this._isTabsReadinessCurrent(readiness)) break;
206
+ work();
207
+ }
208
+ });
209
+ void readiness.promise.catch(error => {
210
+ if (this._isTabsReadinessCurrent(readiness)) this.$errorHandler(error);
211
+ });
212
+ }
213
+ return readiness.promise;
214
+ }
215
+
216
+ private _initializeTabs() {
217
+ this._resetAllPageDirty();
218
+ if (this.$currentRoute) this.forwardRoute(this.$currentRoute);
219
+ }
220
+
221
+ private _deferTabsWork(work: () => void): boolean {
222
+ if (this._tabsDisposed || this.ctx.disposed) return true;
223
+ if (!this.tabsOptions.cache) return false;
224
+ const readiness = this._getTabsReadiness();
225
+ if (readiness.ready) return false;
226
+ readiness.work.push(work);
227
+ this._startTabsRestore(readiness);
228
+ return true;
229
+ }
230
+
231
+ private _captureTabsOperation() {
232
+ const ctx = this.ctx;
233
+ const persistence = this.$modelPersistence;
234
+ const token = persistence.capture();
235
+ return () => !this._tabsDisposed && !ctx.disposed && persistence.current(token);
236
+ }
237
+
117
238
  get cache() {
118
239
  return this.tabsOptions.cache;
119
240
  }
120
241
 
121
242
  // need not async
122
243
  addTab(tab: RouteTabTransient, affix?: boolean): boolean {
244
+ if (this._deferTabsWork(() => this.addTab(tab, affix))) return false;
123
245
  const res = this._addTab(tab, affix);
124
246
  // current
125
247
  this.tabKeyCurrent = res ? tab.tabKey : undefined;
@@ -129,6 +251,7 @@ export class ModelTabs extends BeanModelBase {
129
251
 
130
252
  // need not async
131
253
  _addTab(tab: Partial<RouteTabTransient>, affix?: boolean): boolean {
254
+ if (this._deferTabsWork(() => this._addTab(tab, affix))) return false;
132
255
  const tabKey = tab.tabKey;
133
256
  if (!tabKey) return false;
134
257
  // tab
@@ -179,10 +302,12 @@ export class ModelTabs extends BeanModelBase {
179
302
  }
180
303
 
181
304
  resetAllTabInfos() {
305
+ if (this._deferTabsWork(() => this.resetAllTabInfos())) return;
182
306
  this.tabs = this._getInitialTabs() as RouteTab[];
183
307
  }
184
308
 
185
309
  updateAllTabInfos(tabInitials?: RouteTabInitial[]) {
310
+ if (this._deferTabsWork(() => this.updateAllTabInfos(tabInitials))) return;
186
311
  for (const tab of this.tabs) {
187
312
  const tabInitial = tabInitials?.find(item => item.tabKey === tab.tabKey);
188
313
  this.updateTabInfo(tab.tabKey, tabInitial);
@@ -190,6 +315,7 @@ export class ModelTabs extends BeanModelBase {
190
315
  }
191
316
 
192
317
  updateTabInfo(tabKey?: string, tabInitial?: RouteTabInitial) {
318
+ if (this._deferTabsWork(() => this.updateTabInfo(tabKey, tabInitial))) return;
193
319
  if (!tabKey) return;
194
320
  // tabs
195
321
  const [index, tabOld] = this.findTab(tabKey);
@@ -205,6 +331,9 @@ export class ModelTabs extends BeanModelBase {
205
331
  }
206
332
 
207
333
  updateTabItemPageMeta(tabKey?: string, componentKey?: string, pageMeta?: IPageMeta) {
334
+ if (this._deferTabsWork(() => this.updateTabItemPageMeta(tabKey, componentKey, pageMeta))) {
335
+ return false;
336
+ }
208
337
  if (!tabKey || !componentKey) return false;
209
338
  // tab
210
339
  const [index, tab] = this.findTab(tabKey);
@@ -227,11 +356,27 @@ export class ModelTabs extends BeanModelBase {
227
356
  return true;
228
357
  }
229
358
 
230
- async deleteTab(tabKey?: string, noActiveNext?: boolean) {
231
- if (!tabKey) return;
359
+ async deleteTab(
360
+ tabKey?: string,
361
+ noActiveNext?: boolean,
362
+ reason: TypeRouteDiscardReason | false = 'workspaceClose',
363
+ ) {
364
+ if (this._tabsDisposed || this.ctx.disposed || !tabKey) return;
365
+ const current = this._captureTabsOperation();
232
366
  // tabs
233
- const [index] = this.findTab(tabKey);
234
- if (index === -1) return;
367
+ const [index, tab] = this.findTab(tabKey);
368
+ if (index === -1 || !tab) return;
369
+ const cleanup = reason
370
+ ? this.$router.notifyRouteDiscard(
371
+ reason,
372
+ (tab.items ?? []).map(item => ({
373
+ fullPath: item.fullPath,
374
+ componentKey: item.componentKey,
375
+ tabKey,
376
+ })),
377
+ this.selector,
378
+ )
379
+ : undefined;
235
380
  // active next
236
381
  let tabKeyActiveNext;
237
382
  if (!noActiveNext && index === this.tabCurrentIndex) {
@@ -246,14 +391,22 @@ export class ModelTabs extends BeanModelBase {
246
391
  this.tabs = mutate(this.tabs, copyState => {
247
392
  copyState.splice(index, 1);
248
393
  });
394
+ await cleanup;
395
+ if (!current()) return;
249
396
  // active next
250
397
  if (tabKeyActiveNext) {
251
398
  await this.activeTab(tabKeyActiveNext);
252
399
  }
253
400
  }
254
401
 
255
- async deleteTabItem(tabKey?: string, componentKey?: string, noActiveNext?: boolean) {
256
- if (!tabKey || !componentKey) return false;
402
+ async deleteTabItem(
403
+ tabKey?: string,
404
+ componentKey?: string,
405
+ noActiveNext?: boolean,
406
+ reason: TypeRouteDiscardReason | false = 'taskClose',
407
+ ) {
408
+ if (this._tabsDisposed || this.ctx.disposed || !tabKey || !componentKey) return false;
409
+ const current = this._captureTabsOperation();
257
410
  if (tabKey === componentKey) return false; // not delete first tabItem
258
411
  // tab
259
412
  const [index, tab] = this.findTab(tabKey);
@@ -265,9 +418,16 @@ export class ModelTabs extends BeanModelBase {
265
418
  // deleteTab if length===1
266
419
  if (tab.items.length === 1 && !tab.affix) {
267
420
  // delete tab
268
- await this.deleteTab(tabKey, noActiveNext);
421
+ await this.deleteTab(tabKey, noActiveNext, reason);
269
422
  return true;
270
423
  }
424
+ const cleanup = reason
425
+ ? this.$router.notifyRouteDiscard(
426
+ reason,
427
+ [{ fullPath: tab.items[indexItem].fullPath, componentKey, tabKey }],
428
+ this.selector,
429
+ )
430
+ : undefined;
271
431
  // active next
272
432
  let componentKeyActiveNext;
273
433
  if (!noActiveNext && componentKey === this.componentKeyCurrent) {
@@ -286,6 +446,8 @@ export class ModelTabs extends BeanModelBase {
286
446
  this.tabs = mutate(this.tabs, copyState => {
287
447
  copyState.splice(index, 1, tabNew);
288
448
  });
449
+ await cleanup;
450
+ if (!current()) return false;
289
451
  // active next
290
452
  if (componentKeyActiveNext) {
291
453
  await this.activeTabItem(tabKey, componentKeyActiveNext);
@@ -306,6 +468,7 @@ export class ModelTabs extends BeanModelBase {
306
468
  }
307
469
 
308
470
  updateTab(tab: Partial<RouteTabTransient>) {
471
+ if (this._deferTabsWork(() => this.updateTab(tab))) return;
309
472
  const tabKey = tab.tabKey!;
310
473
  const [index, tabOld] = this.findTab(tabKey);
311
474
  if (index === -1 || !tabOld) return;
@@ -342,7 +505,7 @@ export class ModelTabs extends BeanModelBase {
342
505
  }
343
506
 
344
507
  async activeTab(tabKey?: string) {
345
- if (!tabKey) return;
508
+ if (this._tabsDisposed || this.ctx.disposed || !tabKey) return;
346
509
  const [_, tab] = this.findTab(tabKey);
347
510
  if (!tab) return;
348
511
  // should not updateTab here
@@ -355,7 +518,7 @@ export class ModelTabs extends BeanModelBase {
355
518
  }
356
519
 
357
520
  async activeTabItem(tabKey?: string, componentKey?: string) {
358
- if (!tabKey || !componentKey) return;
521
+ if (this._tabsDisposed || this.ctx.disposed || !tabKey || !componentKey) return;
359
522
  const [_, tab] = this.findTab(tabKey);
360
523
  if (!tab) return;
361
524
  const tabItem = tab.items?.find(item => item.componentKey === componentKey);
@@ -422,6 +585,8 @@ export class ModelTabs extends BeanModelBase {
422
585
  }
423
586
 
424
587
  async pruneTabs() {
588
+ if (this._tabsDisposed || this.ctx.disposed) return;
589
+ const current = this._captureTabsOperation();
425
590
  let max = this.tabsOptions.max;
426
591
  if (max === undefined || max === -1) return;
427
592
  if (max < 1) max = 1;
@@ -437,12 +602,14 @@ export class ModelTabs extends BeanModelBase {
437
602
  }
438
603
  }
439
604
  if (!tabKey) break;
440
- await this.deleteTab(tabKey, true);
605
+ await this.deleteTab(tabKey, true, false);
606
+ if (!current()) return;
441
607
  }
442
608
  }
443
609
 
444
610
  async pruneTabItems(tabKey?: string) {
445
- if (!tabKey) return;
611
+ if (this._tabsDisposed || this.ctx.disposed || !tabKey) return;
612
+ const current = this._captureTabsOperation();
446
613
  let maxItems = this.tabsOptions.maxItems;
447
614
  if (maxItems === undefined || maxItems === -1) return;
448
615
  if (maxItems < 1) maxItems = 1;
@@ -460,7 +627,8 @@ export class ModelTabs extends BeanModelBase {
460
627
  }
461
628
  }
462
629
  if (!componentKey) break;
463
- await this.deleteTabItem(tabKey, componentKey, true);
630
+ await this.deleteTabItem(tabKey, componentKey, true, false);
631
+ if (!current()) return;
464
632
  }
465
633
  }
466
634
 
@@ -518,16 +686,20 @@ export class ModelTabs extends BeanModelBase {
518
686
  }
519
687
 
520
688
  backRoute(route: RouteLocationNormalizedLoadedGeneric) {
689
+ if (this._deferTabsWork(() => this.backRoute(route))) return;
521
690
  const [tabKey, componentKey] = this.findTabItemByFullPath(route.fullPath);
522
- this.deleteTabItem(tabKey, componentKey, true);
691
+ // The router emits the Back notification once; replace/pruning are not explicit discards.
692
+ void this.deleteTabItem(tabKey, componentKey, true, false);
523
693
  }
524
694
 
525
695
  forwardRoute(route: RouteLocationNormalizedLoadedGeneric) {
696
+ if (this._deferTabsWork(() => this.forwardRoute(route))) return;
526
697
  const routeMeta = this.prepareRouteMeta(route);
527
698
  this.addTab(routeMeta);
528
699
  }
529
700
 
530
701
  public setPageMeta(route: RouteLocationNormalizedLoadedGeneric, pageMeta: IPageMeta) {
702
+ if (this._deferTabsWork(() => this.setPageMeta(route, pageMeta))) return;
531
703
  const [tabKey, componentKey] = this.findTabItemByFullPath(route.fullPath);
532
704
  this.updateTabItemPageMeta(tabKey, componentKey, pageMeta);
533
705
  }