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 +97 -1
- package/dist/index.d.mts +224 -62
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +426 -345
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Context, Deferred, Duration, Effect, Layer, Option, Request, RequestResolver, Result, Schema, SchemaGetter, Stream } from "effect";
|
|
2
|
+
import * as Domain from "foldkit-entity";
|
|
3
|
+
import { Entity as Entity$1, SelectionTypeId } from "foldkit-entity";
|
|
4
|
+
import * as Subscription from "foldkit/subscription";
|
|
2
5
|
import { Metadata } from "foldkit-surface";
|
|
3
6
|
import { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
4
7
|
import { KeyValueStore } from "effect/unstable/persistence";
|
|
@@ -266,6 +269,7 @@ const emptyMutationState = {
|
|
|
266
269
|
pending: /* @__PURE__ */ new Set(),
|
|
267
270
|
applied: /* @__PURE__ */ new Set(),
|
|
268
271
|
failed: /* @__PURE__ */ new Set(),
|
|
272
|
+
errors: /* @__PURE__ */ new Map(),
|
|
269
273
|
sequence: 0
|
|
270
274
|
};
|
|
271
275
|
/**
|
|
@@ -288,13 +292,26 @@ const beginMutation = (state, requestId) => ({
|
|
|
288
292
|
pending: /* @__PURE__ */ new Set([...state.pending, requestId]),
|
|
289
293
|
sequence: state.sequence + 1
|
|
290
294
|
});
|
|
291
|
-
const failMutation = (state, requestId) => {
|
|
295
|
+
const failMutation = (state, requestId, error) => {
|
|
292
296
|
const pending = new Set(state.pending);
|
|
293
297
|
pending.delete(requestId);
|
|
298
|
+
const failed = remember(state.failed, requestId);
|
|
299
|
+
const errors = new Map([...state.errors, [requestId, error]]);
|
|
300
|
+
for (const id of errors.keys()) if (!failed.has(id)) errors.delete(id);
|
|
294
301
|
return {
|
|
295
302
|
...state,
|
|
296
303
|
pending,
|
|
297
|
-
failed
|
|
304
|
+
failed,
|
|
305
|
+
errors
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
const mutationStatus = (state, requestId) => {
|
|
309
|
+
if (state.pending.has(requestId)) return { _tag: "Pending" };
|
|
310
|
+
if (state.applied.has(requestId)) return { _tag: "Applied" };
|
|
311
|
+
const error = state.errors.get(requestId);
|
|
312
|
+
return error === void 0 ? { _tag: "Unknown" } : {
|
|
313
|
+
_tag: "Failed",
|
|
314
|
+
error
|
|
298
315
|
};
|
|
299
316
|
};
|
|
300
317
|
/**
|
|
@@ -302,8 +319,8 @@ const failMutation = (state, requestId) => {
|
|
|
302
319
|
* with the same `requestId` (a retry, or a live event describing the same change)
|
|
303
320
|
* does not re-apply the entities, but still clears `pending`.
|
|
304
321
|
*/
|
|
305
|
-
const reconcileMutation = (store, state, requestId, entities) => {
|
|
306
|
-
const next = state.applied.has(requestId) ? store : entities.reduce((current, entity) => writeEntity(current, entityKey(entity.entity, entity.id), entity.values), store);
|
|
322
|
+
const reconcileMutation = (store, state, requestId, entities, deleted = []) => {
|
|
323
|
+
const next = state.applied.has(requestId) ? store : deleted.reduce((current, gone) => tombstone(current, entityKey(gone.entity, gone.id)), entities.reduce((current, entity) => writeEntity(current, entityKey(entity.entity, entity.id), entity.values), store));
|
|
307
324
|
const applied = remember(state.applied, requestId);
|
|
308
325
|
const pending = new Set(state.pending);
|
|
309
326
|
pending.delete(requestId);
|
|
@@ -501,7 +518,12 @@ const MutationResult = Schema.Struct({
|
|
|
501
518
|
output: Schema.Unknown,
|
|
502
519
|
entities: Schema.Array(NormalizedEntity),
|
|
503
520
|
/** Connection changes the mutation made, applied alongside its entity patches. */
|
|
504
|
-
connections: Schema.optional(Schema.Array(ConnectionChangeSchema))
|
|
521
|
+
connections: Schema.optional(Schema.Array(ConnectionChangeSchema)),
|
|
522
|
+
/** Entities the mutation deleted; the client knows them absent from then on. */
|
|
523
|
+
deleted: Schema.optional(Schema.Array(Schema.Struct({
|
|
524
|
+
entity: Schema.String,
|
|
525
|
+
id: Schema.String
|
|
526
|
+
})))
|
|
505
527
|
});
|
|
506
528
|
const LiveRequirement = Schema.Struct({
|
|
507
529
|
version: Schema.Number,
|
|
@@ -825,7 +847,8 @@ const mutateRemote = Effect.fn("Remote.mutate")(function* (mutation, input, requ
|
|
|
825
847
|
return {
|
|
826
848
|
output: yield* Schema.decodeUnknownEffect(mutation.Output)(result.output).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new RemoteMutationError({ message: error.message })))),
|
|
827
849
|
entities: result.entities,
|
|
828
|
-
connections: (result.connections ?? []).map(connectionChangeOf)
|
|
850
|
+
connections: (result.connections ?? []).map(connectionChangeOf),
|
|
851
|
+
deleted: result.deleted ?? []
|
|
829
852
|
};
|
|
830
853
|
});
|
|
831
854
|
//#endregion
|
|
@@ -993,6 +1016,322 @@ const hasNext = (connection) => {
|
|
|
993
1016
|
/** A connection with more than one segment has an explicit unknown gap. */
|
|
994
1017
|
const isGapped = (connection) => connection.segments.length > 1;
|
|
995
1018
|
//#endregion
|
|
1019
|
+
//#region src/relation.ts
|
|
1020
|
+
/** Marks the codec `Entity.ref`/`Entity.refPage` produce, so a Selection can tell the field's shape. */
|
|
1021
|
+
const RelationAnnotation = "foldkitRemoteRelation";
|
|
1022
|
+
/** The target entity's name on the same codec, so a nested selection of another entity is refused. */
|
|
1023
|
+
const RelationEntityAnnotation = "foldkitRemoteRelationEntity";
|
|
1024
|
+
/** Splits a ref key (`"Entity:id"`, the store key too) back into its parts. */
|
|
1025
|
+
const refParts = (encoded) => {
|
|
1026
|
+
const separator = encoded.indexOf(":");
|
|
1027
|
+
return separator === -1 ? {
|
|
1028
|
+
entity: encoded,
|
|
1029
|
+
id: ""
|
|
1030
|
+
} : {
|
|
1031
|
+
entity: encoded.slice(0, separator),
|
|
1032
|
+
id: encoded.slice(separator + 1)
|
|
1033
|
+
};
|
|
1034
|
+
};
|
|
1035
|
+
/**
|
|
1036
|
+
* The refs a relation value carries, in order. Anything that is not a ref key,
|
|
1037
|
+
* an array of them, or a page of them contributes nothing.
|
|
1038
|
+
*/
|
|
1039
|
+
const refsIn = (value) => {
|
|
1040
|
+
if (typeof value === "string") return [refParts(value)];
|
|
1041
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string").map(refParts);
|
|
1042
|
+
if (value !== null && typeof value === "object") {
|
|
1043
|
+
const refs = value.refs;
|
|
1044
|
+
if (Array.isArray(refs)) return refs.filter((item) => typeof item === "string").map(refParts);
|
|
1045
|
+
}
|
|
1046
|
+
return [];
|
|
1047
|
+
};
|
|
1048
|
+
/** The refs in a relation value that name the relation's target entity, and no other. */
|
|
1049
|
+
const targetsOf = (value, relation) => refsIn(value).filter((ref) => ref.entity === relation.entity);
|
|
1050
|
+
const isRefPage = (value) => {
|
|
1051
|
+
if (value === null || typeof value !== "object") return false;
|
|
1052
|
+
const page = value;
|
|
1053
|
+
return Array.isArray(page.refs) && page.refs.every((ref) => typeof ref === "string") && typeof page.hasNext === "boolean" && typeof page.hasPrevious === "boolean";
|
|
1054
|
+
};
|
|
1055
|
+
/**
|
|
1056
|
+
* The relation shape an entity field schema declares: a ref (`one`), a nullable
|
|
1057
|
+
* ref, an array of refs (`many`), or a page of refs (`page`). `undefined` for a
|
|
1058
|
+
* scalar field, which cannot take a nested selection.
|
|
1059
|
+
*/
|
|
1060
|
+
const relationShape = (schema) => shapeOf(schema.ast, false);
|
|
1061
|
+
const shapeOf = (ast, nullable) => {
|
|
1062
|
+
const annotated = ast.annotations?.[RelationAnnotation];
|
|
1063
|
+
if (annotated === "one" || annotated === "page") return {
|
|
1064
|
+
kind: annotated,
|
|
1065
|
+
nullable,
|
|
1066
|
+
entity: String(ast.annotations?.[RelationEntityAnnotation])
|
|
1067
|
+
};
|
|
1068
|
+
if (ast._tag === "Arrays") {
|
|
1069
|
+
const item = ast.rest?.[0];
|
|
1070
|
+
const inner = item === void 0 ? void 0 : shapeOf(item, false);
|
|
1071
|
+
return inner?.kind === "one" ? {
|
|
1072
|
+
kind: "many",
|
|
1073
|
+
nullable,
|
|
1074
|
+
entity: inner.entity
|
|
1075
|
+
} : void 0;
|
|
1076
|
+
}
|
|
1077
|
+
if (ast._tag === "Union") {
|
|
1078
|
+
const members = ast.types ?? [];
|
|
1079
|
+
const optional = members.some((member) => member._tag === "Null" || member._tag === "Undefined");
|
|
1080
|
+
for (const member of members) {
|
|
1081
|
+
const shape = shapeOf(member, optional);
|
|
1082
|
+
if (shape !== void 0) return shape;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/selection.ts
|
|
1088
|
+
/**
|
|
1089
|
+
* Selections: the fields a Surface picks of an entity (nested through
|
|
1090
|
+
* relations, paged through connections), the requirement graph they imply,
|
|
1091
|
+
* and their assembly from the normalized store.
|
|
1092
|
+
*/
|
|
1093
|
+
const pageSchemas = /* @__PURE__ */ new WeakMap();
|
|
1094
|
+
const pageSchema = (item) => {
|
|
1095
|
+
const cached = pageSchemas.get(item);
|
|
1096
|
+
if (cached !== void 0) return cached;
|
|
1097
|
+
const built = Schema.Struct({
|
|
1098
|
+
items: Schema.Array(item),
|
|
1099
|
+
hasNext: Schema.Boolean,
|
|
1100
|
+
hasPrevious: Schema.Boolean
|
|
1101
|
+
});
|
|
1102
|
+
pageSchemas.set(item, built);
|
|
1103
|
+
return built;
|
|
1104
|
+
};
|
|
1105
|
+
/** The requirement a nested selection contributes for its relation's target. */
|
|
1106
|
+
const relationOf = (selection) => ({
|
|
1107
|
+
entity: selection.entity,
|
|
1108
|
+
fields: selection.fields,
|
|
1109
|
+
...selection.connections === void 0 ? {} : { windows: selection.connections },
|
|
1110
|
+
...selection.relations === void 0 ? {} : { relations: selection.relations }
|
|
1111
|
+
});
|
|
1112
|
+
const Selection = {
|
|
1113
|
+
/**
|
|
1114
|
+
* The Remote Selection of a `foldkit-entity` Selection: the same members, read
|
|
1115
|
+
* from Remote's store. `true` on a relation reads its refs, as it does here.
|
|
1116
|
+
*/
|
|
1117
|
+
from: (selection) => {
|
|
1118
|
+
const spec = {};
|
|
1119
|
+
for (const [key, selected] of Object.entries(selection.members)) spec[key] = selected === true ? true : Domain.SelectionPageTypeId in selected ? Selection.connection(Entity.from(selected.selection.entity), selected.window, Selection.from(selected.selection)) : Selection.from(selected);
|
|
1120
|
+
return Selection.make(Entity.from(selection.entity), spec);
|
|
1121
|
+
},
|
|
1122
|
+
/**
|
|
1123
|
+
* The fields to read of `entity`. A nested `Selection` on a relation field
|
|
1124
|
+
* reads through the ref (or refs, or page of refs) the field holds into the
|
|
1125
|
+
* target's fields; a scalar field cannot take one.
|
|
1126
|
+
*/
|
|
1127
|
+
make: (entity, selection) => {
|
|
1128
|
+
if (Object.keys(selection).length === 0) throw new Error(`Selection.make: a selection of "${entity.name}" picks at least one field`);
|
|
1129
|
+
const picked = {};
|
|
1130
|
+
const connections = {};
|
|
1131
|
+
const relations = {};
|
|
1132
|
+
for (const key of Object.keys(selection)) {
|
|
1133
|
+
const choice = selection[key];
|
|
1134
|
+
if (choice === true) {
|
|
1135
|
+
picked[key] = entity.fields[key];
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
const nested = choice;
|
|
1139
|
+
const shape = relationShape(entity.fields[key]);
|
|
1140
|
+
if (shape === void 0) throw new Error(`Selection.make: "${key}" on "${entity.name}" is not a relation field, so it cannot take a nested selection`);
|
|
1141
|
+
if (nested.entity !== shape.entity) throw new Error(`Selection.make: "${key}" on "${entity.name}" refers to "${shape.entity}", not "${nested.entity}"`);
|
|
1142
|
+
const nullable = (codec) => shape.nullable ? Schema.NullOr(codec) : codec;
|
|
1143
|
+
if (nested.window !== void 0) {
|
|
1144
|
+
connections[key] = nested.window;
|
|
1145
|
+
picked[key] = nullable(nested.schema);
|
|
1146
|
+
if (nested.fields.length > 0) relations[key] = relationOf(nested);
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
const item = nested.schema;
|
|
1150
|
+
picked[key] = nullable(shape.kind === "many" ? Schema.Array(item) : shape.kind === "page" ? pageSchema(item) : item);
|
|
1151
|
+
relations[key] = relationOf(nested);
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
entity: entity.name,
|
|
1155
|
+
fields: Object.keys(selection),
|
|
1156
|
+
schema: Schema.Struct(picked),
|
|
1157
|
+
...Object.keys(connections).length === 0 ? {} : { connections },
|
|
1158
|
+
...Object.keys(relations).length === 0 ? {} : { relations }
|
|
1159
|
+
};
|
|
1160
|
+
},
|
|
1161
|
+
/**
|
|
1162
|
+
* A paginated relation: one page of refs to `entity` with a window, or, with
|
|
1163
|
+
* a nested selection, one page of targets assembled through it.
|
|
1164
|
+
*/
|
|
1165
|
+
connection: ((entity, window, selection) => {
|
|
1166
|
+
if (selection !== void 0 && selection.entity !== entity.name) throw new Error(`Selection.connection: a page of "${entity.name}" cannot select "${selection.entity}"`);
|
|
1167
|
+
return {
|
|
1168
|
+
entity: entity.name,
|
|
1169
|
+
fields: selection?.fields ?? [],
|
|
1170
|
+
schema: selection === void 0 ? Entity.refPage(entity) : pageSchema(selection.schema),
|
|
1171
|
+
window,
|
|
1172
|
+
...selection?.connections === void 0 ? {} : { connections: selection.connections },
|
|
1173
|
+
...selection?.relations === void 0 ? {} : { relations: selection.relations }
|
|
1174
|
+
};
|
|
1175
|
+
})
|
|
1176
|
+
};
|
|
1177
|
+
/**
|
|
1178
|
+
* Reads an entity's selected fields out of the store, following each nested
|
|
1179
|
+
* relation into its targets. `undefined` means some field, at any depth, is
|
|
1180
|
+
* not present yet. A tombstoned target reads as `null` (or is dropped from a
|
|
1181
|
+
* list), so the Selection's codec decides whether that is a failure.
|
|
1182
|
+
*/
|
|
1183
|
+
const assemble = (store, key, requirement) => {
|
|
1184
|
+
const values = {};
|
|
1185
|
+
let refreshing = false;
|
|
1186
|
+
for (const field of requirement.fields) {
|
|
1187
|
+
const value = readField(store, key, field);
|
|
1188
|
+
if (Option.isNone(value)) return void 0;
|
|
1189
|
+
refreshing ||= isFieldStale(store, key, field);
|
|
1190
|
+
const relation = requirement.relations?.[field];
|
|
1191
|
+
if (relation === void 0) {
|
|
1192
|
+
values[field] = value.value;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const nested = assembleRelation(store, value.value, relation);
|
|
1196
|
+
if (nested === void 0) return void 0;
|
|
1197
|
+
values[field] = nested.values;
|
|
1198
|
+
refreshing ||= nested.refreshing;
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
values,
|
|
1202
|
+
refreshing
|
|
1203
|
+
};
|
|
1204
|
+
};
|
|
1205
|
+
const assembleTarget = (store, ref, relation) => {
|
|
1206
|
+
const key = entityKey(ref.entity, ref.id);
|
|
1207
|
+
return isTombstone(store, key) ? "absent" : assemble(store, key, relation);
|
|
1208
|
+
};
|
|
1209
|
+
/** Assembles the targets a relation value refers to, in the value's own shape. */
|
|
1210
|
+
const assembleRelation = (store, value, relation) => {
|
|
1211
|
+
if (value === null || value === void 0) return {
|
|
1212
|
+
values: null,
|
|
1213
|
+
refreshing: false
|
|
1214
|
+
};
|
|
1215
|
+
const targets = [];
|
|
1216
|
+
let refreshing = false;
|
|
1217
|
+
for (const ref of targetsOf(value, relation)) {
|
|
1218
|
+
const target = assembleTarget(store, ref, relation);
|
|
1219
|
+
if (target === void 0) return void 0;
|
|
1220
|
+
if (target === "absent") continue;
|
|
1221
|
+
targets.push(target.values);
|
|
1222
|
+
refreshing ||= target.refreshing;
|
|
1223
|
+
}
|
|
1224
|
+
if (typeof value === "string") return {
|
|
1225
|
+
values: targets.length === 0 ? null : targets[0],
|
|
1226
|
+
refreshing
|
|
1227
|
+
};
|
|
1228
|
+
if (isRefPage(value)) return {
|
|
1229
|
+
values: {
|
|
1230
|
+
items: targets,
|
|
1231
|
+
hasNext: value.hasNext,
|
|
1232
|
+
hasPrevious: value.hasPrevious
|
|
1233
|
+
},
|
|
1234
|
+
refreshing
|
|
1235
|
+
};
|
|
1236
|
+
return {
|
|
1237
|
+
values: targets,
|
|
1238
|
+
refreshing
|
|
1239
|
+
};
|
|
1240
|
+
};
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/entity.ts
|
|
1243
|
+
/**
|
|
1244
|
+
* Entities: the descriptor an application declares, refs, ref pages, and
|
|
1245
|
+
* patches, with the codecs that carry refs on the wire.
|
|
1246
|
+
*/
|
|
1247
|
+
/**
|
|
1248
|
+
* Relations are **references**, never inline target schemas: a ref cannot
|
|
1249
|
+
* reconstruct a full entity, and dereferencing is a store concern. Because the
|
|
1250
|
+
* target schema is not inlined, recursive relations cannot arise through the
|
|
1251
|
+
* schema graph.
|
|
1252
|
+
*/
|
|
1253
|
+
const refCodec = (entity) => Schema.Struct({
|
|
1254
|
+
entity: Schema.String,
|
|
1255
|
+
id: Schema.String
|
|
1256
|
+
}).pipe(Schema.encodeTo(Schema.String, {
|
|
1257
|
+
decode: SchemaGetter.transform(refParts),
|
|
1258
|
+
encode: SchemaGetter.transform((ref) => entityKey(ref.entity, ref.id))
|
|
1259
|
+
})).annotate({
|
|
1260
|
+
[RelationAnnotation]: "one",
|
|
1261
|
+
[RelationEntityAnnotation]: entity
|
|
1262
|
+
});
|
|
1263
|
+
const refPageCodec = (entity) => Schema.Struct({
|
|
1264
|
+
refs: Schema.Array(refCodec(entity)),
|
|
1265
|
+
hasNext: Schema.Boolean,
|
|
1266
|
+
hasPrevious: Schema.Boolean
|
|
1267
|
+
}).annotate({
|
|
1268
|
+
[RelationAnnotation]: "page",
|
|
1269
|
+
[RelationEntityAnnotation]: entity
|
|
1270
|
+
});
|
|
1271
|
+
const descriptors = /* @__PURE__ */ new WeakMap();
|
|
1272
|
+
const relationField = (relation) => {
|
|
1273
|
+
const ref = refCodec(relation.target().name);
|
|
1274
|
+
return relation.cardinality === "many" ? Schema.Array(ref) : relation.optional ? Schema.NullOr(ref) : ref;
|
|
1275
|
+
};
|
|
1276
|
+
const Entity = {
|
|
1277
|
+
/**
|
|
1278
|
+
* The Remote descriptor of a `foldkit-entity` Entity. Relations declared with
|
|
1279
|
+
* `Entity.relate` become ref fields, so the store, the planner, and the server
|
|
1280
|
+
* treat it as they treat an `Entity.make` descriptor.
|
|
1281
|
+
*/
|
|
1282
|
+
from: (entity) => {
|
|
1283
|
+
const cached = descriptors.get(entity);
|
|
1284
|
+
if (cached !== void 0) return cached;
|
|
1285
|
+
const members = entity.members;
|
|
1286
|
+
if (members.id?._tag !== "Field") throw new Error(`Entity.from: "${entity.name}" needs an "id" field for Remote to key it by`);
|
|
1287
|
+
const fields = {};
|
|
1288
|
+
for (const [key, member] of Object.entries(members)) fields[key] = member._tag === "Relation" ? relationField(member) : member.schema;
|
|
1289
|
+
const descriptor = Entity.make(entity.name, Schema.Struct(fields));
|
|
1290
|
+
descriptors.set(entity, descriptor);
|
|
1291
|
+
return descriptor;
|
|
1292
|
+
},
|
|
1293
|
+
make: (name, schema) => {
|
|
1294
|
+
const descriptor = {
|
|
1295
|
+
name,
|
|
1296
|
+
schema,
|
|
1297
|
+
fields: schema.fields,
|
|
1298
|
+
ref: (id) => ({
|
|
1299
|
+
entity: name,
|
|
1300
|
+
id: String(id)
|
|
1301
|
+
}),
|
|
1302
|
+
select: (selection) => Selection.make(descriptor, selection),
|
|
1303
|
+
patch: (id, values) => ({
|
|
1304
|
+
entity: name,
|
|
1305
|
+
id: String(id),
|
|
1306
|
+
values
|
|
1307
|
+
})
|
|
1308
|
+
};
|
|
1309
|
+
return descriptor;
|
|
1310
|
+
},
|
|
1311
|
+
/** A relation to a known entity, decoded as a reference. */
|
|
1312
|
+
ref: (entity) => refCodec(entity.name),
|
|
1313
|
+
/** A relation by name, for recursive or forward references. */
|
|
1314
|
+
refTo: (name) => refCodec(name),
|
|
1315
|
+
/**
|
|
1316
|
+
* The wire key a relation field carries (`"Entity:id"`). Adapters that read a
|
|
1317
|
+
* foreign key emit this so the ref codec can decode it on the client.
|
|
1318
|
+
*/
|
|
1319
|
+
refKey: (ref) => entityKey(ref.entity, ref.id),
|
|
1320
|
+
/** Splits a ref key back into its entity and id. */
|
|
1321
|
+
refParts,
|
|
1322
|
+
/** A relation value of one authoritative page of refs. */
|
|
1323
|
+
refPage: (entity) => refPageCodec(entity.name),
|
|
1324
|
+
/**
|
|
1325
|
+
* A normalized patch for an entity, typed against its fields' wire shape:
|
|
1326
|
+
* write a relation as its ref key (`Entity.refKey`), as the server does.
|
|
1327
|
+
*/
|
|
1328
|
+
patch: (ref, patch) => ({
|
|
1329
|
+
entity: ref.entity,
|
|
1330
|
+
id: ref.id,
|
|
1331
|
+
values: patch
|
|
1332
|
+
})
|
|
1333
|
+
};
|
|
1334
|
+
//#endregion
|
|
996
1335
|
//#region src/inspect.ts
|
|
997
1336
|
const inspectEntry = (key, entry) => ({
|
|
998
1337
|
key,
|
|
@@ -1138,8 +1477,8 @@ const release = (optimistic, requestId) => removeOverlay(removeLayer(optimistic,
|
|
|
1138
1477
|
* a later layer re-wins on rebase. Confirmed connection changes are recorded
|
|
1139
1478
|
* once per request, as overlays the request no longer owns.
|
|
1140
1479
|
*/
|
|
1141
|
-
const settleSuccess = (base, optimistic, state, requestId, entities, connections = []) => {
|
|
1142
|
-
const reconciled = reconcileMutation(base, state, requestId, entities);
|
|
1480
|
+
const settleSuccess = (base, optimistic, state, requestId, entities, connections = [], deleted = []) => {
|
|
1481
|
+
const reconciled = reconcileMutation(base, state, requestId, entities, deleted);
|
|
1143
1482
|
return {
|
|
1144
1483
|
store: reconciled.store,
|
|
1145
1484
|
state: reconciled.state,
|
|
@@ -1294,101 +1633,33 @@ const applyConnectionEvent = (state, optimistic, event, policy = {}) => {
|
|
|
1294
1633
|
position: "remove"
|
|
1295
1634
|
}),
|
|
1296
1635
|
outcome
|
|
1297
|
-
};
|
|
1298
|
-
case "ConnectionInvalidate": return {
|
|
1299
|
-
state: invalidateConnection(advance(state, event.cursor), event.connection),
|
|
1300
|
-
optimistic,
|
|
1301
|
-
outcome
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
};
|
|
1305
|
-
const isStale = (state, connection) => state.stale.has(connection);
|
|
1306
|
-
/**
|
|
1307
|
-
* Clears a connection's stale mark once a fresh page has been adopted. An
|
|
1308
|
-
* invalidating event only records that a refetch is due; whoever merges the
|
|
1309
|
-
* refetched page calls this so `isStale` stops reporting.
|
|
1310
|
-
*/
|
|
1311
|
-
const refreshConnection = (state, connection) => {
|
|
1312
|
-
if (!state.stale.has(connection)) return state;
|
|
1313
|
-
const stale = new Set(state.stale);
|
|
1314
|
-
stale.delete(connection);
|
|
1315
|
-
return {
|
|
1316
|
-
...state,
|
|
1317
|
-
stale
|
|
1318
|
-
};
|
|
1319
|
-
};
|
|
1320
|
-
/** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
|
|
1321
|
-
const liveHasPrevious = (connection, state, connectionId) => hasPrevious(connection) || (state.boundary[connectionId]?.before ?? 0) > 0;
|
|
1322
|
-
/** `hasNext` accounting for edges recorded outside the loaded boundary. */
|
|
1323
|
-
const liveHasNext = (connection, state, connectionId) => hasNext(connection) || (state.boundary[connectionId]?.after ?? 0) > 0;
|
|
1324
|
-
//#endregion
|
|
1325
|
-
//#region src/relation.ts
|
|
1326
|
-
/** Marks the codec `Entity.ref`/`Entity.refPage` produce, so a Selection can tell the field's shape. */
|
|
1327
|
-
const RelationAnnotation = "foldkitRemoteRelation";
|
|
1328
|
-
/** The target entity's name on the same codec, so a nested selection of another entity is refused. */
|
|
1329
|
-
const RelationEntityAnnotation = "foldkitRemoteRelationEntity";
|
|
1330
|
-
/** Splits a ref key (`"Entity:id"`, the store key too) back into its parts. */
|
|
1331
|
-
const refParts = (encoded) => {
|
|
1332
|
-
const separator = encoded.indexOf(":");
|
|
1333
|
-
return separator === -1 ? {
|
|
1334
|
-
entity: encoded,
|
|
1335
|
-
id: ""
|
|
1336
|
-
} : {
|
|
1337
|
-
entity: encoded.slice(0, separator),
|
|
1338
|
-
id: encoded.slice(separator + 1)
|
|
1339
|
-
};
|
|
1340
|
-
};
|
|
1341
|
-
/**
|
|
1342
|
-
* The refs a relation value carries, in order. Anything that is not a ref key,
|
|
1343
|
-
* an array of them, or a page of them contributes nothing.
|
|
1344
|
-
*/
|
|
1345
|
-
const refsIn = (value) => {
|
|
1346
|
-
if (typeof value === "string") return [refParts(value)];
|
|
1347
|
-
if (Array.isArray(value)) return value.filter((item) => typeof item === "string").map(refParts);
|
|
1348
|
-
if (value !== null && typeof value === "object") {
|
|
1349
|
-
const refs = value.refs;
|
|
1350
|
-
if (Array.isArray(refs)) return refs.filter((item) => typeof item === "string").map(refParts);
|
|
1351
|
-
}
|
|
1352
|
-
return [];
|
|
1353
|
-
};
|
|
1354
|
-
/** The refs in a relation value that name the relation's target entity, and no other. */
|
|
1355
|
-
const targetsOf = (value, relation) => refsIn(value).filter((ref) => ref.entity === relation.entity);
|
|
1356
|
-
const isRefPage = (value) => {
|
|
1357
|
-
if (value === null || typeof value !== "object") return false;
|
|
1358
|
-
const page = value;
|
|
1359
|
-
return Array.isArray(page.refs) && page.refs.every((ref) => typeof ref === "string") && typeof page.hasNext === "boolean" && typeof page.hasPrevious === "boolean";
|
|
1360
|
-
};
|
|
1361
|
-
/**
|
|
1362
|
-
* The relation shape an entity field schema declares: a ref (`one`), a nullable
|
|
1363
|
-
* ref, an array of refs (`many`), or a page of refs (`page`). `undefined` for a
|
|
1364
|
-
* scalar field, which cannot take a nested selection.
|
|
1365
|
-
*/
|
|
1366
|
-
const relationShape = (schema) => shapeOf(schema.ast, false);
|
|
1367
|
-
const shapeOf = (ast, nullable) => {
|
|
1368
|
-
const annotated = ast.annotations?.[RelationAnnotation];
|
|
1369
|
-
if (annotated === "one" || annotated === "page") return {
|
|
1370
|
-
kind: annotated,
|
|
1371
|
-
nullable,
|
|
1372
|
-
entity: String(ast.annotations?.[RelationEntityAnnotation])
|
|
1373
|
-
};
|
|
1374
|
-
if (ast._tag === "Arrays") {
|
|
1375
|
-
const item = ast.rest?.[0];
|
|
1376
|
-
const inner = item === void 0 ? void 0 : shapeOf(item, false);
|
|
1377
|
-
return inner?.kind === "one" ? {
|
|
1378
|
-
kind: "many",
|
|
1379
|
-
nullable,
|
|
1380
|
-
entity: inner.entity
|
|
1381
|
-
} : void 0;
|
|
1382
|
-
}
|
|
1383
|
-
if (ast._tag === "Union") {
|
|
1384
|
-
const members = ast.types ?? [];
|
|
1385
|
-
const optional = members.some((member) => member._tag === "Null" || member._tag === "Undefined");
|
|
1386
|
-
for (const member of members) {
|
|
1387
|
-
const shape = shapeOf(member, optional);
|
|
1388
|
-
if (shape !== void 0) return shape;
|
|
1389
|
-
}
|
|
1636
|
+
};
|
|
1637
|
+
case "ConnectionInvalidate": return {
|
|
1638
|
+
state: invalidateConnection(advance(state, event.cursor), event.connection),
|
|
1639
|
+
optimistic,
|
|
1640
|
+
outcome
|
|
1641
|
+
};
|
|
1390
1642
|
}
|
|
1391
1643
|
};
|
|
1644
|
+
const isStale = (state, connection) => state.stale.has(connection);
|
|
1645
|
+
/**
|
|
1646
|
+
* Clears a connection's stale mark once a fresh page has been adopted. An
|
|
1647
|
+
* invalidating event only records that a refetch is due; whoever merges the
|
|
1648
|
+
* refetched page calls this so `isStale` stops reporting.
|
|
1649
|
+
*/
|
|
1650
|
+
const refreshConnection = (state, connection) => {
|
|
1651
|
+
if (!state.stale.has(connection)) return state;
|
|
1652
|
+
const stale = new Set(state.stale);
|
|
1653
|
+
stale.delete(connection);
|
|
1654
|
+
return {
|
|
1655
|
+
...state,
|
|
1656
|
+
stale
|
|
1657
|
+
};
|
|
1658
|
+
};
|
|
1659
|
+
/** `hasPrevious` accounting for edges recorded outside the loaded boundary. */
|
|
1660
|
+
const liveHasPrevious = (connection, state, connectionId) => hasPrevious(connection) || (state.boundary[connectionId]?.before ?? 0) > 0;
|
|
1661
|
+
/** `hasNext` accounting for edges recorded outside the loaded boundary. */
|
|
1662
|
+
const liveHasNext = (connection, state, connectionId) => hasNext(connection) || (state.boundary[connectionId]?.after ?? 0) > 0;
|
|
1392
1663
|
//#endregion
|
|
1393
1664
|
//#region src/plan.ts
|
|
1394
1665
|
/**
|
|
@@ -1801,7 +2072,11 @@ const remoteMessageCases = {
|
|
|
1801
2072
|
MutationSucceeded: {
|
|
1802
2073
|
requestId: Schema.String,
|
|
1803
2074
|
entities: Schema.Array(NormalizedEntity),
|
|
1804
|
-
connections: Schema.optional(Schema.Array(Schema.Unknown))
|
|
2075
|
+
connections: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
2076
|
+
deleted: Schema.optional(Schema.Array(Schema.Struct({
|
|
2077
|
+
entity: Schema.String,
|
|
2078
|
+
id: Schema.String
|
|
2079
|
+
})))
|
|
1805
2080
|
},
|
|
1806
2081
|
MutationFailed: {
|
|
1807
2082
|
requestId: Schema.String,
|
|
@@ -1936,7 +2211,7 @@ const updateRemote = (model, message) => {
|
|
|
1936
2211
|
optimistic: beginOptimistic(model.optimistic, message.requestId, message.optimistic ?? [])
|
|
1937
2212
|
};
|
|
1938
2213
|
case "MutationSucceeded": {
|
|
1939
|
-
const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message.entities, message.connections ?? []);
|
|
2214
|
+
const settled = settleSuccess(model.entities, model.optimistic, model.mutations, message.requestId, message.entities, message.connections ?? [], message.deleted ?? []);
|
|
1940
2215
|
return {
|
|
1941
2216
|
...model,
|
|
1942
2217
|
entities: settled.store,
|
|
@@ -1946,7 +2221,7 @@ const updateRemote = (model, message) => {
|
|
|
1946
2221
|
}
|
|
1947
2222
|
case "MutationFailed": return {
|
|
1948
2223
|
...model,
|
|
1949
|
-
mutations: failMutation(model.mutations, message.requestId),
|
|
2224
|
+
mutations: failMutation(model.mutations, message.requestId, message.error),
|
|
1950
2225
|
optimistic: settleFailure(model.optimistic, message.requestId)
|
|
1951
2226
|
};
|
|
1952
2227
|
case "LiveReceived": {
|
|
@@ -2082,7 +2357,12 @@ const writeRead = (store, requests, result, now = 0) => {
|
|
|
2082
2357
|
windows: entry?.windows
|
|
2083
2358
|
});
|
|
2084
2359
|
}
|
|
2085
|
-
|
|
2360
|
+
let answered = store;
|
|
2361
|
+
for (const request of requests) {
|
|
2362
|
+
const key = entityKey(request.entity, request.id);
|
|
2363
|
+
if (!returned.has(key)) answered = tombstone(answered, key);
|
|
2364
|
+
}
|
|
2365
|
+
return writeEntities(answered, writes, now);
|
|
2086
2366
|
};
|
|
2087
2367
|
//#endregion
|
|
2088
2368
|
//#region src/policy.ts
|
|
@@ -2115,224 +2395,6 @@ const RemotePolicy = {
|
|
|
2115
2395
|
refreshes: (policy) => policy._tag !== "CacheFirst"
|
|
2116
2396
|
};
|
|
2117
2397
|
//#endregion
|
|
2118
|
-
//#region src/entity.ts
|
|
2119
|
-
/**
|
|
2120
|
-
* Entities: the descriptor an application declares, refs, ref pages, and
|
|
2121
|
-
* patches, with the codecs that carry refs on the wire.
|
|
2122
|
-
*/
|
|
2123
|
-
/**
|
|
2124
|
-
* Relations are **references**, never inline target schemas: a ref cannot
|
|
2125
|
-
* reconstruct a full entity, and dereferencing is a store concern. Because the
|
|
2126
|
-
* target schema is not inlined, recursive relations cannot arise through the
|
|
2127
|
-
* schema graph.
|
|
2128
|
-
*/
|
|
2129
|
-
const refCodec = (entity) => Schema.Struct({
|
|
2130
|
-
entity: Schema.String,
|
|
2131
|
-
id: Schema.String
|
|
2132
|
-
}).pipe(Schema.encodeTo(Schema.String, {
|
|
2133
|
-
decode: SchemaGetter.transform(refParts),
|
|
2134
|
-
encode: SchemaGetter.transform((ref) => entityKey(ref.entity, ref.id))
|
|
2135
|
-
})).annotate({
|
|
2136
|
-
[RelationAnnotation]: "one",
|
|
2137
|
-
[RelationEntityAnnotation]: entity
|
|
2138
|
-
});
|
|
2139
|
-
const refPageCodec = (entity) => Schema.Struct({
|
|
2140
|
-
refs: Schema.Array(refCodec(entity)),
|
|
2141
|
-
hasNext: Schema.Boolean,
|
|
2142
|
-
hasPrevious: Schema.Boolean
|
|
2143
|
-
}).annotate({
|
|
2144
|
-
[RelationAnnotation]: "page",
|
|
2145
|
-
[RelationEntityAnnotation]: entity
|
|
2146
|
-
});
|
|
2147
|
-
const Entity = {
|
|
2148
|
-
make: (name, schema) => {
|
|
2149
|
-
const descriptor = {
|
|
2150
|
-
name,
|
|
2151
|
-
schema,
|
|
2152
|
-
fields: schema.fields,
|
|
2153
|
-
ref: (id) => ({
|
|
2154
|
-
entity: name,
|
|
2155
|
-
id: String(id)
|
|
2156
|
-
}),
|
|
2157
|
-
select: (selection) => Selection.make(descriptor, selection),
|
|
2158
|
-
patch: (id, values) => ({
|
|
2159
|
-
entity: name,
|
|
2160
|
-
id: String(id),
|
|
2161
|
-
values
|
|
2162
|
-
})
|
|
2163
|
-
};
|
|
2164
|
-
return descriptor;
|
|
2165
|
-
},
|
|
2166
|
-
/** A relation to a known entity, decoded as a reference. */
|
|
2167
|
-
ref: (entity) => refCodec(entity.name),
|
|
2168
|
-
/** A relation by name, for recursive or forward references. */
|
|
2169
|
-
refTo: (name) => refCodec(name),
|
|
2170
|
-
/**
|
|
2171
|
-
* The wire key a relation field carries (`"Entity:id"`). Adapters that read a
|
|
2172
|
-
* foreign key emit this so the ref codec can decode it on the client.
|
|
2173
|
-
*/
|
|
2174
|
-
refKey: (ref) => entityKey(ref.entity, ref.id),
|
|
2175
|
-
/** Splits a ref key back into its entity and id. */
|
|
2176
|
-
refParts,
|
|
2177
|
-
/** A relation value of one authoritative page of refs. */
|
|
2178
|
-
refPage: (entity) => refPageCodec(entity.name),
|
|
2179
|
-
/**
|
|
2180
|
-
* A normalized patch for an entity, typed against its fields' wire shape:
|
|
2181
|
-
* write a relation as its ref key (`Entity.refKey`), as the server does.
|
|
2182
|
-
*/
|
|
2183
|
-
patch: (ref, patch) => ({
|
|
2184
|
-
entity: ref.entity,
|
|
2185
|
-
id: ref.id,
|
|
2186
|
-
values: patch
|
|
2187
|
-
})
|
|
2188
|
-
};
|
|
2189
|
-
//#endregion
|
|
2190
|
-
//#region src/selection.ts
|
|
2191
|
-
/**
|
|
2192
|
-
* Selections: the fields a Surface picks of an entity (nested through
|
|
2193
|
-
* relations, paged through connections), the requirement graph they imply,
|
|
2194
|
-
* and their assembly from the normalized store.
|
|
2195
|
-
*/
|
|
2196
|
-
const pageSchemas = /* @__PURE__ */ new WeakMap();
|
|
2197
|
-
const pageSchema = (item) => {
|
|
2198
|
-
const cached = pageSchemas.get(item);
|
|
2199
|
-
if (cached !== void 0) return cached;
|
|
2200
|
-
const built = Schema.Struct({
|
|
2201
|
-
items: Schema.Array(item),
|
|
2202
|
-
hasNext: Schema.Boolean,
|
|
2203
|
-
hasPrevious: Schema.Boolean
|
|
2204
|
-
});
|
|
2205
|
-
pageSchemas.set(item, built);
|
|
2206
|
-
return built;
|
|
2207
|
-
};
|
|
2208
|
-
/** The requirement a nested selection contributes for its relation's target. */
|
|
2209
|
-
const relationOf = (selection) => ({
|
|
2210
|
-
entity: selection.entity,
|
|
2211
|
-
fields: selection.fields,
|
|
2212
|
-
...selection.connections === void 0 ? {} : { windows: selection.connections },
|
|
2213
|
-
...selection.relations === void 0 ? {} : { relations: selection.relations }
|
|
2214
|
-
});
|
|
2215
|
-
const Selection = {
|
|
2216
|
-
/**
|
|
2217
|
-
* The fields to read of `entity`. A nested `Selection` on a relation field
|
|
2218
|
-
* reads through the ref (or refs, or page of refs) the field holds into the
|
|
2219
|
-
* target's fields; a scalar field cannot take one.
|
|
2220
|
-
*/
|
|
2221
|
-
make: (entity, selection) => {
|
|
2222
|
-
if (Object.keys(selection).length === 0) throw new Error(`Selection.make: a selection of "${entity.name}" picks at least one field`);
|
|
2223
|
-
const picked = {};
|
|
2224
|
-
const connections = {};
|
|
2225
|
-
const relations = {};
|
|
2226
|
-
for (const key of Object.keys(selection)) {
|
|
2227
|
-
const choice = selection[key];
|
|
2228
|
-
if (choice === true) {
|
|
2229
|
-
picked[key] = entity.fields[key];
|
|
2230
|
-
continue;
|
|
2231
|
-
}
|
|
2232
|
-
const nested = choice;
|
|
2233
|
-
const shape = relationShape(entity.fields[key]);
|
|
2234
|
-
if (shape === void 0) throw new Error(`Selection.make: "${key}" on "${entity.name}" is not a relation field, so it cannot take a nested selection`);
|
|
2235
|
-
if (nested.entity !== shape.entity) throw new Error(`Selection.make: "${key}" on "${entity.name}" refers to "${shape.entity}", not "${nested.entity}"`);
|
|
2236
|
-
const nullable = (codec) => shape.nullable ? Schema.NullOr(codec) : codec;
|
|
2237
|
-
if (nested.window !== void 0) {
|
|
2238
|
-
connections[key] = nested.window;
|
|
2239
|
-
picked[key] = nullable(nested.schema);
|
|
2240
|
-
if (nested.fields.length > 0) relations[key] = relationOf(nested);
|
|
2241
|
-
continue;
|
|
2242
|
-
}
|
|
2243
|
-
const item = nested.schema;
|
|
2244
|
-
picked[key] = nullable(shape.kind === "many" ? Schema.Array(item) : shape.kind === "page" ? pageSchema(item) : item);
|
|
2245
|
-
relations[key] = relationOf(nested);
|
|
2246
|
-
}
|
|
2247
|
-
return {
|
|
2248
|
-
entity: entity.name,
|
|
2249
|
-
fields: Object.keys(selection),
|
|
2250
|
-
schema: Schema.Struct(picked),
|
|
2251
|
-
...Object.keys(connections).length === 0 ? {} : { connections },
|
|
2252
|
-
...Object.keys(relations).length === 0 ? {} : { relations }
|
|
2253
|
-
};
|
|
2254
|
-
},
|
|
2255
|
-
/**
|
|
2256
|
-
* A paginated relation: one page of refs to `entity` with a window, or, with
|
|
2257
|
-
* a nested selection, one page of targets assembled through it.
|
|
2258
|
-
*/
|
|
2259
|
-
connection: ((entity, window, selection) => {
|
|
2260
|
-
if (selection !== void 0 && selection.entity !== entity.name) throw new Error(`Selection.connection: a page of "${entity.name}" cannot select "${selection.entity}"`);
|
|
2261
|
-
return {
|
|
2262
|
-
entity: entity.name,
|
|
2263
|
-
fields: selection?.fields ?? [],
|
|
2264
|
-
schema: selection === void 0 ? Entity.refPage(entity) : pageSchema(selection.schema),
|
|
2265
|
-
window,
|
|
2266
|
-
...selection?.connections === void 0 ? {} : { connections: selection.connections },
|
|
2267
|
-
...selection?.relations === void 0 ? {} : { relations: selection.relations }
|
|
2268
|
-
};
|
|
2269
|
-
})
|
|
2270
|
-
};
|
|
2271
|
-
/**
|
|
2272
|
-
* Reads an entity's selected fields out of the store, following each nested
|
|
2273
|
-
* relation into its targets. `undefined` means some field, at any depth, is
|
|
2274
|
-
* not present yet. A tombstoned target reads as `null` (or is dropped from a
|
|
2275
|
-
* list), so the Selection's codec decides whether that is a failure.
|
|
2276
|
-
*/
|
|
2277
|
-
const assemble = (store, key, requirement) => {
|
|
2278
|
-
const values = {};
|
|
2279
|
-
let refreshing = false;
|
|
2280
|
-
for (const field of requirement.fields) {
|
|
2281
|
-
const value = readField(store, key, field);
|
|
2282
|
-
if (Option.isNone(value)) return void 0;
|
|
2283
|
-
refreshing ||= isFieldStale(store, key, field);
|
|
2284
|
-
const relation = requirement.relations?.[field];
|
|
2285
|
-
if (relation === void 0) {
|
|
2286
|
-
values[field] = value.value;
|
|
2287
|
-
continue;
|
|
2288
|
-
}
|
|
2289
|
-
const nested = assembleRelation(store, value.value, relation);
|
|
2290
|
-
if (nested === void 0) return void 0;
|
|
2291
|
-
values[field] = nested.values;
|
|
2292
|
-
refreshing ||= nested.refreshing;
|
|
2293
|
-
}
|
|
2294
|
-
return {
|
|
2295
|
-
values,
|
|
2296
|
-
refreshing
|
|
2297
|
-
};
|
|
2298
|
-
};
|
|
2299
|
-
const assembleTarget = (store, ref, relation) => {
|
|
2300
|
-
const key = entityKey(ref.entity, ref.id);
|
|
2301
|
-
return isTombstone(store, key) ? "absent" : assemble(store, key, relation);
|
|
2302
|
-
};
|
|
2303
|
-
/** Assembles the targets a relation value refers to, in the value's own shape. */
|
|
2304
|
-
const assembleRelation = (store, value, relation) => {
|
|
2305
|
-
if (value === null || value === void 0) return {
|
|
2306
|
-
values: null,
|
|
2307
|
-
refreshing: false
|
|
2308
|
-
};
|
|
2309
|
-
const targets = [];
|
|
2310
|
-
let refreshing = false;
|
|
2311
|
-
for (const ref of targetsOf(value, relation)) {
|
|
2312
|
-
const target = assembleTarget(store, ref, relation);
|
|
2313
|
-
if (target === void 0) return void 0;
|
|
2314
|
-
if (target === "absent") continue;
|
|
2315
|
-
targets.push(target.values);
|
|
2316
|
-
refreshing ||= target.refreshing;
|
|
2317
|
-
}
|
|
2318
|
-
if (typeof value === "string") return {
|
|
2319
|
-
values: targets.length === 0 ? null : targets[0],
|
|
2320
|
-
refreshing
|
|
2321
|
-
};
|
|
2322
|
-
if (isRefPage(value)) return {
|
|
2323
|
-
values: {
|
|
2324
|
-
items: targets,
|
|
2325
|
-
hasNext: value.hasNext,
|
|
2326
|
-
hasPrevious: value.hasPrevious
|
|
2327
|
-
},
|
|
2328
|
-
refreshing
|
|
2329
|
-
};
|
|
2330
|
-
return {
|
|
2331
|
-
values: targets,
|
|
2332
|
-
refreshing
|
|
2333
|
-
};
|
|
2334
|
-
};
|
|
2335
|
-
//#endregion
|
|
2336
2398
|
//#region src/index.ts
|
|
2337
2399
|
/**
|
|
2338
2400
|
* `foldkit-remote` — normalized application-facing server state.
|
|
@@ -2341,6 +2403,8 @@ const assembleRelation = (store, value, relation) => {
|
|
|
2341
2403
|
* live classification, optimistic layers) performs no I/O. The `Remote.*`
|
|
2342
2404
|
* helpers that read or mutate go through the `RemoteClient` Effect service.
|
|
2343
2405
|
*/
|
|
2406
|
+
const descriptorOf = (entity) => Entity$1.is(entity) ? Entity.from(entity) : entity;
|
|
2407
|
+
const selectionOf = (selection) => SelectionTypeId in selection ? Selection.from(selection) : selection;
|
|
2344
2408
|
/**
|
|
2345
2409
|
* The store a read or plan sees: the base with every pending optimistic layer
|
|
2346
2410
|
* applied, so a request's patches show until it settles and a temporary id is
|
|
@@ -2386,21 +2450,24 @@ const identityMessage = (message) => message;
|
|
|
2386
2450
|
/** A stable key for a live subscription's requirement set. */
|
|
2387
2451
|
const liveStreamKey = (requirements) => requirements.map((requirement) => `${entityKey(requirement.entity, requirement.id)}:${[...requirement.fields].sort().join(",")}`).sort().join("|");
|
|
2388
2452
|
/** Declares a Remote domain: its entities, queries, and mutations, plus the submodel. */
|
|
2389
|
-
const defineRemote = (config) =>
|
|
2390
|
-
entities
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2453
|
+
const defineRemote = (config) => {
|
|
2454
|
+
const entities = config.entities.map(descriptorOf);
|
|
2455
|
+
return {
|
|
2456
|
+
entities,
|
|
2457
|
+
queries: config.queries ?? [],
|
|
2458
|
+
mutations: config.mutations ?? [],
|
|
2459
|
+
Model: remoteModelSchema(),
|
|
2460
|
+
initial: initialRemoteModel,
|
|
2461
|
+
Message: remoteMessageSchema,
|
|
2462
|
+
update: updateRemote,
|
|
2463
|
+
rpc: RemoteRpc,
|
|
2464
|
+
registry: {
|
|
2465
|
+
entities: new Map(entities.map((entity) => [entity.name, entity])),
|
|
2466
|
+
queries: new Map((config.queries ?? []).map((query) => [query.name, query])),
|
|
2467
|
+
mutations: new Map((config.mutations ?? []).map((mutation) => [mutation.name, mutation]))
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
};
|
|
2404
2471
|
/** Binds a Remote domain to its store's location in the application Model. */
|
|
2405
2472
|
const bindRemote = (definition, store) => ({
|
|
2406
2473
|
definition,
|
|
@@ -2678,7 +2745,8 @@ const Remote = {
|
|
|
2678
2745
|
* as `Failed` instead of being asserted into `Value`. A present value with a
|
|
2679
2746
|
* stale field reads as `Refreshing`: an observer is refetching it.
|
|
2680
2747
|
*/
|
|
2681
|
-
select: (bound,
|
|
2748
|
+
select: (bound, given) => {
|
|
2749
|
+
const selection = selectionOf(given);
|
|
2682
2750
|
assertRegistered(bound, "Entity", bound.definition.registry.entities, selection.entity);
|
|
2683
2751
|
const relation = relationOf(selection);
|
|
2684
2752
|
return (id) => ({
|
|
@@ -2896,7 +2964,8 @@ const Remote = {
|
|
|
2896
2964
|
_tag: "MutationSucceeded",
|
|
2897
2965
|
requestId,
|
|
2898
2966
|
entities: outcome.entities,
|
|
2899
|
-
connections: outcome.connections
|
|
2967
|
+
connections: outcome.connections,
|
|
2968
|
+
deleted: outcome.deleted
|
|
2900
2969
|
});
|
|
2901
2970
|
return {
|
|
2902
2971
|
output: outcome.output,
|
|
@@ -2920,11 +2989,12 @@ const Remote = {
|
|
|
2920
2989
|
*/
|
|
2921
2990
|
live: (bound, surface, params, toMessage = identityMessage, options = {}) => liveEntry(bound, () => requirementsOf(surface.projection(params)), toMessage, options)
|
|
2922
2991
|
};
|
|
2992
|
+
const brandEntries = (entries) => Subscription.make()(() => entries);
|
|
2923
2993
|
/** The bound domain: the descriptor, the binding, and the operations over them. */
|
|
2924
2994
|
const bindDomain = (definition, store) => {
|
|
2925
2995
|
const bound = bindRemote(definition, store);
|
|
2926
2996
|
const reduce = (model, message) => store.set(model, updateRemote(store.get(model), message));
|
|
2927
|
-
|
|
2997
|
+
const domain = {
|
|
2928
2998
|
...definition,
|
|
2929
2999
|
...bound,
|
|
2930
3000
|
get: (selection, id) => Remote.select(bound, selection)(id),
|
|
@@ -2989,7 +3059,8 @@ const bindDomain = (definition, store) => {
|
|
|
2989
3059
|
}),
|
|
2990
3060
|
query: (query, input, options) => {
|
|
2991
3061
|
assertRegistered(bound, "Query", definition.registry.queries, query.name);
|
|
2992
|
-
const { select, ...window } = options;
|
|
3062
|
+
const { select: given, ...window } = options;
|
|
3063
|
+
const select = selectionOf(given);
|
|
2993
3064
|
for (const [side, size] of [["first", window.first], ["last", window.last]]) if (size !== void 0 && !(Number.isInteger(size) && size >= 0)) throw new Error(`Remote: query "${query.name}" asks for ${side}: ${String(size)}; a page size is a non-negative integer`);
|
|
2994
3065
|
const listed = query.Result.entity;
|
|
2995
3066
|
if (listed === void 0) throw new Error(`Remote: query "${query.name}" is not a connection over an entity, so it has no page to select`);
|
|
@@ -3118,15 +3189,25 @@ const bindDomain = (definition, store) => {
|
|
|
3118
3189
|
_tag: "MutationSucceeded",
|
|
3119
3190
|
requestId,
|
|
3120
3191
|
entities: outcome.entities,
|
|
3121
|
-
connections: outcome.connections
|
|
3192
|
+
connections: outcome.connections,
|
|
3193
|
+
deleted: outcome.deleted
|
|
3122
3194
|
})
|
|
3123
3195
|
}))
|
|
3124
3196
|
}
|
|
3125
3197
|
};
|
|
3126
3198
|
},
|
|
3199
|
+
mutation: (model, requestId) => mutationStatus(store.get(model).mutations, requestId),
|
|
3127
3200
|
reduce,
|
|
3128
|
-
inspect: (model) => inspectRemote(store.get(model))
|
|
3201
|
+
inspect: (model) => inspectRemote(store.get(model)),
|
|
3202
|
+
wiring: (active, options) => ({
|
|
3203
|
+
key: `remote:${bound.contract.name}`,
|
|
3204
|
+
handles: Object.keys(remoteMessageCases),
|
|
3205
|
+
route: (model, message) => isRemoteMessage(message) ? Option.some({ model: reduce(model, message) }) : Option.none(),
|
|
3206
|
+
subscriptions: brandEntries(domain.subscriptions(active, options)),
|
|
3207
|
+
contract: bound.contract
|
|
3208
|
+
})
|
|
3129
3209
|
};
|
|
3210
|
+
return domain;
|
|
3130
3211
|
};
|
|
3131
3212
|
/** The window keys of a `QueryOptions`, and only those, without the undefined ones. */
|
|
3132
3213
|
const pickWindow = (window) => ({
|
|
@@ -3141,6 +3222,6 @@ const pageSize = (window, key) => {
|
|
|
3141
3222
|
return size === void 0 ? {} : { [key]: size };
|
|
3142
3223
|
};
|
|
3143
3224
|
//#endregion
|
|
3144
|
-
export { ConnectionChange, ConnectionChangeSchema, Entity, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, Requirement, Selection, WindowSchema, WireBoundary, addLayer, addOverlay, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyStore, entityKey, entry, failMutation, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, invalidateConnection, isFieldStale, isGapped, isLoading, isRefPage, isRemoteMessage, isStale, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, merge, missingFields, mutateRemote, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshConnection, refsIn, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, settleFailure, settleSuccess, shouldWake, stableStringify, targetsOf, terminal, tombstone, unknown, updateRemote, visibleItems, visibleStore, windowKey, writeEntities, writeEntity, writeRead };
|
|
3225
|
+
export { ConnectionChange, ConnectionChangeSchema, Entity, Live, LiveChange, LiveEdge, LiveRequirement, MAX_FIELDS_PER_REQUEST, MAX_RELATION_DEPTH, Mutate, Mutation, MutationRequest, MutationResult, NormalizedEntity, PageSize, Query, QueryEdge, QueryRequest, QueryResult, QueryRpc, REMOTE_CACHE_VERSION, REMOTE_PROTOCOL_VERSION, Read, ReadBatch, ReadBatchResult, ReadRequest, RelationAnnotation, RelationEntityAnnotation, RelationRequest, Remote, RemoteClient, RemoteConnections, RemoteData, RemoteLiveError, RemoteMutationError, RemotePersistence, RemotePolicy, RemoteProtocolError, RemoteQueryError, RemoteReadError, RemoteRequirements, RemoteRpc, Requirement, Selection, WindowSchema, WireBoundary, addLayer, addOverlay, applyConnectionEvent, applyEntityEvent, assemble, beginMutation, beginOptimistic, classifyLive, clearStale, coalesceQueries, coalesceReads, coalescedLayer, connectionChangeOf, connectionIdentity, connectionsOf, cursor, edge, emptyConnection, emptyLiveState, emptyMutationState, emptyOptimistic, emptyStore, entityKey, entry, failMutation, gc, hasField, hasNext, hasPrevious, initialRemoteModel, inspectEntity, inspectRemote, invalidateConnection, isFieldStale, isGapped, isLoading, isRefPage, isRemoteMessage, isStale, isTombstone, items, liveEventOf, liveHasNext, liveHasPrevious, markStale, merge, missingFields, mutateRemote, mutationStatus, pageSchema, plan, pruneOverlays, reachable, readField, reconcileMutation, refParts, refreshConnection, refsIn, relationOf, relationShape, remoteDataSchema, remoteError, remoteErrorSchema, remoteMessageCases, remoteMessageSchema, remoteModelSchema, remove, removeLayer, removeOverlay, requirementKey, requirementsOf, retentionRootsSchema, schemaOf, segment, setStale, settleFailure, settleSuccess, shouldWake, stableStringify, targetsOf, terminal, tombstone, unknown, updateRemote, visibleItems, visibleStore, windowKey, writeEntities, writeEntity, writeRead };
|
|
3145
3226
|
|
|
3146
3227
|
//# sourceMappingURL=index.mjs.map
|