C6: Runtime kernel owns data, status, error — adapters subscribe

The kernel is no longer a blind refetch pipe. Each context entry has:
  { data, status: idle|loading|success|error, error }

registerContext() returns { getState, subscribe, refetch, unregister }.
Adapters subscribe to state changes via callbacks. The kernel does
the fetch and notifies subscribers with the new state.

React adapter uses useSyncExternalStore for tear-free reads.
Vue adapter uses ref + subscribe callback.
Svelte adapter uses readable store backed by kernel subscription.

All three adapters also get:
- Mutation hooks with { mutate, isPending, error } (fixes H5)
- Vue: onServerPrefetch for Nuxt SSR (fixes M9)
- Svelte: readable store auto-cleans up on unsubscribe (fixes H9)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 12:38:53 -04:00
parent 07f1c7842c
commit bb88fd984b
7 changed files with 586 additions and 361 deletions

View File

@@ -1,96 +1,229 @@
// AUTO-GENERATED by mizan — do not edit
import { ref, computed, watch, onMounted, onUnmounted, provide, inject, type Ref, type ComputedRef, type InjectionKey } from 'vue'
import { registerContext } from '@mizan/runtime'
import { ref, computed, onMounted, onUnmounted, onServerPrefetch, type ComputedRef } from 'vue'
import { registerContext, type ContextState } from '@mizan/runtime'
import { fetchGlobalContext, type GlobalContextData, type GlobalContextParams, fetchLocalContext, type LocalContextData, type LocalContextParams, callEcho, callAdd, callWhoami, callHttpOnlyEcho, callStaffOnly, callSuperuserOnly, callVerifiedOnly, callMultiply, callNotImplementedFn, callBuggyFn, callPermissionCheckFn, callWsWhoami, callJwtObtain, callJwtRefresh } from '../index'
// Global context
const GlobalKey: InjectionKey<{ data: Ref<GlobalContextData | null>, loading: Ref<boolean> }> = Symbol('global')
export function useGlobalContext() {
const state = ref<ContextState<GlobalContextData>>({ data: null, status: 'idle', error: null })
let handle: ReturnType<typeof registerContext> | null = null
export function provideGlobalContext() {
const data = ref<GlobalContextData | null>(null)
const loading = ref(true)
const refetch = async () => {
loading.value = true
try {
data.value = await fetchGlobalContext({} as any)
} catch (e) { console.error('[mizan] global fetch failed:', e) }
loading.value = false
}
let unregister: (() => void) | null = null
onMounted(() => {
refetch()
unregister = registerContext('global', {}, refetch)
handle = registerContext('global', {} as any, () => fetchGlobalContext({} as any))
handle.subscribe(() => { state.value = handle!.getState() })
handle.refetch()
})
onUnmounted(() => { unregister?.() })
provide(GlobalKey, { data, loading })
}
onServerPrefetch(async () => {
handle = registerContext('global', {} as any, () => fetchGlobalContext({} as any))
await handle.refetch()
state.value = handle.getState()
})
export function useCurrentUser(): ComputedRef<currentUserOutput | null> {
const ctx = inject(GlobalKey)
if (!ctx) throw new Error('useCurrentUser requires provideGlobalContext in a parent')
return computed(() => ctx.data.value?.current_user ?? null)
}
onUnmounted(() => { handle?.unregister() })
// Local context
const LocalKey: InjectionKey<{ data: Ref<LocalContextData | null>, loading: Ref<boolean> }> = Symbol('local')
export function provideLocalContext(params: { name: string }) {
const data = ref<LocalContextData | null>(null)
const loading = ref(true)
const refetch = async () => {
loading.value = true
try {
data.value = await fetchLocalContext(params as any)
} catch (e) { console.error('[mizan] local fetch failed:', e) }
loading.value = false
return {
state,
currentUser: computed(() => state.value.data?.current_user ?? null) as ComputedRef<currentUserOutput | null>,
loading: computed(() => state.value.status === 'loading'),
error: computed(() => state.value.error),
}
}
export function useLocalContext(params: LocalContextParams) {
const state = ref<ContextState<LocalContextData>>({ data: null, status: 'idle', error: null })
let handle: ReturnType<typeof registerContext> | null = null
let unregister: (() => void) | null = null
onMounted(() => {
refetch()
unregister = registerContext('local', params, refetch)
handle = registerContext('local', params, () => fetchLocalContext(params))
handle.subscribe(() => { state.value = handle!.getState() })
handle.refetch()
})
onUnmounted(() => { unregister?.() })
provide(LocalKey, { data, loading })
onServerPrefetch(async () => {
handle = registerContext('local', params, () => fetchLocalContext(params))
await handle.refetch()
state.value = handle.getState()
})
onUnmounted(() => { handle?.unregister() })
return {
state,
greet: computed(() => state.value.data?.greet ?? null) as ComputedRef<greetOutput | null>,
loading: computed(() => state.value.status === 'loading'),
error: computed(() => state.value.error),
}
}
export function useGreet(): ComputedRef<greetOutput | null> {
const ctx = inject(LocalKey)
if (!ctx) throw new Error('useGreet requires provideLocalContext in a parent')
return computed(() => ctx.data.value?.greet ?? null)
export function useEcho() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate(args: Parameters<typeof callEcho>[0]) {
isPending.value = true; error.value = null
try { return await callEcho(args) }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useEcho = callEcho
export function useAdd() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate(args: Parameters<typeof callAdd>[0]) {
isPending.value = true; error.value = null
try { return await callAdd(args) }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useAdd = callAdd
export function useWhoami() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate() {
isPending.value = true; error.value = null
try { return await callWhoami() }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useWhoami = callWhoami
export function useHttpOnlyEcho() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate(args: Parameters<typeof callHttpOnlyEcho>[0]) {
isPending.value = true; error.value = null
try { return await callHttpOnlyEcho(args) }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useHttpOnlyEcho = callHttpOnlyEcho
export function useStaffOnly() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate() {
isPending.value = true; error.value = null
try { return await callStaffOnly() }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useStaffOnly = callStaffOnly
export function useSuperuserOnly() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate() {
isPending.value = true; error.value = null
try { return await callSuperuserOnly() }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useSuperuserOnly = callSuperuserOnly
export function useVerifiedOnly() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate() {
isPending.value = true; error.value = null
try { return await callVerifiedOnly() }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useVerifiedOnly = callVerifiedOnly
export function useMultiply() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate(args: Parameters<typeof callMultiply>[0]) {
isPending.value = true; error.value = null
try { return await callMultiply(args) }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useMultiply = callMultiply
export function useNotImplementedFn() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate() {
isPending.value = true; error.value = null
try { return await callNotImplementedFn() }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useNotImplementedFn = callNotImplementedFn
export function useBuggyFn() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate() {
isPending.value = true; error.value = null
try { return await callBuggyFn() }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useBuggyFn = callBuggyFn
export function usePermissionCheckFn() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate(args: Parameters<typeof callPermissionCheckFn>[0]) {
isPending.value = true; error.value = null
try { return await callPermissionCheckFn(args) }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const usePermissionCheckFn = callPermissionCheckFn
export function useWsWhoami() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate() {
isPending.value = true; error.value = null
try { return await callWsWhoami() }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useWsWhoami = callWsWhoami
export function useJwtObtain() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate() {
isPending.value = true; error.value = null
try { return await callJwtObtain() }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useJwtObtain = callJwtObtain
export function useJwtRefresh() {
const isPending = ref(false)
const error = ref<Error | null>(null)
async function mutate(args: Parameters<typeof callJwtRefresh>[0]) {
isPending.value = true; error.value = null
try { return await callJwtRefresh(args) }
catch (e) { error.value = e as Error; throw e }
finally { isPending.value = false }
}
return { mutate, isPending, error }
}
export const useJwtRefresh = callJwtRefresh
export type { ContextState } from '@mizan/runtime'
export { configure, initSession, MizanError } from '@mizan/runtime'