foldkit-remote 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -293,6 +293,32 @@ const clientLayer = Remote.clientLayer(rpcClient)
293
293
  `RemoteClient` service used by subscriptions and Commands. The transport itself
294
294
  is not owned by Remote.
295
295
 
296
+ ## One list per application with `Data.wiring`
297
+
298
+ The integration steps above — spread `Remote.messages`, reduce by tag, derive
299
+ `Data.subscriptions`, provide the client — are one value when the application
300
+ uses `foldkit-bundle`:
301
+
302
+ ```ts
303
+ import { Bundle } from 'foldkit-bundle'
304
+
305
+ const Page = Bundle.parent({ Model, Message })
306
+ const wiring = Page.assemble(
307
+ Data.wiring({
308
+ project: Surface.at(ProjectPage, model =>
309
+ model.route._tag === 'project' ? { projectId: model.route.projectId } : undefined,
310
+ ),
311
+ }),
312
+ )
313
+ const update = wiring.update(model => ({ model }))
314
+ ```
315
+
316
+ `Data.wiring` routes Remote's Messages into `Data.reduce`, brings the active
317
+ Surfaces' Subscriptions and the domain's contract, and requires `RemoteClient`
318
+ from the runtime's resources — the same client layer from above. One assembly
319
+ holds at most one Remote domain: every domain claims the same Message tags,
320
+ and the assembly refuses a second claimant at startup, naming both.
321
+
296
322
  ## `RemoteData`: what does the Model know right now?
297
323
 
298
324
  A Remote Projection never lies by pretending an absent value is present. It
@@ -329,7 +355,13 @@ The states are:
329
355
  - **`Ready`** — every selected field is present and decodes.
330
356
  - **`Refreshing`** — the current value remains visible while it is being refetched.
331
357
  - **`Failed`** — stored server data does not decode against the Selection.
332
- - **`NotFound`** — the entity is represented by a tombstone.
358
+ - **`NotFound`** — the entity is represented by a tombstone: a live event
359
+ deleted it, or the server was asked for it by id and answered without it. That
360
+ id is then known absent, whether it never existed, is gone, or is not this
361
+ principal's to see. It is not planned again until `Data.refresh` forces it or a
362
+ write brings it back. The target of a returned ref is not marked this way: a
363
+ server need not expand a relation that rides on a request, and the planner asks
364
+ for such a target by id next, which is when its absence is learned.
333
365
 
334
366
  `Initial` is intentionally different from `Loading`. A Projection belonging to
335
367
  no active Surface may remain `Initial` forever. Rendering a spinner for
@@ -384,6 +416,50 @@ An unknown field or a nested Selection for the wrong entity is a type error.
384
416
  Recursive relations remain finite because the entity schema stores references
385
417
  (`Entity.ref(User)` / `Entity.refTo('Node')`), not recursively inlined schemas.
386
418
 
419
+ ### Entities declared with `foldkit-entity`
420
+
421
+ [`foldkit-entity`](../entity/README.md) declares a domain without Remote in it:
422
+ fields, relations as navigation edges, derived members. `Remote.make` registers
423
+ those Entities, and `Data.get`, `Data.live`, and a query's `select` take their
424
+ Selections. Remote compiles both into the descriptor and Selection above, so
425
+ the store, the planner, and the server see nothing new.
426
+
427
+ ```ts
428
+ import { Schema } from 'effect'
429
+ import { Entity, Relation } from 'foldkit-entity'
430
+ import { Remote } from 'foldkit-remote'
431
+
432
+ const User = Entity.define('User', Schema.Struct({ id: Schema.String, name: Schema.String }))
433
+ const Project = Entity.define('Project', Schema.Struct({ id: Schema.String, name: Schema.String }))
434
+ const Work = Entity.relate({ User, Project }, { Project: { owner: Relation.one(User) } })
435
+
436
+ const ProjectCard = Entity.select(Work.Project, {
437
+ name: true,
438
+ owner: Entity.select(Work.User, { name: true }),
439
+ })
440
+
441
+ const Data = Remote.make({ model: App.model.remote, entities: [Work.User, Work.Project] })
442
+ const card = Data.get(ProjectCard, 'p1') // Projection<Model, RemoteData<{ name; owner: { name } }>>
443
+ ```
444
+
445
+ - A relation becomes a ref field: `one` a ref, an optional `one` a nullable ref,
446
+ `many` an array of refs. A derived member becomes a field the server supplies.
447
+ - The Entity needs an `id` field; Remote keys the store by it.
448
+ - Register the Entities of one `Entity.relate` result and select from that same
449
+ result; `Object.values(Work)` registers them all.
450
+ - `Entity.from(entity)` and `Selection.from(selection)` are the compile steps,
451
+ for when you need the descriptor itself: `patch` and `ref` in a mutation
452
+ handler.
453
+ - `Entity.page(selection, window)` in an Entity Selection is Remote's
454
+ `Selection.connection`: the window travels with the read, and the server
455
+ answers with a page of refs. One relation of one entity is held whole or as
456
+ one window at a time, so two views of the same entity should not read the
457
+ same relation both ways at once.
458
+
459
+ `Entity.make` with `Entity.ref` keeps working, and both kinds can share one
460
+ domain. Both packages export `Entity`; a module that needs `Entity.from` beside
461
+ `Entity.define` aliases one of them.
462
+
387
463
  Presence is tracked separately from the JavaScript value. These are distinct:
