@astrale-os/shell-react 0.2.9 → 0.2.10-beta.2
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/dist/application/apps/app-host.d.ts +4 -15
- package/dist/application/apps/app-host.d.ts.map +1 -1
- package/dist/application/apps/app-host.js +1 -1
- package/dist/application/apps/apps.hook.d.ts +7 -4
- package/dist/application/apps/apps.hook.d.ts.map +1 -1
- package/dist/application/apps/apps.hook.js +10 -2
- package/dist/application/apps/domains.hook.d.ts +1 -1
- package/dist/application/apps/domains.hook.d.ts.map +1 -1
- package/dist/application/apps/domains.hook.js +1 -1
- package/dist/application/apps/open-app.hook.d.ts +2 -3
- package/dist/application/apps/open-app.hook.d.ts.map +1 -1
- package/dist/application/index.d.ts +2 -4
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +2 -3
- package/dist/application/spaces/space.hook.d.ts +2 -2
- package/dist/application/spaces/space.hook.d.ts.map +1 -1
- package/dist/application/spaces/space.hook.js +2 -2
- package/dist/application/spaces/spaces.hook.d.ts +2 -3
- package/dist/application/spaces/spaces.hook.d.ts.map +1 -1
- package/dist/application/spaces/spaces.hook.js +10 -3
- package/dist/auth/act-as.context.d.ts +15 -32
- package/dist/auth/act-as.context.d.ts.map +1 -1
- package/dist/auth/act-as.context.js +22 -66
- package/dist/auth/auth.hook.d.ts +1 -0
- package/dist/auth/auth.hook.d.ts.map +1 -1
- package/dist/auth/auth.hook.js +2 -22
- package/dist/auth/can.hook.d.ts +10 -38
- package/dist/auth/can.hook.d.ts.map +1 -1
- package/dist/auth/can.hook.js +35 -110
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/self.hook.d.ts +2 -2
- package/dist/auth/self.hook.d.ts.map +1 -1
- package/dist/auth/self.hook.js +0 -7
- package/dist/auth/session-kernel.d.ts +2 -3
- package/dist/auth/session-kernel.d.ts.map +1 -1
- package/dist/auth/session-kernel.js +4 -13
- package/dist/components/boundaries.d.ts +47 -0
- package/dist/components/boundaries.d.ts.map +1 -0
- package/dist/components/boundaries.js +55 -0
- package/dist/components/can.d.ts +11 -0
- package/dist/components/can.d.ts.map +1 -0
- package/dist/components/can.js +5 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +4 -0
- package/dist/components/mount-domain-frontend.d.ts +22 -0
- package/dist/components/mount-domain-frontend.d.ts.map +1 -0
- package/dist/components/mount-domain-frontend.js +85 -0
- package/dist/components/view-app.d.ts +16 -0
- package/dist/components/view-app.d.ts.map +1 -0
- package/dist/components/view-app.js +16 -0
- package/dist/graph/call/call.hook.d.ts +8 -18
- package/dist/graph/call/call.hook.d.ts.map +1 -1
- package/dist/graph/call/call.hook.js +7 -13
- package/dist/graph/call/stream.hook.d.ts +12 -20
- package/dist/graph/call/stream.hook.d.ts.map +1 -1
- package/dist/graph/call/stream.hook.js +55 -111
- package/dist/graph/graph.context.d.ts +4 -3
- package/dist/graph/graph.context.d.ts.map +1 -1
- package/dist/graph/graph.context.js +2 -8
- package/dist/graph/guards.error.d.ts +7 -8
- package/dist/graph/guards.error.d.ts.map +1 -1
- package/dist/graph/guards.error.js +11 -7
- package/dist/graph/index.d.ts +8 -16
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/index.js +3 -7
- package/dist/graph/memory/read/envelope.d.ts +22 -54
- package/dist/graph/memory/read/envelope.d.ts.map +1 -1
- package/dist/graph/memory/read/envelope.js +26 -20
- package/dist/graph/memory/read/idle.d.ts +1 -15
- package/dist/graph/memory/read/idle.d.ts.map +1 -1
- package/dist/graph/memory/read/idle.js +4 -13
- package/dist/graph/query-definition.hook.d.ts +11 -0
- package/dist/graph/query-definition.hook.d.ts.map +1 -0
- package/dist/graph/query-definition.hook.js +205 -0
- package/dist/graph/resource.d.ts +34 -0
- package/dist/graph/resource.d.ts.map +1 -0
- package/dist/graph/resource.js +244 -0
- package/dist/graph/store.d.ts +25 -0
- package/dist/graph/store.d.ts.map +1 -0
- package/dist/graph/store.js +69 -0
- package/dist/host/actions.hook.d.ts +34 -0
- package/dist/host/actions.hook.d.ts.map +1 -0
- package/dist/host/actions.hook.js +125 -0
- package/dist/host/components.d.ts +15 -0
- package/dist/host/components.d.ts.map +1 -0
- package/dist/host/components.js +14 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +3 -0
- package/dist/index.d.ts +26 -37
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -31
- package/dist/intents/envelope.d.ts +7 -19
- package/dist/intents/envelope.d.ts.map +1 -1
- package/dist/intents/envelope.js +2 -3
- package/dist/intents/send-intent.hook.d.ts +5 -7
- package/dist/intents/send-intent.hook.d.ts.map +1 -1
- package/dist/intents/send-intent.hook.js +17 -75
- package/dist/navigation/adapters/adapter.d.ts +5 -0
- package/dist/navigation/adapters/adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/memory.adapter.js +4 -0
- package/dist/navigation/adapters/url.adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/url.adapter.js +57 -10
- package/dist/navigation/codec.d.ts +60 -0
- package/dist/navigation/codec.d.ts.map +1 -0
- package/dist/navigation/codec.js +121 -0
- package/dist/navigation/history.d.ts +11 -9
- package/dist/navigation/history.d.ts.map +1 -1
- package/dist/navigation/history.js +6 -8
- package/dist/navigation/hooks/ancestors.hook.d.ts +5 -7
- package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/ancestors.hook.js +7 -51
- package/dist/navigation/hooks/location.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/location.hook.js +4 -6
- package/dist/navigation/hooks/scope.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/scope.hook.js +4 -6
- package/dist/navigation/index.d.ts +4 -2
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +2 -1
- package/dist/navigation/link.d.ts +12 -0
- package/dist/navigation/link.d.ts.map +1 -1
- package/dist/navigation/link.js +13 -2
- package/dist/navigation/location.d.ts +13 -40
- package/dist/navigation/location.d.ts.map +1 -1
- package/dist/navigation/location.js +14 -76
- package/dist/navigation/scope.context.d.ts +17 -26
- package/dist/navigation/scope.context.d.ts.map +1 -1
- package/dist/navigation/scope.context.js +140 -108
- package/dist/navigation/snapshot.d.ts +14 -19
- package/dist/navigation/snapshot.d.ts.map +1 -1
- package/dist/navigation/snapshot.js +48 -44
- package/dist/schema/action.hook.d.ts +44 -0
- package/dist/schema/action.hook.d.ts.map +1 -0
- package/dist/schema/action.hook.js +72 -0
- package/dist/schema/domain.hook.d.ts +12 -7
- package/dist/schema/domain.hook.d.ts.map +1 -1
- package/dist/schema/domain.hook.js +101 -4
- package/dist/schema/index.d.ts +9 -8
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +5 -7
- package/dist/schema/kernel.hook.d.ts +4 -9
- package/dist/schema/kernel.hook.d.ts.map +1 -1
- package/dist/schema/kernel.hook.js +4 -6
- package/dist/schema/mutation.hook.d.ts +9 -0
- package/dist/schema/mutation.hook.d.ts.map +1 -0
- package/dist/schema/mutation.hook.js +41 -0
- package/dist/schema/object.hook.d.ts +16 -0
- package/dist/schema/object.hook.d.ts.map +1 -0
- package/dist/schema/object.hook.js +46 -0
- package/dist/schema/relation.hook.d.ts +46 -0
- package/dist/schema/relation.hook.d.ts.map +1 -0
- package/dist/schema/relation.hook.js +136 -0
- package/dist/selection/live.d.ts +9 -17
- package/dist/selection/live.d.ts.map +1 -1
- package/dist/selection/live.js +65 -112
- package/dist/selection/scope.context.js +5 -4
- package/dist/session/boot/boot.d.ts +1 -13
- package/dist/session/boot/boot.d.ts.map +1 -1
- package/dist/session/boot/boot.js +9 -27
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/kernel-instance.hook.d.ts +1 -4
- package/dist/session/kernel-instance.hook.d.ts.map +1 -1
- package/dist/session/kernel-instance.hook.js +1 -5
- package/dist/session/provider.d.ts +9 -23
- package/dist/session/provider.d.ts.map +1 -1
- package/dist/session/provider.js +29 -75
- package/dist/session/session.context.d.ts +5 -0
- package/dist/session/session.context.d.ts.map +1 -1
- package/dist/window/adopt.d.ts +0 -1
- package/dist/window/adopt.d.ts.map +1 -1
- package/dist/window/hooks/target-node.hook.d.ts +10 -6
- package/dist/window/hooks/target-node.hook.d.ts.map +1 -1
- package/dist/window/hooks/target-node.hook.js +14 -72
- package/dist/window/hooks/views-for.hook.d.ts +4 -3
- package/dist/window/hooks/views-for.hook.d.ts.map +1 -1
- package/dist/window/hooks/views-for.hook.js +6 -22
- package/dist/window/hooks/windows.hook.d.ts.map +1 -1
- package/dist/window/hooks/windows.hook.js +3 -4
- package/dist/window/host.d.ts +9 -11
- package/dist/window/host.d.ts.map +1 -1
- package/dist/window/host.js +26 -62
- package/dist/window/hub.d.ts +2 -0
- package/dist/window/hub.d.ts.map +1 -1
- package/dist/window/hub.js +33 -0
- package/dist/window/index.d.ts +2 -2
- package/dist/window/index.d.ts.map +1 -1
- package/dist/window/index.js +2 -2
- package/dist/window/lifecycle/mount.d.ts +5 -44
- package/dist/window/lifecycle/mount.d.ts.map +1 -1
- package/dist/window/lifecycle/mount.js +14 -120
- package/dist/window/lifecycle/states.d.ts +0 -5
- package/dist/window/lifecycle/states.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/application/apps/app-host.tsx +11 -15
- package/src/application/apps/apps.hook.ts +13 -6
- package/src/application/apps/domains.hook.ts +1 -1
- package/src/application/apps/open-app.hook.ts +3 -4
- package/src/application/index.ts +4 -6
- package/src/application/spaces/space.hook.ts +5 -6
- package/src/application/spaces/spaces.hook.ts +14 -9
- package/src/auth/act-as.context.tsx +37 -91
- package/src/auth/auth.hook.ts +2 -30
- package/src/auth/can.hook.ts +44 -135
- package/src/auth/index.ts +1 -0
- package/src/auth/self.hook.ts +2 -9
- package/src/auth/session-kernel.ts +6 -16
- package/src/components/boundaries.tsx +113 -0
- package/src/components/can.tsx +16 -0
- package/src/components/index.ts +14 -0
- package/src/components/mount-domain-frontend.tsx +158 -0
- package/src/components/view-app.tsx +49 -0
- package/src/graph/call/call.hook.ts +22 -27
- package/src/graph/call/stream.hook.ts +66 -143
- package/src/graph/graph.context.ts +6 -11
- package/src/graph/guards.error.ts +17 -55
- package/src/graph/index.ts +23 -17
- package/src/graph/memory/read/envelope.ts +40 -65
- package/src/graph/memory/read/idle.ts +5 -18
- package/src/graph/query-definition.hook.ts +270 -0
- package/src/graph/resource.ts +358 -0
- package/src/graph/store.ts +97 -0
- package/src/host/actions.hook.ts +165 -0
- package/src/host/components.tsx +29 -0
- package/src/host/index.ts +17 -0
- package/src/index.ts +136 -124
- package/src/intents/envelope.ts +7 -23
- package/src/intents/send-intent.hook.ts +24 -79
- package/src/navigation/adapters/adapter.ts +3 -0
- package/src/navigation/adapters/memory.adapter.ts +4 -0
- package/src/navigation/adapters/url.adapter.ts +48 -8
- package/src/navigation/codec.ts +190 -0
- package/src/navigation/history.ts +20 -11
- package/src/navigation/hooks/ancestors.hook.ts +10 -60
- package/src/navigation/hooks/location.hook.ts +4 -6
- package/src/navigation/hooks/scope.hook.ts +4 -6
- package/src/navigation/index.ts +18 -2
- package/src/navigation/link.tsx +27 -2
- package/src/navigation/location.ts +26 -93
- package/src/navigation/scope.context.tsx +151 -136
- package/src/navigation/snapshot.ts +69 -50
- package/src/schema/action.hook.ts +146 -0
- package/src/schema/domain.hook.ts +127 -11
- package/src/schema/index.ts +9 -30
- package/src/schema/kernel.hook.ts +6 -13
- package/src/schema/mutation.hook.ts +63 -0
- package/src/schema/object.hook.ts +85 -0
- package/src/schema/relation.hook.ts +253 -0
- package/src/selection/live.ts +75 -125
- package/src/selection/scope.context.tsx +4 -4
- package/src/session/boot/boot.ts +8 -33
- package/src/session/index.ts +1 -0
- package/src/session/kernel-instance.hook.ts +2 -11
- package/src/session/provider.tsx +34 -103
- package/src/session/session.context.ts +6 -0
- package/src/window/adopt.ts +0 -1
- package/src/window/hooks/target-node.hook.ts +20 -85
- package/src/window/hooks/views-for.hook.ts +12 -25
- package/src/window/hooks/windows.hook.ts +3 -4
- package/src/window/host.tsx +35 -77
- package/src/window/hub.ts +33 -0
- package/src/window/index.ts +3 -3
- package/src/window/lifecycle/mount.ts +15 -160
- package/src/window/lifecycle/states.ts +1 -5
- package/dist/application/roles.hook.d.ts +0 -7
- package/dist/application/roles.hook.d.ts.map +0 -1
- package/dist/application/roles.hook.js +0 -23
- package/dist/graph/memory/base.d.ts +0 -21
- package/dist/graph/memory/base.d.ts.map +0 -1
- package/dist/graph/memory/base.js +0 -33
- package/dist/graph/memory/read/read.d.ts +0 -9
- package/dist/graph/memory/read/read.d.ts.map +0 -1
- package/dist/graph/memory/read/read.js +0 -87
- package/dist/graph/memory/scope.d.ts +0 -7
- package/dist/graph/memory/scope.d.ts.map +0 -1
- package/dist/graph/memory/scope.js +0 -25
- package/dist/graph/mutate/move.hook.d.ts +0 -7
- package/dist/graph/mutate/move.hook.d.ts.map +0 -1
- package/dist/graph/mutate/move.hook.js +0 -49
- package/dist/graph/mutate/mutate.hook.d.ts +0 -17
- package/dist/graph/mutate/mutate.hook.d.ts.map +0 -1
- package/dist/graph/mutate/mutate.hook.js +0 -45
- package/dist/graph/query/children.hook.d.ts +0 -29
- package/dist/graph/query/children.hook.d.ts.map +0 -1
- package/dist/graph/query/children.hook.js +0 -64
- package/dist/graph/query/graph.hook.d.ts +0 -9
- package/dist/graph/query/graph.hook.d.ts.map +0 -1
- package/dist/graph/query/graph.hook.js +0 -5
- package/dist/graph/query/neighbors.hook.d.ts +0 -7
- package/dist/graph/query/neighbors.hook.d.ts.map +0 -1
- package/dist/graph/query/neighbors.hook.js +0 -104
- package/dist/graph/query/node.hook.d.ts +0 -14
- package/dist/graph/query/node.hook.d.ts.map +0 -1
- package/dist/graph/query/node.hook.js +0 -43
- package/dist/graph/query/query.hook.d.ts +0 -14
- package/dist/graph/query/query.hook.d.ts.map +0 -1
- package/dist/graph/query/query.hook.js +0 -23
- package/dist/schema/bind-read.d.ts +0 -37
- package/dist/schema/bind-read.d.ts.map +0 -1
- package/dist/schema/bind-read.js +0 -205
- package/dist/schema/binding.d.ts +0 -21
- package/dist/schema/binding.d.ts.map +0 -1
- package/dist/schema/binding.js +0 -99
- package/dist/schema/function.hook.d.ts +0 -9
- package/dist/schema/function.hook.d.ts.map +0 -1
- package/dist/schema/function.hook.js +0 -40
- package/dist/schema/method.hook.d.ts +0 -14
- package/dist/schema/method.hook.d.ts.map +0 -1
- package/dist/schema/method.hook.js +0 -49
- package/dist/schema/tag.d.ts +0 -11
- package/dist/schema/tag.d.ts.map +0 -1
- package/dist/schema/tag.js +0 -11
- package/dist/schema/types.d.ts +0 -73
- package/dist/schema/types.d.ts.map +0 -1
- package/dist/schema/types.js +0 -1
- package/dist/schema/write.d.ts +0 -16
- package/dist/schema/write.d.ts.map +0 -1
- package/dist/schema/write.js +0 -56
- package/src/application/roles.hook.ts +0 -41
- package/src/graph/memory/base.ts +0 -50
- package/src/graph/memory/read/read.ts +0 -103
- package/src/graph/memory/scope.ts +0 -29
- package/src/graph/mutate/move.hook.ts +0 -70
- package/src/graph/mutate/mutate.hook.ts +0 -69
- package/src/graph/query/children.hook.ts +0 -136
- package/src/graph/query/graph.hook.ts +0 -13
- package/src/graph/query/neighbors.hook.ts +0 -159
- package/src/graph/query/node.hook.ts +0 -83
- package/src/graph/query/query.hook.ts +0 -42
- package/src/schema/bind-read.ts +0 -319
- package/src/schema/binding.ts +0 -146
- package/src/schema/function.hook.ts +0 -68
- package/src/schema/method.hook.ts +0 -103
- package/src/schema/tag.ts +0 -19
- package/src/schema/types.ts +0 -150
- package/src/schema/write.ts +0 -96
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { GraphStore, StoreQueryResponse, StoreSnapshot } from '@astrale-os/kernel-client/store'
|
|
2
|
+
import type {
|
|
3
|
+
QueryAstExecutionOptions,
|
|
4
|
+
QueryDefinition,
|
|
5
|
+
QueryValueBinder,
|
|
6
|
+
} from '@astrale-os/sdk/query'
|
|
7
|
+
import type { QueryAST } from '@astrale-os/sdk/query'
|
|
8
|
+
import type { DomainBinding } from '@astrale-os/shell'
|
|
9
|
+
|
|
10
|
+
import { bindQueries } from '@astrale-os/sdk/query'
|
|
11
|
+
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
|
12
|
+
|
|
13
|
+
import type { GraphResource, GraphResourceState } from './resource.js'
|
|
14
|
+
|
|
15
|
+
import { useDynamicDomain } from '../schema/domain.hook.js'
|
|
16
|
+
import { retainRefreshScheduler } from './resource.js'
|
|
17
|
+
import { useGraphStore } from './store.js'
|
|
18
|
+
|
|
19
|
+
export interface QueryResourceOptions {
|
|
20
|
+
readonly keepPrevious?: boolean
|
|
21
|
+
readonly refreshInterval?: number
|
|
22
|
+
readonly refreshWhen?: 'always' | 'visible'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TouchedShape {
|
|
26
|
+
readonly ast: QueryAST
|
|
27
|
+
readonly options: QueryAstExecutionOptions
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface DefinitionSnapshot<Output> {
|
|
31
|
+
readonly state: GraphResourceState
|
|
32
|
+
readonly data?: Output
|
|
33
|
+
readonly error?: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class DefinitionObserver<Input, Output, Query extends QueryAST> {
|
|
37
|
+
private readonly listeners = new Set<() => void>()
|
|
38
|
+
private subscriptions: readonly (() => void)[] = Object.freeze([])
|
|
39
|
+
private touched: readonly TouchedShape[] = Object.freeze([])
|
|
40
|
+
private current: DefinitionSnapshot<Output> = Object.freeze({ state: 'loading' })
|
|
41
|
+
private generation = 0
|
|
42
|
+
private pending: Promise<void> | null = null
|
|
43
|
+
private started = false
|
|
44
|
+
private rerunQueued = false
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
private readonly store: GraphStore,
|
|
48
|
+
private readonly binding: DomainBinding,
|
|
49
|
+
private readonly definition: QueryDefinition<Input, Output, Query>,
|
|
50
|
+
private readonly input: Input,
|
|
51
|
+
) {}
|
|
52
|
+
|
|
53
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
54
|
+
this.listeners.add(listener)
|
|
55
|
+
return () => this.listeners.delete(listener)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getSnapshot = (): DefinitionSnapshot<Output> => this.current
|
|
59
|
+
|
|
60
|
+
start(): () => void {
|
|
61
|
+
this.started = true
|
|
62
|
+
void this.run('cached')
|
|
63
|
+
return () => {
|
|
64
|
+
this.started = false
|
|
65
|
+
this.generation += 1
|
|
66
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe())
|
|
67
|
+
this.subscriptions = Object.freeze([])
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
refresh = async (): Promise<void> => {
|
|
72
|
+
await this.run('refresh')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private run(freshness: 'cached' | 'refresh'): Promise<void> {
|
|
76
|
+
if (this.pending !== null && freshness === 'cached') return this.pending
|
|
77
|
+
const generation = ++this.generation
|
|
78
|
+
const touched: TouchedShape[] = []
|
|
79
|
+
this.update({
|
|
80
|
+
state: this.current.data === undefined ? 'loading' : 'refreshing',
|
|
81
|
+
...(this.current.data === undefined ? {} : { data: this.current.data }),
|
|
82
|
+
})
|
|
83
|
+
const execute = async <Ast extends QueryAST>(
|
|
84
|
+
ast: Ast,
|
|
85
|
+
options: QueryAstExecutionOptions,
|
|
86
|
+
): Promise<StoreQueryResponse<Ast>> => {
|
|
87
|
+
touched.push(Object.freeze({ ast, options }))
|
|
88
|
+
return this.store.read(ast, { ...options, freshness })
|
|
89
|
+
}
|
|
90
|
+
const bind = this.binding.$.bind as unknown as QueryValueBinder
|
|
91
|
+
const query = bindQueries(execute, { bind })
|
|
92
|
+
const running = query(this.definition, this.input)
|
|
93
|
+
.then((data) => {
|
|
94
|
+
if (!this.started || generation !== this.generation) return
|
|
95
|
+
this.touched = dedupeShapes(touched)
|
|
96
|
+
this.bindShapes()
|
|
97
|
+
this.update({ state: this.storeState(), data })
|
|
98
|
+
})
|
|
99
|
+
.catch((error: unknown) => {
|
|
100
|
+
if (!this.started || generation !== this.generation) return
|
|
101
|
+
this.touched = dedupeShapes(touched)
|
|
102
|
+
this.bindShapes()
|
|
103
|
+
this.update({
|
|
104
|
+
state: 'failed',
|
|
105
|
+
...(this.current.data === undefined ? {} : { data: this.current.data }),
|
|
106
|
+
error,
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
.finally(() => {
|
|
110
|
+
if (this.pending === running) this.pending = null
|
|
111
|
+
})
|
|
112
|
+
this.pending = running
|
|
113
|
+
return running
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private bindShapes(): void {
|
|
117
|
+
this.subscriptions.forEach((unsubscribe) => unsubscribe())
|
|
118
|
+
this.subscriptions = Object.freeze(
|
|
119
|
+
this.touched.map(({ ast, options }) =>
|
|
120
|
+
this.store.subscribe(ast, this.onStoreChange, options),
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private onStoreChange = (): void => {
|
|
126
|
+
const state = this.storeState()
|
|
127
|
+
this.update({
|
|
128
|
+
state,
|
|
129
|
+
...(this.current.data === undefined ? {} : { data: this.current.data }),
|
|
130
|
+
...(this.current.error === undefined ? {} : { error: this.current.error }),
|
|
131
|
+
})
|
|
132
|
+
if ((state === 'ready' || state === 'optimistic') && !this.rerunQueued) {
|
|
133
|
+
this.rerunQueued = true
|
|
134
|
+
queueMicrotask(() => {
|
|
135
|
+
this.rerunQueued = false
|
|
136
|
+
if (this.started) void this.run('cached')
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private storeState(): GraphResourceState {
|
|
142
|
+
const snapshots = this.touched.map(({ ast, options }) => this.store.getSnapshot(ast, options))
|
|
143
|
+
if (snapshots.length === 0) return this.current.state
|
|
144
|
+
return combine(snapshots)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private update(next: DefinitionSnapshot<Output>): void {
|
|
148
|
+
if (
|
|
149
|
+
next.state === this.current.state &&
|
|
150
|
+
next.data === this.current.data &&
|
|
151
|
+
next.error === this.current.error
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
this.current = Object.freeze(next)
|
|
155
|
+
this.listeners.forEach((listener) => listener())
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Execute one named SDK Query definition exclusively through the current GraphStore. */
|
|
160
|
+
export function useQuery<Input, Output, Query extends QueryAST>(
|
|
161
|
+
definition: QueryDefinition<Input, Output, Query> | null,
|
|
162
|
+
input: Input,
|
|
163
|
+
options: QueryResourceOptions = {},
|
|
164
|
+
): GraphResource<Output> {
|
|
165
|
+
const store = useGraphStore()
|
|
166
|
+
const binding = useDynamicDomain()
|
|
167
|
+
const inputIdentity = stableIdentity(input)
|
|
168
|
+
const observer = useMemo(
|
|
169
|
+
() => (definition === null ? null : new DefinitionObserver(store, binding, definition, input)),
|
|
170
|
+
[store, binding, definition, inputIdentity],
|
|
171
|
+
)
|
|
172
|
+
const idle = useMemo<DefinitionSnapshot<Output>>(() => Object.freeze({ state: 'idle' }), [])
|
|
173
|
+
const subscribe = useCallback(
|
|
174
|
+
(listener: () => void) => observer?.subscribe(listener) ?? (() => {}),
|
|
175
|
+
[observer],
|
|
176
|
+
)
|
|
177
|
+
const snapshot = useCallback(() => observer?.getSnapshot() ?? idle, [observer, idle])
|
|
178
|
+
const current = useSyncExternalStore(subscribe, snapshot, snapshot)
|
|
179
|
+
useEffect(() => observer?.start(), [observer])
|
|
180
|
+
|
|
181
|
+
const identity = definition === null ? 'idle' : `${definition.id}\u0000${inputIdentity}`
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (observer === null || options.refreshInterval === undefined) return
|
|
184
|
+
return retainRefreshScheduler(
|
|
185
|
+
store,
|
|
186
|
+
identity,
|
|
187
|
+
observer.refresh,
|
|
188
|
+
options.refreshInterval,
|
|
189
|
+
options.refreshWhen ?? 'visible',
|
|
190
|
+
)
|
|
191
|
+
}, [store, observer, identity, options.refreshInterval, options.refreshWhen])
|
|
192
|
+
|
|
193
|
+
const previousRef = useRef<{ readonly identity: string; readonly data: Output } | undefined>(
|
|
194
|
+
undefined,
|
|
195
|
+
)
|
|
196
|
+
const previous =
|
|
197
|
+
options.keepPrevious && previousRef.current?.identity !== identity
|
|
198
|
+
? previousRef.current
|
|
199
|
+
: undefined
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (current.data !== undefined) {
|
|
202
|
+
previousRef.current = Object.freeze({ identity, data: current.data })
|
|
203
|
+
}
|
|
204
|
+
}, [identity, current.data])
|
|
205
|
+
|
|
206
|
+
return useMemo(
|
|
207
|
+
() =>
|
|
208
|
+
Object.freeze({
|
|
209
|
+
state: current.state,
|
|
210
|
+
data: current.data,
|
|
211
|
+
error: current.error,
|
|
212
|
+
...(previous === undefined ? {} : { previous }),
|
|
213
|
+
transitioning: current.state === 'loading' || current.state === 'refreshing',
|
|
214
|
+
live: false as const,
|
|
215
|
+
sequence: 0 as const,
|
|
216
|
+
refresh: observer?.refresh ?? (() => Promise.resolve()),
|
|
217
|
+
more: Object.freeze({ has: false, pending: false, load: () => Promise.resolve() }),
|
|
218
|
+
}),
|
|
219
|
+
[current, previous, observer],
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function dedupeShapes(input: readonly TouchedShape[]): readonly TouchedShape[] {
|
|
224
|
+
const seen = new Set<string>()
|
|
225
|
+
return Object.freeze(
|
|
226
|
+
input.filter(({ ast, options }) => {
|
|
227
|
+
const key = `${JSON.stringify(ast)}\u0000${JSON.stringify(options.page)}`
|
|
228
|
+
if (seen.has(key)) return false
|
|
229
|
+
seen.add(key)
|
|
230
|
+
return true
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function combine(snapshots: readonly StoreSnapshot[]): GraphResourceState {
|
|
236
|
+
if (snapshots.some((snapshot) => snapshot.state === 'cold')) return 'loading'
|
|
237
|
+
for (const state of ['failed', 'loading', 'refreshing', 'optimistic', 'stale'] as const) {
|
|
238
|
+
if (snapshots.some((snapshot) => snapshot.state === state)) return state
|
|
239
|
+
}
|
|
240
|
+
return 'ready'
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function stableIdentity(value: unknown): string {
|
|
244
|
+
const seen = new WeakSet<object>()
|
|
245
|
+
const encode = (input: unknown): unknown => {
|
|
246
|
+
if (input === null) return ['null']
|
|
247
|
+
if (typeof input !== 'object') {
|
|
248
|
+
if (typeof input === 'function' || typeof input === 'symbol' || typeof input === 'bigint') {
|
|
249
|
+
throw new TypeError('Query input must be portable and serializable.')
|
|
250
|
+
}
|
|
251
|
+
if (input === undefined) return ['undefined']
|
|
252
|
+
if (typeof input === 'number' && !Number.isFinite(input)) {
|
|
253
|
+
throw new TypeError('Query input numbers must be finite.')
|
|
254
|
+
}
|
|
255
|
+
return [typeof input, input]
|
|
256
|
+
}
|
|
257
|
+
if (seen.has(input)) throw new TypeError('Query input cannot contain cycles.')
|
|
258
|
+
seen.add(input)
|
|
259
|
+
if (Array.isArray(input)) return ['array', input.map(encode)]
|
|
260
|
+
return [
|
|
261
|
+
'object',
|
|
262
|
+
Object.fromEntries(
|
|
263
|
+
Object.keys(input as Record<string, unknown>)
|
|
264
|
+
.sort()
|
|
265
|
+
.map((key) => [key, encode((input as Record<string, unknown>)[key])]),
|
|
266
|
+
),
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
return JSON.stringify(encode(value))
|
|
270
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import type { GraphStore, StoreQueryResponse, StoreSnapshot } from '@astrale-os/kernel-client/store'
|
|
2
|
+
import type { QueryAST } from '@astrale-os/sdk/query'
|
|
3
|
+
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
|
5
|
+
|
|
6
|
+
import { useGraphStore } from './store.js'
|
|
7
|
+
|
|
8
|
+
export type GraphResourceState =
|
|
9
|
+
| 'idle'
|
|
10
|
+
| 'loading'
|
|
11
|
+
| 'ready'
|
|
12
|
+
| 'stale'
|
|
13
|
+
| 'refreshing'
|
|
14
|
+
| 'optimistic'
|
|
15
|
+
| 'failed'
|
|
16
|
+
|
|
17
|
+
export interface GraphResource<T> {
|
|
18
|
+
readonly state: GraphResourceState
|
|
19
|
+
/** Current-scope data only. Previous-scope data is never substituted here. */
|
|
20
|
+
readonly data: T | undefined
|
|
21
|
+
readonly error: unknown
|
|
22
|
+
readonly previous?: { readonly identity: string; readonly data: T }
|
|
23
|
+
readonly transitioning: boolean
|
|
24
|
+
readonly live: false
|
|
25
|
+
readonly sequence: 0
|
|
26
|
+
refresh(): Promise<void>
|
|
27
|
+
readonly more: {
|
|
28
|
+
readonly has: boolean
|
|
29
|
+
readonly pending: boolean
|
|
30
|
+
load(): Promise<void>
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StoreResourceOptions<Output> {
|
|
35
|
+
readonly identity?: string
|
|
36
|
+
readonly keepPrevious?: boolean
|
|
37
|
+
readonly window?: 'page' | 'append'
|
|
38
|
+
readonly merge?: (pages: readonly Output[]) => Output
|
|
39
|
+
readonly refreshInterval?: number
|
|
40
|
+
readonly refreshWhen?: 'always' | 'visible'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PageRef {
|
|
44
|
+
readonly after?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ObserverSnapshot<Query extends QueryAST> {
|
|
48
|
+
readonly snapshots: readonly StoreSnapshot<Query>[]
|
|
49
|
+
readonly pendingMore: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class StoreObserver<Query extends QueryAST> {
|
|
53
|
+
private readonly listeners = new Set<() => void>()
|
|
54
|
+
private pages: readonly PageRef[] = Object.freeze([Object.freeze({})])
|
|
55
|
+
private unsubs: readonly (() => void)[] = Object.freeze([])
|
|
56
|
+
private current: ObserverSnapshot<Query> | undefined
|
|
57
|
+
private morePromise: Promise<void> | null = null
|
|
58
|
+
private started = false
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly store: GraphStore,
|
|
62
|
+
private readonly query: Query,
|
|
63
|
+
private readonly size: number,
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
67
|
+
this.listeners.add(listener)
|
|
68
|
+
return () => this.listeners.delete(listener)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getSnapshot = (): ObserverSnapshot<Query> => {
|
|
72
|
+
if (this.current !== undefined) return this.current
|
|
73
|
+
this.current = Object.freeze({
|
|
74
|
+
snapshots: Object.freeze(
|
|
75
|
+
this.pages.map((page) => this.store.getSnapshot(this.query, this.pageOptions(page))),
|
|
76
|
+
),
|
|
77
|
+
pendingMore: this.morePromise !== null,
|
|
78
|
+
})
|
|
79
|
+
return this.current
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
start(): () => void {
|
|
83
|
+
if (!this.started) {
|
|
84
|
+
this.started = true
|
|
85
|
+
this.bindPages()
|
|
86
|
+
void this.store
|
|
87
|
+
.read(this.query, {
|
|
88
|
+
...this.pageOptions(this.pages[0]!),
|
|
89
|
+
freshness: 'cached',
|
|
90
|
+
})
|
|
91
|
+
.catch(() => {})
|
|
92
|
+
}
|
|
93
|
+
return () => {
|
|
94
|
+
this.started = false
|
|
95
|
+
this.unsubs.forEach((unsubscribe) => unsubscribe())
|
|
96
|
+
this.unsubs = Object.freeze([])
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async refresh(): Promise<void> {
|
|
101
|
+
await Promise.all(
|
|
102
|
+
this.pages.map((page) =>
|
|
103
|
+
this.store.read(this.query, { ...this.pageOptions(page), freshness: 'refresh' }),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
hasMore(): boolean {
|
|
109
|
+
const snapshots = this.getSnapshot().snapshots
|
|
110
|
+
const last = snapshots[snapshots.length - 1]
|
|
111
|
+
const response = last === undefined ? undefined : responseOf(last)
|
|
112
|
+
return response?.page.next !== undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
loadMore(): Promise<void> {
|
|
116
|
+
if (this.morePromise !== null) return this.morePromise
|
|
117
|
+
const snapshots = this.getSnapshot().snapshots
|
|
118
|
+
const last = snapshots[snapshots.length - 1]
|
|
119
|
+
const after = last === undefined ? undefined : responseOf(last)?.page.next
|
|
120
|
+
if (after === undefined) return Promise.resolve()
|
|
121
|
+
if (this.pages.some((page) => page.after === after)) return Promise.resolve()
|
|
122
|
+
|
|
123
|
+
this.pages = Object.freeze([...this.pages, Object.freeze({ after })])
|
|
124
|
+
this.bindPages()
|
|
125
|
+
this.morePromise = this.store
|
|
126
|
+
.read(this.query, { ...this.pageOptions({ after }), freshness: 'cached' })
|
|
127
|
+
.then(() => {})
|
|
128
|
+
.finally(() => {
|
|
129
|
+
this.morePromise = null
|
|
130
|
+
this.publish()
|
|
131
|
+
})
|
|
132
|
+
this.publish()
|
|
133
|
+
return this.morePromise
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private pageOptions(page: PageRef) {
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
page: Object.freeze({
|
|
139
|
+
size: this.size,
|
|
140
|
+
...(page.after === undefined ? {} : { after: page.after }),
|
|
141
|
+
}),
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private bindPages(): void {
|
|
146
|
+
this.unsubs.forEach((unsubscribe) => unsubscribe())
|
|
147
|
+
this.unsubs = Object.freeze(
|
|
148
|
+
this.pages.map((page) =>
|
|
149
|
+
this.store.subscribe(this.query, this.publish, this.pageOptions(page)),
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
this.publish()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private publish = (): void => {
|
|
156
|
+
this.current = undefined
|
|
157
|
+
this.listeners.forEach((listener) => listener())
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface PollSubscriber {
|
|
162
|
+
readonly interval: number
|
|
163
|
+
readonly when: 'always' | 'visible'
|
|
164
|
+
readonly refresh: () => Promise<void>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface PollScheduler {
|
|
168
|
+
readonly subscribers: Map<symbol, PollSubscriber>
|
|
169
|
+
timer: ReturnType<typeof setTimeout> | undefined
|
|
170
|
+
onVisibility?: () => void
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const pollers = new WeakMap<GraphStore, Map<string, PollScheduler>>()
|
|
174
|
+
|
|
175
|
+
/** Adapt exact Store pages into a tear-free React resource without a parallel entity cache. */
|
|
176
|
+
export function useStoreResource<Query extends QueryAST, Output>(
|
|
177
|
+
query: Query | null,
|
|
178
|
+
pageSize: number,
|
|
179
|
+
project: (response: StoreQueryResponse<Query>) => Output,
|
|
180
|
+
options: StoreResourceOptions<Output> = {},
|
|
181
|
+
): GraphResource<Output> {
|
|
182
|
+
const store = useGraphStore()
|
|
183
|
+
const observer = useMemo(
|
|
184
|
+
() => (query === null ? null : new StoreObserver(store, query, positivePage(pageSize))),
|
|
185
|
+
[store, query, pageSize],
|
|
186
|
+
)
|
|
187
|
+
const subscribe = useCallback(
|
|
188
|
+
(listener: () => void) => (observer === null ? () => {} : observer.subscribe(listener)),
|
|
189
|
+
[observer],
|
|
190
|
+
)
|
|
191
|
+
const idle = useMemo<ObserverSnapshot<Query>>(
|
|
192
|
+
() => Object.freeze({ snapshots: Object.freeze([]), pendingMore: false }),
|
|
193
|
+
[],
|
|
194
|
+
)
|
|
195
|
+
const getSnapshot = useCallback(() => observer?.getSnapshot() ?? idle, [observer, idle])
|
|
196
|
+
const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
197
|
+
|
|
198
|
+
useEffect(() => observer?.start(), [observer])
|
|
199
|
+
|
|
200
|
+
const pollIdentity =
|
|
201
|
+
query === null ? null : `${options.identity ?? JSON.stringify(query)}\u0000${pageSize}`
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (query === null || observer === null || options.refreshInterval === undefined) return
|
|
204
|
+
return retainRefreshScheduler(
|
|
205
|
+
store,
|
|
206
|
+
pollIdentity!,
|
|
207
|
+
() => observer.refresh(),
|
|
208
|
+
options.refreshInterval,
|
|
209
|
+
options.refreshWhen ?? 'visible',
|
|
210
|
+
)
|
|
211
|
+
}, [store, query, observer, pollIdentity, options.refreshInterval, options.refreshWhen])
|
|
212
|
+
|
|
213
|
+
const refresh = useCallback(async (): Promise<void> => observer?.refresh(), [observer])
|
|
214
|
+
const loadMore = useCallback(async (): Promise<void> => observer?.loadMore(), [observer])
|
|
215
|
+
const identity = options.identity ?? pollIdentity ?? 'idle'
|
|
216
|
+
const projected = useMemo(() => {
|
|
217
|
+
const responses: StoreQueryResponse<Query>[] = current.snapshots.flatMap((snapshot) => {
|
|
218
|
+
const response = responseOf(snapshot)
|
|
219
|
+
return response === undefined ? [] : [response]
|
|
220
|
+
})
|
|
221
|
+
if (responses.length === 0) return undefined
|
|
222
|
+
const pages = responses.map(project)
|
|
223
|
+
if ((options.window ?? 'page') === 'page') return pages[0]
|
|
224
|
+
return options.merge?.(pages) ?? mergeAppendPages(pages)
|
|
225
|
+
}, [current, project, options.window, options.merge])
|
|
226
|
+
const previousRef = useRef<{ readonly identity: string; readonly data: Output } | undefined>(
|
|
227
|
+
undefined,
|
|
228
|
+
)
|
|
229
|
+
const prior = previousRef.current
|
|
230
|
+
const previous =
|
|
231
|
+
options.keepPrevious && prior !== undefined && prior.identity !== identity ? prior : undefined
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (projected !== undefined) previousRef.current = Object.freeze({ identity, data: projected })
|
|
234
|
+
}, [identity, projected])
|
|
235
|
+
|
|
236
|
+
return useMemo(() => {
|
|
237
|
+
const snapshots = current.snapshots
|
|
238
|
+
const state = query === null ? 'idle' : combinedState(snapshots)
|
|
239
|
+
const failed = snapshots.find((snapshot) => snapshot.state === 'failed')
|
|
240
|
+
return Object.freeze({
|
|
241
|
+
state,
|
|
242
|
+
data: projected,
|
|
243
|
+
...(failed?.state === 'failed' ? { error: failed.error } : { error: undefined }),
|
|
244
|
+
...(previous === undefined ? {} : { previous }),
|
|
245
|
+
transitioning: state === 'loading' || state === 'refreshing',
|
|
246
|
+
live: false as const,
|
|
247
|
+
sequence: 0 as const,
|
|
248
|
+
refresh,
|
|
249
|
+
more: Object.freeze({
|
|
250
|
+
has: observer?.hasMore() ?? false,
|
|
251
|
+
pending: current.pendingMore,
|
|
252
|
+
load: loadMore,
|
|
253
|
+
}),
|
|
254
|
+
})
|
|
255
|
+
}, [current, query, projected, previous, refresh, observer, loadMore])
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function combinedState(snapshots: readonly StoreSnapshot[]): GraphResourceState {
|
|
259
|
+
if (snapshots.length === 0 || snapshots.some((snapshot) => snapshot.state === 'cold'))
|
|
260
|
+
return 'loading'
|
|
261
|
+
for (const state of ['failed', 'loading', 'refreshing', 'optimistic', 'stale'] as const) {
|
|
262
|
+
if (snapshots.some((snapshot) => snapshot.state === state)) return state
|
|
263
|
+
}
|
|
264
|
+
return 'ready'
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function responseOf<Query extends QueryAST>(
|
|
268
|
+
snapshot: StoreSnapshot<Query>,
|
|
269
|
+
): StoreQueryResponse<Query> | undefined {
|
|
270
|
+
return 'response' in snapshot ? snapshot.response : undefined
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function mergeAppendPages<Output>(pages: readonly Output[]): Output {
|
|
274
|
+
const first = pages[0]
|
|
275
|
+
if (!Array.isArray(first)) return pages[pages.length - 1] as Output
|
|
276
|
+
const seen = new Set<unknown>()
|
|
277
|
+
const merged: unknown[] = []
|
|
278
|
+
for (const page of pages as readonly (readonly unknown[])[]) {
|
|
279
|
+
for (const value of page) {
|
|
280
|
+
const identity =
|
|
281
|
+
value !== null && typeof value === 'object' && 'id' in value
|
|
282
|
+
? (value as { readonly id: unknown }).id
|
|
283
|
+
: value
|
|
284
|
+
if (seen.has(identity)) continue
|
|
285
|
+
seen.add(identity)
|
|
286
|
+
merged.push(value)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return Object.freeze(merged) as Output
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function retainRefreshScheduler(
|
|
293
|
+
store: GraphStore,
|
|
294
|
+
identity: string,
|
|
295
|
+
refresh: () => Promise<void>,
|
|
296
|
+
interval: number,
|
|
297
|
+
when: 'always' | 'visible',
|
|
298
|
+
): () => void {
|
|
299
|
+
if (!Number.isSafeInteger(interval) || interval < 250) {
|
|
300
|
+
throw new TypeError('refreshInterval must be an integer of at least 250ms.')
|
|
301
|
+
}
|
|
302
|
+
let byIdentity = pollers.get(store)
|
|
303
|
+
if (byIdentity === undefined) {
|
|
304
|
+
byIdentity = new Map()
|
|
305
|
+
pollers.set(store, byIdentity)
|
|
306
|
+
}
|
|
307
|
+
let scheduler = byIdentity.get(identity)
|
|
308
|
+
if (scheduler === undefined) {
|
|
309
|
+
scheduler = { subscribers: new Map(), timer: undefined }
|
|
310
|
+
byIdentity.set(identity, scheduler)
|
|
311
|
+
}
|
|
312
|
+
const token = Symbol(identity)
|
|
313
|
+
scheduler.subscribers.set(token, { interval, when, refresh })
|
|
314
|
+
schedulePoll(scheduler)
|
|
315
|
+
if (typeof document !== 'undefined' && scheduler.onVisibility === undefined) {
|
|
316
|
+
scheduler.onVisibility = () => schedulePoll(scheduler!, true)
|
|
317
|
+
document.addEventListener('visibilitychange', scheduler.onVisibility)
|
|
318
|
+
}
|
|
319
|
+
return () => {
|
|
320
|
+
scheduler!.subscribers.delete(token)
|
|
321
|
+
if (scheduler!.subscribers.size !== 0) {
|
|
322
|
+
schedulePoll(scheduler!)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
if (scheduler!.timer !== undefined) clearTimeout(scheduler!.timer)
|
|
326
|
+
if (typeof document !== 'undefined' && scheduler!.onVisibility !== undefined) {
|
|
327
|
+
document.removeEventListener('visibilitychange', scheduler!.onVisibility)
|
|
328
|
+
}
|
|
329
|
+
byIdentity!.delete(identity)
|
|
330
|
+
if (byIdentity!.size === 0) pollers.delete(store)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function schedulePoll(scheduler: PollScheduler, immediate = false): void {
|
|
335
|
+
if (scheduler.timer !== undefined) clearTimeout(scheduler.timer)
|
|
336
|
+
const subscribers = [...scheduler.subscribers.values()]
|
|
337
|
+
if (subscribers.length === 0) return
|
|
338
|
+
const interval = Math.min(...subscribers.map((subscriber) => subscriber.interval))
|
|
339
|
+
scheduler.timer = setTimeout(
|
|
340
|
+
() => {
|
|
341
|
+
scheduler.timer = undefined
|
|
342
|
+
const visible = typeof document === 'undefined' || document.visibilityState !== 'hidden'
|
|
343
|
+
const eligible = [...scheduler.subscribers.values()].filter(
|
|
344
|
+
(subscriber) => subscriber.when === 'always' || visible,
|
|
345
|
+
)
|
|
346
|
+
eligible.forEach((subscriber) => void subscriber.refresh().catch(() => {}))
|
|
347
|
+
schedulePoll(scheduler)
|
|
348
|
+
},
|
|
349
|
+
immediate ? 0 : interval,
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function positivePage(input: number): number {
|
|
354
|
+
if (!Number.isSafeInteger(input) || input < 1) {
|
|
355
|
+
throw new TypeError('Graph resource page size must be a positive safe integer.')
|
|
356
|
+
}
|
|
357
|
+
return input
|
|
358
|
+
}
|