@lunora/angular 1.0.0-alpha.3 → 1.0.0-alpha.5

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.d.mts CHANGED
@@ -1,7 +1,39 @@
1
- import { InjectionToken, EnvironmentProviders, DestroyRef, Signal } from '@angular/core';
2
- import { LunoraClient, LunoraClientOptions, ConnectionStatus, SubscriptionError, FunctionReference, ArgsOf, ReturnOf, MutationCallOptions } from '@lunora/client';
3
- export type { ArgsOf, ConnectionStatus, FunctionReference, LunoraClient, LunoraClientOptions, MutationCallOptions, ReturnOf, SubscriptionError, Unsubscribe } from '@lunora/client';
1
+ import { DestroyRef, Signal, InjectionToken, EnvironmentProviders } from '@angular/core';
2
+ import { LunoraClient, User, LunoraClientOptions, ConnectionStatus, SubscriptionError, Preloaded, FunctionReference, ArgsOf, ReturnOf, MutationCallOptions, MutatorHandle } from '@lunora/client';
3
+ export type { ArgsOf, ConnectionStatus, FunctionReference, LunoraClient, LunoraClientOptions, MutationCallOptions, Preloaded, ReturnOf, SubscriptionError, Unsubscribe } from '@lunora/client';
4
+ import { PaginationStatus } from '@lunora/client/pagination';
5
+ import { RateLimitStatus, RateLimitConfig } from '@lunora/ratelimit';
4
6
  export { SKIP } from '@lunora/client/query';
7
+ interface AuthOptions {
8
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
9
+ client?: LunoraClient;
10
+ /** `DestroyRef` whose `onDestroy` removes the listeners. Defaults to `inject(DestroyRef)`. */
11
+ destroyRef?: DestroyRef;
12
+ }
13
+ interface AuthResult {
14
+ /** Set the auth token (sign-in / sign-out). */
15
+ setToken: (token: string | null) => void;
16
+ /** The current auth token, or `null`. */
17
+ token: Signal<string | null>;
18
+ /** The resolved user from `store.getUser()`, or `null`. */
19
+ user: Signal<User | null>;
20
+ }
21
+ /**
22
+ * Token + identity plumbing for Angular. `token` is a signal tracking the
23
+ * client's auth token; `user` is a signal resolved from `getCurrentUser()`
24
+ * whenever the token changes. `setToken(jwt)` after sign-in makes subsequent
25
+ * RPC calls carry the `Authorization` header.
26
+ *
27
+ * Multiple `auth` instances on the same client share a single per-client
28
+ * identity store (from `@lunora/client/auth`) — a `setToken` from one component
29
+ * re-renders every watcher with the freshly-resolved user.
30
+ *
31
+ * Call from an injection context (component/service field or constructor):
32
+ * ```ts
33
+ * const { token, user, setToken } = auth();
34
+ * ```
35
+ */
36
+ declare const auth: (options?: AuthOptions) => AuthResult;
5
37
  /**
6
38
  * DI token carrying the framework-neutral {@link LunoraClient}. Every reactive
7
39
  * primitive in this adapter (`liveQuery`, `mutate`, `connectionStatus`) reads the
@@ -68,6 +100,94 @@ interface ConnectionStatusOptions {
68
100
  * injection context (component/service field or constructor).
69
101
  */
70
102
  declare const connectionStatus: (options?: ConnectionStatusOptions) => Signal<ConnectionStatus>;
