@lunora/solid 1.0.0-alpha.4 → 1.0.0-alpha.6

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/README.md CHANGED
@@ -103,6 +103,8 @@ render(
103
103
  | `createAuth` | `useAuth` | Reactive auth state (`token`, `user` signals + `setToken`). |
104
104
  | `Authenticated` / `AuthLoading` / `Unauthenticated` | — | Auth-gate components rendering `children` per identity state. |
105
105
  | `createPresence` | `usePresence` | Collaborative-awareness — heartbeat + live present-members signal. |
106
+ | `createFlag` | `useFlag` | Live OpenFeature flag accessor — returns `default` until the server answers. |
107
+ | `createFlags` | `useFlags` | Batch variant — an accessor of one value per key in the defaults map. |
106
108
  | `createRateLimit` | `useRateLimit` | Client-side rate-limit mirror — `ok`, `disabled`, `retryAfter` as signals. |
107
109
  | `createConnectionStatus` | `useConnectionStatus` | Reactive connection state signal. |
108
110
  | `hydratePreloaded` | `usePreloadedQuery` | Seed a query synchronously from an SSR `Preloaded` token, then go live. |
package/dist/index.d.mts CHANGED
@@ -62,6 +62,43 @@ declare const Unauthenticated: (props: AuthGateProps) => JSX.Element;
62
62
  * scope disposes (component unmount). Call inside a component / reactive root.
63
63
  */
64
64
  declare const createConnectionStatus: () => Accessor<ConnectionStatus>;
65
+ /** A targeting context merged on top of the app's default (`defineFlags({ identify })`). */
66
+ type FlagContext = Record<string, unknown>;
67
+ /** The value kinds a flag resolves to — OpenFeature's boolean / number / string / structured (JSON) flags. */
68
+ type FlagValue = boolean | number | string | {
69
+ [key: string]: unknown;
70
+ } | unknown[] | null;
71
+ /** A plain value or a Solid accessor of it — matching `createQuery`'s reactive-args contract. */
72
+ type MaybeAccessor<T> = Accessor<T> | T;
73
+ /**
74
+ * Subscribe to a single feature flag and return a reactive accessor of its value.
75
+ *
76
+ * The accessor reads `defaultValue` until the first evaluation lands, then the
77
+ * server's resolved value — re-pushed whenever the provider re-evaluates (e.g. a
78
+ * flag is toggled in Cloudflare Flagship). The flag's kind is inferred from
79
+ * `defaultValue`'s runtime type, so `createFlag("dark", false)` reads a boolean
80
+ * and `createFlag("hero", "control")` a string.
81
+ *
82
+ * `key` and `context` may be plain values or accessors; passing an accessor makes
83
+ * the subscription reactive — when it changes the old subscription is torn down
84
+ * (via `onCleanup`) and a fresh one opens. `context` supplies a per-call targeting
85
+ * context merged on top of the app's default `identify` targeting key.
86
+ *
87
+ * Evaluation runs through whatever OpenFeature provider the app wired in
88
+ * `lunora/flags.ts`; the read never throws — a provider error resolves the
89
+ * default (the same fail-open contract as server-side `ctx.flags`).
90
+ */
91
+ declare const createFlag: <T extends FlagValue>(key: MaybeAccessor<string>, defaultValue: T, context?: MaybeAccessor<FlagContext | undefined>) => Accessor<T>;
92
+ /**
93
+ * Subscribe to several feature flags at once and return a reactive accessor of
94
+ * the resolved record.
95
+ *
96
+ * Pass a record of `key → defaultValue`; each flag's kind is inferred from its
97
+ * default, and the accessor reads the same-shaped record with resolved values
98
+ * (the defaults until each evaluation lands). A single `context` applies to every
99
+ * flag and may be an accessor. This is the batched form of {@link createFlag}.
100
+ */
101
+ declare const createFlags: <T extends Record<string, FlagValue>>(flags: T, context?: MaybeAccessor<FlagContext | undefined>) => Accessor<T>;
65
102
  interface MutationHandle<F extends FunctionReference> {
66
103
  /** The latest invocation's resolved value, or `undefined` before the first success. */
67
104
  data: Accessor<ReturnOf<F> | undefined>;
@@ -336,4 +373,4 @@ interface LunoraProviderProps {
336
373
  * ```
337
374
  */
338
375
  declare const LunoraProvider: (props: LunoraProviderProps) => JSX.Element;
339
- export { AuthLoading, Authenticated, type CreateInfiniteQueryOptions, type CreateInfiniteQueryResult, type CreatePaginatedQueryOptions, type CreatePaginatedQueryResult, type CreatePresenceOptions, type CreatePresenceResult, type CreateQueryOptions, type CreateRateLimitOptions, type CreateRateLimitResult, type CreateSubscriptionResult, type HeartbeatReference, type ListPresentReference, LunoraContext, LunoraProvider, type LunoraProviderProps, type MutationClient, type MutationHandle, type PageItemOf, type PaginatedArgs, Unauthenticated, type UseAuthResult, createAuth, createConnectionStatus, createInfiniteQuery, createMutation, createMutationForClient, createPaginatedQuery, createPresence, createQuery, createRateLimit, createSubscription, hydratePreloaded, useLunora };
376
+ export { AuthLoading, Authenticated, type CreateInfiniteQueryOptions, type CreateInfiniteQueryResult, type CreatePaginatedQueryOptions, type CreatePaginatedQueryResult, type CreatePresenceOptions, type CreatePresenceResult, type CreateQueryOptions, type CreateRateLimitOptions, type CreateRateLimitResult, type CreateSubscriptionResult, type FlagContext, type FlagValue, type HeartbeatReference, type ListPresentReference, LunoraContext, LunoraProvider, type LunoraProviderProps, type MutationClient, type MutationHandle, type PageItemOf, type PaginatedArgs, Unauthenticated, type UseAuthResult, createAuth, createConnectionStatus, createFlag, createFlags, createInfiniteQuery, createMutation, createMutationForClient, createPaginatedQuery, createPresence, createQuery, createRateLimit, createSubscription, hydratePreloaded, useLunora };
package/dist/index.d.ts CHANGED
@@ -62,6 +62,43 @@ declare const Unauthenticated: (props: AuthGateProps) => JSX.Element;
62
62
  * scope disposes (component unmount). Call inside a component / reactive root.
63
63
  */
64
64
  declare const createConnectionStatus: () => Accessor<ConnectionStatus>;
65
+ /** A targeting context merged on top of the app's default (`defineFlags({ identify })`). */
66
+ type FlagContext = Record<string, unknown>;
67
+ /** The value kinds a flag resolves to — OpenFeature's boolean / number / string / structured (JSON) flags. */
68
+ type FlagValue = boolean | number | string | {
69
+ [key: string]: unknown;
70
+ } | unknown[] | null;
71
+ /** A plain value or a Solid accessor of it — matching `createQuery`'s reactive-args contract. */
72
+ type MaybeAccessor<T> = Accessor<T> | T;
73
+ /**
74
+ * Subscribe to a single feature flag and return a reactive accessor of its value.
75
+ *
76
+ * The accessor reads `defaultValue` until the first evaluation lands, then the
77
+ * server's resolved value — re-pushed whenever the provider re-evaluates (e.g. a
78
+ * flag is toggled in Cloudflare Flagship). The flag's kind is inferred from
79
+ * `defaultValue`'s runtime type, so `createFlag("dark", false)` reads a boolean
80
+ * and `createFlag("hero", "control")` a string.
81
+ *
82
+ * `key` and `context` may be plain values or accessors; passing an accessor makes
83
+ * the subscription reactive — when it changes the old subscription is torn down
84
+ * (via `onCleanup`) and a fresh one opens. `context` supplies a per-call targeting
85
+ * context merged on top of the app's default `identify` targeting key.
86
+ *
87
+ * Evaluation runs through whatever OpenFeature provider the app wired in
88
+ * `lunora/flags.ts`; the read never throws — a provider error resolves the
89
+ * default (the same fail-open contract as server-side `ctx.flags`).
90
+ */
91
+ declare const createFlag: <T extends FlagValue>(key: MaybeAccessor<string>, defaultValue: T, context?: MaybeAccessor<FlagContext | undefined>) => Accessor<T>;
92
+ /**
93
+ * Subscribe to several feature flags at once and return a reactive accessor of
94
+ * the resolved record.
95
+ *
96
+ * Pass a record of `key → defaultValue`; each flag's kind is inferred from its
97
+ * default, and the accessor reads the same-shaped record with resolved values
98
+ * (the defaults until each evaluation lands). A single `context` applies to every
99
+ * flag and may be an accessor. This is the batched form of {@link createFlag}.
100
+ */
101
+ declare const createFlags: <T extends Record<string, FlagValue>>(flags: T, context?: MaybeAccessor<FlagContext | undefined>) => Accessor<T>;
65
102
  interface MutationHandle<F extends FunctionReference> {
66
103
  /** The latest invocation's resolved value, or `undefined` before the first success. */
67
104
  data: Accessor<ReturnOf<F> | undefined>;
@@ -336,4 +373,4 @@ interface LunoraProviderProps {
336
373
  * ```
337
374
  */
338
375
  declare const LunoraProvider: (props: LunoraProviderProps) => JSX.Element;
339
- export { AuthLoading, Authenticated, type CreateInfiniteQueryOptions, type CreateInfiniteQueryResult, type CreatePaginatedQueryOptions, type CreatePaginatedQueryResult, type CreatePresenceOptions, type CreatePresenceResult, type CreateQueryOptions, type CreateRateLimitOptions, type CreateRateLimitResult, type CreateSubscriptionResult, type HeartbeatReference, type ListPresentReference, LunoraContext, LunoraProvider, type LunoraProviderProps, type MutationClient, type MutationHandle, type PageItemOf, type PaginatedArgs, Unauthenticated, type UseAuthResult, createAuth, createConnectionStatus, createInfiniteQuery, createMutation, createMutationForClient, createPaginatedQuery, createPresence, createQuery, createRateLimit, createSubscription, hydratePreloaded, useLunora };
376
+ export { AuthLoading, Authenticated, type CreateInfiniteQueryOptions, type CreateInfiniteQueryResult, type CreatePaginatedQueryOptions, type CreatePaginatedQueryResult, type CreatePresenceOptions, type CreatePresenceResult, type CreateQueryOptions, type CreateRateLimitOptions, type CreateRateLimitResult, type CreateSubscriptionResult, type FlagContext, type FlagValue, type HeartbeatReference, type ListPresentReference, LunoraContext, LunoraProvider, type LunoraProviderProps, type MutationClient, type MutationHandle, type PageItemOf, type PaginatedArgs, Unauthenticated, type UseAuthResult, createAuth, createConnectionStatus, createFlag, createFlags, createInfiniteQuery, createMutation, createMutationForClient, createPaginatedQuery, createPresence, createQuery, createRateLimit, createSubscription, hydratePreloaded, useLunora };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  export { LunoraContext, useLunora } from './packem_shared/LunoraContext-C9SpKj54.mjs';
2
2
  export { AuthLoading, Authenticated, Unauthenticated, createAuth } from './packem_shared/AuthLoading-RMT5Q_eE.mjs';
3
3
  export { default as createConnectionStatus } from './packem_shared/createConnectionStatus-D8GPatZX.mjs';
4
+ export { createFlag, createFlags } from './packem_shared/createFlag-BDunYuaH.mjs';
4
5
  export { createMutation, createMutationForClient } from './packem_shared/createMutation-C7BxzO0y.mjs';
5
6
  export { createInfiniteQuery, createPaginatedQuery } from './packem_shared/createInfiniteQuery-DyMvQ2Qy.mjs';
6
7
  export { createPresence } from './packem_shared/createPresence-DiAak1Jw.mjs';
@@ -0,0 +1,106 @@
1
+ import { createSignal, createEffect, on, onCleanup } from 'solid-js';
2
+ import { useLunora } from './LunoraContext-C9SpKj54.mjs';
3
+
4
+ const compareKeys = (a, b) => {
5
+ if (a < b) {
6
+ return -1;
7
+ }
8
+ return a > b ? 1 : 0;
9
+ };
10
+ const stableStringify = (value) => {
11
+ if (value === void 0) {
12
+ return "null";
13
+ }
14
+ if (value === null || typeof value !== "object") {
15
+ return JSON.stringify(value);
16
+ }
17
+ if (Array.isArray(value)) {
18
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
19
+ }
20
+ const record = value;
21
+ const keys = Object.keys(record).toSorted(compareKeys);
22
+ const parts = [];
23
+ for (const key of keys) {
24
+ const raw = record[key];
25
+ if (raw === void 0) {
26
+ continue;
27
+ }
28
+ parts.push(`${JSON.stringify(key)}:${stableStringify(raw)}`);
29
+ }
30
+ return `{${parts.join(",")}}`;
31
+ };
32
+
33
+ const FLAGS_EVAL_PATH = "__lunora_flags__:eval";
34
+ const flagKind = (value) => {
35
+ const kind = typeof value;
36
+ if (kind === "boolean" || kind === "number" || kind === "string") {
37
+ return kind;
38
+ }
39
+ return "object";
40
+ };
41
+ const flagsReference = {
42
+ __lunoraRef: FLAGS_EVAL_PATH
43
+ };
44
+ const resolveMaybe = (value) => typeof value === "function" ? value() : value;
45
+ const serializeContext = (context) => context === void 0 ? "" : stableStringify(context);
46
+ const createFlag = (key, defaultValue, context) => {
47
+ const client = useLunora();
48
+ const type = flagKind(defaultValue);
49
+ const [value, setValue] = createSignal(defaultValue);
50
+ createEffect(on(() => `${resolveMaybe(key)} ${serializeContext(resolveMaybe(context))}`, () => {
51
+ const currentKey = resolveMaybe(key);
52
+ const currentContext = resolveMaybe(context);
53
+ setValue(() => defaultValue);
54
+ let unsubscribe;
55
+ try {
56
+ unsubscribe = client.subscribe(flagsReference, {
57
+ context: currentContext,
58
+ default: defaultValue,
59
+ key: currentKey,
60
+ type
61
+ }, (next) => {
62
+ setValue(() => next);
63
+ });
64
+ } catch {
65
+ return;
66
+ }
67
+ onCleanup(unsubscribe);
68
+ }));
69
+ return value;
70
+ };
71
+ const createFlags = (flags, context) => {
72
+ const client = useLunora();
73
+ const [values, setValues] = createSignal(flags);
74
+ const spec = stableStringify(flags);
75
+ createEffect(on(() => `${spec} ${serializeContext(resolveMaybe(context))}`, () => {
76
+ const currentContext = resolveMaybe(context);
77
+ setValues(() => flags);
78
+ const unsubscribes = [];
79
+ for (const [key, defaultValue] of Object.entries(flags)) {
80
+ try {
81
+ unsubscribes.push(client.subscribe(flagsReference, {
82
+ context: currentContext,
83
+ default: defaultValue,
84
+ key,
85
+ type: flagKind(defaultValue)
86
+ }, (next) => {
87
+ setValues((previous) => {
88
+ return {
89
+ ...previous,
90
+ [key]: next
91
+ };
92
+ });
93
+ }));
94
+ } catch {
95
+ }
96
+ }
97
+ onCleanup(() => {
98
+ for (const unsubscribe of unsubscribes) {
99
+ unsubscribe();
100
+ }
101
+ });
102
+ }));
103
+ return values;
104
+ };
105
+
106
+ export { createFlag, createFlags };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/solid",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "SolidJS adapter for Lunora — live queries, optimistic mutations, and reactive loaders",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -50,7 +50,7 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@lunora/client": "1.0.0-alpha.3",
53
+ "@lunora/client": "1.0.0-alpha.4",
54
54
  "@lunora/ratelimit": "1.0.0-alpha.3"
55
55
  },
56
56
  "peerDependencies": {