@furystack/shades 10.0.6 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/esm/compile-route.d.ts +2 -0
  2. package/esm/compile-route.d.ts.map +1 -0
  3. package/esm/compile-route.js +4 -0
  4. package/esm/compile-route.js.map +1 -0
  5. package/esm/components/lazy-load.d.ts +2 -2
  6. package/esm/components/lazy-load.d.ts.map +1 -1
  7. package/esm/components/lazy-load.spec.js +2 -2
  8. package/esm/components/lazy-load.spec.js.map +1 -1
  9. package/esm/components/link-to-route.d.ts +1 -1
  10. package/esm/components/link-to-route.d.ts.map +1 -1
  11. package/esm/components/link-to-route.js +3 -3
  12. package/esm/components/link-to-route.js.map +1 -1
  13. package/esm/components/route-link.d.ts +2 -2
  14. package/esm/components/route-link.d.ts.map +1 -1
  15. package/esm/components/router.d.ts +4 -4
  16. package/esm/components/router.d.ts.map +1 -1
  17. package/esm/components/router.js +4 -4
  18. package/esm/components/router.js.map +1 -1
  19. package/esm/components/router.spec.js +1 -1
  20. package/esm/components/router.spec.js.map +1 -1
  21. package/esm/index.d.ts +5 -4
  22. package/esm/index.d.ts.map +1 -1
  23. package/esm/index.js +5 -4
  24. package/esm/index.js.map +1 -1
  25. package/esm/models/render-options.d.ts +2 -2
  26. package/esm/models/render-options.d.ts.map +1 -1
  27. package/esm/models/shade-component.d.ts.map +1 -1
  28. package/esm/services/location-service.d.ts +6 -7
  29. package/esm/services/location-service.d.ts.map +1 -1
  30. package/esm/services/location-service.js +24 -26
  31. package/esm/services/location-service.js.map +1 -1
  32. package/esm/services/location-service.spec.js +21 -21
  33. package/esm/services/location-service.spec.js.map +1 -1
  34. package/esm/services/resource-manager.d.ts +4 -5
  35. package/esm/services/resource-manager.d.ts.map +1 -1
  36. package/esm/services/resource-manager.js +18 -5
  37. package/esm/services/resource-manager.js.map +1 -1
  38. package/esm/services/resource-manager.spec.js +55 -7
  39. package/esm/services/resource-manager.spec.js.map +1 -1
  40. package/esm/services/screen-service.d.ts +1 -2
  41. package/esm/services/screen-service.d.ts.map +1 -1
  42. package/esm/services/screen-service.js +1 -1
  43. package/esm/services/screen-service.js.map +1 -1
  44. package/esm/shade-component.d.ts +5 -5
  45. package/esm/shade-component.d.ts.map +1 -1
  46. package/esm/shade-component.js +8 -5
  47. package/esm/shade-component.js.map +1 -1
  48. package/esm/shade-resources.integration.spec.js +6 -5
  49. package/esm/shade-resources.integration.spec.js.map +1 -1
  50. package/esm/shade.d.ts +1 -1
  51. package/esm/shade.d.ts.map +1 -1
  52. package/esm/shade.js +5 -5
  53. package/esm/shade.js.map +1 -1
  54. package/esm/shades.integration.spec.js +79 -4
  55. package/esm/shades.integration.spec.js.map +1 -1
  56. package/esm/styled-element.d.ts.map +1 -1
  57. package/esm/styled-shade.d.ts.map +1 -1
  58. package/esm/styled-shade.js +13 -7
  59. package/esm/styled-shade.js.map +1 -1
  60. package/package.json +8 -8
  61. package/src/compile-route.ts +6 -0
  62. package/src/components/lazy-load.spec.tsx +3 -3
  63. package/src/components/link-to-route.tsx +4 -4
  64. package/src/components/router.spec.tsx +1 -1
  65. package/src/components/router.tsx +7 -7
  66. package/src/index.ts +5 -4
  67. package/src/models/render-options.ts +2 -2
  68. package/src/services/location-service.spec.ts +23 -23
  69. package/src/services/location-service.tsx +29 -31
  70. package/src/services/resource-manager.spec.ts +74 -7
  71. package/src/services/resource-manager.ts +29 -10
  72. package/src/services/screen-service.ts +1 -2
  73. package/src/shade-component.ts +16 -12
  74. package/src/shade-resources.integration.spec.tsx +7 -5
  75. package/src/shade.ts +6 -6
  76. package/src/shades.integration.spec.tsx +107 -4
  77. package/src/styled-shade.ts +13 -7
