@livestore/livestore 0.0.21 → 0.0.23
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/README.md +14 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/react/fixture.d.ts.map +1 -1
- package/dist/__tests__/react/fixture.js +0 -2
- package/dist/__tests__/react/fixture.js.map +1 -1
- package/dist/__tests__/react/useQuery.test.js +1 -1
- package/dist/__tests__/react/useQuery.test.js.map +1 -1
- package/dist/__tests__/react/utils/stack-info.test.d.ts +2 -0
- package/dist/__tests__/react/utils/stack-info.test.d.ts.map +1 -0
- package/dist/__tests__/react/utils/stack-info.test.js +43 -0
- package/dist/__tests__/react/utils/stack-info.test.js.map +1 -0
- package/dist/__tests__/reactive.test.js +13 -1
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/__tests__/reactiveQueries/sql.test.js +3 -3
- package/dist/__tests__/reactiveQueries/sql.test.js.map +1 -1
- package/dist/inMemoryDatabase.d.ts +2 -1
- package/dist/inMemoryDatabase.d.ts.map +1 -1
- package/dist/inMemoryDatabase.js +3 -2
- package/dist/inMemoryDatabase.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/useComponentState.d.ts.map +1 -1
- package/dist/react/useComponentState.js +19 -27
- package/dist/react/useComponentState.js.map +1 -1
- package/dist/react/useQuery.d.ts.map +1 -1
- package/dist/react/useQuery.js +46 -26
- package/dist/react/useQuery.js.map +1 -1
- package/dist/react/useTemporaryQuery.d.ts.map +1 -1
- package/dist/react/useTemporaryQuery.js +2 -0
- package/dist/react/useTemporaryQuery.js.map +1 -1
- package/dist/react/utils/stack-info.d.ts +11 -0
- package/dist/react/utils/stack-info.d.ts.map +1 -0
- package/dist/react/utils/stack-info.js +49 -0
- package/dist/react/utils/stack-info.js.map +1 -0
- package/dist/reactive.d.ts +33 -43
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +66 -255
- package/dist/reactive.js.map +1 -1
- package/dist/reactiveQueries/base-class.d.ts +15 -13
- package/dist/reactiveQueries/base-class.d.ts.map +1 -1
- package/dist/reactiveQueries/base-class.js +5 -8
- package/dist/reactiveQueries/base-class.js.map +1 -1
- package/dist/reactiveQueries/graphql.d.ts +4 -3
- package/dist/reactiveQueries/graphql.d.ts.map +1 -1
- package/dist/reactiveQueries/graphql.js +29 -34
- package/dist/reactiveQueries/graphql.js.map +1 -1
- package/dist/reactiveQueries/js.d.ts +2 -1
- package/dist/reactiveQueries/js.d.ts.map +1 -1
- package/dist/reactiveQueries/js.js +8 -9
- package/dist/reactiveQueries/js.js.map +1 -1
- package/dist/reactiveQueries/sql.d.ts +11 -5
- package/dist/reactiveQueries/sql.d.ts.map +1 -1
- package/dist/reactiveQueries/sql.js +31 -34
- package/dist/reactiveQueries/sql.js.map +1 -1
- package/dist/store.d.ts +26 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +41 -255
- package/dist/store.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/react/fixture.tsx +0 -3
- package/src/__tests__/react/useQuery.test.tsx +1 -1
- package/src/__tests__/react/utils/{extractStackInfoFromStackTrace.test.ts → stack-info.test.ts} +25 -20
- package/src/__tests__/reactive.test.ts +20 -1
- package/src/__tests__/reactiveQueries/sql.test.ts +3 -3
- package/src/inMemoryDatabase.ts +9 -6
- package/src/react/index.ts +1 -0
- package/src/react/useComponentState.ts +25 -30
- package/src/react/useQuery.ts +66 -34
- package/src/react/useTemporaryQuery.ts +2 -0
- package/src/react/utils/{extractStackInfoFromStackTrace.ts → stack-info.ts} +21 -5
- package/src/reactive.ts +148 -339
- package/src/reactiveQueries/base-class.ts +23 -22
- package/src/reactiveQueries/graphql.ts +34 -36
- package/src/reactiveQueries/js.ts +14 -10
- package/src/reactiveQueries/sql.ts +55 -48
- package/src/store.ts +70 -305
package/src/react/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { LiveStoreProvider } from './LiveStoreProvider.js'
|
|
|
11
11
|
export { useComponentState } from './useComponentState.js'
|
|
12
12
|
export { useQuery } from './useQuery.js'
|
|
13
13
|
export { useTemporaryQuery } from './useTemporaryQuery.js'
|
|
14
|
+
export { useStackInfo } from './utils/stack-info.js'
|
|
14
15
|
|
|
15
16
|
// Needed to make TS happy
|
|
16
17
|
export type { TypedDocumentNode } from '@graphql-typed-document-node/core'
|
|
@@ -17,7 +17,7 @@ import { SCHEMA_META_TABLE } from '../schema.js'
|
|
|
17
17
|
import type { BaseGraphQLContext, LiveStoreQuery, Store } from '../store.js'
|
|
18
18
|
import { sql } from '../util.js'
|
|
19
19
|
import { useStore } from './LiveStoreContext.js'
|
|
20
|
-
import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/
|
|
20
|
+
import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
|
|
21
21
|
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
22
22
|
|
|
23
23
|
export interface QueryDefinitions {
|
|
@@ -100,6 +100,14 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
100
100
|
|
|
101
101
|
const componentKeyLabel = React.useMemo(() => labelForKey(componentKey), [componentKey])
|
|
102
102
|
|
|
103
|
+
const stackInfo = React.useMemo(() => {
|
|
104
|
+
Error.stackTraceLimit = 10
|
|
105
|
+
// eslint-disable-next-line unicorn/error-message
|
|
106
|
+
const stack = new Error().stack!
|
|
107
|
+
Error.stackTraceLimit = originalStackLimit
|
|
108
|
+
return extractStackInfoFromStackTrace(stack)
|
|
109
|
+
}, [])
|
|
110
|
+
|
|
103
111
|
// The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
|
|
104
112
|
const { span, otelContext } = React.useMemo(() => {
|
|
105
113
|
const existingSpan = spanAlreadyStartedCache.get(componentKeyLabel)
|
|
@@ -107,7 +115,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
107
115
|
|
|
108
116
|
const span = store.otel.tracer.startSpan(
|
|
109
117
|
`LiveStore:useComponentState:${componentKeyLabel}`,
|
|
110
|
-
{},
|
|
118
|
+
{ attributes: { stackInfo: JSON.stringify(stackInfo) } },
|
|
111
119
|
store.otel.queriesSpanContext,
|
|
112
120
|
)
|
|
113
121
|
|
|
@@ -116,7 +124,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
116
124
|
spanAlreadyStartedCache.set(componentKeyLabel, { span, otelContext })
|
|
117
125
|
|
|
118
126
|
return { span, otelContext }
|
|
119
|
-
}, [componentKeyLabel, store.otel.queriesSpanContext, store.otel.tracer])
|
|
127
|
+
}, [componentKeyLabel, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
|
|
120
128
|
|
|
121
129
|
React.useEffect(
|
|
122
130
|
() => () => {
|
|
@@ -143,7 +151,6 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
143
151
|
)
|
|
144
152
|
|
|
145
153
|
const state$ = React.useMemo(() => {
|
|
146
|
-
console.log('useComponentState make state$', labelForKey(componentKey))
|
|
147
154
|
// create state query
|
|
148
155
|
if (stateSchema === undefined) {
|
|
149
156
|
// TODO don't set up a query if there's no state schema (keeps the graph more clean)
|
|
@@ -198,7 +205,10 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
198
205
|
|
|
199
206
|
// Step 1:
|
|
200
207
|
// Synchronously create state and queries for initial render pass.
|
|
201
|
-
const initialComponentState = React.useMemo(
|
|
208
|
+
const initialComponentState = React.useMemo(
|
|
209
|
+
() => state$.run(otelContext, { _tag: 'react', api: 'useComponentState', label: state$.label, stackInfo }),
|
|
210
|
+
[otelContext, stackInfo, state$],
|
|
211
|
+
)
|
|
202
212
|
|
|
203
213
|
// Now that we've computed the initial state synchronously,
|
|
204
214
|
// we can set up our useState calls w/ a default value populated...
|
|
@@ -240,14 +250,6 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
240
250
|
return store.applyEvent('updateComponentState', { componentKey, columnNames, ...columnValues })
|
|
241
251
|
}
|
|
242
252
|
|
|
243
|
-
const subscriptionInfo = React.useMemo(() => {
|
|
244
|
-
Error.stackTraceLimit = 10
|
|
245
|
-
// eslint-disable-next-line unicorn/error-message
|
|
246
|
-
const stack = new Error().stack!
|
|
247
|
-
Error.stackTraceLimit = originalStackLimit
|
|
248
|
-
return { stack: extractStackInfoFromStackTrace(stack) }
|
|
249
|
-
}, [])
|
|
250
|
-
|
|
251
253
|
// OK, now all the synchronous work is done;
|
|
252
254
|
// time to set up our long-running queries in an effect
|
|
253
255
|
React.useEffect(() => {
|
|
@@ -258,11 +260,12 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
258
260
|
(span) => {
|
|
259
261
|
const unsubs: (() => void)[] = []
|
|
260
262
|
|
|
263
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
261
264
|
if (stateSchema !== undefined) {
|
|
262
|
-
insertRowForComponentInstance({ store, componentKey, stateSchema })
|
|
265
|
+
insertRowForComponentInstance({ store, componentKey, stateSchema, otelContext })
|
|
263
266
|
}
|
|
264
267
|
|
|
265
|
-
state$.activeSubscriptions.add(
|
|
268
|
+
state$.activeSubscriptions.add(stackInfo)
|
|
266
269
|
|
|
267
270
|
unsubs.push(
|
|
268
271
|
store.subscribe(
|
|
@@ -273,9 +276,9 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
273
276
|
}
|
|
274
277
|
},
|
|
275
278
|
undefined,
|
|
276
|
-
{ label: `useComponentState:localState:subscribe:${state$.label}
|
|
279
|
+
{ label: `useComponentState:localState:subscribe:${state$.label}`, otelContext },
|
|
277
280
|
),
|
|
278
|
-
() => state$.activeSubscriptions.delete(
|
|
281
|
+
() => state$.activeSubscriptions.delete(stackInfo),
|
|
279
282
|
)
|
|
280
283
|
|
|
281
284
|
return () => {
|
|
@@ -287,13 +290,9 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
287
290
|
}
|
|
288
291
|
},
|
|
289
292
|
)
|
|
290
|
-
// NOTE excluding `setComponentState_` and `setQueryResults_` from the deps array as it seems to cause an infinite loop
|
|
291
|
-
// This should probably be improved
|
|
292
|
-
// TODO is this still true?
|
|
293
|
-
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
294
293
|
}, [
|
|
295
294
|
store,
|
|
296
|
-
|
|
295
|
+
stackInfo,
|
|
297
296
|
stateSchema,
|
|
298
297
|
defaultComponentState,
|
|
299
298
|
otelContext,
|
|
@@ -303,14 +302,7 @@ export const useComponentState = <TStateColumns extends ComponentColumns>({
|
|
|
303
302
|
componentKey,
|
|
304
303
|
])
|
|
305
304
|
|
|
306
|
-
|
|
307
|
-
React.useEffect(
|
|
308
|
-
() => () => {
|
|
309
|
-
console.log('useComponentState destroy', labelForKey(componentKey))
|
|
310
|
-
return state$.destroy()
|
|
311
|
-
},
|
|
312
|
-
[state$],
|
|
313
|
-
)
|
|
305
|
+
React.useEffect(() => () => state$.destroy(), [state$])
|
|
314
306
|
|
|
315
307
|
const state = componentStateRef.current
|
|
316
308
|
|
|
@@ -377,10 +369,12 @@ const insertRowForComponentInstance = ({
|
|
|
377
369
|
store,
|
|
378
370
|
componentKey,
|
|
379
371
|
stateSchema,
|
|
372
|
+
otelContext,
|
|
380
373
|
}: {
|
|
381
374
|
store: Store<BaseGraphQLContext>
|
|
382
375
|
componentKey: ComponentKey
|
|
383
376
|
stateSchema: SqliteDsl.TableDefinition<string, SqliteDsl.Columns>
|
|
377
|
+
otelContext: otel.Context
|
|
384
378
|
}) => {
|
|
385
379
|
const columnNames = ['id', ...Object.keys(stateSchema.columns)]
|
|
386
380
|
const columnValues = columnNames.map((name) => `$${name}`).join(', ')
|
|
@@ -397,6 +391,7 @@ const insertRowForComponentInstance = ({
|
|
|
397
391
|
id: componentKey.id,
|
|
398
392
|
},
|
|
399
393
|
[tableName],
|
|
394
|
+
otelContext,
|
|
400
395
|
)
|
|
401
396
|
}
|
|
402
397
|
|
package/src/react/useQuery.ts
CHANGED
|
@@ -1,58 +1,90 @@
|
|
|
1
|
+
import * as otel from '@opentelemetry/api'
|
|
1
2
|
import { isEqual } from 'lodash-es'
|
|
2
3
|
import React from 'react'
|
|
3
4
|
|
|
4
5
|
import type { ILiveStoreQuery } from '../reactiveQueries/base-class.js'
|
|
5
6
|
import { useStore } from './LiveStoreContext.js'
|
|
6
|
-
import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/
|
|
7
|
+
import { extractStackInfoFromStackTrace, originalStackLimit } from './utils/stack-info.js'
|
|
7
8
|
import { useStateRefWithReactiveInput } from './utils/useStateRefWithReactiveInput.js'
|
|
9
|
+
/**
|
|
10
|
+
* This is needed because the `React.useMemo` call below, can sometimes be called multiple times 🤷,
|
|
11
|
+
* so we need to "cache" the fact that we've already started a span for this component.
|
|
12
|
+
* The map entry is being removed again in the `React.useEffect` call below.
|
|
13
|
+
*/
|
|
14
|
+
const spanAlreadyStartedCache = new Map<ILiveStoreQuery<any>, { span: otel.Span; otelContext: otel.Context }>()
|
|
8
15
|
|
|
9
16
|
export const useQuery = <TResult>(query: ILiveStoreQuery<TResult>): TResult => {
|
|
10
17
|
const { store } = useStore()
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
const initialResult = React.useMemo(() => query.run(), [query])
|
|
14
|
-
|
|
15
|
-
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
16
|
-
const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
|
|
17
|
-
|
|
18
|
-
const subscriptionInfo = React.useMemo(() => {
|
|
19
|
+
const stackInfo = React.useMemo(() => {
|
|
19
20
|
Error.stackTraceLimit = 10
|
|
20
21
|
// eslint-disable-next-line unicorn/error-message
|
|
21
22
|
const stack = new Error().stack!
|
|
22
23
|
Error.stackTraceLimit = originalStackLimit
|
|
23
|
-
return
|
|
24
|
+
return extractStackInfoFromStackTrace(stack)
|
|
24
25
|
}, [])
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
-
React.
|
|
28
|
-
|
|
27
|
+
// The following `React.useMemo` and `React.useEffect` calls are used to start and end a span for the lifetime of this component.
|
|
28
|
+
const { span, otelContext } = React.useMemo(() => {
|
|
29
|
+
const existingSpan = spanAlreadyStartedCache.get(query)
|
|
30
|
+
if (existingSpan !== undefined) return existingSpan
|
|
31
|
+
|
|
32
|
+
const span = store.otel.tracer.startSpan(
|
|
29
33
|
`LiveStore:useQuery:${query.label}`,
|
|
30
|
-
|
|
31
|
-
{ attributes: { label: query.label } },
|
|
34
|
+
{ attributes: { label: query.label, stackInfo: JSON.stringify(stackInfo) } },
|
|
32
35
|
store.otel.queriesSpanContext,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const otelContext = otel.trace.setSpan(otel.context.active(), span)
|
|
39
|
+
|
|
40
|
+
spanAlreadyStartedCache.set(query, { span, otelContext })
|
|
41
|
+
|
|
42
|
+
return { span, otelContext }
|
|
43
|
+
}, [query, stackInfo, store.otel.queriesSpanContext, store.otel.tracer])
|
|
44
|
+
|
|
45
|
+
const initialResult = React.useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
query.run(otelContext, {
|
|
48
|
+
_tag: 'react',
|
|
49
|
+
api: 'useQuery',
|
|
50
|
+
label: query.label,
|
|
51
|
+
stackInfo,
|
|
52
|
+
}),
|
|
53
|
+
[otelContext, query, stackInfo],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// We know the query has a result by the time we use it; so we can synchronously populate a default state
|
|
57
|
+
const [valueRef, setValue] = useStateRefWithReactiveInput<TResult>(initialResult)
|
|
58
|
+
|
|
59
|
+
React.useEffect(
|
|
60
|
+
() => () => {
|
|
61
|
+
spanAlreadyStartedCache.delete(query)
|
|
62
|
+
span.end()
|
|
63
|
+
},
|
|
64
|
+
[query, span],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// Subscribe to future updates for this query
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
query.activeSubscriptions.add(stackInfo)
|
|
70
|
+
const unsub = store.subscribe(
|
|
71
|
+
query,
|
|
72
|
+
(newValue) => {
|
|
73
|
+
// NOTE: we return a reference to the result object within LiveStore;
|
|
74
|
+
// this implies that app code must not mutate the results, or else
|
|
75
|
+
// there may be weird reactivity bugs.
|
|
76
|
+
if (isEqual(newValue, valueRef.current) === false) {
|
|
77
|
+
setValue(newValue)
|
|
52
78
|
}
|
|
53
79
|
},
|
|
80
|
+
undefined,
|
|
81
|
+
{ label: query.label, otelContext },
|
|
54
82
|
)
|
|
55
|
-
|
|
83
|
+
return () => {
|
|
84
|
+
query.activeSubscriptions.delete(stackInfo)
|
|
85
|
+
unsub()
|
|
86
|
+
}
|
|
87
|
+
}, [stackInfo, query, setValue, store, valueRef, otelContext, span])
|
|
56
88
|
|
|
57
89
|
return valueRef.current
|
|
58
90
|
}
|
|
@@ -9,6 +9,8 @@ import { useQuery } from './useQuery.js'
|
|
|
9
9
|
* Make sure `makeQuery` is a memoized function.
|
|
10
10
|
*/
|
|
11
11
|
export const useTemporaryQuery = <TResult>(makeQuery: () => ILiveStoreQuery<TResult>): TResult => {
|
|
12
|
+
// TODO cache the query outside of the `useMemo` since `useMemo` might be called multiple times
|
|
13
|
+
// also need to update the `useEffect` below https://stackoverflow.com/questions/66446642/react-usememo-memory-clean/77457605#77457605
|
|
12
14
|
const query = React.useMemo(() => makeQuery(), [makeQuery])
|
|
13
15
|
|
|
14
16
|
React.useEffect(() => {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
1
3
|
export const originalStackLimit = Error.stackTraceLimit
|
|
2
4
|
|
|
3
5
|
export type StackInfo = {
|
|
6
|
+
frames: StackFrame[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type StackFrame = {
|
|
4
10
|
name: string
|
|
5
11
|
filePath: string
|
|
6
12
|
}
|
|
@@ -24,10 +30,10 @@ Approach:
|
|
|
24
30
|
- Start filtering at `at useQuery` (including)
|
|
25
31
|
- Stop filtering at `at renderWithHooks` (excluding)
|
|
26
32
|
*/
|
|
27
|
-
export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo
|
|
33
|
+
export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo => {
|
|
28
34
|
const namePattern = /at (\S+) \((.+)\)/g
|
|
29
35
|
let match: RegExpExecArray | null
|
|
30
|
-
const
|
|
36
|
+
const frames: StackFrame[] = []
|
|
31
37
|
let hasReachedStart = false
|
|
32
38
|
|
|
33
39
|
while ((match = namePattern.exec(stackTrace)) !== null) {
|
|
@@ -35,13 +41,23 @@ export const extractStackInfoFromStackTrace = (stackTrace: string): StackInfo[]
|
|
|
35
41
|
if (name.startsWith('use')) {
|
|
36
42
|
hasReachedStart = true
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
frames.unshift({ name, filePath })
|
|
39
45
|
} else if (hasReachedStart) {
|
|
40
46
|
// We've reached the end of the `use*` functions, so we're adding the component name and stop
|
|
41
|
-
|
|
47
|
+
frames.unshift({ name, filePath })
|
|
42
48
|
break
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
|
|
46
|
-
return
|
|
52
|
+
return { frames }
|
|
47
53
|
}
|
|
54
|
+
|
|
55
|
+
export const useStackInfo = (): StackInfo =>
|
|
56
|
+
React.useMemo(() => {
|
|
57
|
+
Error.stackTraceLimit = 10
|
|
58
|
+
// eslint-disable-next-line unicorn/error-message
|
|
59
|
+
const stack = new Error().stack!
|
|
60
|
+
console.log('stack', stack)
|
|
61
|
+
Error.stackTraceLimit = originalStackLimit
|
|
62
|
+
return extractStackInfoFromStackTrace(stack)
|
|
63
|
+
}, [])
|