@astrale-os/shell-react 0.4.0-beta.17 → 0.4.0-beta.18
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/auth/act-as.context.js +26 -12
- package/dist/graph/query-definition.hook.js +16 -11
- package/dist/graph/resource.js +11 -3
- package/dist/graph/store.d.ts +5 -5
- package/dist/graph/store.js +13 -13
- package/dist/session/provider.js +3 -3
- package/package.json +3 -3
- package/src/auth/act-as.context.tsx +32 -17
- package/src/graph/query-definition.hook.ts +16 -12
- package/src/graph/resource.ts +19 -13
- package/src/graph/store.ts +16 -16
- package/src/session/provider.tsx +3 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect,
|
|
2
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
3
|
+
import { GraphContext } from '../graph/graph.context.js';
|
|
4
|
+
import { resolveGraphStoreScope, retainStore, storeFor } from '../graph/store.js';
|
|
3
5
|
import { ShellContext } from '../session/session.context.js';
|
|
4
6
|
export const IdentityContext = createContext(null);
|
|
5
7
|
IdentityContext.displayName = 'AstraleIdentityContext';
|
|
@@ -19,22 +21,34 @@ export function ActAs({ client, fallback, children }) {
|
|
|
19
21
|
const [state, setState] = useState({ status: 'pending' });
|
|
20
22
|
useEffect(() => {
|
|
21
23
|
let active = true;
|
|
24
|
+
let release;
|
|
22
25
|
setState({ status: 'pending' });
|
|
23
|
-
void
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
void (async () => {
|
|
27
|
+
try {
|
|
28
|
+
const self = await client.auth.whoami();
|
|
29
|
+
if (!active)
|
|
30
|
+
return;
|
|
31
|
+
const scope = await resolveGraphStoreScope(client, self);
|
|
32
|
+
if (!active)
|
|
33
|
+
return;
|
|
34
|
+
const entry = storeFor(client, scope);
|
|
35
|
+
release = retainStore(entry);
|
|
36
|
+
setState({ status: 'ready', binding: { kernel: client, self }, store: entry.store });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (active)
|
|
40
|
+
setState({ status: 'failed', client, error });
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
30
43
|
return () => {
|
|
31
44
|
active = false;
|
|
45
|
+
release?.();
|
|
32
46
|
};
|
|
33
47
|
}, [client]);
|
|
34
|
-
|
|
35
|
-
if (state.status === 'failed')
|
|
48
|
+
if (state.status === 'failed' && state.client === client)
|
|
36
49
|
throw state.error;
|
|
37
|
-
if (
|
|
50
|
+
if (state.status !== 'ready' || state.binding.kernel !== client) {
|
|
38
51
|
return fallback === undefined ? null : _jsx(_Fragment, { children: fallback });
|
|
39
|
-
|
|
52
|
+
}
|
|
53
|
+
return (_jsx(IdentityContext.Provider, { value: state.binding, children: _jsx(GraphContext.Provider, { value: state.store, children: children }) }));
|
|
40
54
|
}
|
|
@@ -183,7 +183,7 @@ function combine(snapshots) {
|
|
|
183
183
|
return 'ready';
|
|
184
184
|
}
|
|
185
185
|
function stableIdentity(value) {
|
|
186
|
-
const
|
|
186
|
+
const ancestors = new WeakSet();
|
|
187
187
|
const encode = (input) => {
|
|
188
188
|
if (input === null)
|
|
189
189
|
return ['null'];
|
|
@@ -198,17 +198,22 @@ function stableIdentity(value) {
|
|
|
198
198
|
}
|
|
199
199
|
return [typeof input, input];
|
|
200
200
|
}
|
|
201
|
-
if (
|
|
201
|
+
if (ancestors.has(input))
|
|
202
202
|
throw new TypeError('Query input cannot contain cycles.');
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
.
|
|
210
|
-
|
|
211
|
-
|
|
203
|
+
ancestors.add(input);
|
|
204
|
+
try {
|
|
205
|
+
if (Array.isArray(input))
|
|
206
|
+
return ['array', input.map(encode)];
|
|
207
|
+
return [
|
|
208
|
+
'object',
|
|
209
|
+
Object.fromEntries(Object.keys(input)
|
|
210
|
+
.sort()
|
|
211
|
+
.map((key) => [key, encode(input[key])])),
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
ancestors.delete(input);
|
|
216
|
+
}
|
|
212
217
|
};
|
|
213
218
|
return JSON.stringify(encode(value));
|
|
214
219
|
}
|
package/dist/graph/resource.js
CHANGED
|
@@ -227,19 +227,27 @@ export function retainRefreshScheduler(store, identity, refresh, interval, when)
|
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
function schedulePoll(scheduler, immediate = false) {
|
|
230
|
-
if (scheduler.timer !== undefined)
|
|
231
|
-
clearTimeout(scheduler.timer);
|
|
232
230
|
const subscribers = [...scheduler.subscribers.values()];
|
|
233
231
|
if (subscribers.length === 0)
|
|
234
232
|
return;
|
|
235
233
|
const interval = Math.min(...subscribers.map((subscriber) => subscriber.interval));
|
|
234
|
+
const delay = immediate ? 0 : interval;
|
|
235
|
+
const dueAt = Date.now() + delay;
|
|
236
|
+
// Observer changes may advance a refresh, but must not postpone pending work.
|
|
237
|
+
if (scheduler.timer !== undefined) {
|
|
238
|
+
if (scheduler.dueAt !== undefined && scheduler.dueAt <= dueAt)
|
|
239
|
+
return;
|
|
240
|
+
clearTimeout(scheduler.timer);
|
|
241
|
+
}
|
|
242
|
+
scheduler.dueAt = dueAt;
|
|
236
243
|
scheduler.timer = setTimeout(() => {
|
|
237
244
|
scheduler.timer = undefined;
|
|
245
|
+
scheduler.dueAt = undefined;
|
|
238
246
|
const visible = typeof document === 'undefined' || document.visibilityState !== 'hidden';
|
|
239
247
|
const eligible = [...scheduler.subscribers.values()].filter((subscriber) => subscriber.when === 'always' || visible);
|
|
240
248
|
eligible.forEach((subscriber) => void subscriber.refresh().catch(() => { }));
|
|
241
249
|
schedulePoll(scheduler);
|
|
242
|
-
},
|
|
250
|
+
}, delay);
|
|
243
251
|
}
|
|
244
252
|
function positivePage(input) {
|
|
245
253
|
if (!Number.isSafeInteger(input) || input < 1) {
|
package/dist/graph/store.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { GraphApi } from '@astrale-os/sdk/client';
|
|
2
2
|
import type { GraphStore as GraphStoreValue } from '@astrale-os/sdk/client/store';
|
|
3
|
-
import type {
|
|
3
|
+
import type { KernelClient } from '@astrale-os/shell';
|
|
4
4
|
interface StoreEntry {
|
|
5
|
-
readonly
|
|
5
|
+
readonly client: KernelClient;
|
|
6
6
|
readonly graph: GraphApi;
|
|
7
7
|
readonly scope: string;
|
|
8
8
|
readonly store: GraphStoreValue;
|
|
@@ -15,9 +15,9 @@ export interface GraphStoreScope {
|
|
|
15
15
|
readonly authority: string;
|
|
16
16
|
readonly schemaRevision: string;
|
|
17
17
|
}
|
|
18
|
-
type StoreIdentity = Awaited<ReturnType<
|
|
19
|
-
export declare function resolveGraphStoreScope(
|
|
20
|
-
export declare function storeFor(
|
|
18
|
+
type StoreIdentity = Awaited<ReturnType<KernelClient['auth']['whoami']>>;
|
|
19
|
+
export declare function resolveGraphStoreScope(client: KernelClient, knownIdentity?: StoreIdentity | null): Promise<GraphStoreScope>;
|
|
20
|
+
export declare function storeFor(client: KernelClient, scope: GraphStoreScope): StoreEntry;
|
|
21
21
|
export declare function retainStore(entry: StoreEntry): () => void;
|
|
22
22
|
/** The exact caller/schema-scoped GraphStore for the current View subtree. */
|
|
23
23
|
export declare function useGraphStore(): GraphStoreValue;
|
package/dist/graph/store.js
CHANGED
|
@@ -2,39 +2,39 @@ import { GraphStore } from '@astrale-os/sdk/client/store';
|
|
|
2
2
|
import { useContext } from 'react';
|
|
3
3
|
import { GraphContext } from './graph.context.js';
|
|
4
4
|
const entries = new WeakMap();
|
|
5
|
-
export async function resolveGraphStoreScope(
|
|
5
|
+
export async function resolveGraphStoreScope(client, knownIdentity) {
|
|
6
6
|
const [identity, snapshot] = await Promise.all([
|
|
7
7
|
knownIdentity === undefined
|
|
8
|
-
?
|
|
8
|
+
? client.auth.whoami().catch(() => null)
|
|
9
9
|
: Promise.resolve(knownIdentity),
|
|
10
|
-
|
|
10
|
+
client.snapshot(),
|
|
11
11
|
]);
|
|
12
12
|
return Object.freeze({
|
|
13
13
|
authority: identity === null ? 'anonymous' : `${identity.iss}\u0000${identity.sub}\u0000${identity.id}`,
|
|
14
14
|
schemaRevision: snapshot.publication.schema.revision,
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
-
export function storeFor(
|
|
18
|
-
const current = entries.get(
|
|
17
|
+
export function storeFor(client, scope) {
|
|
18
|
+
const current = entries.get(client);
|
|
19
19
|
const scopeKey = `${scope.authority}\u0000${scope.schemaRevision}`;
|
|
20
|
-
if (current !== undefined && current.graph ===
|
|
20
|
+
if (current !== undefined && current.graph === client.graph && current.scope === scopeKey) {
|
|
21
21
|
return current;
|
|
22
22
|
}
|
|
23
|
-
const supplied =
|
|
23
|
+
const supplied = client.store;
|
|
24
24
|
const created = {
|
|
25
|
-
|
|
26
|
-
graph:
|
|
25
|
+
client,
|
|
26
|
+
graph: client.graph,
|
|
27
27
|
scope: scopeKey,
|
|
28
28
|
// A supplied Store is trusted only for the KernelClient's first declared scope. A later
|
|
29
29
|
// authority/revision/GraphApi replacement gets a new Store and cannot inherit old memory.
|
|
30
30
|
store: current === undefined && supplied !== undefined
|
|
31
31
|
? supplied
|
|
32
|
-
: new GraphStore({ graph:
|
|
32
|
+
: new GraphStore({ graph: client.graph }),
|
|
33
33
|
owned: current !== undefined || supplied === undefined,
|
|
34
34
|
retained: 0,
|
|
35
35
|
generation: 0,
|
|
36
36
|
};
|
|
37
|
-
entries.set(
|
|
37
|
+
entries.set(client, created);
|
|
38
38
|
if (current !== undefined && current.retained === 0 && current.owned)
|
|
39
39
|
current.store.close();
|
|
40
40
|
return created;
|
|
@@ -52,8 +52,8 @@ export function retainStore(entry) {
|
|
|
52
52
|
queueMicrotask(() => {
|
|
53
53
|
if (entry.retained !== 0 || entry.generation !== generation || !entry.owned)
|
|
54
54
|
return;
|
|
55
|
-
if (entries.get(entry.
|
|
56
|
-
entries.delete(entry.
|
|
55
|
+
if (entries.get(entry.client) === entry)
|
|
56
|
+
entries.delete(entry.client);
|
|
57
57
|
entry.store.close();
|
|
58
58
|
});
|
|
59
59
|
};
|
package/dist/session/provider.js
CHANGED
|
@@ -45,10 +45,10 @@ export function ShellProvider(props) {
|
|
|
45
45
|
await primeUser(shell);
|
|
46
46
|
if (generation !== ref.generation)
|
|
47
47
|
return;
|
|
48
|
-
const storeScope = await resolveGraphStoreScope(shell, self);
|
|
48
|
+
const storeScope = await resolveGraphStoreScope(shell.kernel, self);
|
|
49
49
|
if (generation !== ref.generation)
|
|
50
50
|
return;
|
|
51
|
-
const graph = storeFor(shell, storeScope);
|
|
51
|
+
const graph = storeFor(shell.kernel, storeScope);
|
|
52
52
|
dispatch({
|
|
53
53
|
type: 'ready',
|
|
54
54
|
runtime: { shell, self, storeScope, store: graph.store, ownsStore: graph.owned },
|
|
@@ -105,7 +105,7 @@ export function ShellProvider(props) {
|
|
|
105
105
|
useEffect(() => {
|
|
106
106
|
if (shell === null || store === null || storeScope === null)
|
|
107
107
|
return;
|
|
108
|
-
return retainStore(storeFor(shell, storeScope));
|
|
108
|
+
return retainStore(storeFor(shell.kernel, storeScope));
|
|
109
109
|
}, [shell, store, storeScope]);
|
|
110
110
|
if (state.status === 'error') {
|
|
111
111
|
if (errorFallback !== undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/shell-react",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.18",
|
|
4
4
|
"description": "Astrale shell-react — the React face of the shell (provider, hooks, view components)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"zod": "4.4.3"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@astrale-os/sdk": "0.5.0-beta.
|
|
34
|
+
"@astrale-os/sdk": "0.5.0-beta.136",
|
|
35
35
|
"@testing-library/dom": "^10.4.0",
|
|
36
36
|
"@testing-library/react": "^16.3.0",
|
|
37
37
|
"@types/node": "^26.2.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"vitest": "4.1.10"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@astrale-os/sdk": ">=0.5.0-beta.
|
|
49
|
+
"@astrale-os/sdk": ">=0.5.0-beta.136 <1.0.0",
|
|
50
50
|
"react": ">=18",
|
|
51
51
|
"react-dom": ">=18"
|
|
52
52
|
},
|
|
@@ -2,8 +2,10 @@ import type { Identity } from '@astrale-os/sdk/auth'
|
|
|
2
2
|
import type { KernelClient } from '@astrale-os/shell'
|
|
3
3
|
import type { ReactElement, ReactNode } from 'react'
|
|
4
4
|
|
|
5
|
-
import { createContext, useContext, useEffect,
|
|
5
|
+
import { createContext, useContext, useEffect, useState } from 'react'
|
|
6
6
|
|
|
7
|
+
import { GraphContext } from '../graph/graph.context.js'
|
|
8
|
+
import { resolveGraphStoreScope, retainStore, storeFor } from '../graph/store.js'
|
|
7
9
|
import { ShellContext } from '../session/session.context.js'
|
|
8
10
|
|
|
9
11
|
export type NodeValue = Identity
|
|
@@ -40,31 +42,44 @@ export interface ActAsProps {
|
|
|
40
42
|
export function ActAs({ client, fallback, children }: ActAsProps): ReactElement | null {
|
|
41
43
|
const [state, setState] = useState<
|
|
42
44
|
| { readonly status: 'pending' }
|
|
43
|
-
| {
|
|
44
|
-
|
|
45
|
+
| {
|
|
46
|
+
readonly status: 'ready'
|
|
47
|
+
readonly binding: IdentityBinding
|
|
48
|
+
readonly store: ReturnType<typeof storeFor>['store']
|
|
49
|
+
}
|
|
50
|
+
| { readonly status: 'failed'; readonly client: KernelClient; readonly error: unknown }
|
|
45
51
|
>({ status: 'pending' })
|
|
46
52
|
|
|
47
53
|
useEffect(() => {
|
|
48
54
|
let active = true
|
|
55
|
+
let release: (() => void) | undefined
|
|
49
56
|
setState({ status: 'pending' })
|
|
50
|
-
void
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (active)
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
void (async () => {
|
|
58
|
+
try {
|
|
59
|
+
const self = await client.auth.whoami()
|
|
60
|
+
if (!active) return
|
|
61
|
+
const scope = await resolveGraphStoreScope(client, self)
|
|
62
|
+
if (!active) return
|
|
63
|
+
const entry = storeFor(client, scope)
|
|
64
|
+
release = retainStore(entry)
|
|
65
|
+
setState({ status: 'ready', binding: { kernel: client, self }, store: entry.store })
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (active) setState({ status: 'failed', client, error })
|
|
68
|
+
}
|
|
69
|
+
})()
|
|
58
70
|
return () => {
|
|
59
71
|
active = false
|
|
72
|
+
release?.()
|
|
60
73
|
}
|
|
61
74
|
}, [client])
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
if (state.status === 'failed' && state.client === client) throw state.error
|
|
77
|
+
if (state.status !== 'ready' || state.binding.kernel !== client) {
|
|
78
|
+
return fallback === undefined ? null : <>{fallback}</>
|
|
79
|
+
}
|
|
80
|
+
return (
|
|
81
|
+
<IdentityContext.Provider value={state.binding}>
|
|
82
|
+
<GraphContext.Provider value={state.store}>{children}</GraphContext.Provider>
|
|
83
|
+
</IdentityContext.Provider>
|
|
66
84
|
)
|
|
67
|
-
if (state.status === 'failed') throw state.error
|
|
68
|
-
if (value === null) return fallback === undefined ? null : <>{fallback}</>
|
|
69
|
-
return <IdentityContext.Provider value={value}>{children}</IdentityContext.Provider>
|
|
70
85
|
}
|
|
@@ -251,7 +251,7 @@ function combine(snapshots: readonly StoreSnapshot[]): GraphResourceState {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
function stableIdentity(value: unknown): string {
|
|
254
|
-
const
|
|
254
|
+
const ancestors = new WeakSet<object>()
|
|
255
255
|
const encode = (input: unknown): unknown => {
|
|
256
256
|
if (input === null) return ['null']
|
|
257
257
|
if (typeof input !== 'object') {
|
|
@@ -264,17 +264,21 @@ function stableIdentity(value: unknown): string {
|
|
|
264
264
|
}
|
|
265
265
|
return [typeof input, input]
|
|
266
266
|
}
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
Object.
|
|
274
|
-
.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
267
|
+
if (ancestors.has(input)) throw new TypeError('Query input cannot contain cycles.')
|
|
268
|
+
ancestors.add(input)
|
|
269
|
+
try {
|
|
270
|
+
if (Array.isArray(input)) return ['array', input.map(encode)]
|
|
271
|
+
return [
|
|
272
|
+
'object',
|
|
273
|
+
Object.fromEntries(
|
|
274
|
+
Object.keys(input as Record<string, unknown>)
|
|
275
|
+
.sort()
|
|
276
|
+
.map((key) => [key, encode((input as Record<string, unknown>)[key])]),
|
|
277
|
+
),
|
|
278
|
+
]
|
|
279
|
+
} finally {
|
|
280
|
+
ancestors.delete(input)
|
|
281
|
+
}
|
|
278
282
|
}
|
|
279
283
|
return JSON.stringify(encode(value))
|
|
280
284
|
}
|
package/src/graph/resource.ts
CHANGED
|
@@ -176,6 +176,7 @@ interface PollSubscriber {
|
|
|
176
176
|
interface PollScheduler {
|
|
177
177
|
readonly subscribers: Map<symbol, PollSubscriber>
|
|
178
178
|
timer: ReturnType<typeof setTimeout> | undefined
|
|
179
|
+
dueAt?: number
|
|
179
180
|
onVisibility?: () => void
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -344,22 +345,27 @@ export function retainRefreshScheduler(
|
|
|
344
345
|
}
|
|
345
346
|
|
|
346
347
|
function schedulePoll(scheduler: PollScheduler, immediate = false): void {
|
|
347
|
-
if (scheduler.timer !== undefined) clearTimeout(scheduler.timer)
|
|
348
348
|
const subscribers = [...scheduler.subscribers.values()]
|
|
349
349
|
if (subscribers.length === 0) return
|
|
350
350
|
const interval = Math.min(...subscribers.map((subscriber) => subscriber.interval))
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
351
|
+
const delay = immediate ? 0 : interval
|
|
352
|
+
const dueAt = Date.now() + delay
|
|
353
|
+
// Observer changes may advance a refresh, but must not postpone pending work.
|
|
354
|
+
if (scheduler.timer !== undefined) {
|
|
355
|
+
if (scheduler.dueAt !== undefined && scheduler.dueAt <= dueAt) return
|
|
356
|
+
clearTimeout(scheduler.timer)
|
|
357
|
+
}
|
|
358
|
+
scheduler.dueAt = dueAt
|
|
359
|
+
scheduler.timer = setTimeout(() => {
|
|
360
|
+
scheduler.timer = undefined
|
|
361
|
+
scheduler.dueAt = undefined
|
|
362
|
+
const visible = typeof document === 'undefined' || document.visibilityState !== 'hidden'
|
|
363
|
+
const eligible = [...scheduler.subscribers.values()].filter(
|
|
364
|
+
(subscriber) => subscriber.when === 'always' || visible,
|
|
365
|
+
)
|
|
366
|
+
eligible.forEach((subscriber) => void subscriber.refresh().catch(() => {}))
|
|
367
|
+
schedulePoll(scheduler)
|
|
368
|
+
}, delay)
|
|
363
369
|
}
|
|
364
370
|
|
|
365
371
|
function positivePage(input: number): number {
|
package/src/graph/store.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GraphApi } from '@astrale-os/sdk/client'
|
|
2
2
|
import type { GraphStore as GraphStoreValue } from '@astrale-os/sdk/client/store'
|
|
3
|
-
import type {
|
|
3
|
+
import type { KernelClient } from '@astrale-os/shell'
|
|
4
4
|
|
|
5
5
|
import { GraphStore } from '@astrale-os/sdk/client/store'
|
|
6
6
|
import { useContext } from 'react'
|
|
@@ -8,7 +8,7 @@ import { useContext } from 'react'
|
|
|
8
8
|
import { GraphContext } from './graph.context.js'
|
|
9
9
|
|
|
10
10
|
interface StoreEntry {
|
|
11
|
-
readonly
|
|
11
|
+
readonly client: KernelClient
|
|
12
12
|
readonly graph: GraphApi
|
|
13
13
|
readonly scope: string
|
|
14
14
|
readonly store: GraphStoreValue
|
|
@@ -17,7 +17,7 @@ interface StoreEntry {
|
|
|
17
17
|
generation: number
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const entries = new WeakMap<
|
|
20
|
+
const entries = new WeakMap<KernelClient, StoreEntry>()
|
|
21
21
|
|
|
22
22
|
/** Authority/schema identity. Session ids and Publication ETags are deliberately absent. */
|
|
23
23
|
export interface GraphStoreScope {
|
|
@@ -25,17 +25,17 @@ export interface GraphStoreScope {
|
|
|
25
25
|
readonly schemaRevision: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
type StoreIdentity = Awaited<ReturnType<
|
|
28
|
+
type StoreIdentity = Awaited<ReturnType<KernelClient['auth']['whoami']>>
|
|
29
29
|
|
|
30
30
|
export async function resolveGraphStoreScope(
|
|
31
|
-
|
|
31
|
+
client: KernelClient,
|
|
32
32
|
knownIdentity?: StoreIdentity | null,
|
|
33
33
|
): Promise<GraphStoreScope> {
|
|
34
34
|
const [identity, snapshot] = await Promise.all([
|
|
35
35
|
knownIdentity === undefined
|
|
36
|
-
?
|
|
36
|
+
? client.auth.whoami().catch(() => null)
|
|
37
37
|
: Promise.resolve(knownIdentity),
|
|
38
|
-
|
|
38
|
+
client.snapshot(),
|
|
39
39
|
])
|
|
40
40
|
return Object.freeze({
|
|
41
41
|
authority:
|
|
@@ -44,28 +44,28 @@ export async function resolveGraphStoreScope(
|
|
|
44
44
|
})
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function storeFor(
|
|
48
|
-
const current = entries.get(
|
|
47
|
+
export function storeFor(client: KernelClient, scope: GraphStoreScope): StoreEntry {
|
|
48
|
+
const current = entries.get(client)
|
|
49
49
|
const scopeKey = `${scope.authority}\u0000${scope.schemaRevision}`
|
|
50
|
-
if (current !== undefined && current.graph ===
|
|
50
|
+
if (current !== undefined && current.graph === client.graph && current.scope === scopeKey) {
|
|
51
51
|
return current
|
|
52
52
|
}
|
|
53
|
-
const supplied =
|
|
53
|
+
const supplied = client.store
|
|
54
54
|
const created: StoreEntry = {
|
|
55
|
-
|
|
56
|
-
graph:
|
|
55
|
+
client,
|
|
56
|
+
graph: client.graph,
|
|
57
57
|
scope: scopeKey,
|
|
58
58
|
// A supplied Store is trusted only for the KernelClient's first declared scope. A later
|
|
59
59
|
// authority/revision/GraphApi replacement gets a new Store and cannot inherit old memory.
|
|
60
60
|
store:
|
|
61
61
|
current === undefined && supplied !== undefined
|
|
62
62
|
? supplied
|
|
63
|
-
: new GraphStore({ graph:
|
|
63
|
+
: new GraphStore({ graph: client.graph }),
|
|
64
64
|
owned: current !== undefined || supplied === undefined,
|
|
65
65
|
retained: 0,
|
|
66
66
|
generation: 0,
|
|
67
67
|
}
|
|
68
|
-
entries.set(
|
|
68
|
+
entries.set(client, created)
|
|
69
69
|
if (current !== undefined && current.retained === 0 && current.owned) current.store.close()
|
|
70
70
|
return created
|
|
71
71
|
}
|
|
@@ -81,7 +81,7 @@ export function retainStore(entry: StoreEntry): () => void {
|
|
|
81
81
|
const generation = ++entry.generation
|
|
82
82
|
queueMicrotask(() => {
|
|
83
83
|
if (entry.retained !== 0 || entry.generation !== generation || !entry.owned) return
|
|
84
|
-
if (entries.get(entry.
|
|
84
|
+
if (entries.get(entry.client) === entry) entries.delete(entry.client)
|
|
85
85
|
entry.store.close()
|
|
86
86
|
})
|
|
87
87
|
}
|
package/src/session/provider.tsx
CHANGED
|
@@ -61,9 +61,9 @@ export function ShellProvider(props: ShellProviderProps): ReactElement | null {
|
|
|
61
61
|
if (generation !== ref.generation) return
|
|
62
62
|
await primeUser(shell)
|
|
63
63
|
if (generation !== ref.generation) return
|
|
64
|
-
const storeScope = await resolveGraphStoreScope(shell, self)
|
|
64
|
+
const storeScope = await resolveGraphStoreScope(shell.kernel, self)
|
|
65
65
|
if (generation !== ref.generation) return
|
|
66
|
-
const graph = storeFor(shell, storeScope)
|
|
66
|
+
const graph = storeFor(shell.kernel, storeScope)
|
|
67
67
|
dispatch({
|
|
68
68
|
type: 'ready',
|
|
69
69
|
runtime: { shell, self, storeScope, store: graph.store, ownsStore: graph.owned },
|
|
@@ -117,7 +117,7 @@ export function ShellProvider(props: ShellProviderProps): ReactElement | null {
|
|
|
117
117
|
const storeScope = state.runtime?.storeScope ?? null
|
|
118
118
|
useEffect(() => {
|
|
119
119
|
if (shell === null || store === null || storeScope === null) return
|
|
120
|
-
return retainStore(storeFor(shell, storeScope))
|
|
120
|
+
return retainStore(storeFor(shell.kernel, storeScope))
|
|
121
121
|
}, [shell, store, storeScope])
|
|
122
122
|
|
|
123
123
|
if (state.status === 'error') {
|