@@ -1 +1 @@
1
- {"version":3,"file":"styled-element.d.ts","sourceRoot":"","sources":["../src/styled-element.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAEjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,aAAa,0DACf,QAAQ,UACT,QAAQ,mBAAmB,CAAC,KACnC,CAAC,CAAC,KAAK,EAAE,eAAe,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,KAAK,WAAW,CAWtG,CAAA"}
1
+ {"version":3,"file":"styled-element.d.ts","sourceRoot":"","sources":["../src/styled-element.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAEjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,SAAS,MAAM,GAAG,CAAC,iBAAiB,WAC/D,QAAQ,UACT,OAAO,CAAC,mBAAmB,CAAC,KACnC,CAAC,CAAC,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,KAAK,GAAG,CAAC,OAAO,CAWtG,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"styled-shade.d.ts","sourceRoot":"","sources":["../src/styled-shade.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,WAAW,qBAAsB,GAAG,aAAa,YAAY,KAAK,WAAW,WAC/E,CAAC,UACF,QAAQ,mBAAmB,CAAC,MAYrC,CAAA"}
1
+ {"version":3,"file":"styled-shade.d.ts","sourceRoot":"","sources":["../src/styled-shade.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAG7D;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,YAAY,KAAK,GAAG,CAAC,OAAO,WAC/E,CAAC,UACF,OAAO,CAAC,mBAAmB,CAAC,KAgB9B,CACP,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { hasStyle } from './shade-component.js';
1
2
  /**
2
3
  * Creates a shortcut for a specific custom Shade element with additional styles
3
4
  * @param element The element instance
@@ -6,13 +7,18 @@
6
7
  */
7
8
  export const styledShade = (element, styles) => {
8
9
  return ((props, childrenList) => {
9
- const mergedProps = {
10
- ...props,
11
- style: {
12
- ...(props?.style || {}),
13
- ...styles,
14
- },
15
- };
10
+ const mergedProps = hasStyle(props)
11
+ ? {
12
+ ...props,
13
+ style: {
14
+ ...props.style,
15
+ ...styles,
16
+ },
17
+ }
18
+ : {
19
+ ...props,
20
+ style: styles,
21
+ };
16
22
  return element(mergedProps, childrenList);
17
23
  });
18
24
  };
@@ -1 +1 @@
1
- {"version":3,"file":"styled-shade.js","sourceRoot":"","sources":["../src/styled-shade.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,OAAU,EACV,MAAoC,EACpC,EAAE;IACF,OAAO,CAAC,CAAC,KAAU,EAAE,YAA2B,EAAE,EAAE;QAClD,MAAM,WAAW,GAAG;YAClB,GAAG,KAAK;YACR,KAAK,EAAE;gBACL,GAAG,CAAE,KAAa,EAAE,KAAK,IAAI,EAAE,CAAC;gBAChC,GAAG,MAAM;aACV;SACF,CAAA;QACD,OAAO,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IAC3C,CAAC,CAAM,CAAA;AACT,CAAC,CAAA"}
1
+ {"version":3,"file":"styled-shade.js","sourceRoot":"","sources":["../src/styled-shade.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,OAAU,EACV,MAAoC,EACpC,EAAE;IACF,OAAO,CAAC,CAAC,KAAU,EAAE,YAA2B,EAAE,EAAE;QAClD,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC;YACjC,CAAC,CAAC;gBACE,GAAG,KAAK;gBACR,KAAK,EAAE;oBACL,GAAG,KAAK,CAAC,KAAK;oBACd,GAAG,MAAM;iBACV;aACF;YACH,CAAC,CAAC;gBACE,GAAG,KAAK;gBACR,KAAK,EAAE,MAAM;aACd,CAAA;QACL,OAAO,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IAC3C,CAAC,CAAM,CAAA;AACT,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furystack/shades",
3
- "version": "10.0.6",
3
+ "version": "11.0.0",
4
4
  "description": "Google Authentication Provider for FuryStack",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -38,16 +38,16 @@
38
38
  "homepage": "https://github.com/furystack/furystack",
39
39
  "devDependencies": {
40
40
  "@types/jsdom": "^21.1.7",
41
- "@types/node": "^20.14.2",
41
+ "@types/node": "^20.14.10",
42
42
  "jsdom": "^24.1.0",
43
- "typescript": "^5.4.5",
44
- "vitest": "^1.6.0"
43
+ "typescript": "^5.5.3",
44
+ "vitest": "^2.0.0"
45
45
  },
46
46
  "dependencies": {
47
- "@furystack/inject": "^11.0.3",
48
- "@furystack/rest": "^7.0.5",
49
- "@furystack/utils": "^7.0.2",
50
- "path-to-regexp": "^6.2.2",
47
+ "@furystack/inject": "^12.0.0",
48
+ "@furystack/rest": "^8.0.0",
49
+ "@furystack/utils": "^8.0.0",
50
+ "path-to-regexp": "^7.0.0",
51
51
  "semaphore-async-await": "^1.5.1"
52
52
  },
53
53
  "gitHead": "76e1d17a71b981984935c9a7a5791cf61ebf5213"
@@ -0,0 +1,6 @@
1
+ import { compile } from 'path-to-regexp'
2
+
3
+ const stringifyObjectValues = (params: Record<string, any>) =>
4
+ Object.fromEntries(Object.entries(params).map(([key, value]) => [key, value?.toString()]))
5
+
6
+ export const compileRoute = <T extends Object>(url: string, params: T) => compile(url)(stringifyObjectValues(params))
@@ -1,13 +1,13 @@
1
- import { TextEncoder, TextDecoder } from 'util'
1
+ import { TextDecoder, TextEncoder } from 'util'
2
2
 
3
3
  global.TextEncoder = TextEncoder
4
- global.TextDecoder = TextDecoder as any
4
+ global.TextDecoder = TextDecoder as typeof global.TextDecoder
5
5
 
6
6
  import { Injector } from '@furystack/inject'
7
7
  import { sleepAsync } from '@furystack/utils'
8
8
  import { LazyLoad } from './lazy-load.js'
9
9
 
10
- import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
10
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
11
11
  import { initializeShadeRoot } from '../initialize.js'
12
12
  import { createComponent } from '../shade-component.js'
13
13
 
@@ -1,8 +1,8 @@
1
- import { compile } from 'path-to-regexp'
2
- import type { Route } from './router.js'
3
- import { Shade } from '../shade.js'
1
+ import { compileRoute } from '../compile-route.js'
4
2
  import type { ChildrenList } from '../models/children-list.js'
5
3
  import { createComponent } from '../shade-component.js'
4
+ import { Shade } from '../shade.js'
5
+ import type { Route } from './router.js'
6
6
 
7
7
  export type LinkToRouteProps<T extends {}> = {
8
8
  route: Route<T>
@@ -16,7 +16,7 @@ export const LinkToRoute: <T extends {}>(props: LinkToRouteProps<T>, children?:
16
16
  render: ({ props, element, children }) => {
17
17
  const { route, params } = props
18
18
 
19
- const url = compile(route.url)(params)
19
+ const url = compileRoute(route.url, params)
20
20
  element.setAttribute('href', url)
21
21
  return <>{children}</>
22
22
  },
@@ -60,7 +60,7 @@ describe('Router', () => {
60
60
  <Router
61
61
  routes={[
62
62
  { url: '/route-a', component: () => <div id="content">route-a</div>, onVisit, onLeave },
63
- { url: '/route-b/:id?', component: ({ match }) => <div id="content">route-b{match.params.id}</div> },
63
+ { url: '/route-b{/:id}?', component: ({ match }) => <div id="content">route-b{match.params.id}</div> },
64
64
  {
65
65
  url: '/route-c',
66
66
  component: () => <div id="content">route-c</div>,
@@ -1,16 +1,16 @@
1
- import { Shade } from '../shade.js'
2
- import { createComponent } from '../shade-component.js'
3
- import { LocationService } from '../services/location-service.js'
4
- import type { MatchResult, TokensToRegexpOptions } from 'path-to-regexp'
1
+ import { ObservableAlreadyDisposedError } from '@furystack/utils'
2
+ import type { MatchOptions, MatchResult } from 'path-to-regexp'
5
3
  import { match } from 'path-to-regexp'
6
- import type { RenderOptions } from '../models/render-options.js'
7
4
  import { Lock } from 'semaphore-async-await'
8
- import { ObservableAlreadyDisposedError } from '@furystack/utils'
5
+ import type { RenderOptions } from '../models/render-options.js'
6
+ import { LocationService } from '../services/location-service.js'
7
+ import { createComponent } from '../shade-component.js'
8
+ import { Shade } from '../shade.js'
9
9
 
10
10
  export interface Route<TMatchResult extends object> {
11
11
  url: string
12
12
  component: (options: { currentUrl: string; match: MatchResult<TMatchResult> }) => JSX.Element
13
- routingOptions?: TokensToRegexpOptions
13
+ routingOptions?: MatchOptions
14
14
  onVisit?: (options: RenderOptions<unknown>) => Promise<void>
15
15
  onLeave?: (options: RenderOptions<unknown>) => Promise<void>
16
16
  }
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
+ export * from './compile-route.js'
2
+ export * from './components/index.js'
3
+ export * from './initialize.js'
4
+ export * from './models/index.js'
1
5
  export * from './services/index.js'
2
- import './jsx.js'
3
6
  export * from './shade-component.js'
4
7
  export * from './shade.js'
5
- export * from './models/index.js'
6
- export * from './components/index.js'
7
- export * from './initialize.js'
8
8
  export * from './styled-element.js'
9
9
  export * from './styled-shade.js'
10
+ import './jsx.js'
@@ -1,6 +1,6 @@
1
1
  import type { Injector } from '@furystack/inject'
2
+ import type { ObservableValue, ValueObserverOptions } from '@furystack/utils'
2
3
  import type { ChildrenList } from './children-list.js'
3
- import type { Disposable, ObservableValue, ValueObserverOptions } from '@furystack/utils'
4
4
  import type { PartialElement } from './partial-element.js'
5
5
 
6
6
  export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement> = {
@@ -15,7 +15,7 @@ export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement
15
15
  * @param factory A factory method for creating the disposable resource
16
16
  * @returns The Disposable instance
17
17
  */
18
- useDisposable: <T extends Disposable>(key: string, factory: () => T) => T
18
+ useDisposable: <T extends Disposable | AsyncDisposable>(key: string, factory: () => T) => T
19
19
 
20
20
  /**
21
21
  *
@@ -1,13 +1,13 @@
1
- import { TextEncoder, TextDecoder } from 'util'
1
+ import { TextDecoder, TextEncoder } from 'util'
2
2
 
3
3
  global.TextEncoder = TextEncoder
4
4
  global.TextDecoder = TextDecoder as any
5
5
 
6
6
  import { Injector } from '@furystack/inject'
7
- import { using } from '@furystack/utils'
8
7
  import { deserializeQueryString, serializeToQueryString, serializeValue } from '@furystack/rest'
8
+ import { usingAsync } from '@furystack/utils'
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
9
10
  import { LocationService, useCustomSearchStateSerializer } from './location-service.js'
10
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
11
11
 
12
12
  describe('LocationService', () => {
13
13
  beforeEach(() => {
@@ -17,15 +17,15 @@ describe('LocationService', () => {
17
17
  document.body.innerHTML = ''
18
18
  })
19
19
 
20
- it('Shuld be constructed', () => {
21
- using(new Injector(), (i) => {
20
+ it('Shuld be constructed', async () => {
21
+ await usingAsync(new Injector(), async (i) => {
22
22
  const s = i.getInstance(LocationService)
23
23
  expect(s).toBeInstanceOf(LocationService)
24
24
  })
25
25
  })
26
26
 
27
- it('Shuld update state on events', () => {
28
- using(new Injector(), (i) => {
27
+ it('Shuld update state on events', async () => {
28
+ await usingAsync(new Injector(), async (i) => {
29
29
  const onLocaionChanged = vi.fn()
30
30
  const s = i.getInstance(LocationService)
31
31
  s.onLocationPathChanged.subscribe(onLocaionChanged)
@@ -44,8 +44,8 @@ describe('LocationService', () => {
44
44
  })
45
45
 
46
46
  describe('useSearchParam', () => {
47
- it('Should create observables lazily', () => {
48
- using(new Injector(), (i) => {
47
+ it('Should create observables lazily', async () => {
48
+ await usingAsync(new Injector(), async (i) => {
49
49
  const service = i.getInstance(LocationService)
50
50
  const observables = service.searchParamObservables
51
51
 
@@ -64,16 +64,16 @@ describe('LocationService', () => {
64
64
  })
65
65
  })
66
66
 
67
- it('Should return the default value, if not present in the query string', () => {
68
- using(new Injector(), (i) => {
67
+ it('Should return the default value, if not present in the query string', async () => {
68
+ await usingAsync(new Injector(), async (i) => {
69
69
  const service = i.getInstance(LocationService)
70
70
  const testSearchParam = service.useSearchParam('test', { value: 'foo' })
71
71
  expect(testSearchParam.getValue()).toEqual({ value: 'foo' })
72
72
  })
73
73
  })
74
74
 
75
- it('Should return the value from the query string', () => {
76
- using(new Injector(), (i) => {
75
+ it('Should return the value from the query string', async () => {
76
+ await usingAsync(new Injector(), async (i) => {
77
77
  const service = i.getInstance(LocationService)
78
78
  history.pushState(null, '', `/loc1?test=${serializeValue(1)}`)
79
79
  const testSearchParam = service.useSearchParam('test', 123)
@@ -81,8 +81,8 @@ describe('LocationService', () => {
81
81
  })
82
82
  })
83
83
 
84
- it('should update the observable value on push / replace states', () => {
85
- using(new Injector(), (i) => {
84
+ it('should update the observable value on push / replace states', async () => {
85
+ await usingAsync(new Injector(), async (i) => {
86
86
  const service = i.getInstance(LocationService)
87
87
  history.pushState(null, '', `/loc1?test=${serializeValue(1)}`)
88
88
  const testSearchParam = service.useSearchParam('test', 234)
@@ -92,8 +92,8 @@ describe('LocationService', () => {
92
92
  })
93
93
  })
94
94
 
95
- it('Should update the URL based on search value change', () => {
96
- using(new Injector(), (i) => {
95
+ it('Should update the URL based on search value change', async () => {
96
+ await usingAsync(new Injector(), async (i) => {
97
97
  const service = i.getInstance(LocationService)
98
98
  history.pushState(null, '', `/loc1?test=${serializeValue('2')}`)
99
99
  const testSearchParam = service.useSearchParam('test', '')
@@ -102,10 +102,10 @@ describe('LocationService', () => {
102
102
  })
103
103
  })
104
104
 
105
- it('Should throw an error when trying to use a custom serializer after LocationService has been instantiated', () => {
106
- using(new Injector(), (i) => {
105
+ it('Should throw an error when trying to use a custom serializer after LocationService has been instantiated', async () => {
106
+ await usingAsync(new Injector(), async (i) => {
107
107
  const customSerializer = vi.fn((value: any) => serializeToQueryString(value))
108
- const customDeserializer = vi.fn((value: any) => deserializeQueryString(value))
108
+ const customDeserializer = vi.fn((value: string) => deserializeQueryString(value))
109
109
  i.getInstance(LocationService)
110
110
  expect(() => useCustomSearchStateSerializer(i, customSerializer, customDeserializer)).toThrowError(
111
111
  'useCustomSearchStateSerializer must be called before the LocationService is instantiated',
@@ -113,10 +113,10 @@ describe('LocationService', () => {
113
113
  })
114
114
  })
115
115
 
116
- it('Should use custom serializer and deserializer', () => {
117
- using(new Injector(), (i) => {
116
+ it('Should use custom serializer and deserializer', async () => {
117
+ await usingAsync(new Injector(), async (i) => {
118
118
  const customSerializer = vi.fn((value: any) => serializeToQueryString(value))
119
- const customDeserializer = vi.fn((value: any) => deserializeQueryString(value))
119
+ const customDeserializer = vi.fn((value: string) => deserializeQueryString(value))
120
120
 
121
121
  useCustomSearchStateSerializer(i, customSerializer, customDeserializer)
122
122
 
@@ -1,10 +1,9 @@
1
- import type { Disposable } from '@furystack/utils'
2
- import { ObservableValue, Trace } from '@furystack/utils'
3
1
  import { Injectable, type Injector } from '@furystack/inject'
4
2
  import {
5
3
  deserializeQueryString as defaultDeserializeQueryString,
6
4
  serializeToQueryString as defaultSerializeToQueryString,
7
5
  } from '@furystack/rest'
6
+ import { ObservableValue } from '@furystack/utils'
8
7
  @Injectable({ lifetime: 'singleton' })
9
8
  export class LocationService implements Disposable {
10
9
  constructor(
@@ -15,32 +14,34 @@ export class LocationService implements Disposable {
15
14
  window.addEventListener('popstate', this.popStateListener)
16
15
  window.addEventListener('hashchange', this.hashChangeListener)
17
16
 
18
- this.pushStateTracer = Trace.method({
19
- object: history,
20
- method: history.pushState,
21
- isAsync: false,
22
- onFinished: () => this.updateState(),
23
- })
24
-
25
- this.replaceStateTracer = Trace.method({
26
- object: history,
27
- method: history.replaceState,
28
- isAsync: false,
29
- onFinished: () => this.updateState(),
30
- })
31
-
32
17
  this.onDeserializedLocationSearchChanged = new ObservableValue(this.deserializeQueryString(location.search))
18
+
19
+ this.originalPushState = window.history.pushState.bind(window.history)
20
+ window.history.pushState = ((...args: Parameters<typeof window.history.pushState>) => {
21
+ this.originalPushState(...args)
22
+ this.updateState()
23
+ }).bind(this)
24
+
25
+ this.originalReplaceState = window.history.replaceState.bind(window.history)
26
+ window.history.replaceState = ((...args: Parameters<typeof window.history.replaceState>) => {
27
+ this.originalReplaceState(...args)
28
+ this.updateState()
29
+ }).bind(this)
33
30
  }
34
31
 
35
- public dispose() {
32
+ private originalPushState: typeof window.history.pushState
33
+ private originalReplaceState: typeof window.history.replaceState
34
+
35
+ public [Symbol.dispose]() {
36
36
  window.removeEventListener('popstate', this.popStateListener)
37
37
  window.removeEventListener('hashchange', this.hashChangeListener)
38
- this.pushStateTracer.dispose()
39
- this.replaceStateTracer.dispose()
40
- this.onLocationPathChanged.dispose()
41
- this.onLocationSearchChanged.dispose()
42
- this.onDeserializedLocationSearchChanged.dispose()
43
- this.locationDeserializerObserver.dispose()
38
+ this.onLocationPathChanged[Symbol.dispose]()
39
+ this.onLocationSearchChanged[Symbol.dispose]()
40
+ this.onDeserializedLocationSearchChanged[Symbol.dispose]()
41
+ this.locationDeserializerObserver[Symbol.dispose]()
42
+
43
+ window.history.pushState = this.originalPushState
44
+ window.history.replaceState = this.originalReplaceState
44
45
  }
45
46
 
46
47
  /**
@@ -51,7 +52,7 @@ export class LocationService implements Disposable {
51
52
  /**
52
53
  * Observable value that will be updated when the location hash (e.g. #hash) changes
53
54
  */
54
- public onLocationHashChanged = new ObservableValue(location.hash)
55
+ public onLocationHashChanged = new ObservableValue(location.hash.replace('#', ''))
55
56
 
56
57
  /**
57
58
  * Observable value that will be updated when the location search (e.g. ?search=1) changes
@@ -66,7 +67,7 @@ export class LocationService implements Disposable {
66
67
 
67
68
  public updateState = (() => {
68
69
  this.onLocationPathChanged.setValue(location.pathname)
69
- this.onLocationHashChanged.setValue(location.hash)
70
+ this.onLocationHashChanged.setValue(location.hash.replace('#', ''))
70
71
  this.onLocationSearchChanged.setValue(location.search)
71
72
  }).bind(this)
72
73
 
@@ -102,23 +103,20 @@ export class LocationService implements Disposable {
102
103
 
103
104
  this.onDeserializedLocationSearchChanged.subscribe((search) => {
104
105
  const value = (search[key] as T) ?? defaultValue
105
- this.searchParamObservables.get(key)?.setValue(value as T)
106
+ this.searchParamObservables.get(key)?.setValue(value)
106
107
  })
107
108
  return newObservable
108
109
  }
109
110
  return existing as ObservableValue<T>
110
111
  }
111
112
 
112
- private pushStateTracer: Disposable
113
- private replaceStateTracer: Disposable
114
-
115
113
  private popStateListener = (_ev: PopStateEvent) => {
116
114
  this.updateState()
117
115
  }
118
116
 
119
- private hashChangeListener = (_ev: HashChangeEvent) => {
117
+ private hashChangeListener = ((_ev: HashChangeEvent) => {
120
118
  this.updateState()
121
- }
119
+ }).bind(this)
122
120
  }
123
121
 
124
122
  export const useCustomSearchStateSerializer = (
@@ -1,9 +1,10 @@
1
- import { ObservableValue, using } from '@furystack/utils'
1
+ import { ObservableValue, usingAsync } from '@furystack/utils'
2
+ import { describe, expect, it, vi } from 'vitest'
2
3
  import { ResourceManager } from './resource-manager.js'
3
- import { describe, it, expect, vi } from 'vitest'
4
+
4
5
  describe('ResourceManager', () => {
5
- it('Should return an observable from cache', () => {
6
- using(new ResourceManager(), (rm) => {
6
+ it('Should return an observable from cache', async () => {
7
+ await usingAsync(new ResourceManager(), async (rm) => {
7
8
  const o = new ObservableValue(1)
8
9
  const [value1] = rm.useObservable('test', o, () => {
9
10
  /** ignore */
@@ -18,10 +19,10 @@ describe('ResourceManager', () => {
18
19
  })
19
20
  })
20
21
 
21
- it('Should return a disposable from cache', () => {
22
- using(new ResourceManager(), (rm) => {
22
+ it('Should return a disposable from cache', async () => {
23
+ await usingAsync(new ResourceManager(), async (rm) => {
23
24
  const factory = vi.fn(() => ({
24
- dispose: () => {
25
+ [Symbol.dispose]: () => {
25
26
  /** ignore */
26
27
  },
27
28
  }))
@@ -32,4 +33,70 @@ describe('ResourceManager', () => {
32
33
  expect(factory).toHaveBeenCalledTimes(1)
33
34
  })
34
35
  })
36
+
37
+ it('Should dispose all disposables on dispose', async () => {
38
+ const disposable = {
39
+ [Symbol.dispose]: vi.fn(),
40
+ }
41
+ const factory = vi.fn(() => disposable)
42
+ await usingAsync(new ResourceManager(), async (rm) => {
43
+ rm.useDisposable('test', factory)
44
+ expect(factory).toHaveBeenCalledTimes(1)
45
+ })
46
+
47
+ expect(disposable[Symbol.dispose]).toHaveBeenCalledTimes(1)
48
+ })
49
+
50
+ it('Should dispose all async disposables on dispose', async () => {
51
+ const disposable = {
52
+ [Symbol.asyncDispose]: vi.fn(),
53
+ }
54
+ const factory = vi.fn(() => disposable)
55
+ await usingAsync(new ResourceManager(), async (rm) => {
56
+ rm.useDisposable('test', factory)
57
+ expect(factory).toHaveBeenCalledTimes(1)
58
+ })
59
+
60
+ expect(disposable[Symbol.asyncDispose]).toHaveBeenCalledTimes(1)
61
+ })
62
+
63
+ it('Should throw an aggregated error when failed to dispose something', async () => {
64
+ const disposable = {
65
+ [Symbol.dispose]: vi.fn(() => {
66
+ throw new Error('Failed to dispose')
67
+ }),
68
+ }
69
+ const factory = vi.fn(() => disposable)
70
+ await expect(
71
+ async () =>
72
+ await usingAsync(new ResourceManager(), async (rm) => {
73
+ rm.useDisposable('test', factory)
74
+ expect(factory).toHaveBeenCalledTimes(1)
75
+ }),
76
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
77
+ `[Error: There was an error during disposing 1 stores: Error: Failed to dispose]`,
78
+ )
79
+
80
+ expect(disposable[Symbol.dispose]).toHaveBeenCalledTimes(1)
81
+ })
82
+
83
+ it('Should throw an aggregated error when failed to async dispose something', async () => {
84
+ const disposable = {
85
+ [Symbol.asyncDispose]: vi.fn(async () => {
86
+ throw new Error('Failed to dispose')
87
+ }),
88
+ }
89
+ const factory = vi.fn(() => disposable)
90
+ await expect(
91
+ async () =>
92
+ await usingAsync(new ResourceManager(), async (rm) => {
93
+ rm.useDisposable('test', factory)
94
+ expect(factory).toHaveBeenCalledTimes(1)
95
+ }),
96
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
97
+ `[Error: There was an error during disposing 1 stores: Error: Failed to dispose]`,
98
+ )
99
+
100
+ expect(disposable[Symbol.asyncDispose]).toHaveBeenCalledTimes(1)
101
+ })
35
102
  })
@@ -1,14 +1,14 @@
1
- import { ObservableValue } from '@furystack/utils'
2
- import type { Disposable, ValueChangeCallback, ValueObserverOptions } from '@furystack/utils'
3
- import type { ValueObserver } from '@furystack/utils'
1
+ import { AggregatedError } from '@furystack/core'
2
+ import type { ValueChangeCallback, ValueObserver, ValueObserverOptions } from '@furystack/utils'
3
+ import { ObservableValue, isAsyncDisposable, isDisposable } from '@furystack/utils'
4
4
 
5
5
  /**
6
6
  * Class for managing observables and disposables for components, based on key-value maps
7
7
  */
8
- export class ResourceManager {
9
- private readonly disposables = new Map<string, Disposable>()
8
+ export class ResourceManager implements AsyncDisposable {
9
+ private readonly disposables = new Map<string, Disposable | AsyncDisposable>()
10
10
 
11
- public useDisposable<T extends Disposable>(key: string, factory: () => T): T {
11
+ public useDisposable<T extends Disposable | AsyncDisposable>(key: string, factory: () => T): T {
12
12
  const existing = this.disposables.get(key)
13
13
  if (!existing) {
14
14
  const created = factory()
@@ -51,13 +51,32 @@ export class ResourceManager {
51
51
  return [observable.getValue(), observable.setValue.bind(observable)]
52
52
  }
53
53
 
54
- public dispose() {
55
- this.disposables.forEach((r) => r.dispose())
54
+ public async [Symbol.asyncDispose]() {
55
+ const disposeResult = await Promise.allSettled(
56
+ [...this.disposables].map(async ([_key, resource]) => {
57
+ if (isDisposable(resource)) {
58
+ resource[Symbol.dispose]()
59
+ }
60
+ if (isAsyncDisposable(resource)) {
61
+ await resource[Symbol.asyncDispose]()
62
+ }
63
+ }),
64
+ )
65
+
66
+ const fails = disposeResult.filter((r) => r.status === 'rejected')
67
+ if (fails && fails.length) {
68
+ const error = new AggregatedError(
69
+ `There was an error during disposing ${fails.length} stores: ${fails.map((f) => f.reason)}`,
70
+ fails,
71
+ )
72
+ throw error
73
+ }
74
+
56
75
  this.disposables.clear()
57
- this.observers.forEach((r) => r.dispose())
76
+ this.observers.forEach((r) => r[Symbol.dispose]())
58
77
  this.observers.clear()
59
78
 
60
- this.stateObservers.forEach((r) => r.dispose())
79
+ this.stateObservers.forEach((r) => r[Symbol.dispose]())
61
80
  this.stateObservers.clear()
62
81
  }
63
82
  }
@@ -1,5 +1,4 @@
1
1
  import { Injectable } from '@furystack/inject'
2
- import type { Disposable } from '@furystack/utils'
3
2
  import { ObservableValue } from '@furystack/utils'
4
3
 
5
4
  export const ScreenSizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const
@@ -26,7 +25,7 @@ export class ScreenService implements Disposable {
26
25
  xs: { minSize: 0 },
27
26
  }
28
27
 
29
- public dispose() {
28
+ public [Symbol.dispose]() {
30
29
  window.removeEventListener('resize', this.onResizeListener)
31
30
  }
32
31