@myna-sh/react 0.9.0 → 0.10.0

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/index.js CHANGED
@@ -175,7 +175,7 @@ function useMynaSingleton(collection, options = {}) {
175
175
  }
176
176
 
177
177
  // src/version.ts
178
- var VERSION = true ? "0.9.0" : "0.0.0-dev";
178
+ var VERSION = true ? "0.10.0" : "0.0.0-dev";
179
179
  export {
180
180
  MynaProvider,
181
181
  MynaStore,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.tsx","../src/context.ts","../src/hooks.ts","../src/store.ts","../src/version.ts"],"sourcesContent":["import { useMemo, type ReactNode } from \"react\";\nimport { createMyna, type CreateMynaOptions, type MynaClient } from \"@myna-sh/sdk\";\nimport { MynaContext } from \"./context.js\";\n\nexport interface MynaProviderProps {\n /** An existing client. Takes precedence over `options`. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client?: MynaClient<any>;\n /** Client options, when you would rather this component construct one. */\n options?: CreateMynaOptions;\n children: ReactNode;\n}\n\n/**\n * Provide a Myna client to the hooks below.\n *\n * Constructing from `options` is memoized on the values that matter, so a\n * parent re-render does not silently create a new client — which would discard\n * the in-flight request coalescing that makes concurrent reads cheap.\n */\nexport function MynaProvider({ client, options, children }: MynaProviderProps): ReactNode {\n const resolved = useMemo(() => {\n if (client) return client;\n if (!options) {\n throw new Error(\"MynaProvider needs either a `client` or `options`.\");\n }\n return createMyna(options);\n }, [\n client,\n options?.project,\n options?.apiUrl,\n options?.apiKey,\n options?.previewToken,\n options?.previewMode,\n ]);\n\n return <MynaContext.Provider value={resolved}>{children}</MynaContext.Provider>;\n}\n","import { createContext, useContext } from \"react\";\nimport type { CollectionShape, MynaClient } from \"@myna-sh/sdk\";\n\n/**\n * The client every hook reads from.\n *\n * Preview propagation is the reason this is context rather than an argument:\n * a preview token has to reach every read on the page, and threading it through\n * each component by hand is how a page ends up half-previewed — the list from\n * the change set, a nested card from published content.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const MynaContext = createContext<MynaClient<any> | null>(null);\n\nexport function useMynaClient<\n T extends Record<keyof T, CollectionShape> = Record<string, CollectionShape>,\n>(): MynaClient<T> {\n const client = useContext(MynaContext);\n if (!client) {\n throw new Error(\n \"No Myna client in context. Wrap your app in <MynaProvider client={createMyna({ project })}>.\",\n );\n }\n return client as MynaClient<T>;\n}\n","import { useCallback, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport type {\n CollectionShape,\n EntryList,\n GetEntryOptions,\n ListEntriesOptions,\n PublishedEntry,\n} from \"@myna-sh/sdk\";\nimport { useMynaClient } from \"./context.js\";\nimport { defaultStore, type EntryState } from \"./store.js\";\n\n/**\n * Reading content from React, without the adapter every site was writing.\n *\n * Each hook gives loading and error state, shares one request between\n * components asking for the same thing, keeps the last good value while\n * revalidating, and inherits the provider's preview token — so a previewed page\n * needs no separate code path.\n */\n\nexport interface UseMynaOptions {\n /** Skip the read entirely (a dependent query whose input is not ready yet). */\n enabled?: boolean;\n /** How long a cached value stays fresh before a background refetch. Default 30s. */\n staleMs?: number;\n /** Throw the in-flight promise so a `<Suspense>` boundary handles loading. */\n suspense?: boolean;\n}\n\nexport interface UseMynaResult<T> {\n data: T | undefined;\n error: Error | undefined;\n isLoading: boolean;\n /** True while revalidating with a value already on screen. */\n isValidating: boolean;\n refetch: () => void;\n}\n\n/** Stable cache key. Options are serialized so two different queries never collide. */\nfunction cacheKey(kind: string, collection: string, rest: unknown): string {\n return `${kind}:${collection}:${stable(rest)}`;\n}\n\nfunction stable(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value) ?? \"null\";\n if (Array.isArray(value)) return `[${value.map(stable).join(\",\")}]`;\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined && typeof v !== \"function\" && !(v instanceof AbortSignal))\n .sort(([a], [b]) => a.localeCompare(b));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stable(v)}`).join(\",\")}}`;\n}\n\nfunction useMynaQuery<T>(\n key: string,\n fetcher: () => Promise<T>,\n options: UseMynaOptions,\n): UseMynaResult<T> {\n const { enabled = true, staleMs, suspense = false } = options;\n\n const subscribe = useCallback((listener: () => void) => defaultStore.subscribe(key, listener), [key]);\n const snapshot = useCallback(() => defaultStore.getState<T>(key), [key]);\n // The server snapshot is the same store read; on the server nothing has been\n // loaded, so this yields the pending state rather than throwing.\n const state: EntryState<T> = useSyncExternalStore(subscribe, snapshot, snapshot);\n\n /**\n * The fetcher closes over fresh props and is a new function every render, so\n * it cannot be an effect dependency without refetching on every render. The\n * cache key already identifies the query; the ref keeps the *call* current.\n */\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n const run = useCallback(() => fetcherRef.current(), []);\n\n useEffect(() => {\n if (!enabled) return;\n void defaultStore.load(key, run, staleMs);\n }, [key, enabled, staleMs, run]);\n\n if (suspense && enabled && state.status === \"pending\") {\n throw defaultStore.load(key, run, staleMs);\n }\n\n const refetch = useCallback(() => {\n defaultStore.invalidate(key);\n void defaultStore.load(key, run, 0);\n }, [key, run]);\n\n return {\n data: state.data,\n error: state.error,\n isLoading: enabled && state.status === \"pending\",\n isValidating: enabled && state.status !== \"pending\" && state.data !== undefined,\n refetch,\n };\n}\n\n/** Read a collection. Returns the entry list, typed by the generated registry. */\nexport function useMynaEntries<\n T extends Record<keyof T, CollectionShape> = Record<string, CollectionShape>,\n K extends keyof T & string = keyof T & string,\n>(\n collection: K,\n options: ListEntriesOptions<T[K]> & UseMynaOptions = {} as ListEntriesOptions<T[K]> & UseMynaOptions,\n): UseMynaResult<EntryList<T[K][\"fields\"]>> {\n const client = useMynaClient<T>();\n const { enabled, staleMs, suspense, ...query } = options;\n return useMynaQuery(\n cacheKey(\"entries\", collection, query),\n () => client.entries.list(collection, query as ListEntriesOptions<T[K]>),\n { enabled, staleMs, suspense },\n );\n}\n\n/** Read one entry by slug or id. */\nexport function useMynaEntry<\n T extends Record<keyof T, CollectionShape> = Record<string, CollectionShape>,\n K extends keyof T & string = keyof T & string,\n>(\n collection: K,\n slugOrId: string | undefined,\n options: GetEntryOptions<T[K]> & UseMynaOptions = {} as GetEntryOptions<T[K]> & UseMynaOptions,\n): UseMynaResult<PublishedEntry<T[K][\"fields\"]>> {\n const client = useMynaClient<T>();\n const { enabled = true, staleMs, suspense, ...query } = options;\n return useMynaQuery(\n cacheKey(\"entry\", `${collection}/${slugOrId ?? \"\"}`, query),\n () => client.entries.get(collection, slugOrId!, query as GetEntryOptions<T[K]>),\n // A route param that has not resolved yet is a reason to wait, not to fetch\n // `undefined` and render a 404.\n { enabled: enabled && Boolean(slugOrId), staleMs, suspense },\n );\n}\n\n/** Read a singleton collection's entry. */\nexport function useMynaSingleton<\n T extends Record<keyof T, CollectionShape> = Record<string, CollectionShape>,\n K extends keyof T & string = keyof T & string,\n>(\n collection: K,\n options: GetEntryOptions<T[K]> & UseMynaOptions = {} as GetEntryOptions<T[K]> & UseMynaOptions,\n): UseMynaResult<PublishedEntry<T[K][\"fields\"]>> {\n const client = useMynaClient<T>();\n const { enabled, staleMs, suspense, ...query } = options;\n return useMynaQuery(\n cacheKey(\"singleton\", collection, query),\n () => client.singleton(collection, query as GetEntryOptions<T[K]>),\n { enabled, staleMs, suspense },\n );\n}\n","/**\n * A minimal cache with stale-while-revalidate semantics.\n *\n * Deliberately not a dependency on a data library: this package's job is to\n * remove the adapter every Myna site was writing by hand, and adding a required\n * peer of TanStack Query or SWR would just move that decision rather than\n * remove it. Anyone who already has one should call the SDK directly through\n * it — this exists for the apps that do not.\n *\n * Entries hold the last successful value, so a re-mount paints immediately and\n * refetches behind the paint instead of flashing a spinner at content the user\n * has already seen.\n */\n\nexport type EntryState<T> =\n | { status: \"pending\"; data: undefined; error: undefined }\n | { status: \"success\"; data: T; error: undefined }\n | { status: \"error\"; data: T | undefined; error: Error };\n\ninterface CacheEntry<T> {\n state: EntryState<T>;\n /** In-flight request, so concurrent subscribers share one fetch. */\n promise?: Promise<void>;\n /** When the value was last written, for staleness checks. */\n updatedAt: number;\n listeners: Set<() => void>;\n}\n\nconst DEFAULT_STALE_MS = 30_000;\n\nexport class MynaStore {\n private readonly entries = new Map<string, CacheEntry<unknown>>();\n\n private entry<T>(key: string): CacheEntry<T> {\n let found = this.entries.get(key) as CacheEntry<T> | undefined;\n if (!found) {\n found = {\n state: { status: \"pending\", data: undefined, error: undefined },\n updatedAt: 0,\n listeners: new Set(),\n };\n this.entries.set(key, found as CacheEntry<unknown>);\n }\n return found;\n }\n\n getState<T>(key: string): EntryState<T> {\n return this.entry<T>(key).state;\n }\n\n subscribe(key: string, listener: () => void): () => void {\n const entry = this.entry(key);\n entry.listeners.add(listener);\n return () => {\n entry.listeners.delete(listener);\n };\n }\n\n private publish<T>(key: string, state: EntryState<T>): void {\n const entry = this.entry<T>(key);\n entry.state = state;\n for (const listener of entry.listeners) listener();\n }\n\n /**\n * Ensure `key` holds a fresh-enough value, fetching if not.\n *\n * Returns the in-flight promise so a Suspense boundary can throw it. Several\n * components asking at once share one request — a second layer of coalescing\n * above the SDK's, because this one also covers sequential re-renders.\n */\n load<T>(key: string, fetcher: () => Promise<T>, staleMs = DEFAULT_STALE_MS): Promise<void> {\n const entry = this.entry<T>(key);\n if (entry.promise) return entry.promise;\n\n const fresh = entry.state.status === \"success\" && Date.now() - entry.updatedAt < staleMs;\n if (fresh) return Promise.resolve();\n\n const promise = fetcher()\n .then((data) => {\n entry.updatedAt = Date.now();\n this.publish<T>(key, { status: \"success\", data, error: undefined });\n })\n .catch((error: unknown) => {\n // Keep the last good value alongside the error: a failed refresh should\n // not blank a page that is already showing correct content.\n this.publish<T>(key, {\n status: \"error\",\n data: entry.state.data,\n error: error instanceof Error ? error : new Error(String(error)),\n });\n })\n .finally(() => {\n entry.promise = undefined;\n });\n\n entry.promise = promise;\n return promise;\n }\n\n /** Drop a cached value, forcing the next read to refetch. */\n invalidate(prefix?: string): void {\n for (const [key, entry] of this.entries) {\n if (prefix && !key.startsWith(prefix)) continue;\n entry.updatedAt = 0;\n for (const listener of entry.listeners) listener();\n }\n }\n}\n\n/** Module-level default store, shared by every hook in a page. */\nexport const defaultStore = new MynaStore();\n","/**\n * Build-time version stamp, matching every other released package: tsup\n * substitutes `__MYNA_VERSION__` from `package.json`, so the bundle can never\n * disagree about which release it came from.\n */\ndeclare const __MYNA_VERSION__: string | undefined;\n\nexport const VERSION: string =\n typeof __MYNA_VERSION__ === \"string\" ? __MYNA_VERSION__ : \"0.0.0-dev\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/provider.tsx","../src/context.ts","../src/hooks.ts","../src/store.ts","../src/version.ts"],"sourcesContent":["import { useMemo, type ReactNode } from \"react\";\nimport { createMyna, type CreateMynaOptions, type MynaClient } from \"@myna-sh/sdk\";\nimport { MynaContext } from \"./context.js\";\n\nexport interface MynaProviderProps {\n /** An existing client. Takes precedence over `options`. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n client?: MynaClient<any>;\n /** Client options, when you would rather this component construct one. */\n options?: CreateMynaOptions;\n children: ReactNode;\n}\n\n/**\n * Provide a Myna client to the hooks below.\n *\n * Constructing from `options` is memoized on the values that matter, so a\n * parent re-render does not silently create a new client — which would discard\n * the in-flight request coalescing that makes concurrent reads cheap.\n */\nexport function MynaProvider({ client, options, children }: MynaProviderProps): ReactNode {\n const resolved = useMemo(() => {\n if (client) return client;\n if (!options) {\n throw new Error(\"MynaProvider needs either a `client` or `options`.\");\n }\n return createMyna(options);\n }, [\n client,\n options?.project,\n options?.apiUrl,\n options?.apiKey,\n options?.previewToken,\n options?.previewMode,\n ]);\n\n return <MynaContext.Provider value={resolved}>{children}</MynaContext.Provider>;\n}\n","import { createContext, useContext } from \"react\";\nimport type { CollectionShape, MynaClient } from \"@myna-sh/sdk\";\n\n/**\n * The client every hook reads from.\n *\n * Preview propagation is the reason this is context rather than an argument:\n * a preview token has to reach every read on the page, and threading it through\n * each component by hand is how a page ends up half-previewed — the list from\n * the change set, a nested card from published content.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const MynaContext = createContext<MynaClient<any> | null>(null);\n\nexport function useMynaClient<\n T extends Record<keyof T, CollectionShape> = Record<string, CollectionShape>,\n>(): MynaClient<T> {\n const client = useContext(MynaContext);\n if (!client) {\n throw new Error(\n \"No Myna client in context. Wrap your app in <MynaProvider client={createMyna({ project })}>.\",\n );\n }\n return client as MynaClient<T>;\n}\n","import { useCallback, useEffect, useRef, useSyncExternalStore } from \"react\";\nimport type {\n CollectionShape,\n EntryList,\n GetEntryOptions,\n ListEntriesOptions,\n PublishedEntry,\n} from \"@myna-sh/sdk\";\nimport { useMynaClient } from \"./context.js\";\nimport { defaultStore, type EntryState } from \"./store.js\";\n\n/**\n * Reading content from React, without the adapter every site was writing.\n *\n * Each hook gives loading and error state, shares one request between\n * components asking for the same thing, keeps the last good value while\n * revalidating, and inherits the provider's preview token — so a previewed page\n * needs no separate code path.\n */\n\nexport interface UseMynaOptions {\n /** Skip the read entirely (a dependent query whose input is not ready yet). */\n enabled?: boolean;\n /** How long a cached value stays fresh before a background refetch. Default 30s. */\n staleMs?: number;\n /** Throw the in-flight promise so a `<Suspense>` boundary handles loading. */\n suspense?: boolean;\n}\n\nexport interface UseMynaResult<T> {\n data: T | undefined;\n error: Error | undefined;\n isLoading: boolean;\n /** True while revalidating with a value already on screen. */\n isValidating: boolean;\n refetch: () => void;\n}\n\n/** Stable cache key. Options are serialized so two different queries never collide. */\nfunction cacheKey(kind: string, collection: string, rest: unknown): string {\n return `${kind}:${collection}:${stable(rest)}`;\n}\n\nfunction stable(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value) ?? \"null\";\n if (Array.isArray(value)) return `[${value.map(stable).join(\",\")}]`;\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined && typeof v !== \"function\" && !(v instanceof AbortSignal))\n .sort(([a], [b]) => a.localeCompare(b));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stable(v)}`).join(\",\")}}`;\n}\n\nfunction useMynaQuery<T>(\n key: string,\n fetcher: () => Promise<T>,\n options: UseMynaOptions,\n): UseMynaResult<T> {\n const { enabled = true, staleMs, suspense = false } = options;\n\n const subscribe = useCallback((listener: () => void) => defaultStore.subscribe(key, listener), [key]);\n const snapshot = useCallback(() => defaultStore.getState<T>(key), [key]);\n // The server snapshot is the same store read; on the server nothing has been\n // loaded, so this yields the pending state rather than throwing.\n const state: EntryState<T> = useSyncExternalStore(subscribe, snapshot, snapshot);\n\n /**\n * The fetcher closes over fresh props and is a new function every render, so\n * it cannot be an effect dependency without refetching on every render. The\n * cache key already identifies the query; the ref keeps the *call* current.\n */\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n const run = useCallback(() => fetcherRef.current(), []);\n\n useEffect(() => {\n if (!enabled) return;\n void defaultStore.load(key, run, staleMs);\n }, [key, enabled, staleMs, run]);\n\n if (suspense && enabled && state.status === \"pending\") {\n throw defaultStore.load(key, run, staleMs);\n }\n\n const refetch = useCallback(() => {\n defaultStore.invalidate(key);\n void defaultStore.load(key, run, 0);\n }, [key, run]);\n\n return {\n data: state.data,\n error: state.error,\n isLoading: enabled && state.status === \"pending\",\n isValidating: enabled && state.status !== \"pending\" && state.data !== undefined,\n refetch,\n };\n}\n\n/** Read a collection. Returns the entry list, typed by the generated registry. */\nexport function useMynaEntries<\n T extends Record<keyof T, CollectionShape> = Record<string, CollectionShape>,\n K extends keyof T & string = keyof T & string,\n>(\n collection: K,\n options: ListEntriesOptions<T[K]> & UseMynaOptions = {} as ListEntriesOptions<T[K]> & UseMynaOptions,\n): UseMynaResult<EntryList<T[K][\"fields\"]>> {\n const client = useMynaClient<T>();\n const { enabled, staleMs, suspense, ...query } = options;\n return useMynaQuery(\n cacheKey(\"entries\", collection, query),\n () => client.entries.list(collection, query as ListEntriesOptions<T[K]>),\n { enabled, staleMs, suspense },\n );\n}\n\n/** Read one entry by slug or id. */\nexport function useMynaEntry<\n T extends Record<keyof T, CollectionShape> = Record<string, CollectionShape>,\n K extends keyof T & string = keyof T & string,\n>(\n collection: K,\n slugOrId: string | undefined,\n options: GetEntryOptions<T[K]> & UseMynaOptions = {} as GetEntryOptions<T[K]> & UseMynaOptions,\n): UseMynaResult<PublishedEntry<T[K][\"fields\"]>> {\n const client = useMynaClient<T>();\n const { enabled = true, staleMs, suspense, ...query } = options;\n return useMynaQuery(\n cacheKey(\"entry\", `${collection}/${slugOrId ?? \"\"}`, query),\n () => client.entries.get(collection, slugOrId!, query as GetEntryOptions<T[K]>),\n // A route param that has not resolved yet is a reason to wait, not to fetch\n // `undefined` and render a 404.\n { enabled: enabled && Boolean(slugOrId), staleMs, suspense },\n );\n}\n\n/** Read a singleton collection's entry. */\nexport function useMynaSingleton<\n T extends Record<keyof T, CollectionShape> = Record<string, CollectionShape>,\n K extends keyof T & string = keyof T & string,\n>(\n collection: K,\n options: GetEntryOptions<T[K]> & UseMynaOptions = {} as GetEntryOptions<T[K]> & UseMynaOptions,\n): UseMynaResult<PublishedEntry<T[K][\"fields\"]>> {\n const client = useMynaClient<T>();\n const { enabled, staleMs, suspense, ...query } = options;\n return useMynaQuery(\n cacheKey(\"singleton\", collection, query),\n () => client.singleton(collection, query as GetEntryOptions<T[K]>),\n { enabled, staleMs, suspense },\n );\n}\n","/**\n * A minimal cache with stale-while-revalidate semantics.\n *\n * Deliberately not a dependency on a data library: this package's job is to\n * remove the adapter every Myna site was writing by hand, and adding a required\n * peer of TanStack Query or SWR would just move that decision rather than\n * remove it. Anyone who already has one should call the SDK directly through\n * it — this exists for the apps that do not.\n *\n * Entries hold the last successful value, so a re-mount paints immediately and\n * refetches behind the paint instead of flashing a spinner at content the user\n * has already seen.\n */\n\nexport type EntryState<T> =\n | { status: \"pending\"; data: undefined; error: undefined }\n | { status: \"success\"; data: T; error: undefined }\n | { status: \"error\"; data: T | undefined; error: Error };\n\ninterface CacheEntry<T> {\n state: EntryState<T>;\n /** In-flight request, so concurrent subscribers share one fetch. */\n promise?: Promise<void>;\n /** When the value was last written, for staleness checks. */\n updatedAt: number;\n listeners: Set<() => void>;\n}\n\nconst DEFAULT_STALE_MS = 30_000;\n\nexport class MynaStore {\n private readonly entries = new Map<string, CacheEntry<unknown>>();\n\n private entry<T>(key: string): CacheEntry<T> {\n let found = this.entries.get(key) as CacheEntry<T> | undefined;\n if (!found) {\n found = {\n state: { status: \"pending\", data: undefined, error: undefined },\n updatedAt: 0,\n listeners: new Set(),\n };\n this.entries.set(key, found as CacheEntry<unknown>);\n }\n return found;\n }\n\n getState<T>(key: string): EntryState<T> {\n return this.entry<T>(key).state;\n }\n\n subscribe(key: string, listener: () => void): () => void {\n const entry = this.entry(key);\n entry.listeners.add(listener);\n return () => {\n entry.listeners.delete(listener);\n };\n }\n\n private publish<T>(key: string, state: EntryState<T>): void {\n const entry = this.entry<T>(key);\n entry.state = state;\n for (const listener of entry.listeners) listener();\n }\n\n /**\n * Ensure `key` holds a fresh-enough value, fetching if not.\n *\n * Returns the in-flight promise so a Suspense boundary can throw it. Several\n * components asking at once share one request — a second layer of coalescing\n * above the SDK's, because this one also covers sequential re-renders.\n */\n load<T>(key: string, fetcher: () => Promise<T>, staleMs = DEFAULT_STALE_MS): Promise<void> {\n const entry = this.entry<T>(key);\n if (entry.promise) return entry.promise;\n\n const fresh = entry.state.status === \"success\" && Date.now() - entry.updatedAt < staleMs;\n if (fresh) return Promise.resolve();\n\n const promise = fetcher()\n .then((data) => {\n entry.updatedAt = Date.now();\n this.publish<T>(key, { status: \"success\", data, error: undefined });\n })\n .catch((error: unknown) => {\n // Keep the last good value alongside the error: a failed refresh should\n // not blank a page that is already showing correct content.\n this.publish<T>(key, {\n status: \"error\",\n data: entry.state.data,\n error: error instanceof Error ? error : new Error(String(error)),\n });\n })\n .finally(() => {\n entry.promise = undefined;\n });\n\n entry.promise = promise;\n return promise;\n }\n\n /** Drop a cached value, forcing the next read to refetch. */\n invalidate(prefix?: string): void {\n for (const [key, entry] of this.entries) {\n if (prefix && !key.startsWith(prefix)) continue;\n entry.updatedAt = 0;\n for (const listener of entry.listeners) listener();\n }\n }\n}\n\n/** Module-level default store, shared by every hook in a page. */\nexport const defaultStore = new MynaStore();\n","/**\n * Build-time version stamp, matching every other released package: tsup\n * substitutes `__MYNA_VERSION__` from `package.json`, so the bundle can never\n * disagree about which release it came from.\n */\ndeclare const __MYNA_VERSION__: string | undefined;\n\nexport const VERSION: string =\n typeof __MYNA_VERSION__ === \"string\" ? __MYNA_VERSION__ : \"0.0.0-dev\";\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myna-sh/react",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,7 +22,7 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@myna-sh/sdk": "^0.9.0",
25
+ "@myna-sh/sdk": "^0.10.0",
26
26
  "react": ">=18"
27
27
  },
28
28
  "devDependencies": {
@@ -31,8 +31,8 @@
31
31
  "eslint": "9.39.5",
32
32
  "react": "19.2.8",
33
33
  "typescript": "5.9.3",
34
- "@myna-sh/sdk": "0.9.0",
35
- "@myna-sh/config": "0.0.0"
34
+ "@myna-sh/config": "0.0.0",
35
+ "@myna-sh/sdk": "0.10.0"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsup",