import { n as UnionSchema } from "./union-schema-B-Lsy588.mjs"; import { _ as LazyPagesOptions, c as LazyFetchOptions, f as LazyPageRequest, i as LazyArray, l as LazyInvalidateOptions, m as LazyPages, n as Lazy, p as LazyPageResult, s as LazyFetch, u as LazyOptions } from "./lazy-C0Y_eCnB.mjs"; import { t as WeakRefMap } from "./weak-ref-map-DVc6VbJ3.mjs"; import { AnnotationMapEntry } from "mobx"; import * as T from "typebox"; //#region src/model/make-model.d.ts /** What a model reports to its stores. Loads never emit — only mutations do. */ type ModelEventType = "created" | "updated" | "deleted"; /** * The one thing a store exposes for a model to keep it in step. Stores register themselves with the * model class, held weakly, so a model needs no reference to any store — which is what lets several * stores over the same resource all stay consistent. */ interface ModelListener { onModelEvent(type: ModelEventType, model: any): void; } /** * Root schema for a model: a single object (`makeModel`) or a discriminated * union of objects (`makeUnionModel`) — nested at any depth, since a union of * unions is just a longer flat union. Shared by both factories and by * `makeStore`, which accepts either. */ type ModelSchema = T.TObject | UnionSchema; /** * Fold several values into one map key. A lone value is handed back as it stands, so a numeric id * stays a number; several are joined on `\u0000`, which no real id contains — so no two different * combinations can spell the same key. * * Shared by the identity map and by a store's keyed collections, which is the point: a record and * the params that select it serialize the same way. */ declare const serializeKey: (values: readonly unknown[]) => string | number; /** * @internal When a record's fields were last replaced. Stamped by `setData`, so every path that * loads a record — `instantiate` from a list, `get`, `reload`, `update`, a custom action — refreshes * it through one choke point. * * A symbol so it never collides with a schema field and never reaches `toJSON`, and deliberately * *not* observable: it is metadata read imperatively by `get`, and making it observable would * re-render every consumer of a record on each load for nothing. */ declare const LOADED_AT: unique symbol; type Resource = T.Static; /** * What `keys` may hold: the schema fields that identify one record, or `false` to declare that this * model has no identity at all. `[]` is neither of those — a resource with no identifying fields is * a singleton, so it identity-maps to exactly one instance. */ type KeySpec = readonly (keyof Resource)[] | false; /** * Every field name across the schema. For a union this is the union of *all* variants' keys, not * just the shared ones — `keyof` a union type resolves to the shared keys alone, but the * constructor annotates every variant's fields, so a variant-specific field is a real field at * runtime and must be nameable here. */ type FieldName = Resource extends infer R ? (R extends unknown ? keyof R & string : never) : never; /** * Per-field observability overrides, keyed by schema field. Anything left out keeps the default of * `observable.ref`. */ type FieldAnnotations = Partial, AnnotationMapEntry>>; /** The identity-key names as a string union — `never` for a model that declared no identity. */ type KeyName = K extends readonly (infer Name)[] ? Name & string : never; /** * What `updateData` accepts on a single-object model: any schema field except the identity keys. * * Keys are excluded because the identity map is keyed on them and `updateData` does not re-register * — changing one locally would leave the instance filed under its old key, so `peek` and * `instantiate` would hand back a record whose id disagrees with theirs. */ type ObjectPatch = Partial, KeyName>>; /** * What `updateData` accepts on a union model, resolved against the *narrowed* instance. * * `Self` is the polymorphic `this`, so an un-narrowed instance exposes only the shared fields and a * patch can name only those. Passing through `is`/`as` first widens `this` to that variant, and its * fields become patchable — which is what stops a patch from grafting one variant's fields onto * another. The discriminator is excluded outright: changing it is a change of variant, which is a * whole-record replacement and so belongs to `setData`. */ type UnionPatch = Partial, keyof Self>, D | KeyName>>>; /** * The variant a union instance is currently known to be, resolved from `Self` — the polymorphic * `this`, whose discriminator narrows to a single literal once `is`/`as` has been through it. * * Inferred rather than indexed (`Self[D]`): the interface only constrains `D` against * `Resource`, so TypeScript will not accept it as a key of `this`. */ type VariantOf = Self extends Record ? Extract, Record> : Resource; /** * Whether the model's methods take a leading params argument. True for both `keys: false` and * `keys: []`, which leave nothing to build params from. * * The empty-array case is asked through `K[number]` rather than `K extends readonly []` because an * inline `keys: []` infers as `never[]`, which is not assignable to `readonly []` and so would read * as keyed — leaving `buildParams()` typed `{}` and stripping the body argument off `update` and * every action. Via `K[number]`, `keys: []` and `keys: [] as const` are identical. */ type Keyless$1 = [K] extends [readonly any[]] ? [K[number]] extends [never] ? true : false : true; /** * Whether the identity map is available. `keys: false` — and the config-less `makeModel(schema)`, * which resolves to the same `false` — drop `instantiate` and the rest of the registry statics off * the class type, so reaching for identity you never declared fails to compile rather than throwing. */ type HasIdentity = [K] extends [false] ? false : true; type KeyShape = Keyless$1 extends true ? undefined : Pick, K extends readonly any[] ? K[number] : never>; type KeyedFn = Keyless$1 extends true ? (...args: any[]) => Promise : (params: KeyShape, ...rest: any[]) => Promise; type KeyedBodyFn = Keyless$1 extends true ? (body: any, ...rest: any[]) => Promise : (params: KeyShape, body: any, ...rest: any[]) => Promise; type IsAny = 0 extends 1 & T ? true : false; /** * The property names a config function's first parameter carries beyond the declared keys. * * Asked of the keys rather than by assignability, which is blind to this: `{ id }` and * `{ id; orgId? }` are *mutually* assignable, so a fetcher declaring more still satisfies the slot. * * `any` and an index signature say nothing about which fields exist, so neither is judged — a mock * or a loosely typed client passes through, and with it the risk this guards against. */ type ParamsBeyondKeys = F extends ((p: infer P, ...rest: any[]) => any) ? IsAny

