@ispb/react-lazy 0.0.1

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.
package/.yarnrc.yml ADDED
@@ -0,0 +1,8 @@
1
+ approvedGitRepositories:
2
+ - "**"
3
+
4
+ enableScripts: true
5
+
6
+ nodeLinker: node-modules
7
+
8
+ npmMinimalAgeGate: 0
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@ispb/react-lazy",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "watch": "tsc --watch"
10
+ },
11
+ "exports": {
12
+ "default": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ },
15
+ "dependencies": {
16
+ "p-defer": "^4.0.1",
17
+ "rxjs": "^7.8.2",
18
+ "state-hooks": "file:../state-hooks",
19
+ "value-or-factory": "^0.0.1"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "^18.3.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "^19.2.7",
26
+ "react": "^18.3.1",
27
+ "typescript": "^5.9.3"
28
+ },
29
+ "packageManager": "yarn@4.18.0"
30
+ }
package/src/async.tsx ADDED
@@ -0,0 +1,124 @@
1
+
2
+ import { ReactNode, useCallback, useEffect, useState } from "react"
3
+ import { ValueOrFactory, callOrGet } from "value-or-factory"
4
+ import { Lazy } from "./components.js"
5
+ import { LazyHOCOptions, lazified } from "./hoc.js"
6
+ import { useIsFirstMount } from "./internal.js"
7
+ import { LazyState } from "./state.js"
8
+ import { LazyEvent, LazyOverrides } from "./types.js"
9
+
10
+ /**
11
+ * Options for the async hook.
12
+ * @typeParam D The data type.
13
+ */
14
+ export interface AsyncLazyOptions<D> {
15
+
16
+ /**
17
+ * A promise or a function that returns a promise. This MUST be memoized - the function will re-run every time a new value is received.
18
+ */
19
+ readonly promise: ValueOrFactory<PromiseLike<D>>
20
+
21
+ /**
22
+ * Don't execute immediately.
23
+ */
24
+ readonly defer?: AsyncLazyDefer<D> | undefined
25
+
26
+ }
27
+
28
+ export interface AsyncLazyDefer<D> {
29
+
30
+ /**
31
+ * The initial value to provide.
32
+ */
33
+ readonly initial: D
34
+
35
+ }
36
+
37
+ export type AsyncLazyEvent<D> = LazyEvent<D> & { readonly retry: Retry }
38
+
39
+ export function useAsyncLazy<D>(input: AsyncLazyOptions<D> | (() => PromiseLike<D>)): AsyncLazyEvent<D> {
40
+ const options = typeof input === "function" ? { promise: input } : input
41
+ const [event, setEvent] = useState<LazyEvent<D>>(options.defer === undefined ? { status: "loading" } : { status: "fulfilled", value: options.defer.initial })
42
+ const run = useCallback(() => {
43
+ callOrGet(options.promise).then(value => {
44
+ setEvent({
45
+ status: "fulfilled",
46
+ value,
47
+ })
48
+ }, reason => {
49
+ setEvent({
50
+ status: "rejected",
51
+ reason,
52
+ })
53
+ })
54
+ }, [
55
+ options.promise
56
+ ])
57
+ const retry = useCallback((suppressReload = false) => {
58
+ if (!suppressReload) {
59
+ setEvent({
60
+ status: "loading"
61
+ })
62
+ }
63
+ run()
64
+ }, [
65
+ run
66
+ ])
67
+ const isFirstMount = useIsFirstMount()
68
+ useEffect(() => {
69
+ if (isFirstMount) {
70
+ if (options.defer !== undefined) {
71
+ return
72
+ }
73
+ }
74
+ else {
75
+ setEvent({
76
+ status: "loading"
77
+ })
78
+ }
79
+ run()
80
+ }, [
81
+ run,
82
+ ])
83
+ return {
84
+ ...event,
85
+ retry
86
+ }
87
+ }
88
+
89
+ export type AsyncLazyState<D> = LazyState<D> & { readonly retry: Retry }
90
+
91
+ export interface AsyncifiedOptions<D, P> extends AsyncLazyOptions<D>, LazyHOCOptions<AsyncLazyState<D>, P> {
92
+ }
93
+
94
+ export function asyncified<I extends {}, D extends {}, P extends {}>(factory: (props: I) => AsyncifiedOptions<D, P>) {
95
+ return lazified((props: I) => {
96
+ const options = factory(props)
97
+ const event = useAsyncLazy(options)
98
+ return {
99
+ event,
100
+ passthrough: state => callOrGet(options.passthrough, { ...state, retry: event.retry }),
101
+ overrides: options.overrides,
102
+ }
103
+ })
104
+ }
105
+
106
+ export type Retry = (suppressReload?: boolean | undefined) => void
107
+
108
+ export interface AsyncifiedProps<D> extends AsyncLazyOptions<D> {
109
+
110
+ readonly children: ValueOrFactory<ReactNode, [D, AsyncLazyState<D>]>
111
+
112
+ /**
113
+ * The lazy overrides.
114
+ */
115
+ readonly overrides?: LazyOverrides | undefined
116
+
117
+ }
118
+
119
+ export const Asyncified = <D,>(props: AsyncifiedProps<D>) => {
120
+ const event = useAsyncLazy(props)
121
+ return <Lazy event={event}
122
+ overrides={props.overrides}
123
+ children={(value, state) => callOrGet(props.children, value, { ...state, retry: event.retry })} />
124
+ }
@@ -0,0 +1,46 @@
1
+
2
+ import { PropsWithChildren, ReactNode, useContext } from "react"
3
+ import { LazyContext } from "./hooks.js"
4
+ import { LazyState, useLazyState } from "./state.js"
5
+ import { LazyEvent, LazyOverrides } from "./types.js"
6
+
7
+ export interface LazyProps<D> {
8
+
9
+ readonly event: LazyEvent<D>
10
+ readonly overrides?: LazyOverrides | undefined
11
+ readonly children: ReactNode | ((value: D, state: LazyState<D>) => ReactNode)
12
+
13
+ }
14
+
15
+ export function Lazy<D>(props: LazyProps<D>): ReactNode {
16
+ const context = useContext(LazyContext)
17
+ const options = { ...context, ...props.overrides }
18
+ const state = useLazyState(props.event)
19
+ if (state.isLoading && !state.isSettled) {
20
+ if (options.onLoading === undefined) {
21
+ return null
22
+ }
23
+ return options.onLoading({
24
+ message: options.loadingMessage ?? options.defaultMessage
25
+ })
26
+ }
27
+ const onReloading = options.onReloading ?? ((props: PropsWithChildren) => props.children)
28
+ return onReloading({
29
+ reloading: state.isLoading,
30
+ message: options.reloadingMessage ?? options.defaultMessage,
31
+ children: (() => {
32
+ if (state.isRejected) {
33
+ if (options.onError === undefined) {
34
+ throw state.reason
35
+ }
36
+ return options.onError({
37
+ reason: state.reason,
38
+ message: options.errorMessage ?? options.defaultMessage,
39
+ })
40
+ }
41
+ else {
42
+ return typeof props.children === "function" ? props.children(state.value, state) : props.children
43
+ }
44
+ })()
45
+ })
46
+ }
package/src/hoc.tsx ADDED
@@ -0,0 +1,37 @@
1
+
2
+ import { ComponentType, createElement } from "react"
3
+ import { callOrGet, ValueOrFactory } from "value-or-factory"
4
+ import { Lazy } from "./components.js"
5
+ import { LazyState } from "./state.js"
6
+ import { LazyEvent, LazyOverrides } from "./types.js"
7
+
8
+ export interface LazyBuilder<D, P> extends LazyHOCOptions<LazyState<D>, P> {
9
+
10
+ readonly event: LazyEvent<D>
11
+
12
+ }
13
+
14
+ export function lazified<I extends {}, D extends {}, P extends {}>(build: (props: I) => LazyBuilder<D, P>) {
15
+ return (component: ComponentType<D & P>) => {
16
+ return (props: I) => {
17
+ const built = build(props)
18
+ return <Lazy event={built.event}
19
+ overrides={built.overrides}
20
+ children={(value, state) => createElement(component, { ...callOrGet(built.passthrough, state), ...value })} />
21
+ }
22
+ }
23
+ }
24
+
25
+ export interface LazyHOCOptions<S, P> {
26
+
27
+ /**
28
+ * Properties to pass through to the component.
29
+ */
30
+ readonly passthrough: ValueOrFactory<P, [S]>
31
+
32
+ /**
33
+ * Overrides for lazy settings.
34
+ */
35
+ readonly overrides?: LazyOverrides | undefined
36
+
37
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,155 @@
1
+ import pDefer from "p-defer"
2
+ import { createContext, useEffect, useMemo, useState } from "react"
3
+ import { Observable, Subject } from "rxjs"
4
+ import { useIsFirstMount } from "state-hooks"
5
+ import { ExtractLazy, LazyEvent, LazyFulfilled, LazyLoading, LazyOverrides, LazyRejected } from "./types.js"
6
+
7
+ export const LazyContext = createContext<LazyOverrides>({})
8
+
9
+ export function isLoading<T>(event: LazyEvent<T>): event is LazyLoading {
10
+ if (event.status === "loading") {
11
+ return true
12
+ }
13
+ else {
14
+ return false
15
+ }
16
+ }
17
+ export function isFulfilled<T>(event: LazyEvent<T>): event is LazyFulfilled<T> {
18
+ if (event.status === "fulfilled") {
19
+ return true
20
+ }
21
+ else {
22
+ return false
23
+ }
24
+ }
25
+ export function isRejected<T>(event: LazyEvent<T>): event is LazyRejected {
26
+ if (event.status === "rejected") {
27
+ return true
28
+ }
29
+ else {
30
+ return false
31
+ }
32
+ }
33
+
34
+ export function useLazyFlatten<O>(event: LazyEvent<LazyEvent<O>>,) {
35
+ return useLazyChain(event, event => event)
36
+ }
37
+ export function useLazyChain<I, O>(event: LazyEvent<I>, map: (value: I) => LazyEvent<O>) {
38
+ return useMemo(() => {
39
+ if (event.status === "fulfilled") {
40
+ return map(event.value)
41
+ }
42
+ else {
43
+ return event
44
+ }
45
+ }, [
46
+ event
47
+ ])
48
+ }
49
+ export function useLazyMap<I, O>(event: LazyEvent<I>, map: (value: I) => O) {
50
+ return useMemo(() => {
51
+ if (event.status === "fulfilled") {
52
+ return {
53
+ status: "fulfilled" as const,
54
+ value: map(event.value)
55
+ }
56
+ }
57
+ else {
58
+ return event
59
+ }
60
+ }, [
61
+ event
62
+ ])
63
+ }
64
+
65
+ export function useObservableAsPromise<T>(observable: Observable<T>) {
66
+ const [promise, setPromise] = useState(() => pDefer<T>())
67
+ const isFirstMount = useIsFirstMount()
68
+ useEffect(() => {
69
+ if (!isFirstMount) {
70
+ setPromise(pDefer<T>())
71
+ }
72
+ }, [
73
+ observable
74
+ ])
75
+ useEffect(() => {
76
+ const sub = observable.subscribe({
77
+ next: value => {
78
+ promise.resolve(value)
79
+ },
80
+ error: reason => {
81
+ promise.reject(reason)
82
+ }
83
+ })
84
+ return () => {
85
+ sub.unsubscribe()
86
+ }
87
+ }, [
88
+ promise
89
+ ])
90
+ return promise.promise
91
+ }
92
+
93
+ export function useLazyAsPromise<T>(event: LazyEvent<T>) {
94
+ const [promise, setPromise] = useState(() => pDefer<T>())
95
+ useEffect(() => {
96
+ if (event.status === "fulfilled") {
97
+ promise.resolve(event.value)
98
+ }
99
+ else if (event.status === "rejected") {
100
+ promise.reject(event.reason)
101
+ }
102
+ else {
103
+ setPromise(pDefer<T>())
104
+ }
105
+ }, [
106
+ event
107
+ ])
108
+ return promise.promise
109
+ }
110
+
111
+ export function useLazyAsObservable<T>(event: LazyEvent<T>) {
112
+ const subject = useMemo(() => new Subject<T>(), [])
113
+ useEffect(() => {
114
+ if (event.status === "fulfilled") {
115
+ subject.next(event.value)
116
+ }
117
+ else if (event.status === "rejected") {
118
+ subject.error(event.reason)
119
+ }
120
+ }, [
121
+ event
122
+ ])
123
+ return subject
124
+ }
125
+
126
+ export function useLazyCombine<T extends Record<string, LazyEvent<unknown>>>(lazies: T): LazyEvent<{ [K in keyof T]: ExtractLazy<T[K]> }> {
127
+ const entries = Object.entries(lazies)
128
+ if (entries.map(entry => entry[1]).some(isLoading)) {
129
+ return {
130
+ status: "loading" as const,
131
+ }
132
+ }
133
+ else {
134
+ const failed = entries.map(entry => entry[1]).filter(isRejected).at(0)
135
+ if (failed !== undefined) {
136
+ return {
137
+ status: "rejected" as const,
138
+ reason: failed
139
+ }
140
+ }
141
+ else {
142
+ return {
143
+ status: "fulfilled",
144
+ value: Object.fromEntries(
145
+ entries.map(([key, value]) => {
146
+ return [
147
+ key,
148
+ value.status === "fulfilled" ? value.value : undefined,
149
+ ] as const
150
+ })
151
+ ) as { [K in keyof T]: ExtractLazy<T[K]> }
152
+ }
153
+ }
154
+ }
155
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,9 @@
1
+
2
+ export * from "./async.js"
3
+ export * from "./components.js"
4
+ export * from "./hoc.js"
5
+ export * from "./hooks.js"
6
+ export * from "./observable.js"
7
+ export * from "./state.js"
8
+ export * from "./types.js"
9
+
@@ -0,0 +1,21 @@
1
+ import { useRef } from "react"
2
+
3
+ export function useIsFirstMount() {
4
+ const isFirst = useRef(true)
5
+ if (isFirst.current) {
6
+ isFirst.current = false
7
+ return true
8
+ }
9
+ return isFirst.current
10
+ }
11
+
12
+ export function usePrevious<T>(value: { readonly current: T } | undefined) {
13
+ const previous = useRef(value?.current)
14
+ const first = useIsFirstMount()
15
+ if (!first) {
16
+ if (value !== undefined) {
17
+ previous.current = value.current
18
+ }
19
+ }
20
+ return previous.current
21
+ }
@@ -0,0 +1,83 @@
1
+
2
+ import { ReactNode, useEffect, useState } from "react"
3
+ import { Observable, ObservedValueOf } from "rxjs"
4
+ import { Lazy } from "./components.js"
5
+ import { LazyHOCOptions, lazified } from "./hoc.js"
6
+ import { useLazyFlatten } from "./hooks.js"
7
+ import { useIsFirstMount } from "./internal.js"
8
+ import { LazyState } from "./state.js"
9
+ import { LazyEvent, LazyOverrides } from "./types.js"
10
+
11
+ export type ObservableLazyInput = Observable<any>// | Record<string, ObservableInput<any>>
12
+ export type ObservableLazyOutput<X extends ObservableLazyInput> = X extends Observable<any> ? ObservedValueOf<X> : { [K in keyof X]: ObservedValueOf<X[K]> }
13
+
14
+ export interface ObservableLazyOptions<D> {
15
+
16
+ readonly of: Observable<D>
17
+
18
+ }
19
+
20
+ export function useObservableLazyWrapped<D>(options: ObservableLazyOptions<LazyEvent<D>>) {
21
+ return useLazyFlatten(useObservableLazy(options))
22
+ }
23
+
24
+ export function useObservableLazy<D>(options: ObservableLazyOptions<D>) {
25
+ const [event, setEvent] = useState<LazyEvent<D>>({ status: "loading" as const })
26
+ const isFirstMount = useIsFirstMount()
27
+ useEffect(() => {
28
+ if (!isFirstMount) {
29
+ setEvent({
30
+ status: "loading"
31
+ })
32
+ }
33
+ const sub = options.of.subscribe({
34
+ next: value => {
35
+ setEvent({
36
+ status: "fulfilled",
37
+ value
38
+ })
39
+ },
40
+ error: reason => {
41
+ setEvent({
42
+ status: "rejected",
43
+ reason
44
+ })
45
+ }
46
+ })
47
+ return () => {
48
+ sub.unsubscribe()
49
+ }
50
+ }, [
51
+ options.of
52
+ ])
53
+ return event
54
+ }
55
+
56
+ export interface ObservingOptions<D, P> extends ObservableLazyOptions<D>, LazyHOCOptions<LazyState<D>, P> {
57
+ }
58
+
59
+ export function observing<I extends {}, D extends {}, P extends {}>(factory: (props: I) => ObservingOptions<D, P>) {
60
+ return lazified((props: I) => {
61
+ const options = factory(props)
62
+ const event = useObservableLazy(options)
63
+ return {
64
+ event,
65
+ passthrough: options.passthrough,
66
+ overrides: options.overrides,
67
+ }
68
+ })
69
+ }
70
+
71
+ export interface ObservingProps<D> extends ObservableLazyOptions<D> {
72
+
73
+ readonly children: (value: D, state: LazyState<D>) => ReactNode
74
+ readonly overrides?: LazyOverrides | undefined
75
+
76
+ }
77
+
78
+ export const Observing = <D,>(props: ObservingProps<D>) => {
79
+ const event = useObservableLazy(props)
80
+ return <Lazy event={event}
81
+ overrides={props.overrides}
82
+ children={props.children} />
83
+ }
package/src/state.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { usePrevious } from "./internal.js"
2
+ import { LazyEvent, LazySettled } from "./types.js"
3
+
4
+ export type LazyState<D> = {
5
+ readonly isLoading: true
6
+ readonly isReloading: false
7
+ readonly isSettled: false
8
+ readonly isFulfilled: false
9
+ readonly isRejected: false
10
+ } | {
11
+ readonly isLoading: true
12
+ readonly isReloading: true
13
+ readonly isSettled: true
14
+ readonly isFulfilled: true
15
+ readonly isRejected: false
16
+ readonly value: D
17
+ } | {
18
+ readonly isLoading: true
19
+ readonly isReloading: true
20
+ readonly isSettled: true
21
+ readonly isFulfilled: false
22
+ readonly isRejected: true
23
+ readonly reason: unknown
24
+ readonly retry?: (() => void) | undefined
25
+ } | {
26
+ readonly isLoading: false
27
+ readonly isReloading: false
28
+ readonly isSettled: true
29
+ readonly isFulfilled: true
30
+ readonly isRejected: false
31
+ readonly value: D
32
+ } | {
33
+ readonly isLoading: false
34
+ readonly isReloading: false
35
+ readonly isSettled: true
36
+ readonly isFulfilled: false
37
+ readonly isRejected: true
38
+ readonly reason: unknown
39
+ readonly retry?: (() => void) | undefined
40
+ }
41
+
42
+ export function useLazyState<D>(current: LazyEvent<D>): LazyState<D> {
43
+ const settled = usePrevious<LazySettled<D>>(current.status === "fulfilled" || current.status === "rejected" ? { current } : undefined)
44
+ if (current.status === "loading") {
45
+ if (settled === undefined) {
46
+ return {
47
+ isLoading: true,
48
+ isReloading: false,
49
+ isSettled: false,
50
+ isFulfilled: false,
51
+ isRejected: false,
52
+ }
53
+ }
54
+ else {
55
+ if (settled.status === "fulfilled") {
56
+ return {
57
+ isLoading: true,
58
+ isReloading: true,
59
+ isSettled: true,
60
+ isFulfilled: true,
61
+ isRejected: false,
62
+ value: settled.value,
63
+ }
64
+ }
65
+ else {
66
+ return {
67
+ isLoading: true,
68
+ isReloading: true,
69
+ isSettled: true,
70
+ isFulfilled: false,
71
+ isRejected: true,
72
+ reason: settled.reason,
73
+ retry: settled.retry,
74
+ }
75
+ }
76
+ }
77
+ }
78
+ if (current.status === "fulfilled") {
79
+ return {
80
+ isLoading: false,
81
+ isReloading: false,
82
+ isSettled: true,
83
+ isFulfilled: true,
84
+ isRejected: false,
85
+ value: current.value,
86
+ }
87
+ }
88
+ else {
89
+ return {
90
+ isLoading: false,
91
+ isReloading: false,
92
+ isSettled: true,
93
+ isFulfilled: false,
94
+ isRejected: true,
95
+ reason: current.reason,
96
+ retry: current.retry,
97
+ }
98
+ }
99
+ }
package/src/types.tsx ADDED
@@ -0,0 +1,75 @@
1
+ import { ReactNode } from "react"
2
+
3
+ export type LazyLoading = { readonly status: "loading" }
4
+ export type LazyFulfilled<D> = { readonly status: "fulfilled", readonly value: D }
5
+ export type LazyRejected = { readonly status: "rejected", readonly reason: unknown, readonly retry?: (() => void) | undefined }
6
+ export type LazySettled<D> = LazyFulfilled<D> | LazyRejected
7
+ export type LazyEvent<D> = LazySettled<D> | LazyLoading
8
+
9
+ export type OnLoading = (props: LoadingProps) => ReactNode
10
+ export type OnReloading = (props: ReloadingProps) => ReactNode
11
+ export type OnError = (props: ErrorProps) => ReactNode
12
+
13
+ type LazyMeta = string
14
+
15
+ export interface LoadingProps {
16
+
17
+ /**
18
+ * The message to display.
19
+ */
20
+ readonly message?: LazyMeta | undefined
21
+
22
+ }
23
+
24
+ export interface ReloadingProps {
25
+
26
+ /**
27
+ * Whether or not the data is currently reloading.
28
+ */
29
+ readonly reloading: boolean
30
+
31
+ /**
32
+ * The message to display.
33
+ */
34
+ readonly message?: LazyMeta | undefined
35
+
36
+ /**
37
+ * The children.
38
+ */
39
+ readonly children: ReactNode
40
+
41
+ }
42
+
43
+ export interface ErrorProps {
44
+
45
+ /**
46
+ * The error.
47
+ */
48
+ readonly reason: unknown
49
+
50
+ /**
51
+ * The message to display.
52
+ */
53
+ readonly message?: LazyMeta | undefined
54
+
55
+ }
56
+
57
+ export interface LazyOptions {
58
+
59
+ readonly onLoading?: OnLoading | undefined
60
+ readonly onReloading?: OnReloading | undefined
61
+ readonly onError?: OnError | undefined
62
+
63
+ readonly defaultMessage?: LazyMeta | undefined
64
+ readonly loadingMessage?: LazyMeta | undefined
65
+ readonly reloadingMessage?: LazyMeta | undefined
66
+ readonly errorMessage?: LazyMeta | undefined
67
+
68
+ }
69
+
70
+ /**
71
+ * Overrides for lazy options.
72
+ */
73
+ export type LazyOverrides = Partial<LazyOptions>
74
+
75
+ export type ExtractLazy<T> = T extends LazyFulfilled<infer D> ? D : never
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "react-jsx",
4
+ "strict": true,
5
+ "alwaysStrict": true,
6
+ "allowUnreachableCode": false,
7
+ "allowUnusedLabels": false,
8
+ "noImplicitAny": true,
9
+ "noImplicitThis": true,
10
+ "noImplicitOverride": true,
11
+ "noUnusedLocals": true,
12
+ "noUnusedParameters": true,
13
+ "noPropertyAccessFromIndexSignature": true,
14
+ "noUncheckedIndexedAccess": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "useUnknownInCatchVariables": true,
17
+ "exactOptionalPropertyTypes": true,
18
+ "moduleResolution": "nodenext",
19
+ "module": "nodenext",
20
+ "target": "esnext",
21
+ "declaration": true,
22
+ "sourceMap": true,
23
+ "skipLibCheck": true,
24
+ "rootDir": "src",
25
+ "outDir": "dist"
26
+ }
27
+ }