@swimmesberger/elarion-jsonrpc-client-generator 0.2.7-preview.145.1 → 0.2.7-preview.147.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/session-client-source.js +125 -1
- package/package.json +1 -1
|
@@ -93,7 +93,8 @@ export function generateSessionClientSource(options) {
|
|
|
93
93
|
// Source: ${sourceLabel}
|
|
94
94
|
//
|
|
95
95
|
// Client-capability snapshot (operation: ${JSON.stringify(operationName)}). Fetch it with the generated RPC client
|
|
96
|
-
// and pass it to createSessionCapabilities / createElarionOpenFeatureProvider
|
|
96
|
+
// and pass it to createSessionCapabilities / createElarionOpenFeatureProvider, or hand the fetcher itself to
|
|
97
|
+
// createSessionCapabilitiesStore for a live snapshot you can refresh().
|
|
97
98
|
|
|
98
99
|
/** The current user's identity and raw grants, as projected into the snapshot. */
|
|
99
100
|
export interface ClientSnapshotUser {
|
|
@@ -110,6 +111,13 @@ export interface ClientSnapshot {
|
|
|
110
111
|
readonly modules: Readonly<Record<string, boolean>>
|
|
111
112
|
readonly flags: Readonly<Record<string, boolean>>
|
|
112
113
|
readonly variants: Readonly<Record<string, string>>
|
|
114
|
+
/**
|
|
115
|
+
* Application-contributed sections, keyed by section name — whatever the backend's
|
|
116
|
+
* IClientSnapshotContributor implementations returned. Absent when the host registers none, and a section
|
|
117
|
+
* whose contributor returned null is absent rather than null. Read one with
|
|
118
|
+
* \`capabilities.getSection<TenantSection>('tenant')\`.
|
|
119
|
+
*/
|
|
120
|
+
readonly sections?: Readonly<Record<string, unknown>>
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
${vocabularySection(capabilities)}
|
|
@@ -155,6 +163,20 @@ export class SessionCapabilities {
|
|
|
155
163
|
return this.snapshot.variants[name]
|
|
156
164
|
}
|
|
157
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Reads an application-contributed section, or \`undefined\` when the backend contributed none under that
|
|
168
|
+
* name. The type parameter is an assertion about the contributor's payload — the wire carries no runtime
|
|
169
|
+
* type information, so declare the section's shape in the app and keep it next to the contributor:
|
|
170
|
+
*
|
|
171
|
+
* \`\`\`ts
|
|
172
|
+
* interface TenantSection { readonly name: string; readonly theme: string }
|
|
173
|
+
* const tenant = capabilities.getSection<TenantSection>('tenant')
|
|
174
|
+
* \`\`\`
|
|
175
|
+
*/
|
|
176
|
+
getSection<T = unknown>(name: string): T | undefined {
|
|
177
|
+
return this.snapshot.sections?.[name] as T | undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
158
180
|
/** Resolves a reserved-namespace boolean key (the OpenFeature boolean read). */
|
|
159
181
|
resolveBoolean(key: string): boolean {
|
|
160
182
|
if (key.startsWith('module.')) return this.isModuleEnabled(key.slice('module.'.length))
|
|
@@ -168,6 +190,108 @@ export function createSessionCapabilities(snapshot: ClientSnapshot): SessionCapa
|
|
|
168
190
|
return new SessionCapabilities(snapshot)
|
|
169
191
|
}
|
|
170
192
|
|
|
193
|
+
/**
|
|
194
|
+
* A live capability snapshot: the same synchronous reads as \`SessionCapabilities\`, plus \`refresh()\` to
|
|
195
|
+
* re-fetch and \`subscribe()\` to learn that the answers changed. Its *identity is stable* while its answers
|
|
196
|
+
* change, so it is placed once — a router context, a DI provider, a React provider — and every later read
|
|
197
|
+
* sees the current snapshot.
|
|
198
|
+
*
|
|
199
|
+
* It structurally satisfies the \`CapabilitySource\` interface of \`@swimmesberger/elarion-contributions\`
|
|
200
|
+
* (the four reader methods plus \`subscribe\`), so it can be handed straight to
|
|
201
|
+
* \`createContributionRegistryStore(manifests, store)\`; neither package references the other.
|
|
202
|
+
*/
|
|
203
|
+
export interface SessionCapabilitiesStore {
|
|
204
|
+
/** The capabilities from the last successful refresh, or \`undefined\` before the first one completes. */
|
|
205
|
+
readonly current: SessionCapabilities | undefined
|
|
206
|
+
/**
|
|
207
|
+
* Re-fetches the snapshot, replaces \`current\`, and notifies subscribers. Call it after any action that can
|
|
208
|
+
* change what the user may see — a login, a tenant switch, a role or subscription change. Contributor-fed
|
|
209
|
+
* sections ride along automatically, since they are part of the same snapshot.
|
|
210
|
+
*
|
|
211
|
+
* A failed fetch rejects and leaves the previous answers in place, so a transient network error never
|
|
212
|
+
* silently strips the UI down to the fail-closed defaults. Overlapping refreshes are safe: results are
|
|
213
|
+
* applied in call order, so a slower earlier fetch can never overwrite a newer snapshot. Each caller still
|
|
214
|
+
* receives its own fetch's capabilities.
|
|
215
|
+
*/
|
|
216
|
+
refresh(): Promise<SessionCapabilities>
|
|
217
|
+
/** Registers a change listener; returns its own unsubscribe function. */
|
|
218
|
+
subscribe(listener: () => void): () => void
|
|
219
|
+
isModuleEnabled(name: ModuleName | (string & {})): boolean
|
|
220
|
+
hasPermission(permission: PermissionName | (string & {})): boolean
|
|
221
|
+
hasRole(role: RoleName | (string & {})): boolean
|
|
222
|
+
isFlagEnabled(name: FlagName | (string & {})): boolean
|
|
223
|
+
getVariant(name: FlagName | (string & {})): string | undefined
|
|
224
|
+
getSection<T = unknown>(name: string): T | undefined
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Builds a {@link SessionCapabilitiesStore} over a snapshot fetcher. The fetcher is injected rather than
|
|
229
|
+
* built in, which is what keeps this module import-free: the caller supplies the transport, typically the
|
|
230
|
+
* generated RPC client.
|
|
231
|
+
*
|
|
232
|
+
* Every read **before the first successful refresh fails closed** — modules, permissions, roles, and flags
|
|
233
|
+
* all answer \`false\`, variants and sections \`undefined\` — so UI gated on a capability stays hidden while
|
|
234
|
+
* the snapshot is in flight rather than flashing and retracting.
|
|
235
|
+
*
|
|
236
|
+
* \`\`\`ts
|
|
237
|
+
* const capabilities = createSessionCapabilitiesStore(
|
|
238
|
+
* async () => (await rpc.elarion.session({})) as ClientSnapshot
|
|
239
|
+
* )
|
|
240
|
+
* await capabilities.refresh()
|
|
241
|
+
* \`\`\`
|
|
242
|
+
*/
|
|
243
|
+
export function createSessionCapabilitiesStore(
|
|
244
|
+
fetchSnapshot: () => Promise<ClientSnapshot>
|
|
245
|
+
): SessionCapabilitiesStore {
|
|
246
|
+
let current: SessionCapabilities | undefined
|
|
247
|
+
// Monotonic call token: concurrent refreshes (a login and a route load, say) can settle out of order, and
|
|
248
|
+
// applying a slower *earlier* fetch last would leave the UI on a stale snapshot. Only a result no newer
|
|
249
|
+
// call has superseded is applied; every caller still gets its own fetch's capabilities back.
|
|
250
|
+
let issued = 0
|
|
251
|
+
let applied = 0
|
|
252
|
+
const listeners = new Set<() => void>()
|
|
253
|
+
return {
|
|
254
|
+
get current() {
|
|
255
|
+
return current
|
|
256
|
+
},
|
|
257
|
+
async refresh() {
|
|
258
|
+
const token = ++issued
|
|
259
|
+
const capabilities = new SessionCapabilities(await fetchSnapshot())
|
|
260
|
+
if (token > applied) {
|
|
261
|
+
applied = token
|
|
262
|
+
current = capabilities
|
|
263
|
+
// Iterate a copy so a listener that unsubscribes itself cannot skip a sibling.
|
|
264
|
+
for (const listener of [...listeners]) listener()
|
|
265
|
+
}
|
|
266
|
+
return capabilities
|
|
267
|
+
},
|
|
268
|
+
subscribe(listener) {
|
|
269
|
+
listeners.add(listener)
|
|
270
|
+
return () => {
|
|
271
|
+
listeners.delete(listener)
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
isModuleEnabled(name) {
|
|
275
|
+
return current?.isModuleEnabled(name) ?? false
|
|
276
|
+
},
|
|
277
|
+
hasPermission(permission) {
|
|
278
|
+
return current?.hasPermission(permission) ?? false
|
|
279
|
+
},
|
|
280
|
+
hasRole(role) {
|
|
281
|
+
return current?.hasRole(role) ?? false
|
|
282
|
+
},
|
|
283
|
+
isFlagEnabled(name) {
|
|
284
|
+
return current?.isFlagEnabled(name) ?? false
|
|
285
|
+
},
|
|
286
|
+
getVariant(name) {
|
|
287
|
+
return current?.getVariant(name)
|
|
288
|
+
},
|
|
289
|
+
getSection<T = unknown>(name: string) {
|
|
290
|
+
return current?.getSection<T>(name)
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
171
295
|
/** A single flag resolution result (structurally compatible with OpenFeature's ResolutionDetails). */
|
|
172
296
|
export interface ResolutionDetails<T> {
|
|
173
297
|
readonly value: T
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swimmesberger/elarion-jsonrpc-client-generator",
|
|
3
|
-
"version": "0.2.7-preview.
|
|
3
|
+
"version": "0.2.7-preview.147.1",
|
|
4
4
|
"description": "Generate TypeScript RPC contracts, Zod schemas, and a fetch client from Elarion JSON-RPC schema exports.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|