extends true ? never : string extends keyof P ? never : Exclude> : never; /** * Rejects a config function whose first parameter carries more than the declared keys. * * The invariant: the params that identify a record are the params every call uses. The instance * methods rebuild that argument from `buildParams()`, which knows only the keys — so a fetcher * taking anything else would be called by `reload()`, `update()`, `delete()` and the actions with * the rest missing, quietly addressing a different record than the load did. `reload()` can also * run on its own, from a background refresh under `optimistic`, so the divergence need not even be * traceable to a call you wrote. * * If a value scopes the record, declare it as a key — then it is part of identity and * `buildParams()` can rebuild it. If it doesn't, bind it in the config where it cannot drift: * `get: (params) => api.getUser({ ...params, expand: "roles" })`. */ type ParamsMustBeKeys = Keyless$1 extends true ? unknown : [ParamsBeyondKeys | ParamsBeyondKeys | ParamsBeyondKeys | (Cfg extends { actions: infer A; } ? { [N in keyof A]: ParamsBeyondKeys }[keyof A] : never)] extends [never] ? unknown : { /** The name is the message: TypeScript reports it as the property the config is missing. */readonly __firstParameterMustCarryOnlyTheDeclaredKeys: never; }; type StripParams = Keyless$1 extends true ? F : F extends ((params: any, ...rest: infer R) => infer Ret) ? (...args: R) => Ret : never; type ReplaceReturn = F extends ((...args: infer A) => Promise) ? (...args: A) => Promise : never; type ReservedActionKey = "reload" | "update" | "delete" | "setData" | "toJSON"; type ActionsConfig = { [name: string]: KeyedFn>; } & { [Key in ReservedActionKey]?: never }; /** * How long a loaded record stays usable without going back to the API. `false` (the default) always * fetches, `true` reuses a loaded record indefinitely, and `{ for: ms }` reuses one loaded within * that window. * * The identity map is the cache — there is no second store of records — so this is purely a policy * over what is already there. It only ever applies to a model that declared `keys`; without identity * there is nothing to reuse. */ type CacheSpec = boolean | { for: number; }; interface ModelConfig { /** * The schema fields that identify one record, or `false` for a model with no identity. `[]` marks * a singleton resource — one with no identifying fields, and so exactly one instance. */ keys: K; /** * Override how individual schema fields are made observable. Every field defaults to * `observable.ref`: a model is a projection of a server resource, replaced wholesale by * `setData`, so reassigning a field is reactive but mutating the value inside it is not. That * keeps `instantiate` cheap on a list of hundreds and stops in-place edits to nested data that * the next load would silently discard. * * Name a field here when it really is edited in place — a draft, a locally-managed array: * * ```ts * makeModel(UserSchema, { keys: ["id"], annotations: { tags: observable } }) * ``` * * `false` opts a field out of observability altogether. This is the only way to change a schema * field's annotation: mobx forbids re-annotating, so a subclass calling `makeObservable` for a * field the base already annotated throws. Subclasses annotate their *own* new members that way * — see the README. * * Only schema fields may be named; anything else is a typo and throws when the class is built. */ annotations?: FieldAnnotations; /** * Fetch one record. Exposed as the static `Model.get(params)`, which returns the identity-mapped * instance, and used to derive the instance's `reload()` — so the endpoint is declared once. */ get?: KeyedFn>; /** * Whether `Model.get` may answer from the identity map instead of the API, and for how long. * Defaults to `false`. * * Only turn this on when this model's payload is the *same shape* wherever it is loaded from. A * list endpoint returning a projection and a detail endpoint returning the whole record are two * different models, not one cached model — see the note on `setData` being a full replace. * * `Model.reload()` ignores this and always goes to the API; `Model.peek()` reads the map without * one. */ cache?: CacheSpec; /** * When `cache` has expired but the record is still in the identity map, hand back the record now * and refresh it in the background rather than making the caller wait. Defaults to `false`. * * The refreshed fields land on the same instance, so anything observing it re-renders when they * do. Only meaningful alongside `cache`: with nothing cached there is nothing to answer with. * * A background refresh that fails is logged and clears the record's load stamp, so the *next* * `get()` goes to the API and reports its failure through the normal path. Nothing new to catch. */ optimistic?: boolean; /** * Create a record. Exposed as the static `Model.create(body)`. * * The body is deliberately unconstrained: its real type comes from whatever you attach or * annotate, and that flows through to `Model.create`. Defaulting it to a partial of the resource * looks more helpful but *rejects* any body sharing no field names with it — TypeScript's * weak-type rule — and a rejected slot makes the whole config fall back to its constraint, * silently removing every generated method. */ create?: (body: any, ...rest: any[]) => Promise>; update?: KeyedBodyFn>; delete?: KeyedFn; actions?: ActionsConfig; } type ModelMethods = (Cfg extends { get: infer F; } ? { reload: ReplaceReturn, any>; } : {}) & (Cfg extends { update: infer F; } ? { update: ReplaceReturn, any>; } : {}) & (Cfg extends { delete: infer F; } ? { delete: StripParams; } : {}) & (Cfg extends { actions: infer A; } ? { [N in keyof A]: A[N] extends ((...args: any[]) => any) ? ReplaceReturn, any> : never } : {}); type ModelInstance = Resource & { setData(data: Resource): void; /** * Apply a partial, purely local edit — every field in one action, so reactions see one * consistent change rather than a torn intermediate state. * * This is local only: no endpoint is called and no `updated` event is emitted, because a store * hearing one would mark its lists stale and refetch, discarding the very edit just made. Nor is * the load stamp refreshed — the record now disagrees with the server, and telling `cache` * otherwise would let a stale record look fresh. To persist an edit, go through `update`. * * Identity keys are not patchable; a key change is a different record, not an edit to this one. */ updateData(patch: ObjectPatch): void; toJSON(): Resource; buildParams(): KeyShape; } & ModelMethods; /** * Mutation fan-out, which every model class has whatever it declared for `keys` — a model with no * identity still creates, updates and deletes records, and stores still need to hear about it. */ interface ModelEvents { /** * Start hearing about mutations to this resource. Held weakly, so registering never keeps a * listener alive — a store that goes out of scope is dropped on the next event. Called for you by * `makeStore`; only needed directly for something hand-rolled that has to stay in step. */ addListener(listener: ModelListener): void; /** @internal Fan a mutation out to every live listener. */ notifyListeners(type: ModelEventType, model: I): void; } /** * The identity-map statics. Present only on a model that declared identity — `keys: false`, and the * config-less `makeModel(schema)` that means the same thing, leave these off the class type. */ interface ModelIdentity { readonly identityCache: WeakRefMap; /** * The record for these params if it is already in the identity map, without ever fetching. * Synchronous, so it can answer during render. * * Presence, not freshness: a record `cache` would consider stale still comes back. Use it to * decide whether a fetch is needed at all, or to reach a record you know is loaded. */ peek(params: KeyShape): I | undefined; /** The registry key for a payload or model. Override on a subclass to scope identity. */ identityKey(source: Resource | I): string | number; /** * The one instance for this record — existing and updated, or newly created and registered. * Typed through the class it is called on, so a subclass's own members come through: * `Admin.instantiate(data)` is an `Admin`, not a base instance. */ instantiate any>(this: This, data: Resource): InstanceType; /** Drop this record's entry so the next `instantiate` builds a fresh instance. */ forget(source: Resource | I): boolean; /** Forget every record. For teardown — a logout, or switching tenant. */ clearIdentity(): void; } /** * Statics generated from the config slots that don't need an instance. Both are typed through the * class they are called on, exactly as `instantiate` is — a generated model class is always * subclassed, and a static that hardcoded the base instance would drop the subclass's own members: * `Admin.get(...)` is an `Admin`, not a base instance. */ type ModelStatics = (Cfg extends { get: (...args: infer A) => any; } ? { /** * Fetch this record, or hand back the one in the identity map when `cache` allows — see the * `cache` and `optimistic` config. */ get any>(this: This, ...args: A): Promise>; /** * Fetch this record from the API, whatever `cache` says, and apply it to the identity-mapped * instance. The static mirror of `instance.reload()`: same endpoint, params passed in rather * than read off a record you already hold. */ reload any>(this: This, ...args: A): Promise>; } : {}) & (Cfg extends { create: (...args: infer A) => any; } ? { create any>(this: This, ...args: A): Promise>; } : {}); type ModelConstructor = { new (data: Resource): ModelInstance; readonly schema: S; /** Exactly what was declared, so `Model.keys` reads back the tuple — or `false`. */ readonly keys: K; } & ModelEvents> & (HasIdentity extends true ? ModelIdentity> : {}) & ModelStatics; declare function makeModel(schema: S): ModelConstructor; declare function makeModel, Cfg extends ModelConfig>(schema: S, config: Cfg & { keys: K; } & ParamsMustBeKeys): ModelConstructor; type SharedFields = { [K in keyof Resource]: Resource[K] }; type VariantFields, V> = Extract, Record>; interface UnionModelMembers, K> { setData(data: Resource): void; /** * Apply a partial, purely local edit — every field in one action. Local only: no endpoint, no * `updated` event, and no load-stamp refresh (see the note on the single-object form). * * On a union, the patch is typed against the *narrowed* instance. Un-narrowed, only the shared * fields can be named; reach a variant's fields by narrowing first, which is what keeps a patch * from grafting one variant's fields onto another: * * ```ts * payment.updateData({ digits: ["4"] }); // ✗ not a shared field * payment.as("card")?.updateData({ digits: ["4"] }); // ✓ * ``` * * The discriminator is not patchable: changing variant replaces the whole record, so it goes * through `setData`. Identity keys are not patchable either. */ updateData(patch: UnionPatch): void; /** * The record as plain data, with any field outside its current variant stripped. * * The return type follows `this` the way `updateData`'s patch does, so a narrowed instance * yields that variant alone rather than the whole union — `payment.as("card")?.toJSON().digits` * compiles, where the un-narrowed `payment.toJSON().digits` correctly does not. */ toJSON(): VariantOf; buildParams(): KeyShape; /** Type guard: true when the discriminator equals `value`, revealing that variant's fields on this same instance. */ is[D]>(value: V): this is this & VariantFields; /** This instance narrowed to the `value` variant (fields exposed directly), or `undefined` if it doesn't match. */ as[D]>(value: V): (this & VariantFields) | undefined; } type UnionModelInstance, K, Cfg> = SharedFields & UnionModelMembers & ModelMethods; type UnionModelConstructor, K, Cfg> = { new (data: Resource): UnionModelInstance; readonly schema: S; readonly discriminator: D; readonly keys: K; } & ModelEvents> & (HasIdentity extends true ? ModelIdentity> : {}) & ModelStatics; declare function makeUnionModel & string>(schema: S, discriminator: D): UnionModelConstructor; declare function makeUnionModel & string, K extends KeySpec, Cfg extends ModelConfig>(schema: S, discriminator: D, config: Cfg & { keys: K; } & ParamsMustBeKeys): UnionModelConstructor; //#endregion //#region src/model/make-store.d.ts /** Orders a collection, like `Array#sort` — but over model instances rather than payloads. */ type Comparator = (a: M, b: M) => number; /** Per-list options: everything a lazy observable takes, plus staleness and ordering. */ interface CollectionOptions extends LazyOptions { /** * Which mutations to this resource mark this list stale. Defaults to the store's `invalidateOn`, * itself `["created"]`. A deletion always removes the model from the list regardless. */ invalidateOn?: readonly ModelEventType[]; /** * Order this list. Defaults to the store's `sort`, since one ordering usually applies to every * collection over a resource. Pass `false` to keep server order on this list alone. */ sort?: Comparator | false; /** * Show a record from `create()` in this list straight away, without waiting for the refetch that * the `created` event triggers. Defaults to the store's `optimisticCreate`, itself `false`. * * Off by default because only the server knows whether a new record belongs in a given list: a * filtered or searched collection would flash a row that does not belong to it. Turn it on for * the lists a new record certainly joins — usually the unfiltered one. */ optimisticCreate?: boolean; /** * Drop this list's rows while it refetches after being marked stale, rather than keeping them * readable. Defaults to the store's `discardOnInvalidate`, itself `false`. * * Keeping them is usually right — the rows are still broadly correct and the list doesn't blank * on every mutation. Discard when stale rows would actively mislead: a filtered list whose * membership an `update` may have changed, for one. */ discardOnInvalidate?: boolean; } /** * Per-list options for a paged collection: every {@link LazyPagesOptions} option, plus the two * store-level concerns that still apply. * * **`sort` is deliberately absent.** A comparator can only ever see the page in front of it, so * ordering a paged list client-side would sort each page against itself and leave the list * globally unordered — the order is the server's, and it is the server's for the same reason the * filtering is. A store-level `sort` is therefore *not* inherited here; nothing silently applies it * to one page at a time. */ interface PagedCollectionOptions extends LazyPagesOptions, Omit, keyof LazyOptions | "sort"> {} /** * How a paged collection is declared to `createStore`: its fetch alone, or its fetch plus that * list's own options — the same two shapes as {@link CollectionSpec}. */ type PagedCollectionSpec = ((request: LazyPageRequest) => Promise>) | ({ fetch: (request: LazyPageRequest) => Promise>; } & PagedCollectionOptions); interface StoreConfig { /** * Order every collection on this store. Sorting is usually the one thing standing between an API * client and being attached directly, and the same ordering almost always applies to every list * over a resource — so it is declared once here, and a single collection can still override it. * * Runs over model instances on every load. */ sort?: Comparator; /** * Whether a record from `create()` appears in this store's lists before the refetch confirms it. * Defaults to `false`; a single collection can still opt in or out. */ optimisticCreate?: boolean; /** * Whether a list drops its rows while refetching after being marked stale, rather than keeping * them readable. Defaults to `false`; a single collection can still opt in or out. */ discardOnInvalidate?: boolean; /** * Which mutations to this resource — from *any* store, or from the model's own statics — mark this * list stale. Defaults to `["created"]`: a new record is the only event whose effect on a list * can't be worked out locally, since only the server knows whether it belongs here. * * `"updated"` is not a default because identity means every list already shows the change — add it * when membership depends on a field that can change. `"deleted"` is not a default either: the * record is removed from every list outright, which needs no refetch — list it only when a deletion * changes the list in some *other* way, a server-side count or ordering, say. */ invalidateOn?: readonly ModelEventType[]; } /** * How a collection is declared to `createStore`: its fetch alone, or its fetch plus that list's own * options. The verbose form is what lets a single collection override the store's `sort`, set its * own `invalidateOn`, or take any lazy option. */ type CollectionSpec = LazyFetch | ({ fetch: LazyFetch; } & CollectionOptions); /** * A family of collections, one per key — the same list fetched separately per tenant, per parent * record, per page. Call it to get that key's list, building it on first use. */ interface CollectionMap { (key: K): LazyArray; /** * Drop one key's list, unregistering it from the store's mutation handling. For a key that is * gone for good — an organization the user just left — so the map doesn't hold a list nothing * will ask for again. The next call for that key builds a fresh one. */ forget(key: K): boolean; /** Drop every list this map has built. For teardown: a logout, a tenant switch. */ clear(): void; } /** Options for the free-form form of `collectionMap`, whose key is whatever you say it is. */ interface CollectionMapOptions extends CollectionOptions { /** * Spell a key as something a map can hold. Only needed for a key that isn't already a string or * a number — a filter object, a params tuple. The declared-fields form has no use for it: those * serialize exactly as the identity map does. */ keyOf?: (key: K) => string | number; } /** The payload a store's collections resolve to arrays of, read off the model class's schema. */ type StoreResource = MC extends { schema: infer S extends ModelSchema; } ? T.Static : never; /** * The fields a collection may be keyed by: those holding something that can be a map key on its * own. Anything else has no obvious spelling, so it belongs in the free-form form with a `keyOf`. */ type ScalarField = { [P in keyof R]-?: NonNullable extends string | number ? P : never }[keyof R]; /** Names a collection may not take, since each is already a member of the store. */ type ReservedCollectionName = "remove" | "collection" | "collectionMap" | "pagedCollection" | "invalidateCollections" | "onModelEvent" | "get" | "create"; /** * `createStore` config: everything `makeStore` takes, plus the collections themselves. They live in * the config here because there is no subclass to hang them off — the moment you do subclass, every * collection is declared the same way, as a field built with `this.collection(...)`. */ interface CreateStoreConfig extends StoreConfig { collections: Record> & { [N in ReservedCollectionName]?: never }; /** * Accumulating lists, declared alongside the ordinary ones and kept apart from them because they * are a different shape of fetch: a page rather than the whole list. Each becomes a * {@link LazyPages} on the instance, under its own name. * * ```ts * createStore(SurveyModel, { * collections: { drafts: api.listDraftSurveys }, * pagedCollections: { feed: ({ cursor, limit }) => api.listSurveys({ cursor, limit }) }, * }); * ``` */ pagedCollections?: Record> & { [N in ReservedCollectionName]?: never }; } type StoreInstance = { remove(model: M): void; /** * Mark every collection on this store stale, for a change no model event describes — a tenant * switch, a filter reset, a refresh button. Unlike the event path this ignores `invalidateOn`: a * list that opted out of refetching on *events* has not opted out of being told directly. * * Named for what it covers: a subclass may hold lazies that aren't collections — a count, a * summary — and those are left alone. */ invalidateCollections(options?: LazyInvalidateOptions): void; onModelEvent(type: ModelEventType, model: M): void; /** * Build another list on this store. Payloads become models, the list joins this * store's mutation handling, and every lazy option is available — so a search, a filtered view, or * a polled list is a field on a subclass: * * ```ts * class SurveySearch extends makeStore(SurveyModel) { * query = ""; * results = this.collection((options) => api.search({ q: this.query, ...options }), { * trackDependencies: { throttle: 300 }, * }); * } * ``` */ collection(fetch: LazyFetch[] : never[]>, options?: CollectionOptions): LazyArray; /** * Build an **accumulating** list on this store — one that grows a page at a time, for a dataset * too large to hand over whole. Payloads become models and the list joins this store's mutation * handling exactly as `collection()`'s does; what differs is that the fetch resolves one page: * * ```ts * class Surveys extends makeStore(SurveyModel) { * feed = this.pagedCollection(({ cursor, limit, signal }) => * api.listSurveys({ cursor, limit, signal }), * ); * } * * surveys.feed.loadMore(); * surveys.feed.total; * ``` * * `Q` is the query type the list is driven by — `TableQuery` when a table owns it. Deduplication * defaults to the model's `identityKey`, and `sort` is not an option: see * {@link PagedCollectionOptions}. */ pagedCollection(fetch: (request: LazyPageRequest) => Promise : never>>, options?: PagedCollectionOptions): LazyPages; /** * Build a *family* of lists on this store, one per key, for a resource that has to be fetched * separately per tenant, per parent record, or per page — keys you can't enumerate in advance. * Each list is built on first use and behaves exactly as a `collection()` does from then on. * * Name the fields that select a list and the fetch's params are typed from the schema, the same * way a model's `keys` type its statics: * * ```ts * class Surveys extends makeStore(SurveyModel) { * byOrg = this.collectionMap(["orgId"], ({ orgId }, options) => * api.listSurveys({ orgId, ...options }), * ); * } * * surveys.byOrg({ orgId }).getOrLoad(); * ``` * * Reach for the free-form form below when the key isn't a field on the resource — a page number, * a filter of your own. */ collectionMap>>(keys: readonly [F, ...F[]], fetch: (params: Pick, F>, options: LazyFetchOptions) => Promise[]>, options?: CollectionOptions): CollectionMap, F>, M>; /** * Keyed by something that isn't a field on the resource: * * ```ts * pages = this.collectionMap((page: number, options) => * api.listSurveys({ page, ...options }), * ); * ``` */ collectionMap(fetch: (key: K, options: LazyFetchOptions) => Promise[]>, options?: CollectionOptions): CollectionMap; /** Keyed by a value a map can't hold as it stands, so `keyOf` says how to spell it. */ collectionMap(fetch: (key: K, options: LazyFetchOptions) => Promise[]>, options: CollectionMapOptions & { keyOf: (key: K) => string | number; }): CollectionMap; } & (MC extends { get: (...args: infer A) => any; } ? { get(...args: A): Promise; } : {}) & (Cfg extends { collections: infer C; } ? { [N in keyof C]: LazyArray } : {}) & (Cfg extends { pagedCollections: infer P; } ? { [N in keyof P]: P[N] extends PagedCollectionSpec ? LazyPages : LazyPages } : {}) & (MC extends { create: (...args: infer A) => any; } ? { create(...args: A): Promise; } : {}); type StoreConstructor = { new (): StoreInstance; }; /** * A class produced by `makeModel`/`makeUnionModel`: it carries its own schema, so passing one to * `makeStore` means not repeating the schema, and its identity map is wired up by default. */ type AnyModelClass = { readonly schema: ModelSchema; new (data: any, store?: any): any; }; declare function makeStore(model: MC): StoreConstructor, MC, {}>; declare function makeStore>>(model: MC, config: Cfg): StoreConstructor, MC, Cfg>; /** * `makeStore` plus `new`, for a store you don't need to subclass. Its collections are named in the * config and land on the instance under those names — so `createStore` is the whole story when a * store is just lists over a resource, and the moment you need behaviour of your own you move to * `makeStore` and declare every collection as a field. * * ```ts * const surveys = createStore(SurveyModel, { * sort: (a, b) => a.name.localeCompare(b.name), * collections: { * all: (options) => api.listSurveys(options), * drafts: { fetch: (options) => api.listSurveys({ status: "draft", ...options }), sort: false }, * }, * }); * * surveys.all.getOrLoad(); * ``` */ declare function createStore, InstanceType>>(model: MC, config: Cfg): StoreInstance, MC, Cfg>; //#endregion //#region src/model/use-collection.d.ts /** The payload a model's collections resolve to arrays of. */ type Payload = T.Static; /** * `CollectionOptions` plus the component's own inputs. Their type is inferred from what you pass, * so the fetch's first argument is typed without declaring anything twice — and since they are the * one reactive part, `trackDependencies` defaults to `true` when they are present. */ interface UseCollectionOptions extends CollectionOptions { params: P; } /** * {@link PagedCollectionOptions} plus the component's own inputs, which arrive as the fetch's * first argument. Same params-first shape as `useCollection`, and the same consequence: * `trackDependencies` defaults to `true` when they are present, because reading them is what makes * a change restart the list. */ interface UsePagedCollectionOptions extends PagedCollectionOptions { params: P; } /** * A collection that belongs to one component: `store.collection()`, for a list whose parameters are * the component's own — a filter, a search box, a route param — where a shared store is the wrong * home for them. * * ```tsx * const list = useCollection(SurveyModel, (options) => api.listSurveys(options)); * ``` * * Pass `params` and they arrive as the fetch's first argument, ahead of the lazy's own options — * the same params-first shape `collectionMap` uses. They are plain React values; the hook keeps * them in an observable the fetch reads through, so a change refetches while leaving the current * rows readable and aborting the request it supersedes: * * ```tsx * const [query, setQuery] = useState(""); * * const list = useCollection( * SurveyModel, * ({ orgId, query }, options) => api.listSurveys({ orgId, q: query, ...options }), * { params: { orgId, query }, trackDependencies: { throttle: 300 } }, * ); * ``` * * Being component-scoped costs nothing global: the model's identity map still hands out one * instance per record, and mutations still fan out — so an edit here shows in the app-wide store * and vice versa. Nothing needs disposing either, since the model holds its listeners weakly. */ declare function useCollection(model: MC, fetch: LazyFetch[]>, options?: CollectionOptions> & { params?: never; }): LazyArray>; declare function useCollection(model: MC, fetch: (params: P, options: LazyFetchOptions) => Promise[]>, options: UseCollectionOptions>): LazyArray>; /** * An accumulating list that belongs to one component — an infinite feed, a load-more table — whose * parameters are the component's own. * * ```tsx * const feed = usePagedCollection(SurveyModel, ({ cursor, limit, signal }) => * api.listSurveys({ cursor, limit, signal }), * ); * ``` * * Everything `pagedCollection()` gives a store-owned list applies here: payloads become * identity-mapped models, duplicates across page boundaries are dropped on `identityKey`, a * `created` event restarts the list, and a deletion removes the record from it. Being * component-scoped costs none of that — the model's identity map and event fan-out are global, so * an edit here shows up in the app-wide store and vice versa, and nothing needs disposing because * the model holds its listeners weakly. * * **Bound to a table, there is nothing else to write.** The table infers `mode: "server"`, pushes * its query in, and asks for the next page as the window nears the end: * * ```tsx * const feed = usePagedCollection( * SurveyModel, * ({ query, cursor, limit, signal }) => * api.listSurveys({ where: query.filters, sort: query.sorts, cursor, limit, signal }), * ); * const table = useTable({ data: feed, columns }); * ``` * * `params` are for inputs the *table doesn't own* — a route param, a parent record. They arrive as * the fetch's first argument and a change restarts the list, leaving the rows readable while page * one of the new list loads. */ declare function usePagedCollection(model: MC, fetch: (request: LazyPageRequest) => Promise>>, options?: PagedCollectionOptions, Q> & { params?: never; }): LazyPages, Q>; declare function usePagedCollection(model: MC, fetch: (params: P, request: LazyPageRequest) => Promise>>, options: UsePagedCollectionOptions, Q>): LazyPages, Q>; //#endregion //#region src/model/use-model.d.ts /** * The params `Model.get` takes, read off the model itself — so a keyed model requires exactly the * fields it declared and a keyless one takes `undefined`, without any of that being restated here. * * A conditional rather than a constraint, matching how `makeStore` reads the same statics: the * generated `get` is generic over the class it is called on, which a plain structural constraint * fails to match. A model with no `get` resolves to `never`, so there is nothing that can be passed * for `params` and the call fails at the argument rather than the type parameter. */ type GetParams = MC extends { get: (params: infer P, ...rest: any[]) => any; } ? P : never; /** * Whether the model declared no key params, so `Model.get` takes none. * * Asked through `K[number]` rather than `K extends readonly []` for the reason `makeModel` documents * on its own `Keyless`: an inline `keys: []` infers as `never[]`, which is not assignable to * `readonly []` and would read as keyed. This is the same question `buildParams()` answers at * runtime, so the type and the call can't disagree about which argument is which. */ type Keyless = MC extends { keys: infer K; } ? [K] extends [readonly any[]] ? [K[number]] extends [never] ? true : false : true : true; /** * What `Model.get` takes after its params — the part of the fetcher's signature this hook has to * be able to satisfy on its own. */ type FetchArgs = Keyless extends true ? MC extends { get: (...args: infer A) => any; } ? A : never : MC extends { get: (params: any, ...rest: infer R) => any; } ? R : never; /** * Whether the hook can drive this model's `get`. * * Every other layer that calls a fetch *declares* the shape it will call — `lazy` takes a * `LazyFetch`, `collection` the same, `collectionMap` a `(key, options)`, `pagedCollection` a * `(request)` — so a fetcher with an argument they can't fill is rejected where it is attached. * This hook is the one that inherits its fetcher's signature from the model config, which is * deliberately pass-through, so it has to state the same contract here instead. * * The contract is `get(keys, options?)`, or `get(options?)` with no keys, and the question asked of * that trailing parameter is whether a `LazyFetchOptions` can *satisfy* it — not whether it is * spelled one. That admits the partial shapes a client declares for itself: `RequestInit`, * `{ signal?: AbortSignal }`, an optional bag, a required one. It also admits a fetcher taking * nothing after its params, where the bag is ignored as in any JavaScript call, and one with an * untyped rest, where there is nothing to check. * * What it rejects is a parameter the bag cannot stand in for: `expand: string`, an options type * needing more than a signal, or a required argument past the bag. Optionality doesn't rescue any * of them, because arguments go by position — `(keys, expand?: string, o?)` would receive the bag * as its `expand`, and the request would get no signal at all. */ type HookFetchable = FetchArgs extends [] ? true : [LazyFetchOptions] extends FetchArgs ? true : false; /** * Attached to the model argument so an unreachable fetcher fails here, at the hook, rather than by * sending the options bag to whatever argument happened to be in the way. Named as a sentence * against the repo's usual style on purpose: TypeScript prints the alias name and elides the * structure, so the name is the only part of this the reader will see. */ type UseLazyInstead_GetTakesMoreThanParamsAndFetchOptions = { readonly __useLazyInstead: never; }; /** * Everything after the model. A keyless model has nothing to pass for params, so it takes options * directly rather than a placeholder ahead of them. */ type UseModelArgs = Keyless extends true ? [options?: LazyOptions] : [params: GetParams, options?: LazyOptions]; /** * One record, loaded in a component — the detail-page counterpart to {@link useCollection}. * * ```tsx * const StudyPage = observer(({ studyId }: { studyId: string }) => { * const study = useModel(StudyModel, { id: studyId }); * * return ( * }> * {(s) => } * * ); * }); * ``` * * What comes back is an ordinary `lazy` over the model's own `get`, so it loads when * something observes it, honours whatever the model declared for `cache`, and hands back the * identity-mapped instance — an edit made anywhere else in the app shows up here. * * **The params are the dependencies.** There is no dependency array to keep in step with them, which * is the whole reason this exists rather than spelling it out with `useLazy`: * * ```tsx * useLazy((o) => StudyModel.get({ id, orgId }, o), [id]); // `orgId` forgotten — silently stale * useModel(StudyModel, { id, orgId }); // can't desync * ``` * * They are compared shallowly, so rebuilding the object every render costs nothing. A change builds * a new lazy — the value starts empty and loads again, which is what you want for a record: showing * the study you navigated away from while the next one loads would be a lie. * * A model with no key params (`keys: []` or `keys: false`) takes no params argument at all — * `useModel(SettingsModel)`, and `useModel(SettingsModel, { keepOnUnobserved: true })` for options. */ declare function useModel(model: MC & (HookFetchable extends true ? unknown : UseLazyInstead_GetTakesMoreThanParamsAndFetchOptions), ...args: UseModelArgs): Lazy>; //#endregion export { type AnnotationMapEntry, AnyModelClass, CacheSpec, CollectionMap, CollectionMapOptions, CollectionOptions, CollectionSpec, Comparator, CreateStoreConfig, FieldAnnotations, KeySpec, LOADED_AT, type LazyArray, ModelConfig, ModelConstructor, ModelEventType, ModelEvents, ModelIdentity, ModelListener, ModelSchema, PagedCollectionOptions, PagedCollectionSpec, ReservedCollectionName, StoreConfig, StoreConstructor, StoreInstance, UnionModelConstructor, UseCollectionOptions, UsePagedCollectionOptions, WeakRefMap, createStore, makeModel, makeStore, makeUnionModel, serializeKey, useCollection, useModel, usePagedCollection }; //# sourceMappingURL=model.d.mts.map