@taicode/common-web 1.1.6 → 1.1.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../source/service/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC5D,OAAO,KAA2D,MAAM,OAAO,CAAA;AAQ/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAClD,wBAAgB,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAgDlF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;AAC9E,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAoG9G;;GAEG;AACH,UAAU,oBAAoB;IAC5B,sBAAsB;IACtB,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAA;IAC7B,UAAU;IACV,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,6CAA6C;AAE7C,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,8EAuB1D"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../source/service/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC5D,OAAO,KAA2D,MAAM,OAAO,CAAA;AA+D/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAClD,wBAAgB,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAgDlF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;AAC9E,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAyF9G;;GAEG;AACH,UAAU,oBAAoB;IAC5B,sBAAsB;IACtB,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAA;IAC7B,UAAU;IACV,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,6CAA6C;AAE7C,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,8EAuB1D"}
@@ -33,7 +33,6 @@
33
33
  * ```
34
34
  */
35
35
  import { reaction, runInAction } from 'mobx';
36
- import { catchIt } from '@taicode/common-base';
37
36
  import { Container } from '@needle-di/core';
38
37
  import React, { useContext, useEffect, useMemo, useState, useRef } from 'react';
39
38
  /**
@@ -41,6 +40,50 @@ import React, { useContext, useEffect, useMemo, useState, useRef } from 'react';
41
40
  * 通过上下文在组件树中共享依赖注入容器
42
41
  */
43
42
  const ctx = React.createContext(undefined);
43
+ const INIT_RETURN_ERROR_MESSAGE = 'Service init must return a boolean value.';
44
+ class InvalidInitReturnError extends Error {
45
+ constructor() {
46
+ super(INIT_RETURN_ERROR_MESSAGE);
47
+ this.name = 'InvalidInitReturnError';
48
+ }
49
+ }
50
+ function setServiceInited(target, value) {
51
+ runInAction(() => {
52
+ if (target != null && typeof target === 'object') {
53
+ target.inited = value;
54
+ }
55
+ });
56
+ }
57
+ function applyInitResult(target, result) {
58
+ if (typeof result === 'boolean') {
59
+ setServiceInited(target, result);
60
+ return;
61
+ }
62
+ setServiceInited(target, false);
63
+ throw new InvalidInitReturnError();
64
+ }
65
+ async function initializeServiceInstance(target) {
66
+ if (target == null ||
67
+ typeof target !== 'object' ||
68
+ !('init' in target) ||
69
+ typeof target.init !== 'function') {
70
+ return;
71
+ }
72
+ const service = target;
73
+ if (service.inited === true) {
74
+ return;
75
+ }
76
+ try {
77
+ const result = await service.init();
78
+ applyInitResult(service, result);
79
+ }
80
+ catch (error) {
81
+ if (error instanceof InvalidInitReturnError) {
82
+ throw error;
83
+ }
84
+ setServiceInited(service, false);
85
+ }
86
+ }
44
87
  export function useService(target, selector) {
45
88
  const container = useContext(ctx);
46
89
  // 使用 useState 强制组件重新渲染
@@ -84,6 +127,7 @@ export function useLocalService(ServiceClass, selector) {
84
127
  const serviceInstanceRef = useRef(null);
85
128
  // 使用 useState 强制组件重新渲染,完全模仿 useService
86
129
  const [, refresh] = useState({});
130
+ const [initError, setInitError] = useState(null);
87
131
  if (parentContainer == null) {
88
132
  throw new Error('Must be a child of ServiceProvider.');
89
133
  }
@@ -125,34 +169,21 @@ export function useLocalService(ServiceClass, selector) {
125
169
  React.useEffect(() => {
126
170
  if (parentContainer == null)
127
171
  return;
172
+ let disposed = false;
173
+ setInitError(prev => (prev === null ? prev : null));
128
174
  const service = serviceInstanceRef.current;
129
- // 自动初始化服务(如果实现了 Service 接口)
130
- if (service != null &&
131
- typeof service === 'object' &&
132
- 'init' in service &&
133
- typeof service.init === 'function') {
134
- // 直接调用 init 方法
135
- const initResult = service.init();
136
- if (initResult && typeof initResult.then === 'function') {
137
- // 异步初始化
138
- initResult.then((result) => {
139
- runInAction(() => {
140
- service.inited = result;
141
- });
142
- }).catch((error) => {
143
- runInAction(() => {
144
- service.inited = false;
145
- });
146
- });
147
- }
148
- else {
149
- // 同步初始化
150
- runInAction(() => {
151
- service.inited = true;
152
- });
175
+ initializeServiceInstance(service).catch((error) => {
176
+ if (error instanceof InvalidInitReturnError && !disposed) {
177
+ setInitError(error);
153
178
  }
154
- }
179
+ });
180
+ return () => {
181
+ disposed = true;
182
+ };
155
183
  }, [parentContainer, ServiceClass]); // 注意:这里不依赖 selector
184
+ if (initError) {
185
+ throw initError;
186
+ }
156
187
  // 返回选择器结果或服务实例
157
188
  return selector ? selector(serviceInstanceRef.current) : serviceInstanceRef.current;
158
189
  }
@@ -228,32 +259,38 @@ export function ServiceProvider(props) {
228
259
  function InitService(props) {
229
260
  const { services, children } = props;
230
261
  const container = useContext(ctx);
262
+ const [initError, setInitError] = React.useState(null);
231
263
  useEffect(() => {
232
264
  if (container == null)
233
265
  return;
234
266
  if (services.length == 0)
235
267
  return;
268
+ let disposed = false;
269
+ setInitError(prev => (prev === null ? prev : null));
236
270
  // 异步初始化所有服务
237
- (async () => {
238
- const initPromises = services.map(async (service) => {
239
- // 尝试从容器中获取服务实例(可选的,避免未注册时报错)
240
- const instance = container.get(service, { optional: true });
241
- if (instance != null && typeof instance === 'object') {
242
- // 检查实例是否实现了 Service 接口
243
- if ('init' in instance && typeof instance.init === 'function') {
244
- // 检查是否已经初始化
245
- if (instance.inited == false) {
246
- // 安全地调用 init 方法,捕获可能的异常
247
- const result = await catchIt(() => instance.init());
248
- // 使用 runInAction 确保状态更新被 Mobx 正确跟踪
249
- runInAction(() => instance.inited = !result.isError());
250
- }
271
+ const initPromises = services.map(async (service) => {
272
+ // 尝试从容器中获取服务实例(可选的,避免未注册时报错)
273
+ const instance = container.get(service, { optional: true });
274
+ if (instance != null && typeof instance === 'object') {
275
+ // 检查实例是否实现了 Service 接口
276
+ if ('init' in instance && typeof instance.init === 'function') {
277
+ if (instance.inited !== true) {
278
+ await initializeServiceInstance(instance).catch((error) => {
279
+ if (error instanceof InvalidInitReturnError && !disposed) {
280
+ setInitError(error);
281
+ }
282
+ });
251
283
  }
252
284
  }
253
- });
254
- // 等待所有服务初始化完成
255
- await Promise.all(initPromises);
256
- })();
285
+ }
286
+ });
287
+ void Promise.all(initPromises);
288
+ return () => {
289
+ disposed = true;
290
+ };
257
291
  }, [container, services]);
292
+ if (initError) {
293
+ throw initError;
294
+ }
258
295
  return children;
259
296
  }
@@ -44,12 +44,30 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
44
44
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
45
45
  };
46
46
  import { jsx as _jsx } from "react/jsx-runtime";
47
- import { act } from 'react';
47
+ import React, { act } from 'react';
48
48
  import { Service } from '@taicode/common-base';
49
49
  import { describe, it, expect, vi, beforeEach } from 'vitest';
50
50
  import { render, renderHook, waitFor } from '@testing-library/react';
51
51
  import { observable, makeObservable, runInAction, action } from 'mobx';
52
52
  import { useService, useLocalService, ServiceProvider } from './service';
53
+ class TestErrorBoundary extends React.Component {
54
+ constructor() {
55
+ super(...arguments);
56
+ this.state = { error: null };
57
+ }
58
+ static getDerivedStateFromError(error) {
59
+ return { error };
60
+ }
61
+ componentDidCatch(error) {
62
+ this.props.onError(error);
63
+ }
64
+ render() {
65
+ if (this.state.error) {
66
+ return null;
67
+ }
68
+ return this.props.children;
69
+ }
70
+ }
53
71
  // 创建测试用的服务类
54
72
  let TestService = (() => {
55
73
  var _a, _TestService_count_accessor_storage, _TestService_users_accessor_storage, _TestService_inited_accessor_storage;
@@ -369,6 +387,63 @@ describe('service integration', () => {
369
387
  expect(result.current.count).toBe(0);
370
388
  expect(result.current.value).toBe('initial');
371
389
  });
390
+ it('在未提供 selector 时应该返回完整服务实例并响应更新', async () => {
391
+ const TestProvider = createTestProvider([TestService]);
392
+ const { result } = renderHook(() => useService(TestService), { wrapper: TestProvider });
393
+ const firstInstance = result.current;
394
+ expect(firstInstance.count).toBe(0);
395
+ act(() => {
396
+ firstInstance.increment();
397
+ });
398
+ await waitFor(() => {
399
+ expect(result.current.count).toBe(1);
400
+ });
401
+ expect(result.current).toBe(firstInstance);
402
+ });
403
+ it('当 selector 返回服务实例时应该退化为监听全部属性', async () => {
404
+ const TestProvider = createTestProvider([TestService]);
405
+ const { result } = renderHook(() => ({
406
+ serviceViaSelector: useService(TestService, service => service),
407
+ counter: useService(TestService, service => service.count)
408
+ }), { wrapper: TestProvider });
409
+ const initialServiceRef = result.current.serviceViaSelector;
410
+ expect(initialServiceRef.count).toBe(0);
411
+ act(() => {
412
+ initialServiceRef.increment();
413
+ });
414
+ await waitFor(() => {
415
+ expect(result.current.counter).toBe(1);
416
+ expect(result.current.serviceViaSelector.count).toBe(1);
417
+ });
418
+ expect(result.current.serviceViaSelector).toBe(initialServiceRef);
419
+ });
420
+ it('useService 的 selector 应该能够响应其他服务的 observable 变化', async () => {
421
+ const TestProvider = createTestProvider([TestService, AnotherService]);
422
+ const { result } = renderHook(() => {
423
+ const globalService = useService(TestService, service => service);
424
+ const anotherSelection = useService(AnotherService, service => ({
425
+ service,
426
+ combined: `${service.value}-${globalService.count}`
427
+ }));
428
+ return {
429
+ globalService,
430
+ anotherSelection
431
+ };
432
+ }, { wrapper: TestProvider });
433
+ expect(result.current.anotherSelection.combined).toBe('initial-0');
434
+ act(() => {
435
+ result.current.anotherSelection.service.setValue('changed');
436
+ });
437
+ await waitFor(() => {
438
+ expect(result.current.anotherSelection.combined).toBe('changed-0');
439
+ });
440
+ act(() => {
441
+ result.current.globalService.increment();
442
+ });
443
+ await waitFor(() => {
444
+ expect(result.current.anotherSelection.combined).toBe('changed-1');
445
+ });
446
+ });
372
447
  });
373
448
  describe('ServiceProvider', () => {
374
449
  it('应该能够渲染子组件', () => {
@@ -446,6 +521,207 @@ describe('service integration', () => {
446
521
  expect(result.current).toBe(false); // 因为初始化失败,inited 应该保持 false
447
522
  });
448
523
  });
524
+ it('在服务已初始化时不会重复调用 init', async () => {
525
+ const initSpy = vi.fn(async () => true);
526
+ let PreInitedService = (() => {
527
+ var _a, _PreInitedService_inited_accessor_storage;
528
+ let _classSuper = Service;
529
+ let _inited_decorators;
530
+ let _inited_initializers = [];
531
+ let _inited_extraInitializers = [];
532
+ return _a = class PreInitedService extends _classSuper {
533
+ get inited() { return __classPrivateFieldGet(this, _PreInitedService_inited_accessor_storage, "f"); }
534
+ set inited(value) { __classPrivateFieldSet(this, _PreInitedService_inited_accessor_storage, value, "f"); }
535
+ constructor() {
536
+ super();
537
+ _PreInitedService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, true));
538
+ this.init = (__runInitializers(this, _inited_extraInitializers), initSpy);
539
+ makeObservable(this);
540
+ }
541
+ },
542
+ _PreInitedService_inited_accessor_storage = new WeakMap(),
543
+ (() => {
544
+ var _b;
545
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
546
+ _inited_decorators = [observable];
547
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
548
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
549
+ })(),
550
+ _a;
551
+ })();
552
+ const TestProvider = createTestProvider([PreInitedService]);
553
+ const { result } = renderHook(() => useService(PreInitedService, service => ({
554
+ inited: service.inited,
555
+ service
556
+ })), { wrapper: TestProvider });
557
+ await waitFor(() => {
558
+ expect(result.current.inited).toBe(true);
559
+ });
560
+ expect(initSpy).not.toHaveBeenCalled();
561
+ });
562
+ it('应该正确处理同步 init 的服务', async () => {
563
+ let SyncInitService = (() => {
564
+ var _a, _SyncInitService_inited_accessor_storage, _SyncInitService_value_accessor_storage;
565
+ let _inited_decorators;
566
+ let _inited_initializers = [];
567
+ let _inited_extraInitializers = [];
568
+ let _value_decorators;
569
+ let _value_initializers = [];
570
+ let _value_extraInitializers = [];
571
+ return _a = class SyncInitService {
572
+ get inited() { return __classPrivateFieldGet(this, _SyncInitService_inited_accessor_storage, "f"); }
573
+ set inited(value) { __classPrivateFieldSet(this, _SyncInitService_inited_accessor_storage, value, "f"); }
574
+ get value() { return __classPrivateFieldGet(this, _SyncInitService_value_accessor_storage, "f"); }
575
+ set value(value) { __classPrivateFieldSet(this, _SyncInitService_value_accessor_storage, value, "f"); }
576
+ constructor() {
577
+ _SyncInitService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
578
+ _SyncInitService_value_accessor_storage.set(this, (__runInitializers(this, _inited_extraInitializers), __runInitializers(this, _value_initializers, 'pending')));
579
+ __runInitializers(this, _value_extraInitializers);
580
+ makeObservable(this);
581
+ }
582
+ init() {
583
+ runInAction(() => {
584
+ this.value = 'ready';
585
+ this.inited = true;
586
+ });
587
+ return true;
588
+ }
589
+ },
590
+ _SyncInitService_inited_accessor_storage = new WeakMap(),
591
+ _SyncInitService_value_accessor_storage = new WeakMap(),
592
+ (() => {
593
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
594
+ _inited_decorators = [observable];
595
+ _value_decorators = [observable];
596
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
597
+ __esDecorate(_a, null, _value_decorators, { kind: "accessor", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value, set: (obj, value) => { obj.value = value; } }, metadata: _metadata }, _value_initializers, _value_extraInitializers);
598
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
599
+ })(),
600
+ _a;
601
+ })();
602
+ const TestProvider = createTestProvider([SyncInitService]);
603
+ const { result } = renderHook(() => useService(SyncInitService, service => ({
604
+ inited: service.inited,
605
+ value: service.value
606
+ })), { wrapper: TestProvider });
607
+ await waitFor(() => {
608
+ expect(result.current.inited).toBe(true);
609
+ expect(result.current.value).toBe('ready');
610
+ });
611
+ });
612
+ it('应该为多个服务分别调用 init', async () => {
613
+ const firstInit = vi.fn(async () => true);
614
+ const secondInit = vi.fn(async () => true);
615
+ let FirstInitService = (() => {
616
+ var _a, _FirstInitService_inited_accessor_storage;
617
+ let _classSuper = Service;
618
+ let _inited_decorators;
619
+ let _inited_initializers = [];
620
+ let _inited_extraInitializers = [];
621
+ return _a = class FirstInitService extends _classSuper {
622
+ get inited() { return __classPrivateFieldGet(this, _FirstInitService_inited_accessor_storage, "f"); }
623
+ set inited(value) { __classPrivateFieldSet(this, _FirstInitService_inited_accessor_storage, value, "f"); }
624
+ constructor() {
625
+ super();
626
+ _FirstInitService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
627
+ this.init = (__runInitializers(this, _inited_extraInitializers), firstInit);
628
+ makeObservable(this);
629
+ }
630
+ },
631
+ _FirstInitService_inited_accessor_storage = new WeakMap(),
632
+ (() => {
633
+ var _b;
634
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
635
+ _inited_decorators = [observable];
636
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
637
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
638
+ })(),
639
+ _a;
640
+ })();
641
+ let SecondInitService = (() => {
642
+ var _a, _SecondInitService_inited_accessor_storage;
643
+ let _classSuper = Service;
644
+ let _inited_decorators;
645
+ let _inited_initializers = [];
646
+ let _inited_extraInitializers = [];
647
+ return _a = class SecondInitService extends _classSuper {
648
+ get inited() { return __classPrivateFieldGet(this, _SecondInitService_inited_accessor_storage, "f"); }
649
+ set inited(value) { __classPrivateFieldSet(this, _SecondInitService_inited_accessor_storage, value, "f"); }
650
+ constructor() {
651
+ super();
652
+ _SecondInitService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
653
+ this.init = (__runInitializers(this, _inited_extraInitializers), secondInit);
654
+ makeObservable(this);
655
+ }
656
+ },
657
+ _SecondInitService_inited_accessor_storage = new WeakMap(),
658
+ (() => {
659
+ var _b;
660
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
661
+ _inited_decorators = [observable];
662
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
663
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
664
+ })(),
665
+ _a;
666
+ })();
667
+ const TestProvider = createTestProvider([FirstInitService, SecondInitService]);
668
+ const { result } = renderHook(() => ({
669
+ firstInited: useService(FirstInitService, service => service.inited),
670
+ secondInited: useService(SecondInitService, service => service.inited)
671
+ }), { wrapper: TestProvider });
672
+ await waitFor(() => {
673
+ expect(result.current.firstInited).toBe(true);
674
+ expect(result.current.secondInited).toBe(true);
675
+ });
676
+ expect(firstInit).toHaveBeenCalledTimes(1);
677
+ expect(secondInit).toHaveBeenCalledTimes(1);
678
+ });
679
+ it('在 init 返回非布尔值时应该抛出错误', async () => {
680
+ var _a;
681
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
682
+ let InvalidReturnService = (() => {
683
+ var _a, _InvalidReturnService_inited_accessor_storage;
684
+ let _classSuper = Service;
685
+ let _inited_decorators;
686
+ let _inited_initializers = [];
687
+ let _inited_extraInitializers = [];
688
+ return _a = class InvalidReturnService extends _classSuper {
689
+ get inited() { return __classPrivateFieldGet(this, _InvalidReturnService_inited_accessor_storage, "f"); }
690
+ set inited(value) { __classPrivateFieldSet(this, _InvalidReturnService_inited_accessor_storage, value, "f"); }
691
+ constructor() {
692
+ super();
693
+ _InvalidReturnService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
694
+ __runInitializers(this, _inited_extraInitializers);
695
+ makeObservable(this);
696
+ }
697
+ async init() {
698
+ return 'invalid';
699
+ }
700
+ },
701
+ _InvalidReturnService_inited_accessor_storage = new WeakMap(),
702
+ (() => {
703
+ var _b;
704
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
705
+ _inited_decorators = [observable];
706
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
707
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
708
+ })(),
709
+ _a;
710
+ })();
711
+ const TestProvider = createTestProvider([InvalidReturnService]);
712
+ const capturedErrors = [];
713
+ const Wrapper = ({ children }) => (_jsx(TestErrorBoundary, { onError: error => capturedErrors.push(error), children: _jsx(TestProvider, { children: children }) }));
714
+ try {
715
+ renderHook(() => useService(InvalidReturnService, service => service.inited), { wrapper: Wrapper });
716
+ await waitFor(() => {
717
+ expect(capturedErrors[0]).toBeInstanceOf(Error);
718
+ });
719
+ expect((_a = capturedErrors[0]) === null || _a === void 0 ? void 0 : _a.message).toBe('Service init must return a boolean value.');
720
+ }
721
+ finally {
722
+ errorSpy.mockRestore();
723
+ }
724
+ });
449
725
  });
450
726
  describe('边界情况', () => {
451
727
  it('useService 应该能够处理选择器返回 undefined 的情况', () => {
@@ -630,6 +906,52 @@ describe('service integration', () => {
630
906
  expect(result.current).toBe(false);
631
907
  });
632
908
  });
909
+ it('在 init 返回非布尔值时应该抛出错误', async () => {
910
+ var _a;
911
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
912
+ let InvalidReturnLocalService = (() => {
913
+ var _a, _InvalidReturnLocalService_inited_accessor_storage;
914
+ let _classSuper = Service;
915
+ let _inited_decorators;
916
+ let _inited_initializers = [];
917
+ let _inited_extraInitializers = [];
918
+ return _a = class InvalidReturnLocalService extends _classSuper {
919
+ get inited() { return __classPrivateFieldGet(this, _InvalidReturnLocalService_inited_accessor_storage, "f"); }
920
+ set inited(value) { __classPrivateFieldSet(this, _InvalidReturnLocalService_inited_accessor_storage, value, "f"); }
921
+ constructor() {
922
+ super();
923
+ _InvalidReturnLocalService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
924
+ __runInitializers(this, _inited_extraInitializers);
925
+ makeObservable(this);
926
+ }
927
+ async init() {
928
+ return 'invalid';
929
+ }
930
+ },
931
+ _InvalidReturnLocalService_inited_accessor_storage = new WeakMap(),
932
+ (() => {
933
+ var _b;
934
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
935
+ _inited_decorators = [observable];
936
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
937
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
938
+ })(),
939
+ _a;
940
+ })();
941
+ const TestProvider = createTestProvider([TestService]);
942
+ const capturedErrors = [];
943
+ const Wrapper = ({ children }) => (_jsx(TestErrorBoundary, { onError: error => capturedErrors.push(error), children: _jsx(TestProvider, { children: children }) }));
944
+ try {
945
+ renderHook(() => useLocalService(InvalidReturnLocalService, service => service.inited), { wrapper: Wrapper });
946
+ await waitFor(() => {
947
+ expect(capturedErrors[0]).toBeInstanceOf(Error);
948
+ });
949
+ expect((_a = capturedErrors[0]) === null || _a === void 0 ? void 0 : _a.message).toBe('Service init must return a boolean value.');
950
+ }
951
+ finally {
952
+ errorSpy.mockRestore();
953
+ }
954
+ });
633
955
  it('应该能够处理没有实现 Service 接口的局部服务', async () => {
634
956
  const TestProvider = createTestProvider([TestService]);
635
957
  const { result } = renderHook(() => useLocalService(PlainClass, service => ({
@@ -756,6 +1078,198 @@ describe('service integration', () => {
756
1078
  expect(result2.current.localService.counter).toBe(2);
757
1079
  expect(result2.current.localService.data).toBe('local2-changed');
758
1080
  });
1081
+ it('当 selector 返回服务实例时应该退化为监听全部属性', async () => {
1082
+ const TestProvider = createTestProvider([TestService]);
1083
+ const { result } = renderHook(() => useLocalService(LocalService, service => service), { wrapper: TestProvider });
1084
+ const firstInstance = result.current;
1085
+ expect(firstInstance.counter).toBe(0);
1086
+ act(() => {
1087
+ firstInstance.increment();
1088
+ });
1089
+ await waitFor(() => {
1090
+ expect(result.current.counter).toBe(1);
1091
+ });
1092
+ expect(result.current).toBe(firstInstance);
1093
+ });
1094
+ it('应该在卸载后重新创建新的服务实例', async () => {
1095
+ const TestProvider = createTestProvider([TestService]);
1096
+ const { result, unmount } = renderHook(() => useLocalService(LocalService, service => service), { wrapper: TestProvider });
1097
+ const firstInstance = result.current;
1098
+ await waitFor(() => {
1099
+ expect(firstInstance.inited).toBe(true);
1100
+ });
1101
+ unmount();
1102
+ const { result: result2 } = renderHook(() => useLocalService(LocalService, service => service), { wrapper: TestProvider });
1103
+ await waitFor(() => {
1104
+ expect(result2.current.inited).toBe(true);
1105
+ });
1106
+ expect(result2.current).not.toBe(firstInstance);
1107
+ });
1108
+ it('应该只在首次挂载时调用一次 init', async () => {
1109
+ const initSpy = vi.fn(async () => true);
1110
+ let InitOnceLocalService = (() => {
1111
+ var _a, _InitOnceLocalService_inited_accessor_storage;
1112
+ let _classSuper = Service;
1113
+ let _inited_decorators;
1114
+ let _inited_initializers = [];
1115
+ let _inited_extraInitializers = [];
1116
+ return _a = class InitOnceLocalService extends _classSuper {
1117
+ get inited() { return __classPrivateFieldGet(this, _InitOnceLocalService_inited_accessor_storage, "f"); }
1118
+ set inited(value) { __classPrivateFieldSet(this, _InitOnceLocalService_inited_accessor_storage, value, "f"); }
1119
+ constructor() {
1120
+ super();
1121
+ _InitOnceLocalService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
1122
+ this.init = (__runInitializers(this, _inited_extraInitializers), initSpy);
1123
+ makeObservable(this);
1124
+ }
1125
+ },
1126
+ _InitOnceLocalService_inited_accessor_storage = new WeakMap(),
1127
+ (() => {
1128
+ var _b;
1129
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
1130
+ _inited_decorators = [observable];
1131
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
1132
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
1133
+ })(),
1134
+ _a;
1135
+ })();
1136
+ const TestProvider = createTestProvider([TestService]);
1137
+ const { result, rerender } = renderHook(() => useLocalService(InitOnceLocalService, service => service.inited), { wrapper: TestProvider });
1138
+ await waitFor(() => {
1139
+ expect(result.current).toBe(true);
1140
+ });
1141
+ rerender();
1142
+ await waitFor(() => {
1143
+ expect(result.current).toBe(true);
1144
+ });
1145
+ expect(initSpy).toHaveBeenCalledTimes(1);
1146
+ });
1147
+ it('在同一组件中混用全局与局部的同类服务时应该保持状态独立', async () => {
1148
+ const TestProvider = createTestProvider([TestService]);
1149
+ const { result } = renderHook(() => ({
1150
+ global: useService(TestService, service => ({
1151
+ service,
1152
+ count: service.count,
1153
+ users: [...service.users]
1154
+ })),
1155
+ local: useLocalService(TestService, service => ({
1156
+ service,
1157
+ count: service.count,
1158
+ users: [...service.users]
1159
+ }))
1160
+ }), { wrapper: TestProvider });
1161
+ await waitFor(() => {
1162
+ expect(result.current.global.users).toEqual(['Alice', 'Bob']);
1163
+ expect(result.current.local.users).toEqual(['Alice', 'Bob']);
1164
+ });
1165
+ expect(result.current.global.service).not.toBe(result.current.local.service);
1166
+ act(() => {
1167
+ result.current.global.service.increment();
1168
+ result.current.global.service.addUser('GlobalCharlie');
1169
+ });
1170
+ await waitFor(() => {
1171
+ expect(result.current.global.count).toBe(1);
1172
+ expect(result.current.global.users).toEqual(['Alice', 'Bob', 'GlobalCharlie']);
1173
+ });
1174
+ expect(result.current.local.count).toBe(0);
1175
+ expect(result.current.local.users).toEqual(['Alice', 'Bob']);
1176
+ act(() => {
1177
+ result.current.local.service.increment();
1178
+ result.current.local.service.addUser('LocalCharlie');
1179
+ });
1180
+ await waitFor(() => {
1181
+ expect(result.current.local.count).toBe(1);
1182
+ expect(result.current.local.users).toEqual(['Alice', 'Bob', 'LocalCharlie']);
1183
+ });
1184
+ expect(result.current.global.count).toBe(1);
1185
+ expect(result.current.global.users).toEqual(['Alice', 'Bob', 'GlobalCharlie']);
1186
+ });
1187
+ it('同一服务在全局与局部同时使用时应该分别初始化', async () => {
1188
+ const initSpy = vi.fn(async function () {
1189
+ runInAction(() => {
1190
+ this.inited = true;
1191
+ });
1192
+ return true;
1193
+ });
1194
+ let InitSpyService = (() => {
1195
+ var _a, _InitSpyService_inited_accessor_storage;
1196
+ let _classSuper = Service;
1197
+ let _inited_decorators;
1198
+ let _inited_initializers = [];
1199
+ let _inited_extraInitializers = [];
1200
+ return _a = class InitSpyService extends _classSuper {
1201
+ get inited() { return __classPrivateFieldGet(this, _InitSpyService_inited_accessor_storage, "f"); }
1202
+ set inited(value) { __classPrivateFieldSet(this, _InitSpyService_inited_accessor_storage, value, "f"); }
1203
+ constructor() {
1204
+ super();
1205
+ _InitSpyService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
1206
+ this.init = (__runInitializers(this, _inited_extraInitializers), initSpy);
1207
+ makeObservable(this);
1208
+ }
1209
+ },
1210
+ _InitSpyService_inited_accessor_storage = new WeakMap(),
1211
+ (() => {
1212
+ var _b;
1213
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
1214
+ _inited_decorators = [observable];
1215
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
1216
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
1217
+ })(),
1218
+ _a;
1219
+ })();
1220
+ const TestProvider = createTestProvider([InitSpyService]);
1221
+ const { result } = renderHook(() => ({
1222
+ global: useService(InitSpyService, service => ({
1223
+ service,
1224
+ inited: service.inited
1225
+ })),
1226
+ local: useLocalService(InitSpyService, service => ({
1227
+ service,
1228
+ inited: service.inited
1229
+ }))
1230
+ }), { wrapper: TestProvider });
1231
+ await waitFor(() => {
1232
+ expect(result.current.global.inited).toBe(true);
1233
+ expect(result.current.local.inited).toBe(true);
1234
+ });
1235
+ expect(result.current.global.service).not.toBe(result.current.local.service);
1236
+ expect(initSpy).toHaveBeenCalledTimes(2);
1237
+ const instances = initSpy.mock.instances;
1238
+ expect(new Set(instances).size).toBe(2);
1239
+ instances.forEach(instance => {
1240
+ expect(instance.inited).toBe(true);
1241
+ });
1242
+ });
1243
+ it('useLocalService 的 selector 应该能够响应全局服务的 observable 变化', async () => {
1244
+ const TestProvider = createTestProvider([TestService]);
1245
+ const { result } = renderHook(() => {
1246
+ const globalService = useService(TestService, service => service);
1247
+ const localSelection = useLocalService(LocalService, service => ({
1248
+ service,
1249
+ combined: `${service.counter}-${globalService.count}`
1250
+ }));
1251
+ return {
1252
+ globalService,
1253
+ localSelection
1254
+ };
1255
+ }, { wrapper: TestProvider });
1256
+ await waitFor(() => {
1257
+ expect(result.current.localSelection.service.inited).toBe(true);
1258
+ });
1259
+ expect(result.current.localSelection.combined).toBe('0-0');
1260
+ act(() => {
1261
+ result.current.globalService.increment();
1262
+ });
1263
+ await waitFor(() => {
1264
+ expect(result.current.localSelection.combined).toBe('0-1');
1265
+ });
1266
+ act(() => {
1267
+ result.current.localSelection.service.increment();
1268
+ });
1269
+ await waitFor(() => {
1270
+ expect(result.current.localSelection.combined).toBe('1-1');
1271
+ });
1272
+ });
759
1273
  });
760
1274
  describe('useLocalService 可选 selector 功能', () => {
761
1275
  it('应该在不传入 selector 时返回完整的服务实例', async () => {
@@ -831,5 +1345,55 @@ describe('service integration', () => {
831
1345
  expect(result.current.data).toBe('changed');
832
1346
  });
833
1347
  });
1348
+ it('应该正确处理同步 init 的局部服务', async () => {
1349
+ const TestProvider = createTestProvider([TestService]);
1350
+ let SyncInitLocalService = (() => {
1351
+ var _a, _SyncInitLocalService_inited_accessor_storage, _SyncInitLocalService_value_accessor_storage;
1352
+ let _inited_decorators;
1353
+ let _inited_initializers = [];
1354
+ let _inited_extraInitializers = [];
1355
+ let _value_decorators;
1356
+ let _value_initializers = [];
1357
+ let _value_extraInitializers = [];
1358
+ return _a = class SyncInitLocalService {
1359
+ get inited() { return __classPrivateFieldGet(this, _SyncInitLocalService_inited_accessor_storage, "f"); }
1360
+ set inited(value) { __classPrivateFieldSet(this, _SyncInitLocalService_inited_accessor_storage, value, "f"); }
1361
+ get value() { return __classPrivateFieldGet(this, _SyncInitLocalService_value_accessor_storage, "f"); }
1362
+ set value(value) { __classPrivateFieldSet(this, _SyncInitLocalService_value_accessor_storage, value, "f"); }
1363
+ constructor() {
1364
+ _SyncInitLocalService_inited_accessor_storage.set(this, __runInitializers(this, _inited_initializers, false));
1365
+ _SyncInitLocalService_value_accessor_storage.set(this, (__runInitializers(this, _inited_extraInitializers), __runInitializers(this, _value_initializers, 'pending')));
1366
+ __runInitializers(this, _value_extraInitializers);
1367
+ makeObservable(this);
1368
+ }
1369
+ init() {
1370
+ runInAction(() => {
1371
+ this.value = 'ready';
1372
+ this.inited = true;
1373
+ });
1374
+ return true;
1375
+ }
1376
+ },
1377
+ _SyncInitLocalService_inited_accessor_storage = new WeakMap(),
1378
+ _SyncInitLocalService_value_accessor_storage = new WeakMap(),
1379
+ (() => {
1380
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
1381
+ _inited_decorators = [observable];
1382
+ _value_decorators = [observable];
1383
+ __esDecorate(_a, null, _inited_decorators, { kind: "accessor", name: "inited", static: false, private: false, access: { has: obj => "inited" in obj, get: obj => obj.inited, set: (obj, value) => { obj.inited = value; } }, metadata: _metadata }, _inited_initializers, _inited_extraInitializers);
1384
+ __esDecorate(_a, null, _value_decorators, { kind: "accessor", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value, set: (obj, value) => { obj.value = value; } }, metadata: _metadata }, _value_initializers, _value_extraInitializers);
1385
+ if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
1386
+ })(),
1387
+ _a;
1388
+ })();
1389
+ const { result } = renderHook(() => useLocalService(SyncInitLocalService, service => ({
1390
+ inited: service.inited,
1391
+ value: service.value
1392
+ })), { wrapper: TestProvider });
1393
+ await waitFor(() => {
1394
+ expect(result.current.inited).toBe(true);
1395
+ expect(result.current.value).toBe('ready');
1396
+ });
1397
+ });
834
1398
  });
835
1399
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taicode/common-web",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "author": "Alain",
5
5
  "license": "ISC",
6
6
  "description": "",