@astrale-os/shell-react 0.2.9 → 0.2.10-beta.3
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/astrale.d.ts +14 -0
- package/dist/components/astrale.d.ts.map +1 -0
- package/dist/components/astrale.js +12 -0
- 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 +6 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +3 -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 +20 -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/astrale.tsx +40 -0
- package/src/components/boundaries.tsx +113 -0
- package/src/components/can.tsx +16 -0
- package/src/components/index.ts +13 -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 +131 -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,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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { GraphApi } from '@astrale-os/kernel-client/graph'
|
|
2
|
+
import type { GraphStore as GraphStoreValue } from '@astrale-os/kernel-client/store'
|
|
3
|
+
import type { Shell } from '@astrale-os/shell'
|
|
4
|
+
|
|
5
|
+
import { GraphStore } from '@astrale-os/kernel-client/store'
|
|
6
|
+
import { useContext } from 'react'
|
|
7
|
+
|
|
8
|
+
import { GraphContext } from './graph.context.js'
|
|
9
|
+
|
|
10
|
+
interface StoreEntry {
|
|
11
|
+
readonly shell: Shell
|
|
12
|
+
readonly graph: GraphApi
|
|
13
|
+
readonly scope: string
|
|
14
|
+
readonly store: GraphStoreValue
|
|
15
|
+
readonly owned: boolean
|
|
16
|
+
retained: number
|
|
17
|
+
generation: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const entries = new WeakMap<Shell, StoreEntry>()
|
|
21
|
+
|
|
22
|
+
/** Authority/schema identity. Session ids and Publication ETags are deliberately absent. */
|
|
23
|
+
export interface GraphStoreScope {
|
|
24
|
+
readonly authority: string
|
|
25
|
+
readonly schemaRevision: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type StoreIdentity = Awaited<ReturnType<Shell['kernel']['auth']['whoami']>>
|
|
29
|
+
|
|
30
|
+
export async function resolveGraphStoreScope(
|
|
31
|
+
shell: Shell,
|
|
32
|
+
knownIdentity?: StoreIdentity | null,
|
|
33
|
+
): Promise<GraphStoreScope> {
|
|
34
|
+
const [identity, snapshot] = await Promise.all([
|
|
35
|
+
knownIdentity === undefined
|
|
36
|
+
? shell.kernel.auth.whoami().catch(() => null)
|
|
37
|
+
: Promise.resolve(knownIdentity),
|
|
38
|
+
shell.kernel.snapshot(),
|
|
39
|
+
])
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
authority:
|
|
42
|
+
identity === null
|
|
43
|
+
? 'anonymous'
|
|
44
|
+
: `${identity.issuer}\u0000${identity.subject}\u0000${identity.id}`,
|
|
45
|
+
schemaRevision: snapshot.publication.schema.revision,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function storeFor(shell: Shell, scope: GraphStoreScope): StoreEntry {
|
|
50
|
+
const current = entries.get(shell)
|
|
51
|
+
const scopeKey = `${scope.authority}\u0000${scope.schemaRevision}`
|
|
52
|
+
if (current !== undefined && current.graph === shell.kernel.graph && current.scope === scopeKey) {
|
|
53
|
+
return current
|
|
54
|
+
}
|
|
55
|
+
const supplied = shell.kernel.store
|
|
56
|
+
const created: StoreEntry = {
|
|
57
|
+
shell,
|
|
58
|
+
graph: shell.kernel.graph,
|
|
59
|
+
scope: scopeKey,
|
|
60
|
+
// A supplied Store is trusted only for the KernelClient's first declared scope. A later
|
|
61
|
+
// authority/revision/GraphApi replacement gets a new Store and cannot inherit old memory.
|
|
62
|
+
store:
|
|
63
|
+
current === undefined && supplied !== undefined
|
|
64
|
+
? supplied
|
|
65
|
+
: new GraphStore({ graph: shell.kernel.graph }),
|
|
66
|
+
owned: current !== undefined || supplied === undefined,
|
|
67
|
+
retained: 0,
|
|
68
|
+
generation: 0,
|
|
69
|
+
}
|
|
70
|
+
entries.set(shell, created)
|
|
71
|
+
if (current !== undefined && current.retained === 0 && current.owned) current.store.close()
|
|
72
|
+
return created
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function retainStore(entry: StoreEntry): () => void {
|
|
76
|
+
entry.generation += 1
|
|
77
|
+
entry.retained += 1
|
|
78
|
+
let released = false
|
|
79
|
+
return () => {
|
|
80
|
+
if (released) return
|
|
81
|
+
released = true
|
|
82
|
+
entry.retained -= 1
|
|
83
|
+
const generation = ++entry.generation
|
|
84
|
+
queueMicrotask(() => {
|
|
85
|
+
if (entry.retained !== 0 || entry.generation !== generation || !entry.owned) return
|
|
86
|
+
if (entries.get(entry.shell) === entry) entries.delete(entry.shell)
|
|
87
|
+
entry.store.close()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The exact caller/schema-scoped GraphStore for the current View subtree. */
|
|
93
|
+
export function useGraphStore(): GraphStoreValue {
|
|
94
|
+
const store = useContext(GraphContext)
|
|
95
|
+
if (store === null) throw new Error('useGraphStore must be used within a <ShellProvider>')
|
|
96
|
+
return store
|
|
97
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccessRequest,
|
|
3
|
+
AccessResult,
|
|
4
|
+
ExternalOpenRequest,
|
|
5
|
+
ExternalOpenResult,
|
|
6
|
+
HostCapabilities,
|
|
7
|
+
ModalRequest,
|
|
8
|
+
ModalResult,
|
|
9
|
+
NotificationRequest,
|
|
10
|
+
NotificationResult,
|
|
11
|
+
OpenViewRequest,
|
|
12
|
+
OpenViewResult,
|
|
13
|
+
} from '@astrale-os/shell'
|
|
14
|
+
|
|
15
|
+
import { useMemo } from 'react'
|
|
16
|
+
|
|
17
|
+
import { useSendIntent } from '../intents/send-intent.hook.js'
|
|
18
|
+
import { useShell } from '../session/shell.hook.js'
|
|
19
|
+
|
|
20
|
+
export function useHostCapabilities(): HostCapabilities {
|
|
21
|
+
return useShell().hostCapabilities
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class HostCapabilityUnavailableError extends Error {
|
|
25
|
+
readonly code = 'SHELL_REACT_HOST_CAPABILITY_UNAVAILABLE' as const
|
|
26
|
+
constructor(readonly capability: string) {
|
|
27
|
+
super(`Host capability '${capability}' is unavailable.`)
|
|
28
|
+
this.name = 'HostCapabilityUnavailableError'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface HostDialogApi {
|
|
33
|
+
readonly supported: boolean
|
|
34
|
+
open(request: ModalRequest): Promise<ModalResult>
|
|
35
|
+
confirm(request: Omit<ModalRequest, 'nested'>): Promise<boolean>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useHostDialog(): HostDialogApi {
|
|
39
|
+
const capabilities = useHostCapabilities()
|
|
40
|
+
const send = useSendIntent()
|
|
41
|
+
return useMemo(
|
|
42
|
+
() => ({
|
|
43
|
+
supported: capabilities.actions.modal !== undefined,
|
|
44
|
+
open: (request: ModalRequest) => {
|
|
45
|
+
if (capabilities.actions.modal === undefined) return unsupported('actions.modal')
|
|
46
|
+
portable(request)
|
|
47
|
+
return send('actions.modal', request)
|
|
48
|
+
},
|
|
49
|
+
confirm: async (request: Omit<ModalRequest, 'nested'>) => {
|
|
50
|
+
if (capabilities.actions.modal === undefined) return unsupported('actions.modal')
|
|
51
|
+
portable(request)
|
|
52
|
+
return (await send('actions.modal', request)).outcome === 'confirmed'
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
[capabilities, send],
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface NotifyApi {
|
|
60
|
+
(request: NotificationRequest): Promise<NotificationResult>
|
|
61
|
+
readonly supported: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function useNotify(): NotifyApi {
|
|
65
|
+
const capabilities = useHostCapabilities()
|
|
66
|
+
const send = useSendIntent()
|
|
67
|
+
return useMemo(() => {
|
|
68
|
+
const notify = ((request: NotificationRequest) => {
|
|
69
|
+
if (capabilities.actions.notify === undefined) return unsupported('actions.notify')
|
|
70
|
+
portable(request)
|
|
71
|
+
return send('actions.notify', request)
|
|
72
|
+
}) as NotifyApi
|
|
73
|
+
Object.defineProperty(notify, 'supported', {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
value: capabilities.actions.notify !== undefined,
|
|
76
|
+
})
|
|
77
|
+
return notify
|
|
78
|
+
}, [capabilities, send])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface OpenViewApi {
|
|
82
|
+
(request: OpenViewRequest): Promise<OpenViewResult>
|
|
83
|
+
readonly supported: boolean
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function useOpenView(): OpenViewApi {
|
|
87
|
+
const capabilities = useHostCapabilities()
|
|
88
|
+
const send = useSendIntent()
|
|
89
|
+
return useMemo(() => {
|
|
90
|
+
const open = ((request: OpenViewRequest) => {
|
|
91
|
+
if (capabilities.navigation.openView === undefined) return unsupported('view.open')
|
|
92
|
+
portable(request)
|
|
93
|
+
return send('view.open', request)
|
|
94
|
+
}) as OpenViewApi
|
|
95
|
+
Object.defineProperty(open, 'supported', {
|
|
96
|
+
enumerable: true,
|
|
97
|
+
value: capabilities.navigation.openView !== undefined,
|
|
98
|
+
})
|
|
99
|
+
return open
|
|
100
|
+
}, [capabilities, send])
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ExternalOpenApi {
|
|
104
|
+
(request: ExternalOpenRequest): Promise<ExternalOpenResult>
|
|
105
|
+
readonly supported: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function useExternalOpen(): ExternalOpenApi {
|
|
109
|
+
const capabilities = useHostCapabilities()
|
|
110
|
+
const send = useSendIntent()
|
|
111
|
+
return useMemo(() => {
|
|
112
|
+
const open = ((request: ExternalOpenRequest) => {
|
|
113
|
+
if (capabilities.navigation.external === undefined) return unsupported('browser.openExternal')
|
|
114
|
+
portable(request)
|
|
115
|
+
return send('browser.openExternal', request)
|
|
116
|
+
}) as ExternalOpenApi
|
|
117
|
+
Object.defineProperty(open, 'supported', {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
value: capabilities.navigation.external !== undefined,
|
|
120
|
+
})
|
|
121
|
+
return open
|
|
122
|
+
}, [capabilities, send])
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface RequestAccessApi {
|
|
126
|
+
(request: AccessRequest): Promise<AccessResult>
|
|
127
|
+
readonly supported: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function useRequestAccess(): RequestAccessApi {
|
|
131
|
+
const capabilities = useHostCapabilities()
|
|
132
|
+
const send = useSendIntent()
|
|
133
|
+
return useMemo(() => {
|
|
134
|
+
const request = ((input: AccessRequest) => {
|
|
135
|
+
if (capabilities.access.request === undefined) return unsupported('access.request')
|
|
136
|
+
portable(input)
|
|
137
|
+
return send('access.request', input)
|
|
138
|
+
}) as RequestAccessApi
|
|
139
|
+
Object.defineProperty(request, 'supported', {
|
|
140
|
+
enumerable: true,
|
|
141
|
+
value: capabilities.access.request !== undefined,
|
|
142
|
+
})
|
|
143
|
+
return request
|
|
144
|
+
}, [capabilities, send])
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function unsupported<Result>(capability: string): Promise<Result> {
|
|
148
|
+
return Promise.reject(new HostCapabilityUnavailableError(capability))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function portable(input: unknown): void {
|
|
152
|
+
const seen = new WeakSet<object>()
|
|
153
|
+
const visit = (value: unknown): void => {
|
|
154
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return
|
|
155
|
+
if (typeof value === 'number' && Number.isFinite(value)) return
|
|
156
|
+
if (typeof value !== 'object' || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
157
|
+
throw new TypeError('Host action payload must be serializable data.')
|
|
158
|
+
}
|
|
159
|
+
if (seen.has(value)) throw new TypeError('Host action payload cannot contain cycles.')
|
|
160
|
+
seen.add(value)
|
|
161
|
+
if (Array.isArray(value)) value.forEach(visit)
|
|
162
|
+
else Object.values(value).forEach(visit)
|
|
163
|
+
}
|
|
164
|
+
visit(input)
|
|
165
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { HostDialogApi, NotifyApi, OpenViewApi, RequestAccessApi } from './actions.hook.js'
|
|
4
|
+
|
|
5
|
+
import { useHostDialog, useNotify, useOpenView, useRequestAccess } from './actions.hook.js'
|
|
6
|
+
|
|
7
|
+
export function HostDialog(props: {
|
|
8
|
+
readonly children: (dialog: HostDialogApi) => ReactNode
|
|
9
|
+
}): ReactElement {
|
|
10
|
+
return <>{props.children(useHostDialog())}</>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Notify(props: {
|
|
14
|
+
readonly children: (notify: NotifyApi) => ReactNode
|
|
15
|
+
}): ReactElement {
|
|
16
|
+
return <>{props.children(useNotify())}</>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function NestedView(props: {
|
|
20
|
+
readonly children: (open: OpenViewApi) => ReactNode
|
|
21
|
+
}): ReactElement {
|
|
22
|
+
return <>{props.children(useOpenView())}</>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function RequestAccess(props: {
|
|
26
|
+
readonly children: (request: RequestAccessApi) => ReactNode
|
|
27
|
+
}): ReactElement {
|
|
28
|
+
return <>{props.children(useRequestAccess())}</>
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
useExternalOpen,
|
|
3
|
+
useHostCapabilities,
|
|
4
|
+
useHostDialog,
|
|
5
|
+
useNotify,
|
|
6
|
+
useOpenView,
|
|
7
|
+
useRequestAccess,
|
|
8
|
+
} from './actions.hook.js'
|
|
9
|
+
export type {
|
|
10
|
+
ExternalOpenApi,
|
|
11
|
+
HostDialogApi,
|
|
12
|
+
NotifyApi,
|
|
13
|
+
OpenViewApi,
|
|
14
|
+
RequestAccessApi,
|
|
15
|
+
} from './actions.hook.js'
|
|
16
|
+
export { HostDialog, NestedView, Notify, RequestAccess } from './components.js'
|
|
17
|
+
export { HostCapabilityUnavailableError } from './actions.hook.js'
|