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/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: remember(state.failed, requestId)
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
- return writeEntities(store, writes, now);
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: config.entities,
2391
- queries: config.queries ?? [],
2392
- mutations: config.mutations ?? [],
2393
- Model: remoteModelSchema(),
2394
- initial: initialRemoteModel,
2395
- Message: remoteMessageSchema,
2396
- update: updateRemote,
2397
- rpc: RemoteRpc,
2398
- registry: {
2399
- entities: new Map(config.entities.map((entity) => [entity.name, entity])),
2400
- queries: new Map((config.queries ?? []).map((query) => [query.name, query])),
2401
- mutations: new Map((config.mutations ?? []).map((mutation) => [mutation.name, mutation]))
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, selection) => {
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
- return {
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