103
+ /** The value kinds a flag resolves to — OpenFeature's boolean / number / string / structured (JSON) flags. */
104
+ type FlagValue = boolean | number | string | Record<string, unknown> | unknown[] | null;
105
+ /** Targeting context bag forwarded to the OpenFeature provider. */
106
+ type FlagContext = Record<string, unknown>;
107
+ interface FlagOptions {
108
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
109
+ client?: LunoraClient;
110
+ /**
111
+ * Per-call targeting context merged on top of the app's default `identify`
112
+ * targeting key.
113
+ */
114
+ context?: FlagContext;
115
+ /** `DestroyRef` whose `onDestroy` tears down the subscription. Defaults to `inject(DestroyRef)`. */
116
+ destroyRef?: DestroyRef;
117
+ }
118
+ /**
119
+ * Subscribe to a single feature flag, live over Lunora's WebSocket.
120
+ *
121
+ * The returned signal holds `defaultValue` until the first evaluation lands, then
122
+ * the server's resolved value — re-pushed whenever the provider re-evaluates.
123
+ * The flag's kind is inferred from `defaultValue`'s runtime type, so
124
+ * `flag("dark", false)` reads a boolean and `flag("hero", "control")` a string.
125
+ *
126
+ * Evaluation runs through whatever OpenFeature provider the app wired in
127
+ * `lunora/flags.ts`; the read never throws — a provider error resolves the
128
+ * default (the same fail-open contract as server-side `ctx.flags`).
129
+ *
130
+ * Call from an injection context:
131
+ * ```ts
132
+ * readonly darkMode = flag("dark-mode", false);
133
+ * ```
134
+ */
135
+ declare const flag: <T extends FlagValue>(key: string, defaultValue: T, options?: FlagOptions) => Signal<T>;
136
+ interface FlagsOptions {
137
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
138
+ client?: LunoraClient;
139
+ /**
140
+ * Targeting context shared by every flag in the set, merged on top of the
141
+ * app's default `identify` targeting key.
142
+ */
143
+ context?: FlagContext;
144
+ /** `DestroyRef` whose `onDestroy` tears down the subscriptions. Defaults to `inject(DestroyRef)`. */
145
+ destroyRef?: DestroyRef;
146
+ }
147
+ /**
148
+ * Subscribe to several feature flags at once, live over Lunora's WebSocket.
149
+ *
150
+ * Pass a record of `key → defaultValue`; each flag's kind is inferred from its
151
+ * default, and the returned signal holds the same-shaped record with resolved
152
+ * values (the defaults until each evaluation lands).
153
+ *
154
+ * Call from an injection context:
155
+ * ```ts
156
+ * readonly features = flags({ "dark-mode": false, "new-editor": false });
157
+ * ```
158
+ */
159
+ declare const flags: <T extends Record<string, FlagValue>>(flagDefaults: T, options?: FlagsOptions) => Signal<T>;
160
+ interface HydratePreloadedOptions {
161
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
162
+ client?: LunoraClient;
163
+ /** `DestroyRef` whose `onDestroy` tears the subscription down. Defaults to `inject(DestroyRef)`. */
164
+ destroyRef?: DestroyRef;
165
+ }
166
+ interface HydratePreloadedResult<T> {
167
+ /** The latest value pushed by the server. Seeded synchronously from the preloaded value. */
168
+ data: Signal<T | undefined>;
169
+ /** The latest subscription error, or `undefined`. */
170
+ error: Signal<SubscriptionError | undefined>;
171
+ }
172
+ /**
173
+ * Hydrate a query from a {@link Preloaded} token produced by `preloadQuery`
174
+ * during SSR, then keep it live — the Angular half of the reactive-loader
175
+ * handoff.
176
+ *
177
+ * The returned signal is seeded **synchronously** from `preloaded.value`, so the
178
+ * very first read (during hydration) shows the server value: no loading flash,
179
+ * no hydration mismatch. After seeding it opens a WebSocket subscription on the
180
+ * same `(functionPath, args, shardKey)` the SSR loader used, so every later
181
+ * server delta updates the signal exactly like `liveQuery`.
182
+ *
183
+ * The subscription tears down when the owning `DestroyRef` fires.
184
+ *
185
+ * Call from an injection context:
186
+ * ```ts
187
+ * readonly { data, error } = hydratePreloaded(preloadedMessages);
188
+ * ```
189
+ */
190
+ declare const hydratePreloaded: <T>(preloaded: Preloaded<T>, options?: HydratePreloadedOptions) => HydratePreloadedResult<T>;
71
191
  interface LiveQueryOptions {
72
192
  /**
73
193
  * Client to bind to. Defaults to the injected `LUNORA_CLIENT`; pass one
@@ -143,4 +263,240 @@ interface MutateOptions<F extends FunctionReference> extends MutationCallOptions
143
263
  * resolve from the injector.
144
264
  */
145
265
  declare const mutate: <F extends FunctionReference>(reference: F, args: ArgsOf<F>, options?: MutateOptions<F>) => Promise<ReturnOf<F>>;
146
- export { type ConnectionStatusOptions, LUNORA_CLIENT, type LiveQueryOptions, type MutateOptions, type ProvideLunoraOptions, connectionStatus, injectLunoraClient, liveQuery, mutate, provideLunora };
266
+ interface MutatorResult<TArgs> {
267
+ /** The latest invocation's error, or `undefined`. */
268
+ error: Signal<Error | undefined>;
269
+ /** `true` when the latest invocation rejected. */
270
+ isError: Signal<boolean>;
271
+ /** Run the mutator; resolves once the write is persisted, rejects on failure. */
272
+ mutate: (args: TArgs) => Promise<void>;
273
+ /** `true` while ANY invocation from this handle is in flight. */
274
+ pending: Signal<boolean>;
275
+ /** Clear the latest `error` back to idle. */
276
+ reset: () => void;
277
+ }
278
+ /**
279
+ * Ergonomic `{ mutate, pending, error, isError, reset }` wrapper over a bound
280
+ * custom-mutator handle from `` `@lunora/db` ``'s `bindMutators` — the Angular
281
+ * equivalent of `` `@lunora/react` ``'s `useMutator`. The optimistic overlay and
282
+ * server-authoritative push are owned by the bound handle; this function only
283
+ * surfaces reactive state for the in-flight/error lifecycle.
284
+ *
285
+ * `pending` is ref-counted across overlapping invocations of THIS handle, so it
286
+ * clears only once every concurrent call has settled.
287
+ *
288
+ * Does NOT require an injection context — it works with plain signals.
289
+ *
290
+ * ```ts
291
+ * private readonly collection = bindMutators(collections);
292
+ * readonly mutator = mutator(this.collection.insert);
293
+ * ```
294
+ */
295
+ declare const mutator: <TArgs = Record<string, unknown>>(handle: MutatorHandle<TArgs>) => MutatorResult<TArgs>;
296
+ /** The args a paginated query exposes minus the framework-supplied page cursor. */
297
+ type PaginatedArgs<F extends FunctionReference> = Omit<ArgsOfRaw<F>, "paginationOpts">;
298
+ /** The element type of the `page` array a paginated query returns. */
299
+ type PageItemOf<F extends FunctionReference> = ReturnTypeOf<F> extends {
300
+ page: (infer T)[];
301
+ } ? T : unknown;
302
+ type ArgsOfRaw<F extends FunctionReference> = F extends FunctionReference<"query", infer A> ? A : never;
303
+ type ReturnTypeOf<F extends FunctionReference> = F extends FunctionReference<"query", unknown, infer R> ? R : never;
304
+ interface PaginatedQueryOptions {
305
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
306
+ client?: LunoraClient;
307
+ /** `DestroyRef` whose `onDestroy` tears down the subscriptions. Defaults to `inject(DestroyRef)`. */
308
+ destroyRef?: DestroyRef;
309
+ /** Page size for the first page (and the default for `loadMore`). */
310
+ initialNumItems: number;
311
+ /** Route to a specific shard when the target function is `.shardBy(...)`-partitioned. */
312
+ shardKey?: string;
313
+ }
314
+ interface PaginatedQueryResult<T> {
315
+ /** `true` while the first page or a `loadMore` page is in flight. */
316
+ isLoading: Signal<boolean>;
317
+ /** Request the next page. A no-op unless `status === "CanLoadMore"`. */
318
+ loadMore: (numberItems: number) => void;
319
+ /** Flattened items across every loaded page, in order. */
320
+ results: Signal<T[]>;
321
+ /** The pagination status. */
322
+ status: Signal<PaginationStatus>;
323
+ }
324
+ interface InfiniteQueryResult<T> {
325
+ /** Request the next page. A no-op unless `status === "CanLoadMore"`. */
326
+ fetchNextPage: (numberItems?: number) => void;
327
+ /** `true` when the loaded tail reports it can load another page. */
328
+ hasNextPage: Signal<boolean>;
329
+ /** `true` while a `fetchNextPage` page (beyond the first) is in flight. */
330
+ isFetchingNextPage: Signal<boolean>;
331
+ /** `true` while the first page is in flight. */
332
+ isLoading: Signal<boolean>;
333
+ /** One inner array per loaded page, in order; unresolved pages are omitted. */
334
+ pages: Signal<T[][]>;
335
+ /** The pagination status. */
336
+ status: Signal<PaginationStatus>;
337
+ }
338
+ /**
339
+ * Subscribe to a reactively-paginated query and grow the feed page by page.
340
+ *
341
+ * The query function must accept a `paginationOpts: { numItems, cursor,
342
+ * endCursor }` arg and return a `PaginationResult`. Pages are tracked as an
343
+ * ordered list of stable boundary cursors; each loaded page is a live
344
+ * subscription over a FIXED `(lower, upper]` range.
345
+ *
346
+ * `loadMore` appends the next page off the open-ended tail's `continueCursor`;
347
+ * it is a no-op unless `status === "CanLoadMore"`.
348
+ *
349
+ * Call from an injection context:
350
+ * ```ts
351
+ * readonly messages = paginatedQuery(api.messages.list, {}, { initialNumItems: 20 });
352
+ * ```
353
+ */
354
+ declare const paginatedQuery: <F extends FunctionReference>(reference: F, args: PaginatedArgs<F> | "skip", options: PaginatedQueryOptions) => PaginatedQueryResult<PageItemOf<F>>;
355
+ /**
356
+ * Subscribe to a reactively-paginated query and expose its pages discretely.
357
+ *
358
+ * Shares `paginatedQuery`'s reactive-pagination engine but keeps each page as
359
+ * its own inner array rather than flattening them, and adds the TanStack-Query-
360
+ * style `fetchNextPage` / `hasNextPage` / `isFetchingNextPage` shape.
361
+ *
362
+ * Call from an injection context:
363
+ * ```ts
364
+ * readonly feed = infiniteQuery(api.messages.list, {}, { initialNumItems: 20 });
365
+ * ```
366
+ */
367
+ declare const infiniteQuery: <F extends FunctionReference>(reference: F, args: PaginatedArgs<F> | "skip", options: PaginatedQueryOptions) => InfiniteQueryResult<PageItemOf<F>>;
368
+ type HeartbeatReference = FunctionReference<"mutation", {
369
+ data?: Record<string, unknown>;
370
+ roomId: string;
371
+ sessionId: string;
372
+ }>;
373
+ type ListPresentReference = FunctionReference<"query", {
374
+ roomId: string;
375
+ }>;
376
+ interface PresenceOptions<H extends HeartbeatReference, L extends ListPresentReference> {
377
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
378
+ client?: LunoraClient;
379
+ /** Awareness blob for the first heartbeat (selection, cursor, name, color…). */
380
+ data?: Record<string, unknown>;
381
+ /** `DestroyRef` whose `onDestroy` cleans up. Defaults to `inject(DestroyRef)`. */
382
+ destroyRef?: DestroyRef;
383
+ /** The `api.*` reference for the presence heartbeat mutation. */
384
+ heartbeat: H;
385
+ /** Heartbeat cadence in ms. Defaults to 10s (10000). */
386
+ intervalMs?: number;
387
+ /** The `api.*` reference for the presence listPresent query. */
388
+ listPresent: L;
389
+ /**
390
+ * Stable id for this presence row. Defaults to a fresh per-call id.
391
+ * Pass a user/connection id to control deduping across tabs.
392
+ */
393
+ sessionId?: string;
394
+ /** Forwarded to the heartbeat mutation / listPresent subscription when sharding by room. */
395
+ shardKey?: string;
396
+ }
397
+ interface PresenceResult<L extends ListPresentReference> {
398
+ /** The present members for the room. `undefined` until the first push. */
399
+ present: Signal<ReturnOf<L> | undefined>;
400
+ /** This mount's session id (generated when not supplied). */
401
+ sessionId: string;
402
+ /** Replace the awareness `data` sent with subsequent heartbeats, and heartbeat immediately. */
403
+ setData: (data: Record<string, unknown> | undefined) => void;
404
+ }
405
+ /**
406
+ * `presence` — collaborative-awareness primitive, the client half of the
407
+ * `@lunora/server` `definePresence` preset.
408
+ *
409
+ * Drives the heartbeat mutation (on mount, interval, and tab re-focus) and
410
+ * subscribes to the live `listPresent` query for the given room.
411
+ *
412
+ * Call from an injection context (component/service field or constructor):
413
+ * ```ts
414
+ * readonly roomPresence = presence("room:general", {
415
+ * heartbeat: api.presence.heartbeat,
416
+ * listPresent: api.presence.listPresent,
417
+ * });
418
+ * ```
419
+ */
420
+ declare const presence: <H extends HeartbeatReference, L extends ListPresentReference>(roomId: string, options: PresenceOptions<H, L>) => PresenceResult<L>;
421
+ interface RateLimitOptions {
422
+ /**
423
+ * `DestroyRef` whose `onDestroy` clears the interval. Defaults to
424
+ * `inject(DestroyRef)` — the calling component/service.
425
+ */
426
+ destroyRef?: DestroyRef;
427
+ /** Clock injection for tests. Defaults to `Date.now`. */
428
+ now?: () => number;
429
+ /**
430
+ * Re-render cadence in milliseconds while throttled, so `retryAfter` ticks
431
+ * down and `disabled` flips back automatically. Defaults to `1000`.
432
+ */
433
+ tickMs?: number;
434
+ }
435
+ interface RateLimitResult {
436
+ /** Would consuming `count` (default 1) succeed right now? Does not consume. */
437
+ check: (count?: number) => boolean;
438
+ /** Optimistically consume `count` (default 1) locally; mirrors the server algorithm. */
439
+ consume: (count?: number) => RateLimitStatus;
440
+ /** `true` while a single unit cannot be consumed — convenient for disabling a control. */
441
+ disabled: Signal<boolean>;
442
+ /** `true` while a single unit can be consumed. */
443
+ ok: Signal<boolean>;
444
+ /** Clear local accounting (e.g. after the server confirms a reset). */
445
+ reset: () => void;
446
+ /** Milliseconds until the next unit is available. `0` when `ok`. */
447
+ retryAfter: Signal<number>;
448
+ }
449
+ /**
450
+ * Client-side mirror of a rate limit for instant UX — disable a button or show
451
+ * a countdown without a round-trip. It runs the same token-bucket / fixed-window
452
+ * math as `@lunora/ratelimit` on the server, so the prediction agrees with the
453
+ * authoritative check; the server remains the source of truth.
454
+ *
455
+ * Requires an Angular injection context unless a `DestroyRef` is passed
456
+ * explicitly via `options.destroyRef`.
457
+ *
458
+ * ```ts
459
+ * readonly sendLimit = rateLimit({ kind: "token bucket", period: 1000, rate: 10 });
460
+ * ```
461
+ */
462
+ declare const rateLimit: (config: RateLimitConfig, options?: RateLimitOptions) => RateLimitResult;
463
+ interface SubscriptionOptions {
464
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
465
+ client?: LunoraClient;
466
+ /**
467
+ * `DestroyRef` whose `onDestroy` tears the subscription down. Defaults to
468
+ * `inject(DestroyRef)` — the calling component/service.
469
+ */
470
+ destroyRef?: DestroyRef;
471
+ /**
472
+ * Called when the subscription errors after the initial attach. Without it,
473
+ * a post-attach failure is dropped silently.
474
+ */
475
+ onError?: (error: SubscriptionError) => void;
476
+ /** Route to a specific shard when the target function is `.shardBy(...)`-partitioned. */
477
+ shardKey?: string;
478
+ }
479
+ interface SubscriptionResult<T> {
480
+ /** The latest value pushed by the server. `undefined` before the first frame. */
481
+ data: Signal<T | undefined>;
482
+ /** The latest error, or `undefined`. */
483
+ error: Signal<SubscriptionError | undefined>;
484
+ }
485
+ /**
486
+ * Subscribe to a reactive server push stream. Returns `{ data, error }` signals
487
+ * that update whenever the server emits a new value.
488
+ *
489
+ * Unlike `liveQuery`, which tracks a single value, `subscription` also
490
+ * exposes an `error` signal for the async error channel. Use it for ephemeral,
491
+ * high-frequency streams where you need error visibility.
492
+ *
493
+ * Pass `"skip"` as `args` to short-circuit — no network call, no socket.
494
+ * The subscription tears down when the owning `DestroyRef` fires.
495
+ *
496
+ * Call from an injection context (component/service field or constructor):
497
+ * ```ts
498
+ * readonly stream = subscription(api.events.stream, { roomId: "general" });
499
+ * ```
500
+ */
501
+ declare const subscription: <F extends FunctionReference>(reference: F, args: ArgsOf<F> | "skip", options?: SubscriptionOptions) => SubscriptionResult<ReturnOf<F>>;
502
+ export { type AuthOptions, type AuthResult, type ConnectionStatusOptions, type FlagContext, type FlagOptions, type FlagValue, type FlagsOptions, type HeartbeatReference, type HydratePreloadedOptions, type HydratePreloadedResult, type InfiniteQueryResult, LUNORA_CLIENT, type ListPresentReference, type LiveQueryOptions, type MutateOptions, type MutatorResult, type PaginatedQueryOptions, type PaginatedQueryResult, type PresenceOptions, type PresenceResult, type ProvideLunoraOptions, type RateLimitOptions, type RateLimitResult, type SubscriptionOptions, type SubscriptionResult, auth, connectionStatus, flag, flags, hydratePreloaded, infiniteQuery, injectLunoraClient, liveQuery, mutate, mutator, paginatedQuery, presence, provideLunora, rateLimit, subscription };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,39 @@
1
- import { InjectionToken, EnvironmentProviders, DestroyRef, Signal } from '@angular/core';
2
- import { LunoraClient, LunoraClientOptions, ConnectionStatus, SubscriptionError, FunctionReference, ArgsOf, ReturnOf, MutationCallOptions } from '@lunora/client';
3
- export type { ArgsOf, ConnectionStatus, FunctionReference, LunoraClient, LunoraClientOptions, MutationCallOptions, ReturnOf, SubscriptionError, Unsubscribe } from '@lunora/client';
1
+ import { DestroyRef, Signal, InjectionToken, EnvironmentProviders } from '@angular/core';
2
+ import { LunoraClient, User, LunoraClientOptions, ConnectionStatus, SubscriptionError, Preloaded, FunctionReference, ArgsOf, ReturnOf, MutationCallOptions, MutatorHandle } from '@lunora/client';
3
+ export type { ArgsOf, ConnectionStatus, FunctionReference, LunoraClient, LunoraClientOptions, MutationCallOptions, Preloaded, ReturnOf, SubscriptionError, Unsubscribe } from '@lunora/client';
4
+ import { PaginationStatus } from '@lunora/client/pagination';
5
+ import { RateLimitStatus, RateLimitConfig } from '@lunora/ratelimit';
4
6
  export { SKIP } from '@lunora/client/query';
7
+ interface AuthOptions {
8
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
9
+ client?: LunoraClient;
10
+ /** `DestroyRef` whose `onDestroy` removes the listeners. Defaults to `inject(DestroyRef)`. */
11
+ destroyRef?: DestroyRef;
12
+ }
13
+ interface AuthResult {
14
+ /** Set the auth token (sign-in / sign-out). */
15
+ setToken: (token: string | null) => void;
16
+ /** The current auth token, or `null`. */
17
+ token: Signal<string | null>;
18
+ /** The resolved user from `store.getUser()`, or `null`. */
19
+ user: Signal<User | null>;
20
+ }
21
+ /**
22
+ * Token + identity plumbing for Angular. `token` is a signal tracking the
23
+ * client's auth token; `user` is a signal resolved from `getCurrentUser()`
24
+ * whenever the token changes. `setToken(jwt)` after sign-in makes subsequent
25
+ * RPC calls carry the `Authorization` header.
26
+ *
27
+ * Multiple `auth` instances on the same client share a single per-client
28
+ * identity store (from `@lunora/client/auth`) — a `setToken` from one component
29
+ * re-renders every watcher with the freshly-resolved user.
30
+ *
31
+ * Call from an injection context (component/service field or constructor):
32
+ * ```ts
33
+ * const { token, user, setToken } = auth();
34
+ * ```
35
+ */
36
+ declare const auth: (options?: AuthOptions) => AuthResult;
5
37
  /**
6
38
  * DI token carrying the framework-neutral {@link LunoraClient}. Every reactive
7
39
  * primitive in this adapter (`liveQuery`, `mutate`, `connectionStatus`) reads the
@@ -68,6 +100,94 @@ interface ConnectionStatusOptions {
68
100
  * injection context (component/service field or constructor).
69
101
  */
70
102
  declare const connectionStatus: (options?: ConnectionStatusOptions) => Signal<ConnectionStatus>;
103
+ /** The value kinds a flag resolves to — OpenFeature's boolean / number / string / structured (JSON) flags. */
104
+ type FlagValue = boolean | number | string | Record<string, unknown> | unknown[] | null;
105
+ /** Targeting context bag forwarded to the OpenFeature provider. */
106
+ type FlagContext = Record<string, unknown>;
107
+ interface FlagOptions {
108
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
109
+ client?: LunoraClient;
110
+ /**
111
+ * Per-call targeting context merged on top of the app's default `identify`
112
+ * targeting key.
113
+ */
114
+ context?: FlagContext;
115
+ /** `DestroyRef` whose `onDestroy` tears down the subscription. Defaults to `inject(DestroyRef)`. */
116
+ destroyRef?: DestroyRef;
117
+ }
118
+ /**
119
+ * Subscribe to a single feature flag, live over Lunora's WebSocket.
120
+ *
121
+ * The returned signal holds `defaultValue` until the first evaluation lands, then
122
+ * the server's resolved value — re-pushed whenever the provider re-evaluates.
123
+ * The flag's kind is inferred from `defaultValue`'s runtime type, so
124
+ * `flag("dark", false)` reads a boolean and `flag("hero", "control")` a string.
125
+ *
126
+ * Evaluation runs through whatever OpenFeature provider the app wired in
127
+ * `lunora/flags.ts`; the read never throws — a provider error resolves the
128
+ * default (the same fail-open contract as server-side `ctx.flags`).
129
+ *
130
+ * Call from an injection context:
131
+ * ```ts
132
+ * readonly darkMode = flag("dark-mode", false);
133
+ * ```
134
+ */
135
+ declare const flag: <T extends FlagValue>(key: string, defaultValue: T, options?: FlagOptions) => Signal<T>;
136
+ interface FlagsOptions {
137
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
138
+ client?: LunoraClient;
139
+ /**
140
+ * Targeting context shared by every flag in the set, merged on top of the
141
+ * app's default `identify` targeting key.
142
+ */
143
+ context?: FlagContext;
144
+ /** `DestroyRef` whose `onDestroy` tears down the subscriptions. Defaults to `inject(DestroyRef)`. */
145
+ destroyRef?: DestroyRef;
146
+ }
147
+ /**
148
+ * Subscribe to several feature flags at once, live over Lunora's WebSocket.
149
+ *
150
+ * Pass a record of `key → defaultValue`; each flag's kind is inferred from its
151
+ * default, and the returned signal holds the same-shaped record with resolved
152
+ * values (the defaults until each evaluation lands).
153
+ *
154
+ * Call from an injection context:
155
+ * ```ts
156
+ * readonly features = flags({ "dark-mode": false, "new-editor": false });
157
+ * ```
158
+ */
159
+ declare const flags: <T extends Record<string, FlagValue>>(flagDefaults: T, options?: FlagsOptions) => Signal<T>;
160
+ interface HydratePreloadedOptions {
161
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
162
+ client?: LunoraClient;
163
+ /** `DestroyRef` whose `onDestroy` tears the subscription down. Defaults to `inject(DestroyRef)`. */
164
+ destroyRef?: DestroyRef;
165
+ }
166
+ interface HydratePreloadedResult<T> {
167
+ /** The latest value pushed by the server. Seeded synchronously from the preloaded value. */
168
+ data: Signal<T | undefined>;
169
+ /** The latest subscription error, or `undefined`. */
170
+ error: Signal<SubscriptionError | undefined>;
171
+ }
172
+ /**
173
+ * Hydrate a query from a {@link Preloaded} token produced by `preloadQuery`
174
+ * during SSR, then keep it live — the Angular half of the reactive-loader
175
+ * handoff.
176
+ *
177
+ * The returned signal is seeded **synchronously** from `preloaded.value`, so the
178
+ * very first read (during hydration) shows the server value: no loading flash,
179
+ * no hydration mismatch. After seeding it opens a WebSocket subscription on the
180
+ * same `(functionPath, args, shardKey)` the SSR loader used, so every later
181
+ * server delta updates the signal exactly like `liveQuery`.
182
+ *
183
+ * The subscription tears down when the owning `DestroyRef` fires.
184
+ *
185
+ * Call from an injection context:
186
+ * ```ts
187
+ * readonly { data, error } = hydratePreloaded(preloadedMessages);
188
+ * ```
189
+ */
190
+ declare const hydratePreloaded: <T>(preloaded: Preloaded<T>, options?: HydratePreloadedOptions) => HydratePreloadedResult<T>;
71
191
  interface LiveQueryOptions {
72
192
  /**
73
193
  * Client to bind to. Defaults to the injected `LUNORA_CLIENT`; pass one
@@ -143,4 +263,240 @@ interface MutateOptions<F extends FunctionReference> extends MutationCallOptions
143
263
  * resolve from the injector.
144
264
  */
145
265
  declare const mutate: <F extends FunctionReference>(reference: F, args: ArgsOf<F>, options?: MutateOptions<F>) => Promise<ReturnOf<F>>;
146
- export { type ConnectionStatusOptions, LUNORA_CLIENT, type LiveQueryOptions, type MutateOptions, type ProvideLunoraOptions, connectionStatus, injectLunoraClient, liveQuery, mutate, provideLunora };
266
+ interface MutatorResult<TArgs> {
267
+ /** The latest invocation's error, or `undefined`. */
268
+ error: Signal<Error | undefined>;
269
+ /** `true` when the latest invocation rejected. */
270
+ isError: Signal<boolean>;
271
+ /** Run the mutator; resolves once the write is persisted, rejects on failure. */
272
+ mutate: (args: TArgs) => Promise<void>;
273
+ /** `true` while ANY invocation from this handle is in flight. */
274
+ pending: Signal<boolean>;
275
+ /** Clear the latest `error` back to idle. */
276
+ reset: () => void;
277
+ }
278
+ /**
279
+ * Ergonomic `{ mutate, pending, error, isError, reset }` wrapper over a bound
280
+ * custom-mutator handle from `` `@lunora/db` ``'s `bindMutators` — the Angular
281
+ * equivalent of `` `@lunora/react` ``'s `useMutator`. The optimistic overlay and
282
+ * server-authoritative push are owned by the bound handle; this function only
283
+ * surfaces reactive state for the in-flight/error lifecycle.
284
+ *
285
+ * `pending` is ref-counted across overlapping invocations of THIS handle, so it
286
+ * clears only once every concurrent call has settled.
287
+ *
288
+ * Does NOT require an injection context — it works with plain signals.
289
+ *
290
+ * ```ts
291
+ * private readonly collection = bindMutators(collections);
292
+ * readonly mutator = mutator(this.collection.insert);
293
+ * ```
294
+ */
295
+ declare const mutator: <TArgs = Record<string, unknown>>(handle: MutatorHandle<TArgs>) => MutatorResult<TArgs>;
296
+ /** The args a paginated query exposes minus the framework-supplied page cursor. */
297
+ type PaginatedArgs<F extends FunctionReference> = Omit<ArgsOfRaw<F>, "paginationOpts">;
298
+ /** The element type of the `page` array a paginated query returns. */
299
+ type PageItemOf<F extends FunctionReference> = ReturnTypeOf<F> extends {
300
+ page: (infer T)[];
301
+ } ? T : unknown;
302
+ type ArgsOfRaw<F extends FunctionReference> = F extends FunctionReference<"query", infer A> ? A : never;
303
+ type ReturnTypeOf<F extends FunctionReference> = F extends FunctionReference<"query", unknown, infer R> ? R : never;
304
+ interface PaginatedQueryOptions {
305
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
306
+ client?: LunoraClient;
307
+ /** `DestroyRef` whose `onDestroy` tears down the subscriptions. Defaults to `inject(DestroyRef)`. */
308
+ destroyRef?: DestroyRef;
309
+ /** Page size for the first page (and the default for `loadMore`). */
310
+ initialNumItems: number;
311
+ /** Route to a specific shard when the target function is `.shardBy(...)`-partitioned. */
312
+ shardKey?: string;
313
+ }
314
+ interface PaginatedQueryResult<T> {
315
+ /** `true` while the first page or a `loadMore` page is in flight. */
316
+ isLoading: Signal<boolean>;
317
+ /** Request the next page. A no-op unless `status === "CanLoadMore"`. */
318
+ loadMore: (numberItems: number) => void;
319
+ /** Flattened items across every loaded page, in order. */
320
+ results: Signal<T[]>;
321
+ /** The pagination status. */
322
+ status: Signal<PaginationStatus>;
323
+ }
324
+ interface InfiniteQueryResult<T> {
325
+ /** Request the next page. A no-op unless `status === "CanLoadMore"`. */
326
+ fetchNextPage: (numberItems?: number) => void;
327
+ /** `true` when the loaded tail reports it can load another page. */
328
+ hasNextPage: Signal<boolean>;
329
+ /** `true` while a `fetchNextPage` page (beyond the first) is in flight. */
330
+ isFetchingNextPage: Signal<boolean>;
331
+ /** `true` while the first page is in flight. */
332
+ isLoading: Signal<boolean>;
333
+ /** One inner array per loaded page, in order; unresolved pages are omitted. */
334
+ pages: Signal<T[][]>;
335
+ /** The pagination status. */
336
+ status: Signal<PaginationStatus>;
337
+ }
338
+ /**
339
+ * Subscribe to a reactively-paginated query and grow the feed page by page.
340
+ *
341
+ * The query function must accept a `paginationOpts: { numItems, cursor,
342
+ * endCursor }` arg and return a `PaginationResult`. Pages are tracked as an
343
+ * ordered list of stable boundary cursors; each loaded page is a live
344
+ * subscription over a FIXED `(lower, upper]` range.
345
+ *
346
+ * `loadMore` appends the next page off the open-ended tail's `continueCursor`;
347
+ * it is a no-op unless `status === "CanLoadMore"`.
348
+ *
349
+ * Call from an injection context:
350
+ * ```ts
351
+ * readonly messages = paginatedQuery(api.messages.list, {}, { initialNumItems: 20 });
352
+ * ```
353
+ */
354
+ declare const paginatedQuery: <F extends FunctionReference>(reference: F, args: PaginatedArgs<F> | "skip", options: PaginatedQueryOptions) => PaginatedQueryResult<PageItemOf<F>>;
355
+ /**
356
+ * Subscribe to a reactively-paginated query and expose its pages discretely.
357
+ *
358
+ * Shares `paginatedQuery`'s reactive-pagination engine but keeps each page as
359
+ * its own inner array rather than flattening them, and adds the TanStack-Query-
360
+ * style `fetchNextPage` / `hasNextPage` / `isFetchingNextPage` shape.
361
+ *
362
+ * Call from an injection context:
363
+ * ```ts
364
+ * readonly feed = infiniteQuery(api.messages.list, {}, { initialNumItems: 20 });
365
+ * ```
366
+ */
367
+ declare const infiniteQuery: <F extends FunctionReference>(reference: F, args: PaginatedArgs<F> | "skip", options: PaginatedQueryOptions) => InfiniteQueryResult<PageItemOf<F>>;
368
+ type HeartbeatReference = FunctionReference<"mutation", {
369
+ data?: Record<string, unknown>;
370
+ roomId: string;
371
+ sessionId: string;
372
+ }>;
373
+ type ListPresentReference = FunctionReference<"query", {
374
+ roomId: string;
375
+ }>;
376
+ interface PresenceOptions<H extends HeartbeatReference, L extends ListPresentReference> {
377
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
378
+ client?: LunoraClient;
379
+ /** Awareness blob for the first heartbeat (selection, cursor, name, color…). */
380
+ data?: Record<string, unknown>;
381
+ /** `DestroyRef` whose `onDestroy` cleans up. Defaults to `inject(DestroyRef)`. */
382
+ destroyRef?: DestroyRef;
383
+ /** The `api.*` reference for the presence heartbeat mutation. */
384
+ heartbeat: H;
385
+ /** Heartbeat cadence in ms. Defaults to 10s (10000). */
386
+ intervalMs?: number;
387
+ /** The `api.*` reference for the presence listPresent query. */
388
+ listPresent: L;
389
+ /**
390
+ * Stable id for this presence row. Defaults to a fresh per-call id.
391
+ * Pass a user/connection id to control deduping across tabs.
392
+ */
393
+ sessionId?: string;
394
+ /** Forwarded to the heartbeat mutation / listPresent subscription when sharding by room. */
395
+ shardKey?: string;
396
+ }
397
+ interface PresenceResult<L extends ListPresentReference> {
398
+ /** The present members for the room. `undefined` until the first push. */
399
+ present: Signal<ReturnOf<L> | undefined>;
400
+ /** This mount's session id (generated when not supplied). */
401
+ sessionId: string;
402
+ /** Replace the awareness `data` sent with subsequent heartbeats, and heartbeat immediately. */
403
+ setData: (data: Record<string, unknown> | undefined) => void;
404
+ }
405
+ /**
406
+ * `presence` — collaborative-awareness primitive, the client half of the
407
+ * `@lunora/server` `definePresence` preset.
408
+ *
409
+ * Drives the heartbeat mutation (on mount, interval, and tab re-focus) and
410
+ * subscribes to the live `listPresent` query for the given room.
411
+ *
412
+ * Call from an injection context (component/service field or constructor):
413
+ * ```ts
414
+ * readonly roomPresence = presence("room:general", {
415
+ * heartbeat: api.presence.heartbeat,
416
+ * listPresent: api.presence.listPresent,
417
+ * });
418
+ * ```
419
+ */
420
+ declare const presence: <H extends HeartbeatReference, L extends ListPresentReference>(roomId: string, options: PresenceOptions<H, L>) => PresenceResult<L>;
421
+ interface RateLimitOptions {
422
+ /**
423
+ * `DestroyRef` whose `onDestroy` clears the interval. Defaults to
424
+ * `inject(DestroyRef)` — the calling component/service.
425
+ */
426
+ destroyRef?: DestroyRef;
427
+ /** Clock injection for tests. Defaults to `Date.now`. */
428
+ now?: () => number;
429
+ /**
430
+ * Re-render cadence in milliseconds while throttled, so `retryAfter` ticks
431
+ * down and `disabled` flips back automatically. Defaults to `1000`.
432
+ */
433
+ tickMs?: number;
434
+ }
435
+ interface RateLimitResult {
436
+ /** Would consuming `count` (default 1) succeed right now? Does not consume. */
437
+ check: (count?: number) => boolean;
438
+ /** Optimistically consume `count` (default 1) locally; mirrors the server algorithm. */
439
+ consume: (count?: number) => RateLimitStatus;
440
+ /** `true` while a single unit cannot be consumed — convenient for disabling a control. */
441
+ disabled: Signal<boolean>;
442
+ /** `true` while a single unit can be consumed. */
443
+ ok: Signal<boolean>;
444
+ /** Clear local accounting (e.g. after the server confirms a reset). */
445
+ reset: () => void;
446
+ /** Milliseconds until the next unit is available. `0` when `ok`. */
447
+ retryAfter: Signal<number>;
448
+ }
449
+ /**
450
+ * Client-side mirror of a rate limit for instant UX — disable a button or show
451
+ * a countdown without a round-trip. It runs the same token-bucket / fixed-window
452
+ * math as `@lunora/ratelimit` on the server, so the prediction agrees with the
453
+ * authoritative check; the server remains the source of truth.
454
+ *
455
+ * Requires an Angular injection context unless a `DestroyRef` is passed
456
+ * explicitly via `options.destroyRef`.
457
+ *
458
+ * ```ts
459
+ * readonly sendLimit = rateLimit({ kind: "token bucket", period: 1000, rate: 10 });
460
+ * ```
461
+ */
462
+ declare const rateLimit: (config: RateLimitConfig, options?: RateLimitOptions) => RateLimitResult;
463
+ interface SubscriptionOptions {
464
+ /** Client to bind to. Defaults to the injected `LUNORA_CLIENT`. */
465
+ client?: LunoraClient;
466
+ /**
467
+ * `DestroyRef` whose `onDestroy` tears the subscription down. Defaults to
468
+ * `inject(DestroyRef)` — the calling component/service.
469
+ */
470
+ destroyRef?: DestroyRef;
471
+ /**
472
+ * Called when the subscription errors after the initial attach. Without it,
473
+ * a post-attach failure is dropped silently.
474
+ */
475
+ onError?: (error: SubscriptionError) => void;
476
+ /** Route to a specific shard when the target function is `.shardBy(...)`-partitioned. */
477
+ shardKey?: string;
478
+ }
479
+ interface SubscriptionResult<T> {
480
+ /** The latest value pushed by the server. `undefined` before the first frame. */
481
+ data: Signal<T | undefined>;
482
+ /** The latest error, or `undefined`. */
483
+ error: Signal<SubscriptionError | undefined>;
484
+ }
485
+ /**
486
+ * Subscribe to a reactive server push stream. Returns `{ data, error }` signals
487
+ * that update whenever the server emits a new value.
488
+ *
489
+ * Unlike `liveQuery`, which tracks a single value, `subscription` also
490
+ * exposes an `error` signal for the async error channel. Use it for ephemeral,
491
+ * high-frequency streams where you need error visibility.
492
+ *
493
+ * Pass `"skip"` as `args` to short-circuit — no network call, no socket.
494
+ * The subscription tears down when the owning `DestroyRef` fires.
495
+ *
496
+ * Call from an injection context (component/service field or constructor):
497
+ * ```ts
498
+ * readonly stream = subscription(api.events.stream, { roomId: "general" });
499
+ * ```
500
+ */
501
+ declare const subscription: <F extends FunctionReference>(reference: F, args: ArgsOf<F> | "skip", options?: SubscriptionOptions) => SubscriptionResult<ReturnOf<F>>;
502
+ export { type AuthOptions, type AuthResult, type ConnectionStatusOptions, type FlagContext, type FlagOptions, type FlagValue, type FlagsOptions, type HeartbeatReference, type HydratePreloadedOptions, type HydratePreloadedResult, type InfiniteQueryResult, LUNORA_CLIENT, type ListPresentReference, type LiveQueryOptions, type MutateOptions, type MutatorResult, type PaginatedQueryOptions, type PaginatedQueryResult, type PresenceOptions, type PresenceResult, type ProvideLunoraOptions, type RateLimitOptions, type RateLimitResult, type SubscriptionOptions, type SubscriptionResult, auth, connectionStatus, flag, flags, hydratePreloaded, infiniteQuery, injectLunoraClient, liveQuery, mutate, mutator, paginatedQuery, presence, provideLunora, rateLimit, subscription };
package/dist/index.mjs CHANGED
@@ -1,5 +1,13 @@
1
+ export { auth } from './packem_shared/auth-Df9N87Z4.mjs';
1
2
  export { LUNORA_CLIENT, injectLunoraClient, provideLunora } from './packem_shared/LUNORA_CLIENT-DHUfNu9x.mjs';
2
3
  export { connectionStatus } from './packem_shared/connectionStatus-BlLodleK.mjs';
4
+ export { flag, flags } from './packem_shared/flag-CGBo90HJ.mjs';
5
+ export { hydratePreloaded } from './packem_shared/hydratePreloaded-ClRc-7rL.mjs';
3
6
  export { liveQuery } from './packem_shared/liveQuery-D5VQBOgf.mjs';
4
7
  export { mutate } from './packem_shared/mutate-D3rEHwbb.mjs';
8
+ export { mutator } from './packem_shared/mutator-BHL8bakL.mjs';
9
+ export { infiniteQuery, paginatedQuery } from './packem_shared/infiniteQuery-DUV60k-n.mjs';
10
+ export { presence } from './packem_shared/presence-h2xonCZM.mjs';
11
+ export { rateLimit } from './packem_shared/rateLimit-CKcAig4M.mjs';
12
+ export { subscription } from './packem_shared/subscription-Dd9dQiBB.mjs';
5
13
  export { SKIP } from '@lunora/client/query';
@@ -0,0 +1,27 @@
1
+ import { inject, DestroyRef, signal } from '@angular/core';
2
+ import { getIdentityStore } from '@lunora/client/auth';
3
+ import { resolveLunoraClient } from './LUNORA_CLIENT-DHUfNu9x.mjs';
4
+
5
+ const auth = (options = {}) => {
6
+ const client = resolveLunoraClient(options.client);
7
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
8
+ const store = getIdentityStore(client);
9
+ const token = signal(client.getAuthToken());
10
+ const user = signal(store.getUser());
11
+ const unsubToken = client.onAuthTokenChange(() => {
12
+ token.set(client.getAuthToken());
13
+ });
14
+ const unsubUser = store.subscribe(() => {
15
+ user.set(store.getUser());
16
+ });
17
+ destroyRef.onDestroy(() => {
18
+ unsubToken();
19
+ unsubUser();
20
+ });
21
+ const setToken = (next) => {
22
+ client.setAuthToken(next);
23
+ };
24
+ return { setToken, token: token.asReadonly(), user: user.asReadonly() };
25
+ };
26
+
27
+ export { auth };
@@ -0,0 +1,70 @@
1
+ import { inject, DestroyRef, signal } from '@angular/core';
2
+ import { resolveLunoraClient } from './LUNORA_CLIENT-DHUfNu9x.mjs';
3
+
4
+ const FLAGS_EVAL_PATH = "__lunora_flags__:eval";
5
+ const flagKind = (value) => {
6
+ const kind = typeof value;
7
+ if (kind === "boolean" || kind === "number" || kind === "string") {
8
+ return kind;
9
+ }
10
+ return "object";
11
+ };
12
+ const flagsReference = { __lunoraRef: FLAGS_EVAL_PATH };
13
+ const flag = (key, defaultValue, options = {}) => {
14
+ const client = resolveLunoraClient(options.client);
15
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
16
+ const type = flagKind(defaultValue);
17
+ const value = signal(defaultValue);
18
+ let unsubscribe;
19
+ try {
20
+ unsubscribe = client.subscribe(
21
+ flagsReference,
22
+ { context: options.context, default: defaultValue, key, type },
23
+ (next) => {
24
+ value.set(next);
25
+ },
26
+ {
27
+ onError: () => {
28
+ value.set(defaultValue);
29
+ }
30
+ }
31
+ );
32
+ } catch {
33
+ }
34
+ if (unsubscribe) {
35
+ destroyRef.onDestroy(unsubscribe);
36
+ }
37
+ return value.asReadonly();
38
+ };
39
+ const flags = (flagDefaults, options = {}) => {
40
+ const client = resolveLunoraClient(options.client);
41
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
42
+ const values = signal({ ...flagDefaults });
43
+ const unsubscribes = [];
44
+ for (const [key, defaultValue] of Object.entries(flagDefaults)) {
45
+ try {
46
+ const unsub = client.subscribe(
47
+ flagsReference,
48
+ { context: options.context, default: defaultValue, key, type: flagKind(defaultValue) },
49
+ (next) => {
50
+ values.set({ ...values(), [key]: next });
51
+ },
52
+ {
53
+ onError: () => {
54
+ values.set({ ...values(), [key]: defaultValue });
55
+ }
56
+ }
57
+ );
58
+ unsubscribes.push(unsub);
59
+ } catch {
60
+ }
61
+ }
62
+ destroyRef.onDestroy(() => {
63
+ for (const unsubscribe of unsubscribes) {
64
+ unsubscribe();
65
+ }
66
+ });
67
+ return values.asReadonly();
68
+ };
69
+
70
+ export { flag, flags };
@@ -0,0 +1,29 @@
1
+ import { inject, DestroyRef, signal } from '@angular/core';
2
+ import { resolveLunoraClient } from './LUNORA_CLIENT-DHUfNu9x.mjs';
3
+
4
+ const hydratePreloaded = (preloaded, options = {}) => {
5
+ const client = resolveLunoraClient(options.client);
6
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
7
+ const { args, functionPath, shardKey, value } = preloaded;
8
+ const data = signal(value);
9
+ const error = signal(void 0);
10
+ const functionReference = { __lunoraRef: functionPath };
11
+ const unsubscribe = client.subscribe(
12
+ functionReference,
13
+ args,
14
+ (next) => {
15
+ data.set(next);
16
+ error.set(void 0);
17
+ },
18
+ {
19
+ onError: (error_) => {
20
+ error.set(error_);
21
+ },
22
+ shardKey
23
+ }
24
+ );
25
+ destroyRef.onDestroy(unsubscribe);
26
+ return { data: data.asReadonly(), error: error.asReadonly() };
27
+ };
28
+
29
+ export { hydratePreloaded };
@@ -0,0 +1,200 @@
1
+ import { computed, inject, DestroyRef, signal } from '@angular/core';
2
+ import { initialPages, rebalance, derivePaginationStatus, applyLoadMore } from '@lunora/client/pagination';
3
+ import { resolveLunoraClient } from './LUNORA_CLIENT-DHUfNu9x.mjs';
4
+
5
+ const buildPageKey = (functionPath, pageArgs) => `${functionPath}::${JSON.stringify(pageArgs)}`;
6
+ const buildPageArgs = (page, baseArgs) => {
7
+ return {
8
+ ...baseArgs,
9
+ paginationOpts: { cursor: page.lower, endCursor: page.upper, numItems: page.numItems }
10
+ };
11
+ };
12
+ const usePaginatedCore = (reference, baseArgs, options) => {
13
+ const client = resolveLunoraClient(options.client);
14
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
15
+ const { initialNumItems, shardKey } = options;
16
+ const functionPath = reference["__lunoraRef"];
17
+ const narrowedArgs = baseArgs === "skip" ? {} : baseArgs;
18
+ const pages = signal(initialPages(initialNumItems));
19
+ const pageResults = signal([]);
20
+ const status = signal("LoadingFirstPage");
21
+ const activeSubs = /* @__PURE__ */ new Map();
22
+ const resultsByKey = /* @__PURE__ */ new Map();
23
+ const pendingPageKeys = /* @__PURE__ */ new Set();
24
+ const doRebuildPageResults = () => {
25
+ const currentPages = pages();
26
+ const items = currentPages.map((page) => {
27
+ const key = buildPageKey(functionPath, buildPageArgs(page, narrowedArgs));
28
+ return resultsByKey.get(key);
29
+ });
30
+ pageResults.set(items);
31
+ const { status: derivedStatus } = derivePaginationStatus(baseArgs === "skip", items);
32
+ status.set(derivedStatus);
33
+ };
34
+ const migrateResultsForRebalance = (oldPages, newPages) => {
35
+ const keyOf = (page) => buildPageKey(functionPath, buildPageArgs(page, narrowedArgs));
36
+ for (const newPage of newPages) {
37
+ const newKey = keyOf(newPage);
38
+ if (resultsByKey.has(newKey)) {
39
+ continue;
40
+ }
41
+ const donor = oldPages.find((op) => op.lower === newPage.lower);
42
+ if (donor) {
43
+ const carried = resultsByKey.get(keyOf(donor));
44
+ if (carried) {
45
+ resultsByKey.set(newKey, carried);
46
+ }
47
+ }
48
+ }
49
+ };
50
+ const syncSubscriptions = (currentPages) => {
51
+ const wantedKeys = /* @__PURE__ */ new Set();
52
+ for (const page of currentPages) {
53
+ wantedKeys.add(buildPageKey(functionPath, buildPageArgs(page, narrowedArgs)));
54
+ }
55
+ for (const [originalKey, entry] of activeSubs) {
56
+ if (!wantedKeys.has(entry.currentKey)) {
57
+ entry.unsub();
58
+ activeSubs.delete(originalKey);
59
+ resultsByKey.delete(entry.currentKey);
60
+ }
61
+ }
62
+ const coveredKeys = new Set([...activeSubs.values()].map((subEntry) => subEntry.currentKey));
63
+ for (const page of currentPages) {
64
+ const pageArgs = buildPageArgs(page, narrowedArgs);
65
+ const key = buildPageKey(functionPath, pageArgs);
66
+ if (coveredKeys.has(key)) {
67
+ continue;
68
+ }
69
+ const entry = {
70
+ currentKey: key,
71
+ unsub: void 0
72
+ };
73
+ pendingPageKeys.add(key);
74
+ const unsub = client.subscribe(
75
+ reference,
76
+ pageArgs,
77
+ (value) => {
78
+ resultsByKey.set(entry.currentKey, value);
79
+ pendingPageKeys.delete(entry.currentKey);
80
+ doRebuildPageResults();
81
+ if (pendingPageKeys.size === 0) {
82
+ const latestPages = pages();
83
+ const next = rebalance(latestPages, pageResults());
84
+ if (next) {
85
+ migrateResultsForRebalance(latestPages, next);
86
+ pages.set(next);
87
+ syncSubscriptions(next);
88
+ doRebuildPageResults();
89
+ }
90
+ }
91
+ },
92
+ {
93
+ onError: () => {
94
+ pendingPageKeys.delete(entry.currentKey);
95
+ doRebuildPageResults();
96
+ },
97
+ shardKey
98
+ }
99
+ );
100
+ entry.unsub = unsub;
101
+ activeSubs.set(key, entry);
102
+ coveredKeys.add(key);
103
+ }
104
+ };
105
+ if (baseArgs !== "skip") {
106
+ syncSubscriptions(pages());
107
+ }
108
+ doRebuildPageResults();
109
+ destroyRef.onDestroy(() => {
110
+ for (const entry of activeSubs.values()) {
111
+ entry.unsub();
112
+ }
113
+ activeSubs.clear();
114
+ resultsByKey.clear();
115
+ });
116
+ const loadMore = (numberItems) => {
117
+ if (baseArgs === "skip") {
118
+ return;
119
+ }
120
+ const { nextCursor, status: currentStatus } = derivePaginationStatus(false, pageResults());
121
+ if (currentStatus !== "CanLoadMore") {
122
+ return;
123
+ }
124
+ const next = applyLoadMore(pages(), nextCursor, numberItems);
125
+ if (!next) {
126
+ return;
127
+ }
128
+ const oldTail = pages().at(-1);
129
+ const newPinnedPage = next.at(-2);
130
+ if (oldTail && newPinnedPage) {
131
+ const oldKey = buildPageKey(functionPath, buildPageArgs(oldTail, narrowedArgs));
132
+ const newKey = buildPageKey(functionPath, buildPageArgs(newPinnedPage, narrowedArgs));
133
+ const entry = activeSubs.get(oldKey);
134
+ if (entry && oldKey !== newKey) {
135
+ const carried = resultsByKey.get(oldKey);
136
+ if (carried) {
137
+ resultsByKey.set(newKey, carried);
138
+ }
139
+ entry.unsub();
140
+ activeSubs.delete(oldKey);
141
+ resultsByKey.delete(oldKey);
142
+ }
143
+ }
144
+ pages.set(next);
145
+ syncSubscriptions(pages());
146
+ doRebuildPageResults();
147
+ };
148
+ return { loadMore, pageResults, status };
149
+ };
150
+ const paginatedQuery = (reference, args, options) => {
151
+ const core = usePaginatedCore(reference, args, options);
152
+ const results = computed(() => {
153
+ const items = [];
154
+ for (const result of core.pageResults()) {
155
+ if (result) {
156
+ items.push(...result.page);
157
+ }
158
+ }
159
+ return items;
160
+ });
161
+ const isLoading = computed(() => {
162
+ const statusValue = core.status();
163
+ return statusValue === "LoadingFirstPage" || statusValue === "LoadingMore";
164
+ });
165
+ return {
166
+ isLoading,
167
+ loadMore: core.loadMore,
168
+ results,
169
+ status: core.status
170
+ };
171
+ };
172
+ const infiniteQuery = (reference, args, options) => {
173
+ const { initialNumItems } = options;
174
+ const core = usePaginatedCore(reference, args, options);
175
+ const pages = computed(() => {
176
+ const resultArrays = [];
177
+ for (const page of core.pageResults()) {
178
+ if (page) {
179
+ resultArrays.push(page.page);
180
+ }
181
+ }
182
+ return resultArrays;
183
+ });
184
+ const isLoading = computed(() => core.status() === "LoadingFirstPage");
185
+ const hasNextPage = computed(() => core.status() === "CanLoadMore");
186
+ const isFetchingNextPage = computed(() => core.status() === "LoadingMore");
187
+ const fetchNextPage = (numberItems) => {
188
+ core.loadMore(numberItems ?? initialNumItems);
189
+ };
190
+ return {
191
+ fetchNextPage,
192
+ hasNextPage,
193
+ isFetchingNextPage,
194
+ isLoading,
195
+ pages,
196
+ status: core.status
197
+ };
198
+ };
199
+
200
+ export { infiniteQuery, paginatedQuery };
@@ -0,0 +1,19 @@
1
+ import { signal, computed } from '@angular/core';
2
+ import { createMutatorRunner } from '@lunora/client';
3
+
4
+ const mutator = (handle) => {
5
+ const error = signal(void 0);
6
+ const pending = signal(false);
7
+ const isError = computed(() => error() !== void 0);
8
+ const { mutate, reset } = createMutatorRunner(handle, {
9
+ setError: (value) => {
10
+ error.set(value);
11
+ },
12
+ setPending: (value) => {
13
+ pending.set(value);
14
+ }
15
+ });
16
+ return { error: error.asReadonly(), isError, mutate, pending: pending.asReadonly(), reset };
17
+ };
18
+
19
+ export { mutator };
@@ -0,0 +1,59 @@
1
+ import { inject, DestroyRef, signal } from '@angular/core';
2
+ import { resolveLunoraClient } from './LUNORA_CLIENT-DHUfNu9x.mjs';
3
+
4
+ const makeSessionId = () => crypto.randomUUID();
5
+ const DEFAULT_INTERVAL_MS = 1e4;
6
+ const presence = (roomId, options) => {
7
+ const client = resolveLunoraClient(options.client);
8
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
9
+ const { heartbeat, listPresent, shardKey } = options;
10
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
11
+ const sessionId = options.sessionId ?? makeSessionId();
12
+ const present = signal(void 0);
13
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
14
+ throw new RangeError(`presence intervalMs must be a positive number, got ${String(intervalMs)}`);
15
+ }
16
+ let latestData = options.data;
17
+ const releaseConnectionContext = client.acquireConnectionContext({ roomId, sessionId }, { shardKey });
18
+ const sendHeartbeat = () => {
19
+ const args = { roomId, sessionId };
20
+ if (latestData !== void 0) {
21
+ args.data = latestData;
22
+ }
23
+ client.mutation(heartbeat, args, { shardKey }).catch(() => void 0);
24
+ };
25
+ const setData = (next) => {
26
+ latestData = next;
27
+ sendHeartbeat();
28
+ };
29
+ sendHeartbeat();
30
+ const intervalHandle = setInterval(sendHeartbeat, intervalMs);
31
+ const onVisible = () => {
32
+ if (typeof document !== "undefined" && document.visibilityState === "visible") {
33
+ sendHeartbeat();
34
+ }
35
+ };
36
+ if (typeof document !== "undefined") {
37
+ document.addEventListener("visibilitychange", onVisible);
38
+ }
39
+ const listArgs = { roomId };
40
+ const unsubscribe = client.subscribe(
41
+ listPresent,
42
+ listArgs,
43
+ (value) => {
44
+ present.set(value);
45
+ },
46
+ { shardKey }
47
+ );
48
+ destroyRef.onDestroy(() => {
49
+ clearInterval(intervalHandle);
50
+ if (typeof document !== "undefined") {
51
+ document.removeEventListener("visibilitychange", onVisible);
52
+ }
53
+ releaseConnectionContext();
54
+ unsubscribe();
55
+ });
56
+ return { present: present.asReadonly(), sessionId, setData };
57
+ };
58
+
59
+ export { presence };
@@ -0,0 +1,60 @@
1
+ import { inject, DestroyRef, signal, computed } from '@angular/core';
2
+ import { evaluate } from '@lunora/ratelimit';
3
+
4
+ const rateLimit = (config, options = {}) => {
5
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
6
+ const now = options.now ?? Date.now;
7
+ const tickMs = options.tickMs ?? 1e3;
8
+ let value;
9
+ const epoch = signal(0);
10
+ const computeStatus = () => evaluate(config, value, { consume: false, count: 1, now: now(), reserve: false }).status;
11
+ const status = signal(computeStatus());
12
+ const bump = () => {
13
+ epoch.update((v) => v + 1);
14
+ status.set(computeStatus());
15
+ };
16
+ let intervalHandle;
17
+ const stopInterval = () => {
18
+ if (intervalHandle !== void 0) {
19
+ clearInterval(intervalHandle);
20
+ intervalHandle = void 0;
21
+ }
22
+ };
23
+ const startIntervalIfThrottled = () => {
24
+ if (status().ok || intervalHandle !== void 0) {
25
+ return;
26
+ }
27
+ intervalHandle = setInterval(() => {
28
+ bump();
29
+ if (status().ok) {
30
+ stopInterval();
31
+ }
32
+ }, tickMs);
33
+ };
34
+ startIntervalIfThrottled();
35
+ destroyRef.onDestroy(() => {
36
+ stopInterval();
37
+ });
38
+ return {
39
+ check: (count = 1) => evaluate(config, value, { consume: false, count, now: now(), reserve: false }).status.ok,
40
+ consume: (count = 1) => {
41
+ const result = evaluate(config, value, { consume: true, count, now: now(), reserve: false });
42
+ if (result.value !== void 0) {
43
+ value = result.value;
44
+ }
45
+ bump();
46
+ startIntervalIfThrottled();
47
+ return result.status;
48
+ },
49
+ disabled: computed(() => !status().ok),
50
+ ok: computed(() => status().ok),
51
+ reset: () => {
52
+ value = void 0;
53
+ stopInterval();
54
+ bump();
55
+ },
56
+ retryAfter: computed(() => status().retryAfter)
57
+ };
58
+ };
59
+
60
+ export { rateLimit };
@@ -0,0 +1,37 @@
1
+ import { inject, DestroyRef, signal } from '@angular/core';
2
+ import { createQuerySubscription } from '@lunora/client/query';
3
+ import { resolveLunoraClient } from './LUNORA_CLIENT-DHUfNu9x.mjs';
4
+
5
+ const subscription = (reference, args, options = {}) => {
6
+ const client = resolveLunoraClient(options.client);
7
+ const destroyRef = options.destroyRef ?? inject(DestroyRef);
8
+ const data = signal(void 0);
9
+ const error = signal(void 0);
10
+ if (args !== "skip") {
11
+ const userOnError = options.onError;
12
+ const unsubscribe = createQuerySubscription(
13
+ client,
14
+ reference,
15
+ args,
16
+ {
17
+ onData: (next) => {
18
+ data.set(next);
19
+ error.set(void 0);
20
+ },
21
+ onError: (error_) => {
22
+ error.set(error_);
23
+ data.set(void 0);
24
+ userOnError?.(error_);
25
+ },
26
+ onReset: () => {
27
+ data.set(void 0);
28
+ }
29
+ },
30
+ { shardKey: options.shardKey }
31
+ );
32
+ destroyRef.onDestroy(unsubscribe);
33
+ }
34
+ return { data: data.asReadonly(), error: error.asReadonly() };
35
+ };
36
+
37
+ export { subscription };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/angular",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.5",
4
4
  "description": "Angular reactive adapter for Lunora — signal-based live queries and mutations",
5
5
  "keywords": [
6
6
  "angular",
@@ -45,7 +45,8 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
- "@lunora/client": "1.0.0-alpha.19"
48
+ "@lunora/client": "1.0.0-alpha.19",
49
+ "@lunora/ratelimit": "1.0.0-alpha.5"
49
50
  },
50
51
  "peerDependencies": {
51
52
  "@angular/core": "^19.2.0 || ^20.0.0 || ^21.0.0 || ^22.0.0"