@swimmesberger/elarion-jsonrpc-client-generator 0.2.7-preview.145.1 → 0.2.7-preview.146.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.
@@ -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.145.1",
3
+ "version": "0.2.7-preview.146.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",