388
464
 
389
465
  ```text
@@ -552,6 +628,17 @@ Those settlement Messages go through `Data.reduce` like every other Remote fact.
552
628
  The request id comes from the Remote Model's sequence, so `update` stays pure.
553
629
  Settling is idempotent per `requestId`.
554
630
 
631
+ A mutation that deletes says so. The server's outcome carries
632
+ `deleted: [{ entity, id }]`, and settling it tombstones those entities: they read
633
+ `NotFound`, and they leave every connection and relation they were in, so the
634
+ server names no list. Patches apply first, so an entity named both ways is
635
+ deleted; a retry of the same request deletes nothing again.
636
+
637
+ `Data.mutation(model, requestId)` reads what became of it from the Model:
638
+ `Pending`, `Applied`, `Failed` with the error the server or transport gave, or
639
+ `Unknown` for an id never started here (or settled so long ago it left the
640
+ bounded ledger). A retry reuses its id, and the latest outcome wins.
641
+
555
642
  Optimistic entity patches are **layers over the base store**, not inverse
556
643
  patches. If multiple mutations overlap, the visible cache is recomputed as base
557
644
  plus the still-pending layers, so settling one does not require trying to undo
@@ -911,6 +998,15 @@ Remote does **not** own:
911
998
  - authentication; server-side authorization belongs at the Source boundary;
912
999
  - a background scheduler or general-purpose database/query engine.
913
1000
 
1001
+ ## How state changes here
1002
+
1003
+ Local application state uses `evo` inside `update`, like anywhere else.
1004
+ Remote Messages go through `Data.reduce` into the embedded `Remote.Model`
1005
+ submodel — Remote owns that reducer, and it is the only writer of the
1006
+ cache. Never install cache state with a ref `set`; the requirements,
1007
+ staleness, and tombstones only stay coherent when every fact arrives as a
1008
+ Remote Message.
1009
+
914
1010
  ## Limits
915
1011
 
916
1012
  - Effect RPC is the wire contract; the application supplies deployment layers.
package/dist/index.d.mts CHANGED
@@ -1,9 +1,64 @@
1
1
  import { Context, Duration, Effect, Layer, Option, Schema, Stream } from "effect";
2
- import { ActiveSurface, Contract, Invalid, Metadata, ModelRef, Projection, Surface } from "foldkit-surface";
2
+ import * as Domain from "foldkit-entity";
3
+ import { EntryWithoutKeepAlive } from "foldkit/subscription";
4
+ import { ActiveSurface, Contract, Invalid, Metadata, ModelRef, Projection, Surface, Wiring } from "foldkit-surface";
3
5
  import { Rpc, RpcGroup } from "effect/unstable/rpc";
4
6
  import { KeyValueStore } from "effect/unstable/persistence";
5
7
  import { Command } from "foldkit/command";
6
- import { EntryWithoutKeepAlive } from "foldkit/subscription";
8
+ //#region ../metadata/dist/index.d.mts
9
+ //#region src/index.d.ts
10
+ /** Marks a `Metadata` value. Its entries are private to this module. */
11
+ declare const MetadataTypeId: unique symbol;
12
+ type MetadataTypeId = typeof MetadataTypeId;
13
+ /**
14
+ * Declarative facts an interpreter attaches to a node it does not own, such as
15
+ * the server data a Remote selection needs. The carrier holds and combines
16
+ * them without knowing what they mean; a package reads only the key it owns.
17
+ *
18
+ * Opaque: only a key's `of` and `Metadata.combine` make one, so entries cannot
19
+ * be forged past a key's type or `merge`, and they are frozen.
20
+ */
21
+ interface Metadata$1 {
22
+ readonly [MetadataTypeId]: MetadataTypeId;
23
+ }
24
+ /**
25
+ * An interpreter's typed slot in `Metadata`. Lookup is by the key object, never
26
+ * its `name`, so two packages cannot collide by choosing the same string.
27
+ */
28
+ interface MetadataKey<A> {
29
+ /** For tooling output only. */
30
+ readonly name: string;
31
+ /** Normalizes the entries gathered from combined values, e.g. unioning duplicates. */
32
+ readonly merge: (values: ReadonlyArray<A>) => ReadonlyArray<A>;
33
+ /** One line per entry for tooling output. */
34
+ readonly summarize: (value: A) => string;
35
+ readonly of: (...values: ReadonlyArray<A>) => Metadata$1;
36
+ readonly get: (metadata: Metadata$1) => ReadonlyArray<A>;
37
+ }
38
+ /** One key's entries as serializable text, named by the key. */
39
+ interface MetadataSummary {
40
+ readonly name: string;
41
+ readonly entries: readonly string[];
42
+ }
43
+ declare const Metadata$1: {
44
+ /**
45
+ * Declares an interpreter's slot. Call once per package, at module level:
46
+ * entries are found by the key object, so two copies of a package (a
47
+ * duplicated install, a reloaded module) do not see each other's entries.
48
+ */
49
+ key: <A>(name: string, options: {
50
+ readonly merge: (values: ReadonlyArray<A>) => ReadonlyArray<A>;
51
+ readonly summarize: (value: A) => string;
52
+ }) => MetadataKey<A>;
53
+ empty: Metadata$1;
54
+ /** True only for a value this module made; a copy or a hand-built brand is not one. */
55
+ is: (value: unknown) => value is Metadata$1;
56
+ /** Gathers every part's entries and runs each key's `merge` over its group. */
57
+ combine: (parts: ReadonlyArray<Metadata$1>) => Metadata$1;
58
+ summarize: (metadata: Metadata$1) => readonly MetadataSummary[];
59
+ };
60
+ //#endregion
61
+ //#endregion
7
62
  //#region src/connection.d.ts
8
63
  export interface EdgeRef {
9
64
  readonly entity: string;
@@ -49,6 +104,51 @@ export declare const hasNext: (connection: Connection) => boolean;
49
104
  /** A connection with more than one segment has an explicit unknown gap. */
50
105
  export declare const isGapped: (connection: Connection) => boolean;
51
106
  //#endregion
107
+ //#region src/remoteData.d.ts
108
+ export interface RemoteError {
109
+ readonly _tag: string;
110
+ readonly message: string;
111
+ }
112
+ export type RemoteData<A> = {
113
+ readonly _tag: 'Initial';
114
+ } | {
115
+ readonly _tag: 'Loading';
116
+ } | {
117
+ readonly _tag: 'Ready';
118
+ readonly value: A;
119
+ } | {
120
+ readonly _tag: 'Refreshing';
121
+ readonly value: A;
122
+ } | {
123
+ readonly _tag: 'Failed';
124
+ readonly error: RemoteError;
125
+ readonly previous?: A;
126
+ } | {
127
+ readonly _tag: 'NotFound';
128
+ };
129
+ export declare const remoteErrorSchema: Schema.Struct<{
130
+ readonly _tag: Schema.String;
131
+ readonly message: Schema.String;
132
+ }>;
133
+ export declare const remoteDataSchema: <A>(value: Schema.Schema<A>) => Schema.Codec<RemoteData<A>, unknown>;
134
+ export declare const RemoteData: {
135
+ /**
136
+ * The `RemoteData` schema for a value schema. Exposed so a `RemoteData` can
137
+ * be embedded in a hand-written Model schema, not only through `Remote.select`.
138
+ */
139
+ schema: <A>(value: Schema.Schema<A>) => Schema.Schema<RemoteData<A>>;
140
+ /** Exhaustive: omitting a state is a compile error. */
141
+ match: <A, R>(data: RemoteData<A>, cases: {
142
+ readonly Initial: () => R;
143
+ readonly Loading: () => R;
144
+ readonly Ready: (value: A) => R;
145
+ readonly Refreshing: (value: A) => R;
146
+ readonly Failed: (error: RemoteError, previous: Option.Option<A>) => R;
147
+ readonly NotFound: () => R;
148
+ }) => R;
149
+ map: <A, B>(data: RemoteData<A>, f: (value: A) => B) => RemoteData<B>;
150
+ };
151
+ //#endregion
52
152
  //#region src/store.d.ts
53
153
  export type EntityKey = string;
54
154
  export interface EntityEntry {
@@ -128,6 +228,8 @@ export interface MutationState {
128
228
  readonly pending: ReadonlySet<string>;
129
229
  readonly applied: ReadonlySet<string>;
130
230
  readonly failed: ReadonlySet<string>;
231
+ /** Why each request in `failed` failed, kept exactly as long as `failed` keeps its id. */
232
+ readonly errors: ReadonlyMap<string, RemoteError>;
131
233
  /**
132
234
  * How many mutations this model has started. A bound domain takes its next
133
235
  * request id from here, so `update` stays pure and the id exists before the
@@ -137,7 +239,21 @@ export interface MutationState {
137
239
  }
138
240
  export declare const emptyMutationState: MutationState;
139
241
  export declare const beginMutation: (state: MutationState, requestId: string) => MutationState;
140
- export declare const failMutation: (state: MutationState, requestId: string) => MutationState;
242
+ export declare const failMutation: (state: MutationState, requestId: string, error: RemoteError) => MutationState;
243
+ /** What Remote knows of one mutation request, by the id `mutate` returned. */
244
+ export type MutationStatus =
245
+ /** Never started here, or settled so long ago its id has left the window. */
246
+ {
247
+ readonly _tag: 'Unknown';
248
+ } | {
249
+ readonly _tag: 'Pending';
250
+ } | {
251
+ readonly _tag: 'Applied';
252
+ } | {
253
+ readonly _tag: 'Failed';
254
+ readonly error: RemoteError;
255
+ };
256
+ export declare const mutationStatus: (state: MutationState, requestId: string) => MutationStatus;
141
257
  export interface Reconciled {
142
258
  readonly store: EntityStore;
143
259
  readonly state: MutationState;
@@ -147,7 +263,10 @@ export interface Reconciled {
147
263
  * with the same `requestId` (a retry, or a live event describing the same change)
148
264
  * does not re-apply the entities, but still clears `pending`.
149
265
  */
150
- export declare const reconcileMutation: (store: EntityStore, state: MutationState, requestId: string, entities: ReadonlyArray<NormalizedPatch>) => Reconciled;
266
+ export declare const reconcileMutation: (store: EntityStore, state: MutationState, requestId: string, entities: ReadonlyArray<NormalizedPatch>, deleted?: ReadonlyArray<{
267
+ readonly entity: string;
268
+ readonly id: string;
269
+ }>) => Reconciled;
151
270
  /** A mutation an application declares: its name and input/output codecs. */
152
271
  export interface MutationDescriptor<Name extends string, Input, Output> {
153
272
  readonly name: Name;
@@ -283,9 +402,9 @@ export declare const Requirement: {
283
402
  mergeConnections: typeof mergeConnections;
284
403
  };
285
404
  /** Remote's entity requirements on a Projection, unioned per entity and id as nodes compose. */
286
- export declare const RemoteRequirements: import("foldkit-surface").MetadataKey<Requirement>;
405
+ export declare const RemoteRequirements: MetadataKey<Requirement>;
287
406
  /** Remote's query connections on a Projection, unioned per connection and window. */
288
- export declare const RemoteConnections: import("foldkit-surface").MetadataKey<ConnectionRequirement>;
407
+ export declare const RemoteConnections: MetadataKey<ConnectionRequirement>;
289
408
  /** The entity requirements a Projection carries. */
290
409
  export declare const requirementsOf: (projection: {
291
410
  readonly metadata: Metadata;
@@ -381,7 +500,10 @@ export declare const pruneOverlays: (optimistic: OptimisticState, connection: st
381
500
  * a later layer re-wins on rebase. Confirmed connection changes are recorded
382
501
  * once per request, as overlays the request no longer owns.
383
502
  */
384
- export declare const settleSuccess: (base: EntityStore, optimistic: OptimisticState, state: MutationState, requestId: string, entities: ReadonlyArray<NormalizedPatch>, connections?: ReadonlyArray<ConnectionChange>) => {
503
+ export declare const settleSuccess: (base: EntityStore, optimistic: OptimisticState, state: MutationState, requestId: string, entities: ReadonlyArray<NormalizedPatch>, connections?: ReadonlyArray<ConnectionChange>, deleted?: ReadonlyArray<{
504
+ readonly entity: string;
505
+ readonly id: string;
506
+ }>) => {
385
507
  readonly store: EntityStore;
386
508
  readonly state: MutationState;
387
509
  readonly optimistic: OptimisticState;
@@ -672,6 +794,11 @@ export declare const MutationResult: Schema.Struct<{
672
794
  readonly key: Schema.String;
673
795
  }>;
674
796
  }>]>>>;
797
+ /** Entities the mutation deleted; the client knows them absent from then on. */
798
+ readonly deleted: Schema.optional<Schema.$Array<Schema.Struct<{
799
+ readonly entity: Schema.String;
800
+ readonly id: Schema.String;
801
+ }>>>;
675
802
  }>;
676
803
  export declare const LiveRequirement: Schema.Struct<{
677
804
  readonly version: Schema.Number;
@@ -760,6 +887,11 @@ export declare const Mutate: Rpc.Rpc<"FoldkitRemoteMutate", Schema.Struct<{
760
887
  readonly key: Schema.String;
761
888
  }>;
762
889
  }>]>>>;
890
+ /** Entities the mutation deleted; the client knows them absent from then on. */
891
+ readonly deleted: Schema.optional<Schema.$Array<Schema.Struct<{
892
+ readonly entity: Schema.String;
893
+ readonly id: Schema.String;
894
+ }>>>;
763
895
  }>, typeof RemoteMutationError, never, never>;
764
896
  export declare const Live: Rpc.Rpc<"FoldkitRemoteLive", Schema.Struct<{
765
897
  readonly version: Schema.Number;
@@ -924,6 +1056,11 @@ export declare const RemoteRpc: RpcGroup.RpcGroup<Rpc.Rpc<"FoldkitRemoteRead", S
924
1056
  readonly key: Schema.String;
925
1057
  }>;
926
1058
  }>]>>>;
1059
+ /** Entities the mutation deleted; the client knows them absent from then on. */
1060
+ readonly deleted: Schema.optional<Schema.$Array<Schema.Struct<{
1061
+ readonly entity: Schema.String;
1062
+ readonly id: Schema.String;
1063
+ }>>>;
927
1064
  }>, typeof RemoteMutationError, never, never> | Rpc.Rpc<"FoldkitRemoteLive", Schema.Struct<{
928
1065
  readonly version: Schema.Number;
929
1066
  readonly requirements: Schema.$Array<Schema.Codec<Requirement, Requirement, never, never>>;
@@ -997,51 +1134,6 @@ export declare const RemoteRpc: RpcGroup.RpcGroup<Rpc.Rpc<"FoldkitRemoteRead", S
997
1134
  }>]>;
998
1135
  }>, typeof RemoteQueryError, never, never>>;
999
1136
  //#endregion
1000
- //#region src/remoteData.d.ts
1001
- export interface RemoteError {
1002
- readonly _tag: string;
1003
- readonly message: string;
1004
- }
1005
- export type RemoteData<A> = {
1006
- readonly _tag: 'Initial';
1007
- } | {
1008
- readonly _tag: 'Loading';
1009
- } | {
1010
- readonly _tag: 'Ready';
1011
- readonly value: A;
1012
- } | {
1013
- readonly _tag: 'Refreshing';
1014
- readonly value: A;
1015
- } | {
1016
- readonly _tag: 'Failed';
1017
- readonly error: RemoteError;
1018
- readonly previous?: A;
1019
- } | {
1020
- readonly _tag: 'NotFound';
1021
- };
1022
- export declare const remoteErrorSchema: Schema.Struct<{
1023
- readonly _tag: Schema.String;
1024
- readonly message: Schema.String;
1025
- }>;
1026
- export declare const remoteDataSchema: <A>(value: Schema.Schema<A>) => Schema.Codec<RemoteData<A>, unknown>;
1027
- export declare const RemoteData: {
1028
- /**
1029
- * The `RemoteData` schema for a value schema. Exposed so a `RemoteData` can
1030
- * be embedded in a hand-written Model schema, not only through `Remote.select`.
1031
- */
1032
- schema: <A>(value: Schema.Schema<A>) => Schema.Schema<RemoteData<A>>;
1033
- /** Exhaustive: omitting a state is a compile error. */
1034
- match: <A, R>(data: RemoteData<A>, cases: {
1035
- readonly Initial: () => R;
1036
- readonly Loading: () => R;
1037
- readonly Ready: (value: A) => R;
1038
- readonly Refreshing: (value: A) => R;
1039
- readonly Failed: (error: RemoteError, previous: Option.Option<A>) => R;
1040
- readonly NotFound: () => R;
1041
- }) => R;
1042
- map: <A, B>(data: RemoteData<A>, f: (value: A) => B) => RemoteData<B>;
1043
- };
1044
- //#endregion
1045
1137
  //#region src/model.d.ts
1046
1138
  /**
1047
1139
  * The normalized cache as a Foldkit Submodel. `Remote.define` returns a schema
@@ -1124,6 +1216,11 @@ export type RemoteMessage = {
1124
1216
  readonly entities: readonly NormalizedPatch[];
1125
1217
  /** Connection changes the server confirmed; they replace the request's own. */
1126
1218
  readonly connections?: ReadonlyArray<ConnectionChange> | undefined;
1219
+ /** Entities the mutation deleted: tombstoned, which hides them from every connection. */
1220
+ readonly deleted?: ReadonlyArray<{
1221
+ readonly entity: string;
1222
+ readonly id: string;
1223
+ }> | undefined;
1127
1224
  } | {
1128
1225
  readonly _tag: 'MutationFailed';
1129
1226
  readonly requestId: string;
@@ -1218,6 +1315,10 @@ export declare const remoteMessageCases: {
1218
1315
  readonly values: Schema.$Record<Schema.String, Schema.Unknown>;
1219
1316
  }>>;
1220
1317
  connections: Schema.optional<Schema.$Array<Schema.Unknown>>;
1318
+ deleted: Schema.optional<Schema.$Array<Schema.Struct<{
1319
+ readonly entity: Schema.String;
1320
+ readonly id: Schema.String;
1321
+ }>>>;
1221
1322
  };
1222
1323
  MutationFailed: {
1223
1324
  requestId: Schema.String;
@@ -1374,6 +1475,10 @@ export declare const mutateRemote: <Name extends string, Input, Output>(mutation
1374
1475
  };
1375
1476
  }[];
1376
1477
  connections: ConnectionChange[];
1478
+ deleted: readonly {
1479
+ readonly entity: string;
1480
+ readonly id: string;
1481
+ }[];
1377
1482
  }, RemoteMutationError, RemoteClient>;
1378
1483
  //#endregion
1379
1484
  //#region src/relation.d.ts
@@ -1458,6 +1563,11 @@ export declare const pageSchema: (item: AnySchema) => AnySchema;
1458
1563
  /** The requirement a nested selection contributes for its relation's target. */
1459
1564
  export declare const relationOf: (selection: Selection<unknown>) => RelationRequirement;
1460
1565
  export declare const Selection: {
1566
+ /**
1567
+ * The Remote Selection of a `foldkit-entity` Selection: the same members, read
1568
+ * from Remote's store. `true` on a relation reads its refs, as it does here.
1569
+ */
1570
+ from: <Name extends string, S extends Schema.Constraint>(selection: Domain.Selection<Name, unknown, S>) => Selection<S["Type"], Name, "entity">;
1461
1571
  /**
1462
1572
  * The fields to read of `entity`. A nested `Selection` on a relation field
1463
1573
  * reads through the ref (or refs, or page of refs) the field holds into the
@@ -1526,7 +1636,25 @@ export interface RefPage<Name extends string, F extends Schema.Struct.Fields = S
1526
1636
  readonly hasNext: boolean;
1527
1637
  readonly hasPrevious: boolean;
1528
1638
  }
1639
+ /** The ref codec a `foldkit-entity` relation becomes: one ref, a nullable ref, or an array of refs. */
1640
+ type RelationField<Spec> = Spec extends Domain.RelationSpec<infer Target, infer Cardinality, infer Optional> ? Cardinality extends 'many' ? Schema.$Array<Schema.Codec<EntityRef<Target['name']>, string>> : Optional extends true ? Schema.NullOr<Schema.Codec<EntityRef<Target['name']>, string>> : Schema.Codec<EntityRef<Target['name']>, string> : never;
1641
+ /**
1642
+ * The fields Remote stores for a `foldkit-entity` Entity: its own fields, its
1643
+ * derived members (the server supplies them like any field), and each relation
1644
+ * as refs.
1645
+ */
1646
+ export type FieldsFrom<E> = E extends Domain.Entity<any, infer Fields, infer Relations, infer Derived> ? Fields & { readonly [K in keyof Derived]: Derived[K]['schema']; } & { readonly [K in keyof Relations]: RelationField<Relations[K]>; } : never;
1529
1647
  declare const Entity$1: {
1648
+ /**
1649
+ * The Remote descriptor of a `foldkit-entity` Entity. Relations declared with
1650
+ * `Entity.relate` become ref fields, so the store, the planner, and the server
1651
+ * treat it as they treat an `Entity.make` descriptor.
1652
+ */
1653
+ from: <E extends Domain.AnyEntity>(entity: E & {
1654
+ readonly fields: {
1655
+ readonly id: unknown;
1656
+ };
1657
+ }) => EntityDescriptor<E["name"], FieldsFrom<E>>;
1530
1658
  make: <const Name extends string, const F extends Schema.Struct.Fields & {
1531
1659
  readonly id: Schema.Schema<unknown>;
1532
1660
  }>(name: Name, schema: Schema.Struct<F>) => EntityDescriptor<Name, F>;
@@ -1640,7 +1768,18 @@ export declare const RemotePolicy: {
1640
1768
  };
1641
1769
  //#endregion
1642
1770
  //#region src/index.d.ts
1643
- type EntityName<D> = D extends EntityDescriptor<infer Name, any> ? Name : never;
1771
+ /** What a domain registers: a Remote descriptor, or a `foldkit-entity` Entity (read through `Entity.from`). */
1772
+ export type EntityLike = EntityDescriptor<any, any> | Domain.AnyEntity;
1773
+ /**
1774
+ * What a read selects: a Remote Selection, or a `foldkit-entity` Selection
1775
+ * (read through `Selection.from`).
1776
+ */
1777
+ export type EntitySelection<Value, Name extends string> = Selection<Value, Name, 'entity'> | Domain.Selection<Name, unknown, Schema.Constraint & {
1778
+ readonly Type: Value;
1779
+ }>;
1780
+ type EntityName<D> = D extends {
1781
+ readonly name: infer Name extends string;
1782
+ } ? Name : never;
1644
1783
  type QueryName<D> = D extends QueryDescriptor<infer Name, any, any> ? Name : never;
1645
1784
  type MutationName<D> = D extends MutationDescriptor<infer Name, any, any> ? Name : never;
1646
1785
  /**
@@ -1651,7 +1790,7 @@ type MutationName<D> = D extends MutationDescriptor<infer Name, any, any> ? Name
1651
1790
  export type Registered<Name extends string, Names extends string, Kind extends string> = [Name] extends [Names] ? unknown : Invalid<`${Kind} "${Name}" is not registered with this Remote domain`>;
1652
1791
  /** Nothing when a selection is of the query's entity; otherwise a branded failure naming both. */
1653
1792
  export type SelectsEntity<Entity extends string, Of extends string> = [Entity] extends [Of] ? unknown : Invalid<`the selection is of "${Entity}", but the query lists "${Of}"`>;
1654
- export interface RemoteDescriptor<Entities extends readonly EntityDescriptor<any, any>[] = readonly EntityDescriptor<any, any>[], Queries extends readonly QueryDescriptor<any, any, any>[] = readonly QueryDescriptor<any, any, any>[], Mutations extends readonly MutationDescriptor<any, any, any>[] = readonly MutationDescriptor<any, any, any>[]> {
1793
+ export interface RemoteDescriptor<Entities extends readonly EntityLike[] = readonly EntityLike[], Queries extends readonly QueryDescriptor<any, any, any>[] = readonly QueryDescriptor<any, any, any>[], Mutations extends readonly MutationDescriptor<any, any, any>[] = readonly MutationDescriptor<any, any, any>[]> {
1655
1794
  readonly entities: Entities;
1656
1795
  readonly queries: Queries;
1657
1796
  readonly mutations: Mutations;
@@ -1701,6 +1840,8 @@ export type SubscriptionEntries<AppModel, Active> = { readonly [K in keyof Activ
1701
1840
  readonly retain: RemoteEntry<AppModel, any>;
1702
1841
  };
1703
1842
  export interface SubscriptionsOptions extends ObserveOptions, LiveOptions, RetainOptions {}
1843
+ /** A Remote domain's wiring for an assembly: it routes Remote's Messages into `reduce`, brings its Subscriptions and contract, and requires `RemoteClient` from the runtime's resources Layer. */
1844
+ export type RemoteWiring<AppModel> = Wiring<AppModel, RemoteMessage | RemoteMessageInput, RemoteClient>;
1704
1845
  /** What `RemoteDomain.mutate` hands `update`: the Model with the request started, and the Command that settles it. */
1705
1846
  export interface MutationStarted<AppModel> {
1706
1847
  readonly model: AppModel;
@@ -1715,15 +1856,15 @@ export interface MutationStarted<AppModel> {
1715
1856
  * operations over them. Every method compiles to the `Remote.*` function of the
1716
1857
  * same name, which stays exported for tooling, SSR, and tests.
1717
1858
  */
1718
- export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities extends readonly EntityDescriptor<any, any>[], Queries extends readonly QueryDescriptor<any, any, any>[], Mutations extends readonly MutationDescriptor<any, any, any>[]> extends BoundRemote<AppModel, Store, EntityName<Entities[number]>>, RemoteDescriptor<Entities, Queries, Mutations> {
1859
+ export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities extends readonly EntityLike[], Queries extends readonly QueryDescriptor<any, any, any>[], Mutations extends readonly MutationDescriptor<any, any, any>[]> extends BoundRemote<AppModel, Store, EntityName<Entities[number]>>, RemoteDescriptor<Entities, Queries, Mutations> {
1719
1860
  /** `Remote.select`: a Projection reading one entity through a selection of a registered entity. */
1720
- get<Value, Name extends string>(selection: Selection<Value, Name, 'entity'> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id: string): Projection<AppModel, RemoteData<Value>>;
1861
+ get<Value, Name extends string>(selection: EntitySelection<Value, Name> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id: string): Projection<AppModel, RemoteData<Value>>;
1721
1862
  /**
1722
1863
  * `get`, and the projection also subscribes to the entity's changes: its
1723
1864
  * requirements are marked `live`, so `subscriptions` derives a live entry for
1724
1865
  * the Surfaces that read it.
1725
1866
  */
1726
- live<Value, Name extends string>(selection: Selection<Value, Name, 'entity'> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id: string): Projection<AppModel, RemoteData<Value>>;
1867
+ live<Value, Name extends string>(selection: EntitySelection<Value, Name> & Registered<Name, EntityName<Entities[number]>, 'Entity'>, id: string): Projection<AppModel, RemoteData<Value>>;
1727
1868
  /**
1728
1869
  * A query connection read as a `Page` of items selected of the query's
1729
1870
  * entity: `Initial` until the page and every item's selected fields are
@@ -1766,8 +1907,21 @@ export interface RemoteDomain<AppModel, Store extends RemoteModel, Entities exte
1766
1907
  * mutation sequence unless `options.requestId` is given.
1767
1908
  */
1768
1909
  mutate<M extends MutationDescriptor<any, any, any>>(model: AppModel, mutation: M & Registered<M['name'], MutationName<Mutations[number]>, 'Mutation'>, input: MutationInput<M>, options?: DomainMutateOptions): MutationStarted<AppModel>;
1910
+ /**
1911
+ * What Remote knows of a mutation `mutate` started, by its request id:
1912
+ * pending, applied, or failed with the error the server or transport gave.
1913
+ */
1914
+ mutation(model: AppModel, requestId: string): MutationStatus;
1769
1915
  /** `updateRemote` on the bound slice: reduces one of Remote's Messages, as `RemoteMessage` or as the application's union constructs it. */
1770
1916
  reduce(model: AppModel, message: RemoteMessage | RemoteMessageInput): AppModel;
1917
+ /**
1918
+ * How this domain joins an assembly: it routes Remote's Messages into
1919
+ * `reduce`, brings its Subscriptions and contract, and requires
1920
+ * `RemoteClient` from the runtime's resources Layer. One assembly holds at
1921
+ * most one Remote wiring: every domain claims the same tags, and routing
1922
+ * takes the first claimant.
1923
+ */
1924
+ wiring: <const Active extends Readonly<Record<string, ActiveSurface<AppModel> | Surface<AppModel, any, any, void>>>>(active: Active, options?: SubscriptionsOptions) => RemoteWiring<AppModel>;
1771
1925
  /** `Remote.inspect` of the bound slice. */
1772
1926
  inspect(model: AppModel): RemoteInspection;
1773
1927
  }
@@ -1789,7 +1943,7 @@ export type QueryWindowOptions = {
1789
1943
  };
1790
1944
  export type QueryOptions<Value, Entity extends string, Of extends string = Entity> = {
1791
1945
  /** What to read of each item: a selection of the query's entity (`Of`). */
1792
- readonly select: Selection<Value, Entity, 'entity'> & SelectsEntity<Entity, Of>;
1946
+ readonly select: EntitySelection<Value, Entity> & SelectsEntity<Entity, Of>;
1793
1947
  } & QueryWindowOptions;
1794
1948
  /** A query connection read as a `Page` of selected items; `ref` is the connection with its first window. */
1795
1949
  export interface QueryProjection<AppModel, Value, Name extends string, Input> extends Projection<AppModel, RemoteData<Page<Value>>> {
@@ -1899,6 +2053,10 @@ export declare const Remote: {
1899
2053
  readonly values: Schema.$Record<Schema.String, Schema.Unknown>;
1900
2054
  }>>;
1901
2055
  connections: Schema.optional<Schema.$Array<Schema.Unknown>>;
2056
+ deleted: Schema.optional<Schema.$Array<Schema.Struct<{
2057
+ readonly entity: Schema.String;
2058
+ readonly id: Schema.String;
2059
+ }>>>;
1902
2060
  };
1903
2061
  MutationFailed: {
1904
2062
  requestId: Schema.String;
@@ -1946,7 +2104,7 @@ export declare const Remote: {
1946
2104
  * in one step; the result carries the application-facing operations. The
1947
2105
  * descriptor alone is `Remote.define`, the binding alone `Remote.at`.
1948
2106
  */
1949
- make: <AppModel, Store extends RemoteModel, const Entities extends readonly EntityDescriptor<any, any>[], const Queries extends readonly QueryDescriptor<any, any, any>[] = readonly [], const Mutations extends readonly MutationDescriptor<any, any, any>[] = readonly []>(config: {
2107
+ make: <AppModel, Store extends RemoteModel, const Entities extends readonly EntityLike[], const Queries extends readonly QueryDescriptor<any, any, any>[] = readonly [], const Mutations extends readonly MutationDescriptor<any, any, any>[] = readonly []>(config: {
1950
2108
  readonly model: ModelRef<AppModel, Store>;
1951
2109
  readonly entities: Entities;
1952
2110
  readonly queries?: Queries;
@@ -1957,7 +2115,7 @@ export declare const Remote: {
1957
2115
  * mutations, plus the submodel. For a domain reused across applications or
1958
2116
  * bound in a test; `Remote.make` is the one-step form.
1959
2117
  */
1960
- define: <const Entities extends readonly EntityDescriptor<any, any>[], const Queries extends readonly QueryDescriptor<any, any, any>[] = readonly QueryDescriptor<any, any, any>[], const Mutations extends readonly MutationDescriptor<any, any, any>[] = readonly MutationDescriptor<any, any, any>[]>(config: {
2118
+ define: <const Entities extends readonly EntityLike[], const Queries extends readonly QueryDescriptor<any, any, any>[] = readonly QueryDescriptor<any, any, any>[], const Mutations extends readonly MutationDescriptor<any, any, any>[] = readonly MutationDescriptor<any, any, any>[]>(config: {
1961
2119
  readonly entities: Entities;
1962
2120
  readonly queries?: Queries;
1963
2121
  readonly mutations?: Mutations;
@@ -1967,7 +2125,7 @@ export declare const Remote: {
1967
2125
  * registered entity names are carried on the returned value, so `Remote.select`
1968
2126
  * rejects a selection for an entity this domain never declared.
1969
2127
  */
1970
- at: <AppModel, Store extends RemoteModel, Entities extends readonly EntityDescriptor<any, any>[], Queries extends readonly QueryDescriptor<any, any, any>[], Mutations extends readonly MutationDescriptor<any, any, any>[]>(definition: RemoteDescriptor<Entities, Queries, Mutations>, store: ModelRef<AppModel, Store>) => BoundRemote<AppModel, Store, EntityName<Entities[number]>>;
2128
+ at: <AppModel, Store extends RemoteModel, Entities extends readonly EntityLike[], Queries extends readonly QueryDescriptor<any, any, any>[], Mutations extends readonly MutationDescriptor<any, any, any>[]>(definition: RemoteDescriptor<Entities, Queries, Mutations>, store: ModelRef<AppModel, Store>) => BoundRemote<AppModel, Store, EntityName<Entities[number]>>;
1971
2129
  /**
1972
2130
  * A Projection node that reads a `RemoteData` value out of the store. The id
1973
2131
  * is supplied by the caller, usually from a Surface's params. The assembled
@@ -1975,7 +2133,7 @@ export declare const Remote: {
1975
2133
  * as `Failed` instead of being asserted into `Value`. A present value with a
1976
2134
  * stale field reads as `Refreshing`: an observer is refetching it.
1977
2135
  */
1978
- select: <AppModel, Store extends RemoteModel, Names extends string, Value, Name extends string>(bound: BoundRemote<AppModel, Store, Names>, selection: Selection<Value, Name, "entity"> & Registered<Name, Names, "Entity">) => (id: string) => Projection<AppModel, RemoteData<Value>>;
2136
+ select: <AppModel, Store extends RemoteModel, Names extends string, Value, Name extends string>(bound: BoundRemote<AppModel, Store, Names>, given: EntitySelection<Value, Name> & Registered<Name, Names, "Entity">) => (id: string) => Projection<AppModel, RemoteData<Value>>;
1979
2137
  /**
1980
2138
  * The pure plan for a projection against a Model: the requirements its
1981
2139
  * remote store does not satisfy, under `options` (freshness, force). A
@@ -2098,6 +2256,10 @@ export declare const Remote: {
2098
2256
  };
2099
2257
  }[];
2100
2258
  connections: ConnectionChange[];
2259
+ deleted: readonly {
2260
+ readonly entity: string;
2261
+ readonly id: string;
2262
+ }[];
2101
2263
  }, RemoteMutationError, RemoteClient>;
2102
2264
  /**
2103
2265
  * Runs a mutation and reconciles its patches into a `RemoteModel` in one step,