@nkzw/fate 0.1.2 → 1.0.0-rc.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.
Files changed (170) hide show
  1. package/README.md +810 -129
  2. package/docs/api/classes/FateClient.md +832 -0
  3. package/docs/api/functions/clientRoot.md +27 -0
  4. package/docs/api/functions/createClient.md +21 -0
  5. package/docs/api/functions/createHTTPTransport.md +51 -0
  6. package/docs/api/functions/createTRPCTransport.md +46 -0
  7. package/docs/api/functions/getListEntries.md +15 -0
  8. package/docs/api/functions/getSelectionPlan.md +36 -0
  9. package/docs/api/functions/isRecord.md +15 -0
  10. package/docs/api/functions/isViewTag.md +17 -0
  11. package/docs/api/functions/liveConnectionTopic.md +19 -0
  12. package/docs/api/functions/liveEntityTopic.md +19 -0
  13. package/docs/api/functions/liveGlobalConnectionTopic.md +15 -0
  14. package/docs/api/functions/mutation.md +32 -0
  15. package/docs/api/functions/toEntityId.md +21 -0
  16. package/docs/api/functions/view.md +26 -0
  17. package/docs/api/index.md +81 -0
  18. package/docs/api/interfaces/FateThenable.md +31 -0
  19. package/docs/api/interfaces/Transport.md +199 -0
  20. package/docs/api/server/classes/FateRequestError.md +67 -0
  21. package/docs/api/server/drizzle/functions/createDrizzleFate.md +431 -0
  22. package/docs/api/server/drizzle/functions/createDrizzleSourceAdapter.md +21 -0
  23. package/docs/api/server/drizzle/functions/createDrizzleSourceRegistry.md +21 -0
  24. package/docs/api/server/drizzle/index.md +23 -0
  25. package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyConfig.md +15 -0
  26. package/docs/api/server/drizzle/type-aliases/DrizzleManyToManyInput.md +5 -0
  27. package/docs/api/server/drizzle/type-aliases/DrizzleQueryExtra.md +21 -0
  28. package/docs/api/server/drizzle/type-aliases/DrizzleSourceAdapter.md +337 -0
  29. package/docs/api/server/drizzle/type-aliases/DrizzleViewConfig.md +29 -0
  30. package/docs/api/server/functions/bindSourceProcedures.md +32 -0
  31. package/docs/api/server/functions/computed.md +43 -0
  32. package/docs/api/server/functions/count.md +21 -0
  33. package/docs/api/server/functions/createFateFetchHandler.md +21 -0
  34. package/docs/api/server/functions/createFateServer.md +41 -0
  35. package/docs/api/server/functions/createHonoFateHandler.md +21 -0
  36. package/docs/api/server/functions/createLiveEventBus.md +17 -0
  37. package/docs/api/server/functions/createNestedSourcePlan.md +103 -0
  38. package/docs/api/server/functions/createResolver.md +74 -0
  39. package/docs/api/server/functions/createSourcePlan.md +95 -0
  40. package/docs/api/server/functions/createSourceProcedures.md +46 -0
  41. package/docs/api/server/functions/dataView.md +32 -0
  42. package/docs/api/server/functions/field.md +15 -0
  43. package/docs/api/server/functions/getNestedSelection.md +19 -0
  44. package/docs/api/server/functions/getScopedArgs.md +21 -0
  45. package/docs/api/server/functions/hasNestedSelection.md +19 -0
  46. package/docs/api/server/functions/list.md +28 -0
  47. package/docs/api/server/functions/refetchSourceById.md +55 -0
  48. package/docs/api/server/functions/resolveSourceById.md +55 -0
  49. package/docs/api/server/functions/resolveSourceByIds.md +55 -0
  50. package/docs/api/server/functions/resolveSourceConnection.md +67 -0
  51. package/docs/api/server/functions/resolver.md +42 -0
  52. package/docs/api/server/functions/toPrismaSelect.md +25 -0
  53. package/docs/api/server/functions/withConnection.md +24 -0
  54. package/docs/api/server/index.md +109 -0
  55. package/docs/api/server/prisma/functions/createPrismaFate.md +435 -0
  56. package/docs/api/server/prisma/functions/createPrismaSourceAdapter.md +21 -0
  57. package/docs/api/server/prisma/functions/createPrismaSourceRegistry.md +21 -0
  58. package/docs/api/server/prisma/functions/prismaConnectionArgs.md +57 -0
  59. package/docs/api/server/prisma/index.md +23 -0
  60. package/docs/api/server/prisma/type-aliases/PrismaDelegate.md +59 -0
  61. package/docs/api/server/prisma/type-aliases/PrismaQueryExtra.md +5 -0
  62. package/docs/api/server/prisma/type-aliases/PrismaSourceAdapter.md +343 -0
  63. package/docs/api/server/prisma/type-aliases/PrismaViewConfig.md +31 -0
  64. package/docs/api/server/type-aliases/ComputedField.md +55 -0
  65. package/docs/api/server/type-aliases/ComputedSelection.md +5 -0
  66. package/docs/api/server/type-aliases/ConnectionItem.md +29 -0
  67. package/docs/api/server/type-aliases/ConnectionPagination.md +39 -0
  68. package/docs/api/server/type-aliases/ConnectionResult.md +29 -0
  69. package/docs/api/server/type-aliases/CountSelection.md +29 -0
  70. package/docs/api/server/type-aliases/CountWhere.md +5 -0
  71. package/docs/api/server/type-aliases/DataViewListOptions.md +13 -0
  72. package/docs/api/server/type-aliases/DataViewOrderBy.md +5 -0
  73. package/docs/api/server/type-aliases/DataViewOrderDirection.md +5 -0
  74. package/docs/api/server/type-aliases/DataViewResult.md +11 -0
  75. package/docs/api/server/type-aliases/Entity.md +21 -0
  76. package/docs/api/server/type-aliases/FateServer.md +75 -0
  77. package/docs/api/server/type-aliases/FateServerManifest.md +5 -0
  78. package/docs/api/server/type-aliases/FieldSelection.md +21 -0
  79. package/docs/api/server/type-aliases/LiveConnectionEventType.md +5 -0
  80. package/docs/api/server/type-aliases/LiveConnectionSourceEvent.md +5 -0
  81. package/docs/api/server/type-aliases/LiveEventBus.md +5 -0
  82. package/docs/api/server/type-aliases/LiveEventType.md +5 -0
  83. package/docs/api/server/type-aliases/LiveSourceEvent.md +5 -0
  84. package/docs/api/server/type-aliases/NativeFateAPI.md +47 -0
  85. package/docs/api/server/type-aliases/OrderDirection.md +5 -0
  86. package/docs/api/server/type-aliases/SourceConfig.md +55 -0
  87. package/docs/api/server/type-aliases/SourceDefinition.md +55 -0
  88. package/docs/api/server/type-aliases/SourceOrder.md +5 -0
  89. package/docs/api/server/type-aliases/SourceOrderField.md +21 -0
  90. package/docs/api/server/type-aliases/SourcePlan.md +29 -0
  91. package/docs/api/server/type-aliases/SourcePlanNode.md +29 -0
  92. package/docs/api/server/type-aliases/SourceRegistry.md +11 -0
  93. package/docs/api/server/type-aliases/SourceRelation.md +15 -0
  94. package/docs/api/server/type-aliases/SourceRelationConfig.md +23 -0
  95. package/docs/api/server/variables/byIdInput.md +5 -0
  96. package/docs/api/server/variables/connectionArgs.md +7 -0
  97. package/docs/api/type-aliases/ConnectionMetadata.md +7 -0
  98. package/docs/api/type-aliases/ConnectionRef.md +13 -0
  99. package/docs/api/type-aliases/Entity.md +15 -0
  100. package/docs/api/type-aliases/EntityId.md +7 -0
  101. package/docs/api/type-aliases/FateLiveConnectionEvent.md +5 -0
  102. package/docs/api/type-aliases/FateLiveEvent.md +5 -0
  103. package/docs/api/type-aliases/FateMutations.md +7 -0
  104. package/docs/api/type-aliases/FateOperation.md +5 -0
  105. package/docs/api/type-aliases/FateProtocolRequest.md +5 -0
  106. package/docs/api/type-aliases/FateProtocolResponse.md +5 -0
  107. package/docs/api/type-aliases/FateRecord.md +7 -0
  108. package/docs/api/type-aliases/FateRoots.md +7 -0
  109. package/docs/api/type-aliases/InferFateAPI.md +11 -0
  110. package/docs/api/type-aliases/List.md +5 -0
  111. package/docs/api/type-aliases/ListEntry.md +5 -0
  112. package/docs/api/type-aliases/ListItem.md +13 -0
  113. package/docs/api/type-aliases/Mask.md +15 -0
  114. package/docs/api/type-aliases/MutationDefinition.md +21 -0
  115. package/docs/api/type-aliases/MutationEntity.md +13 -0
  116. package/docs/api/type-aliases/MutationIdentifier.md +19 -0
  117. package/docs/api/type-aliases/MutationInput.md +13 -0
  118. package/docs/api/type-aliases/MutationResult.md +13 -0
  119. package/docs/api/type-aliases/NodesItem.md +13 -0
  120. package/docs/api/type-aliases/Pagination.md +39 -0
  121. package/docs/api/type-aliases/Request.md +7 -0
  122. package/docs/api/type-aliases/RequestMode.md +7 -0
  123. package/docs/api/type-aliases/RequestOptions.md +7 -0
  124. package/docs/api/type-aliases/RequestResult.md +18 -0
  125. package/docs/api/type-aliases/Selection.md +13 -0
  126. package/docs/api/type-aliases/Snapshot.md +7 -0
  127. package/docs/api/type-aliases/TypeConfig.md +41 -0
  128. package/docs/api/type-aliases/View.md +17 -0
  129. package/docs/api/type-aliases/ViewData.md +18 -0
  130. package/docs/api/type-aliases/ViewEntity.md +13 -0
  131. package/docs/api/type-aliases/ViewEntityName.md +13 -0
  132. package/docs/api/type-aliases/ViewRef.md +13 -0
  133. package/docs/api/type-aliases/ViewSelection.md +13 -0
  134. package/docs/api/type-aliases/ViewSnapshot.md +18 -0
  135. package/docs/api/type-aliases/ViewTag.md +7 -0
  136. package/docs/api/variables/ConnectionTag.md +7 -0
  137. package/docs/guide/actions.md +263 -0
  138. package/docs/guide/core-concepts.md +22 -0
  139. package/docs/guide/getting-started.md +57 -0
  140. package/docs/guide/list-views.md +86 -0
  141. package/docs/guide/live-views.md +245 -0
  142. package/docs/guide/requests.md +130 -0
  143. package/docs/guide/server-integration.md +630 -0
  144. package/docs/guide/views.md +278 -0
  145. package/docs/guide/void-integration.md +167 -0
  146. package/docs/index.md +4 -0
  147. package/lib/cli.mjs +17 -250
  148. package/lib/clientStub.d.mts +6 -0
  149. package/lib/clientStub.mjs +6 -0
  150. package/lib/executor-BqwHdN3n.d.mts +468 -0
  151. package/lib/index.d.mts +64 -3
  152. package/lib/index.mjs +1965 -559
  153. package/lib/list-4wRNSVgI.d.mts +10 -0
  154. package/lib/list-C_mi6GnE.mjs +16 -0
  155. package/lib/list.d.mts +2 -0
  156. package/lib/list.mjs +2 -0
  157. package/lib/liveTopics-DzNtJaBD.mjs +197 -0
  158. package/lib/{record-DnhZuvUe.mjs → record-AeZJC9fd.mjs} +1 -2
  159. package/lib/record-ENh92gyb.d.mts +315 -0
  160. package/lib/server/drizzle.d.mts +304 -0
  161. package/lib/server/drizzle.mjs +688 -0
  162. package/lib/server/prisma.d.mts +309 -0
  163. package/lib/server/prisma.mjs +345 -0
  164. package/lib/server.d.mts +82 -184
  165. package/lib/server.mjs +870 -416
  166. package/lib/sourceRouter-Bwpkr062.mjs +972 -0
  167. package/lib/{types-Djlh7mMH.d.mts → types-Dz46PXr3.d.mts} +327 -197
  168. package/lib/vite.d.mts +17 -0
  169. package/lib/vite.mjs +592 -0
  170. package/package.json +44 -6
package/lib/server.mjs CHANGED
@@ -1,458 +1,912 @@
1
- import { t as isRecord$1 } from "./record-DnhZuvUe.mjs";
2
- import { z } from "zod";
3
-
4
- //#region src/server/prismaSelect.ts
5
- const toPrismaArgs = (args$1) => {
6
- const prismaArgs = {};
7
- const isBackward = args$1.before !== void 0 || typeof args$1.last === "number";
8
- if (typeof args$1.first === "number") prismaArgs.take = args$1.first + 1;
9
- if (typeof args$1.last === "number") prismaArgs.take = -(args$1.last + 1);
10
- const cursor = isBackward ? args$1.before : args$1.after;
11
- if (cursor !== void 0) {
12
- prismaArgs.cursor = { id: cursor };
13
- prismaArgs.skip = 1;
14
- }
15
- return prismaArgs;
1
+ import { t as isRecord } from "./record-AeZJC9fd.mjs";
2
+ import { d as hashArgs, i as FateRequestError, n as liveEntityTopic, o as toProtocolError, r as liveGlobalConnectionTopic, t as liveConnectionTopic, u as filterConnectionArgs } from "./liveTopics-DzNtJaBD.mjs";
3
+ import { C as list, D as resolveConnection, E as connectionArgs, O as withConnection, S as isDataView, T as toPrismaSelect, _ as computed, a as refetchSourceById, b as dataView, c as resolveSourceConnection, f as createSourcePlan, h as hasNestedSelection, k as getScopedArgs, m as getNestedSelection, n as createSourceProcedures, o as resolveSourceById, r as byIdInput, s as resolveSourceByIds, t as bindSourceProcedures, u as createNestedSourcePlan, v as count, w as resolver, x as field, y as createResolver } from "./sourceRouter-Bwpkr062.mjs";
4
+ import { EventEmitter, on } from "node:events";
5
+ //#region src/server/live.ts
6
+ const eventName = (type, id) => `${type}:${String(id)}`;
7
+ const globalConnectionEventName = (procedure) => `connection:${procedure}:*`;
8
+ const normalizeConnectionArgs = (args) => {
9
+ const filtered = filterConnectionArgs(args);
10
+ if (!filtered) return;
11
+ const id = filtered.id;
12
+ return typeof id === "string" || typeof id === "number" ? {
13
+ ...filtered,
14
+ id: String(id)
15
+ } : filtered;
16
16
  };
17
- const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
18
- /**
19
- * Narrows nested args to the slice relevant for a particular selection path.
20
- */
21
- function getScopedArgs(args$1, path) {
22
- if (!args$1) return;
23
- const segments = path.split(".");
24
- let current = args$1;
25
- for (const segment of segments) {
26
- if (!isRecord(current)) return;
27
- current = current[segment];
28
- }
29
- return isRecord(current) ? current : void 0;
30
- }
31
- /**
32
- * Builds a Prisma `select` object from flattened selection paths and optional
33
- * scoped args, always including the `id` field.
34
- */
35
- function prismaSelect(paths, args$1) {
36
- const allPaths = [...new Set([...paths, "id"])];
37
- const select = {};
38
- for (const path of allPaths) {
39
- const segments = path.split(".");
40
- let current = select;
41
- let currentPath = "";
42
- segments.forEach((segment, index) => {
43
- currentPath = currentPath ? `${currentPath}.${segment}` : segment;
44
- if (index === segments.length - 1) {
45
- if (segment === "cursor") return;
46
- current[segment] = true;
47
- return;
48
- }
49
- const existing = current[segment];
50
- const relation = existing && typeof existing === "object" && existing !== null && "select" in existing ? existing : { select: {} };
51
- const scopedArgs = getScopedArgs(args$1, currentPath);
52
- if (scopedArgs) Object.assign(relation, toPrismaArgs(scopedArgs));
53
- current[segment] = relation;
54
- current = relation.select;
17
+ const connectionEventName = (procedure, args) => `connection:${procedure}:${hashArgs(normalizeConnectionArgs(args) ?? {})}`;
18
+ const mergeEvents = (emitter, names, signal) => ({ [Symbol.asyncIterator]() {
19
+ const queue = [];
20
+ let pending = null;
21
+ let done = false;
22
+ const cleanup = () => {
23
+ done = true;
24
+ for (const name of names) emitter.off(name, listener);
25
+ signal?.removeEventListener("abort", onAbort);
26
+ pending?.({
27
+ done: true,
28
+ value: void 0
55
29
  });
56
- }
57
- return select;
58
- }
59
-
60
- //#endregion
61
- //#region src/server/connection.ts
62
- const args = z.object({}).catchall(z.union([z.unknown(), z.lazy(() => args)]));
63
- /**
64
- * Zod schema for connection args (`after`, `before`, `first`, `last`, etc.).
65
- */
66
- const connectionArgs = args.optional();
67
- const connectionInput = z.strictObject({
68
- args: connectionArgs,
69
- select: z.array(z.string())
70
- });
71
- const paginationArgKeys = new Set([
72
- "after",
73
- "before",
74
- "first",
75
- "last"
76
- ]);
77
- const paginationArgsSchema = z.strictObject({
78
- after: z.string().optional(),
79
- before: z.string().optional(),
80
- first: z.number().int().positive().optional(),
81
- last: z.number().int().positive().optional()
82
- }).partial().refine(({ after, before }) => !(after && before), "Connection args can't include both 'after' and 'before'.").refine(({ first, last }) => !(first && last), "Connection args can't include both 'first' and 'last'.").refine(({ before, last }) => !last || before !== void 0, "Connection args using 'last' must also include 'before'.");
83
- const extractPaginationArgs = (args$1) => args$1 ? Object.fromEntries(Object.entries(args$1).filter(([key]) => paginationArgKeys.has(key))) : {};
84
- /**
85
- * Converts an array of nodes into a list view connection result the client
86
- * can normalize.
87
- */
88
- function arrayToConnection(nodes, { args: args$1, getCursor = (node) => String(node.id) } = {}) {
89
- if (!nodes) return;
90
- const paginationArgs = paginationArgsSchema.parse(extractPaginationArgs(args$1));
91
- if (Object.keys(paginationArgs).length === 0) return {
92
- items: nodes.map((node) => ({
93
- cursor: getCursor(node),
94
- node
95
- })),
96
- pagination: {
97
- hasNext: false,
98
- hasPrevious: false,
99
- nextCursor: void 0,
100
- previousCursor: void 0
101
- }
102
- };
103
- const isBackward = paginationArgs.before !== void 0 || paginationArgs.last !== void 0;
104
- const cursor = isBackward ? paginationArgs.before : paginationArgs.after;
105
- const pageSize = paginationArgs.first ?? paginationArgs.last ?? nodes.length;
106
- const cursorIndex = cursor === void 0 ? -1 : nodes.findIndex((node) => getCursor(node) === cursor);
107
- const selectedNodes = cursorIndex < 0 ? nodes : isBackward ? nodes.slice(0, cursorIndex) : nodes.slice(cursorIndex + 1);
108
- const hasNext = selectedNodes.length > pageSize;
109
- const hasPrevious = nodes.length > selectedNodes.length;
110
- const items = (isBackward ? selectedNodes.slice(Math.max(0, selectedNodes.length - pageSize)) : selectedNodes.slice(0, pageSize)).map((node) => ({
111
- cursor: getCursor(node),
112
- node
113
- }));
114
- const firstItem = items[0];
115
- const lastItem = items.at(-1);
116
- return {
117
- items,
118
- pagination: {
119
- hasNext: isBackward ? hasPrevious : hasNext,
120
- hasPrevious: isBackward ? hasNext : hasPrevious,
121
- nextCursor: lastItem?.cursor,
122
- previousCursor: (isBackward ? hasNext : hasPrevious) && firstItem ? firstItem.cursor : void 0
123
- }
30
+ pending = null;
124
31
  };
125
- }
126
- const isDataViewField$1 = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
127
- const assignIfChanged = (current, key, next, existing) => {
128
- if (next === existing) return current;
129
- if (!current) return { [key]: next };
130
- current[key] = next;
131
- return current;
132
- };
133
- function toConnectionResult({ args: args$1, item, path, view }) {
134
- if (!isRecord$1(item)) return item;
135
- let result = null;
136
- const base = () => result ? {
137
- ...item,
138
- ...result
139
- } : item;
140
- for (const [field, config] of Object.entries(view.fields)) {
141
- if (!isDataViewField$1(config)) continue;
142
- const current = base()[field];
143
- const nextPath = path ? `${path}.${field}` : field;
144
- if (config.kind === "list") {
145
- if (!Array.isArray(current)) continue;
146
- const connection = arrayToConnection(current.map((item$1) => toConnectionResult({
147
- args: args$1,
148
- item: item$1,
149
- path: nextPath,
150
- view: config
151
- })), { args: getScopedArgs(args$1, nextPath) });
152
- result = assignIfChanged(result, field, connection, current);
153
- continue;
154
- }
155
- if (Array.isArray(current)) {
156
- const wrapped = current.map((entry) => toConnectionResult({
157
- args: args$1,
158
- item: entry,
159
- path: nextPath,
160
- view: config
161
- }));
162
- result = assignIfChanged(result, field, wrapped, current);
163
- continue;
164
- }
165
- if (isRecord$1(current)) {
166
- const wrapped = toConnectionResult({
167
- args: args$1,
168
- item: current,
169
- path: nextPath,
170
- view: config
32
+ const listener = (value) => {
33
+ if (pending) {
34
+ const resolve = pending;
35
+ pending = null;
36
+ resolve({
37
+ done: false,
38
+ value: [value]
171
39
  });
172
- result = assignIfChanged(result, field, wrapped, current);
40
+ return;
173
41
  }
174
- }
175
- return result ? {
176
- ...item,
177
- ...result
178
- } : item;
179
- }
180
- function withConnection(procedure) {
181
- return ({ defaultSize = 20, getCursor = (node) => node.id, input: additionalInput, map, query }) => {
182
- const inputSchema = additionalInput ? connectionInput.extend({ args: connectionArgs.and(additionalInput) }) : connectionInput;
183
- return procedure.input(inputSchema).query(async (resolverOptions) => {
184
- const { ctx, input } = resolverOptions;
185
- const paginationArgs = paginationArgsSchema.parse(extractPaginationArgs(input.args));
186
- const isBackward = paginationArgs.before !== void 0 || paginationArgs.last !== void 0;
187
- const cursor = isBackward ? paginationArgs.before : paginationArgs.after;
188
- const direction = isBackward ? "backward" : "forward";
189
- const pageSize = paginationArgs.first ?? paginationArgs.last ?? defaultSize;
190
- const rawItems = await query({
191
- ctx,
192
- cursor,
193
- direction,
194
- input,
195
- skip: cursor ? 1 : void 0,
196
- take: pageSize + 1
42
+ queue.push(value);
43
+ };
44
+ const onAbort = () => cleanup();
45
+ for (const name of names) emitter.on(name, listener);
46
+ signal?.addEventListener("abort", onAbort, { once: true });
47
+ return {
48
+ async next() {
49
+ if (queue.length > 0) return {
50
+ done: false,
51
+ value: [queue.shift()]
52
+ };
53
+ if (done || signal?.aborted) {
54
+ cleanup();
55
+ return {
56
+ done: true,
57
+ value: void 0
58
+ };
59
+ }
60
+ return await new Promise((resolve) => {
61
+ pending = resolve;
197
62
  });
198
- const hasMore = rawItems.length > pageSize;
199
- const limitedItems = isBackward ? rawItems.slice(Math.max(0, rawItems.length - pageSize)) : rawItems.slice(0, pageSize);
200
- const items = (map ? await map({
201
- ctx,
202
- input,
203
- items: limitedItems
204
- }) : limitedItems).map((node) => ({
205
- cursor: getCursor(node),
206
- node
207
- }));
208
- const firstItem = items[0];
209
- const lastItem = items.at(-1);
63
+ },
64
+ async return() {
65
+ cleanup();
210
66
  return {
211
- items,
212
- pagination: {
213
- hasNext: isBackward ? Boolean(cursor) : hasMore,
214
- hasPrevious: isBackward ? hasMore : Boolean(cursor),
215
- nextCursor: lastItem?.cursor,
216
- previousCursor: (isBackward ? hasMore : Boolean(cursor)) ? firstItem?.cursor : void 0
217
- }
67
+ done: true,
68
+ value: void 0
218
69
  };
219
- });
70
+ }
220
71
  };
221
- }
222
-
223
- //#endregion
224
- //#region src/server/dataView.ts
225
- const dataViewFieldsKey = Symbol("__fate__DataViewFields");
72
+ } });
226
73
  /**
227
- * Declares a server data view that exposes an object's available fields to the client.
74
+ * Creates a small in-memory event bus for Fate live view subscriptions.
228
75
  *
229
- * @example
230
- * const Post = dataView<PostItem>('Post')({
231
- * id: true,
232
- * title: true,
233
- * });
76
+ * The bus signals that an entity changed and can optionally include changed
77
+ * field paths. The native live handler refetches the selected record, or only
78
+ * the changed selected fields when paths are provided, before sending it to
79
+ * clients. It does not persist events or replay events after reconnects; provide
80
+ * a durable custom bus for lossless `lastEventId` resume behavior.
234
81
  */
235
- function dataView(typeName) {
236
- return (fields) => {
237
- return {
238
- [dataViewFieldsKey]: fields,
239
- fields,
240
- typeName
241
- };
82
+ function createLiveEventBus() {
83
+ const emitter = new EventEmitter();
84
+ emitter.setMaxListeners(0);
85
+ const emit = (type, id, options = {}) => {
86
+ emitter.emit(eventName(type, id), {
87
+ changed: options.changed,
88
+ data: options.data,
89
+ eventId: options.eventId,
90
+ id,
91
+ type: options.type ?? "update"
92
+ });
242
93
  };
243
- }
244
- /**
245
- * Marks a data view as a list resolver so the server can respond with
246
- * connection information.
247
- */
248
- const list = (view) => {
249
- return {
250
- ...view,
251
- kind: "list"
94
+ const listen = (names, handler, signal) => {
95
+ const listener = (event) => handler(event);
96
+ const cleanup = () => {
97
+ for (const name of names) emitter.off(name, listener);
98
+ signal?.removeEventListener("abort", cleanup);
99
+ };
100
+ for (const name of names) emitter.on(name, listener);
101
+ signal?.addEventListener("abort", cleanup, { once: true });
102
+ return cleanup;
252
103
  };
253
- };
254
- /**
255
- * Declares a resolver field inside a data view, optionally providing a
256
- * selection for any data dependencies.
257
- */
258
- function resolver(config) {
259
104
  return {
260
- kind: "resolver",
261
- ...config
105
+ connection(procedure, args) {
106
+ const emitConnection = (type, options = {}) => {
107
+ const event = {
108
+ cursor: options.cursor,
109
+ eventId: options.eventId,
110
+ id: options.id,
111
+ node: options.node,
112
+ nodeType: options.nodeType,
113
+ targetCursor: options.targetCursor,
114
+ type
115
+ };
116
+ if (args) emitter.emit(connectionEventName(procedure, args), event);
117
+ else emitter.emit(globalConnectionEventName(procedure), event);
118
+ };
119
+ return {
120
+ appendEdge(nodeType, id, options) {
121
+ emitConnection("appendEdge", {
122
+ ...options,
123
+ id,
124
+ nodeType
125
+ });
126
+ },
127
+ appendNode(nodeType, id, options) {
128
+ emitConnection("appendNode", {
129
+ ...options,
130
+ id,
131
+ nodeType
132
+ });
133
+ },
134
+ deleteEdge(nodeType, id, options) {
135
+ emitConnection("deleteEdge", {
136
+ ...options,
137
+ id,
138
+ nodeType
139
+ });
140
+ },
141
+ emit: emitConnection,
142
+ insertEdgeAfter(nodeType, id, targetCursor, options) {
143
+ emitConnection("insertEdgeAfter", {
144
+ ...options,
145
+ id,
146
+ nodeType,
147
+ targetCursor
148
+ });
149
+ },
150
+ insertEdgeBefore(nodeType, id, targetCursor, options) {
151
+ emitConnection("insertEdgeBefore", {
152
+ ...options,
153
+ id,
154
+ nodeType,
155
+ targetCursor
156
+ });
157
+ },
158
+ invalidate(options) {
159
+ emitConnection("invalidate", options);
160
+ },
161
+ prependEdge(nodeType, id, options) {
162
+ emitConnection("prependEdge", {
163
+ ...options,
164
+ id,
165
+ nodeType
166
+ });
167
+ },
168
+ prependNode(nodeType, id, options) {
169
+ emitConnection("prependNode", {
170
+ ...options,
171
+ id,
172
+ nodeType
173
+ });
174
+ }
175
+ };
176
+ },
177
+ delete(type, id, options) {
178
+ emit(type, id, {
179
+ ...options,
180
+ type: "delete"
181
+ });
182
+ },
183
+ emit,
184
+ listen(type, id, handler, options) {
185
+ return listen([eventName(type, id)], handler, options?.signal);
186
+ },
187
+ listenConnection(target, handler, options) {
188
+ return listen([connectionEventName(target.procedure, target.args), globalConnectionEventName(target.procedure)], handler, options?.signal);
189
+ },
190
+ subscribe(type, id, options) {
191
+ return on(emitter, eventName(type, id), { signal: options?.signal });
192
+ },
193
+ subscribeConnection(target, options) {
194
+ return mergeEvents(emitter, [connectionEventName(target.procedure, target.args), globalConnectionEventName(target.procedure)], options?.signal);
195
+ },
196
+ update(type, id, options) {
197
+ emit(type, id, {
198
+ ...options,
199
+ type: "update"
200
+ });
201
+ }
262
202
  };
263
203
  }
264
- const isResolverField = (field) => Boolean(field) && typeof field === "object" && "kind" in field && field.kind === "resolver";
265
- const isDataViewField = (field) => Boolean(field) && typeof field === "object" && "fields" in field;
266
- const filterToViewFields = (item, view, selectedPaths, prefix = null) => {
267
- if (!isRecord$1(item)) return item;
268
- const filtered = {};
269
- for (const [field, config] of Object.entries(view.fields)) {
270
- const path = prefix ? `${prefix}.${field}` : field;
271
- let hasSelection = selectedPaths.has(path);
272
- if (!hasSelection) {
273
- for (const selected of selectedPaths) if (selected.startsWith(`${path}.`)) {
274
- hasSelection = true;
275
- break;
276
- }
277
- }
278
- if (!hasSelection) continue;
279
- if (!(field in item)) continue;
280
- const value = item[field];
281
- if (isDataViewField(config)) {
282
- if (Array.isArray(value)) {
283
- filtered[field] = value.map((entry) => isRecord$1(entry) ? filterToViewFields(entry, config, selectedPaths, path) : entry);
284
- continue;
285
- }
286
- if (isRecord$1(value)) {
287
- filtered[field] = filterToViewFields(value, config, selectedPaths, path);
288
- continue;
289
- }
290
- }
291
- filtered[field] = value;
204
+ //#endregion
205
+ //#region src/server/http.ts
206
+ const jsonHeaders = { "content-type": "application/json; charset=utf-8" };
207
+ const sseHeaders = {
208
+ "cache-control": "no-cache",
209
+ connection: "keep-alive",
210
+ "content-type": "text/event-stream; charset=utf-8"
211
+ };
212
+ const normalizeRootConfig = (root) => isDataView(root) ? { view: root } : root;
213
+ const rootProcedureName = (name, _root) => {
214
+ return name;
215
+ };
216
+ const getLiveBus = (live) => {
217
+ if (!live) return null;
218
+ if ("subscribe" in live) return live;
219
+ return live.bus;
220
+ };
221
+ const getWaitUntil = (adapterContext) => {
222
+ const context = adapterContext;
223
+ if (!context) return;
224
+ try {
225
+ const executionCtx = context.executionCtx;
226
+ const waitUntil = executionCtx?.waitUntil;
227
+ if (waitUntil) return waitUntil.bind(executionCtx);
228
+ } catch {}
229
+ try {
230
+ return context.waitUntil?.bind(context);
231
+ } catch {
232
+ return;
292
233
  }
293
- return filtered;
294
234
  };
295
- const mergeObject = (target, source) => {
296
- for (const [key, value] of Object.entries(source)) {
297
- const existing = target[key];
298
- if (isRecord$1(existing) && isRecord$1(value)) {
299
- mergeObject(existing, value);
300
- continue;
301
- }
302
- target[key] = value;
235
+ const parseJSON = async (request) => {
236
+ try {
237
+ return await request.json();
238
+ } catch {
239
+ throw new FateRequestError("BAD_REQUEST", "Request body must be valid JSON.");
303
240
  }
304
241
  };
305
- const ensureRelationSelect = (select, path) => {
306
- if (!path) return select;
307
- const segments = path.split(".");
308
- let current = select;
309
- for (const segment of segments) {
310
- const existing = current[segment];
311
- if (isRecord$1(existing) && "select" in existing) {
312
- const relation$1 = existing;
313
- if (!isRecord$1(relation$1.select)) relation$1.select = {};
314
- current = relation$1.select;
315
- continue;
316
- }
317
- const relation = { select: {} };
318
- current[segment] = relation;
319
- current = relation.select;
242
+ const validationError = (issues) => new FateRequestError("VALIDATION_ERROR", "Validation failed.", { issues });
243
+ const parseInput = async (schema, value) => {
244
+ if (!schema) return value;
245
+ const standard = schema["~standard"];
246
+ if (standard) try {
247
+ const result = await standard.validate(value);
248
+ if (isRecord(result) && "issues" in result) throw validationError(result.issues);
249
+ return isRecord(result) && "value" in result ? result.value : value;
250
+ } catch (error) {
251
+ if (error instanceof FateRequestError) throw error;
252
+ throw validationError(isRecord(error) ? error.issues : void 0);
253
+ }
254
+ if (schema.parse) try {
255
+ return schema.parse(value);
256
+ } catch (error) {
257
+ throw validationError(isRecord(error) ? error.issues : void 0);
320
258
  }
321
- return current;
259
+ return value;
322
260
  };
323
- const createSelectedNode = (view, path) => ({
324
- path,
325
- relations: /* @__PURE__ */ new Map(),
326
- resolvers: /* @__PURE__ */ new Map(),
327
- view
328
- });
329
- const assignPath = (node, segments, path, selectedPaths, view, allowedPaths) => {
330
- if (segments.length === 0) return;
331
- const [segment, ...rest] = segments;
332
- const field = view.fields[segment];
333
- if (!field) return;
334
- const nextPath = path ? `${path}.${segment}` : segment;
335
- if (isResolverField(field)) {
336
- if (rest.length === 0) {
337
- node.resolvers.set(segment, field);
338
- selectedPaths.add(nextPath);
339
- }
340
- return;
261
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
262
+ const isProtocolId = (value) => typeof value === "string" || typeof value === "number";
263
+ const operationKinds = new Set([
264
+ "byId",
265
+ "list",
266
+ "mutation",
267
+ "query"
268
+ ]);
269
+ const assertProtocolRequest = (value) => {
270
+ if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.operations)) throw new FateRequestError("BAD_REQUEST", "Invalid Fate protocol request.");
271
+ for (const operation of value.operations) {
272
+ if (!isRecord(operation) || typeof operation.id !== "string" || typeof operation.kind !== "string" || !operationKinds.has(operation.kind) || !isStringArray(operation.select) || "args" in operation && operation.args !== void 0 && !isRecord(operation.args)) throw new FateRequestError("BAD_REQUEST", "Invalid Fate protocol operation.");
273
+ if (operation.kind === "byId" && (typeof operation.type !== "string" || !Array.isArray(operation.ids) || !operation.ids.every(isProtocolId))) throw new FateRequestError("BAD_REQUEST", "Invalid Fate byId operation.");
274
+ if ((operation.kind === "list" || operation.kind === "mutation" || operation.kind === "query") && typeof operation.name !== "string") throw new FateRequestError("BAD_REQUEST", "Invalid Fate named operation.");
341
275
  }
342
- if (isDataViewField(field)) {
343
- let relationNode = node.relations.get(segment);
344
- if (!relationNode) {
345
- relationNode = createSelectedNode(field, nextPath);
346
- node.relations.set(segment, relationNode);
347
- }
348
- if (field.fields.id === true) {
349
- allowedPaths.add(`${nextPath}.id`);
350
- selectedPaths.add(`${nextPath}.id`);
276
+ return value;
277
+ };
278
+ const assertLiveControlRequest = (value) => {
279
+ if (!isRecord(value) || value.version !== 1 || typeof value.connectionId !== "string" || !Array.isArray(value.operations)) throw new FateRequestError("BAD_REQUEST", "Invalid Fate live request.");
280
+ for (const operation of value.operations) {
281
+ if (!isRecord(operation) || typeof operation.id !== "string" || typeof operation.kind !== "string") throw new FateRequestError("BAD_REQUEST", "Invalid Fate live operation.");
282
+ if (operation.kind === "subscribe") {
283
+ if (typeof operation.type !== "string" || !isProtocolId(operation.entityId) || "args" in operation && operation.args !== void 0 && !isRecord(operation.args) || "lastEventId" in operation && operation.lastEventId !== void 0 && typeof operation.lastEventId !== "string" || !isStringArray(operation.select)) throw new FateRequestError("BAD_REQUEST", "Invalid Fate live subscribe operation.");
284
+ continue;
351
285
  }
352
- if (rest.length === 0) {
353
- selectedPaths.add(nextPath);
354
- return;
286
+ if (operation.kind === "subscribeConnection") {
287
+ if (typeof operation.type !== "string" || typeof operation.procedure !== "string" || "args" in operation && operation.args !== void 0 && !isRecord(operation.args) || "selectionArgs" in operation && operation.selectionArgs !== void 0 && !isRecord(operation.selectionArgs) || "lastEventId" in operation && operation.lastEventId !== void 0 && typeof operation.lastEventId !== "string" || !isStringArray(operation.select)) throw new FateRequestError("BAD_REQUEST", "Invalid Fate live connection subscribe operation.");
288
+ continue;
355
289
  }
356
- assignPath(relationNode, rest, nextPath, selectedPaths, field, allowedPaths);
357
- return;
290
+ if (operation.kind !== "unsubscribe") throw new FateRequestError("BAD_REQUEST", "Invalid Fate live operation.");
291
+ }
292
+ return value;
293
+ };
294
+ const sse = (message, eventId) => {
295
+ const lines = [];
296
+ if (eventId) lines.push(`id: ${eventId}`);
297
+ lines.push(`event: ${message.kind}`);
298
+ lines.push(`data: ${JSON.stringify(message)}`);
299
+ return `${lines.join("\n")}\n\n`;
300
+ };
301
+ const sseComment = (message) => `: ${message}\n\n`;
302
+ const livePayload = (event, data, select) => event.type === "delete" || data == null ? {
303
+ delete: true,
304
+ id: event.id
305
+ } : {
306
+ data,
307
+ ...select ? { select: [...select] } : void 0
308
+ };
309
+ const pathsIntersect = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
310
+ const filterLiveSelection = (source, select, changed) => {
311
+ if (!changed) return null;
312
+ const changedPaths = changed.filter((path) => path.length > 0);
313
+ if (changedPaths.length === 0) return [];
314
+ const selected = select.filter((path) => changedPaths.some((changedPath) => pathsIntersect(path, changedPath)));
315
+ if (selected.length === 0) return [];
316
+ const result = new Set(selected);
317
+ result.add(source.id);
318
+ for (const path of select) {
319
+ if (!path.endsWith(".id")) continue;
320
+ const parentPath = path.slice(0, -3);
321
+ if (selected.some((selectedPath) => pathsIntersect(selectedPath, parentPath))) result.add(path);
358
322
  }
359
- if (rest.length === 0) {
360
- allowedPaths.add(nextPath);
361
- selectedPaths.add(nextPath);
323
+ return [...result];
324
+ };
325
+ const hasSelectedDataPath = (value, segments) => {
326
+ if (segments.length === 0) return true;
327
+ if (Array.isArray(value)) return value.every((entry) => hasSelectedDataPath(entry, segments));
328
+ if (!isRecord(value)) return false;
329
+ const [field, ...rest] = segments;
330
+ return field in value && hasSelectedDataPath(value[field], rest);
331
+ };
332
+ const canUseLiveEventData = (data, selectedPaths) => {
333
+ if (!isRecord(data)) return false;
334
+ for (const path of selectedPaths) if (!hasSelectedDataPath(data, path.split("."))) return false;
335
+ return true;
336
+ };
337
+ const resolveLiveSourceData = async ({ ctx, data, id, input, registry, source }) => {
338
+ if (data !== void 0) {
339
+ if (data == null) return data;
340
+ const plan = createSourcePlan({
341
+ ...input,
342
+ ctx,
343
+ source
344
+ });
345
+ if (canUseLiveEventData(data, plan.selectedPaths)) return await plan.resolve(data);
362
346
  }
347
+ return id == null ? null : await resolveSourceById({
348
+ ctx,
349
+ id: String(id),
350
+ input,
351
+ registry,
352
+ source
353
+ });
363
354
  };
364
- const collectResolvers = (node, select, args$1, context) => {
365
- for (const resolver$1 of node.resolvers.values()) {
366
- if (!resolver$1.select) continue;
367
- const addition = typeof resolver$1.select === "function" ? resolver$1.select({
368
- args: args$1,
369
- context
370
- }) : resolver$1.select;
371
- if (addition && isRecord$1(addition)) mergeObject(ensureRelationSelect(select, node.path), addition);
355
+ const liveConnectionPayload = (event, node) => {
356
+ switch (event.type) {
357
+ case "deleteEdge": return {
358
+ id: event.id,
359
+ nodeType: event.nodeType,
360
+ type: "deleteEdge"
361
+ };
362
+ case "invalidate": return { type: "invalidate" };
363
+ default: return {
364
+ edge: {
365
+ cursor: event.cursor,
366
+ node
367
+ },
368
+ nodeType: event.nodeType,
369
+ targetCursor: event.targetCursor,
370
+ type: event.type
371
+ };
372
372
  }
373
- for (const relation of node.relations.values()) collectResolvers(relation, select, args$1, context);
374
373
  };
375
- const resolveNode = async ({ item, node, options: resolverOptions }) => {
376
- if (!isRecord$1(item)) return item;
377
- let result = null;
378
- const assign = (key, value) => {
379
- if (!result) result = { ...item };
380
- result[key] = value;
374
+ const sourceScore = (source) => Object.keys(source.view.fields).length + Object.keys(source.relations ?? {}).length;
375
+ function createFateServer({ context, lists, live, mutations, queries, roots, sources }) {
376
+ const rootLists = /* @__PURE__ */ new Map();
377
+ const rootQueries = /* @__PURE__ */ new Map();
378
+ const sourcesByType = /* @__PURE__ */ new Map();
379
+ const registerSourceByType = (source) => {
380
+ const existing = sourcesByType.get(source.view.typeName);
381
+ if (!existing || sourceScore(source) > sourceScore(existing)) sourcesByType.set(source.view.typeName, source);
382
+ };
383
+ const visit = (view) => {
384
+ registerSourceByType(sources.getSource(view));
385
+ for (const field of Object.values(view.fields)) if (isDataView(field)) visit(field);
381
386
  };
382
- const getItem = () => result ?? item;
383
- for (const [field, resolver$1] of node.resolvers) {
384
- if (resolver$1.authorize) {
385
- if (!await resolver$1.authorize(getItem(), resolverOptions.context, resolverOptions.args)) {
386
- assign(field, null);
387
- continue;
387
+ for (const [name, root] of Object.entries(roots)) {
388
+ const config = normalizeRootConfig(root);
389
+ const procedure = rootProcedureName(name, root);
390
+ const source = sources.getSource(config.view);
391
+ visit(config.view);
392
+ if (config.view.kind === "list") rootLists.set(procedure, {
393
+ source,
394
+ view: config.view
395
+ });
396
+ else rootQueries.set(procedure, {
397
+ source,
398
+ view: config.view
399
+ });
400
+ }
401
+ for (const name of rootQueries.keys()) if (!queries?.[name]) throw new Error(`Native fate root query '${name}' requires a matching resolver in createFateServer({ queries }).`);
402
+ const liveBus = getLiveBus(live);
403
+ const getContext = async (request, adapterContext) => context ? await context({
404
+ adapterContext,
405
+ request
406
+ }) : void 0;
407
+ const executeOperation = async (operation, ctx) => {
408
+ try {
409
+ const input = {
410
+ args: operation.args,
411
+ select: operation.select
412
+ };
413
+ if (operation.kind === "byId") {
414
+ if (!operation.type || !operation.ids) throw new FateRequestError("BAD_REQUEST", "byId operations require type and ids.");
415
+ const source = sourcesByType.get(operation.type);
416
+ if (!source) throw new FateRequestError("NOT_FOUND", `No source registered for '${operation.type}'.`);
417
+ return {
418
+ data: await resolveSourceByIds({
419
+ ctx,
420
+ ids: operation.ids.map(String),
421
+ input,
422
+ registry: sources.registry,
423
+ source
424
+ }),
425
+ id: operation.id,
426
+ ok: true
427
+ };
428
+ }
429
+ if (!operation.name) throw new FateRequestError("BAD_REQUEST", `${operation.kind} operations require a name.`);
430
+ if (operation.kind === "list") {
431
+ const customList = lists?.[operation.name];
432
+ if (customList) return {
433
+ data: await customList.resolve({
434
+ ctx,
435
+ input: { args: operation.args },
436
+ select: operation.select
437
+ }),
438
+ id: operation.id,
439
+ ok: true
440
+ };
441
+ const root = rootLists.get(operation.name);
442
+ if (!root) throw new FateRequestError("NOT_FOUND", `No list registered for '${operation.name}'.`);
443
+ return {
444
+ data: await resolveConnection({
445
+ ctx,
446
+ input,
447
+ query: ({ ctx, cursor, direction, input, skip, take }) => resolveSourceConnection({
448
+ ctx,
449
+ cursor,
450
+ direction,
451
+ input,
452
+ registry: sources.registry,
453
+ skip,
454
+ source: root.source,
455
+ take
456
+ })
457
+ }),
458
+ id: operation.id,
459
+ ok: true
460
+ };
388
461
  }
462
+ if (operation.kind === "query") {
463
+ const customQuery = queries?.[operation.name];
464
+ if (customQuery) return {
465
+ data: await customQuery.resolve({
466
+ ctx,
467
+ input: { args: operation.args },
468
+ select: operation.select
469
+ }),
470
+ id: operation.id,
471
+ ok: true
472
+ };
473
+ throw new FateRequestError("NOT_FOUND", `No query registered for '${operation.name}'.`);
474
+ }
475
+ const mutation = mutations?.[operation.name];
476
+ if (!mutation) throw new FateRequestError("NOT_FOUND", `No mutation registered for '${operation.name}'.`);
477
+ return {
478
+ data: await mutation.resolve({
479
+ ctx,
480
+ input: await parseInput(mutation.input, operation.input),
481
+ select: operation.select
482
+ }),
483
+ id: operation.id,
484
+ ok: true
485
+ };
486
+ } catch (error) {
487
+ return {
488
+ error: toProtocolError(error),
489
+ id: operation.id,
490
+ ok: false
491
+ };
389
492
  }
390
- const value = await resolver$1.resolve(getItem(), resolverOptions.context, resolverOptions.args);
391
- if (value !== void 0) assign(field, value);
392
- }
393
- for (const [field, relationNode] of node.relations) {
394
- const current = getItem()[field];
395
- if (Array.isArray(current)) {
396
- const resolved = await Promise.all(current.map((entry) => resolveNode({
397
- item: entry,
398
- node: relationNode,
399
- options: resolverOptions
400
- })));
401
- if (resolved.some((value, index) => value !== current[index])) assign(field, resolved);
402
- continue;
493
+ };
494
+ const manifest = {
495
+ lists: Object.fromEntries([...[...rootLists.entries()].map(([name, entry]) => [name, { type: entry.view.typeName }]), ...Object.entries(lists ?? {}).map(([name, list]) => [name, { type: list.type ?? rootLists.get(name)?.view.typeName ?? "Unknown" }])].sort(([a], [b]) => a.localeCompare(b))),
496
+ live: liveBus ? Object.fromEntries([...sourcesByType.keys()].sort().map((type) => [type, true])) : {},
497
+ mutations: Object.fromEntries(Object.entries(mutations ?? {}).map(([name, mutation]) => [name, { type: mutation.type }]).sort(([a], [b]) => a.localeCompare(b))),
498
+ queries: Object.fromEntries([...[...rootQueries.entries()].map(([name, entry]) => [name, { type: entry.view.typeName }]), ...Object.entries(queries ?? {}).map(([name, query]) => [name, { type: query.type ?? rootQueries.get(name)?.view.typeName ?? "Unknown" }])].sort(([a], [b]) => a.localeCompare(b))),
499
+ types: Object.fromEntries([...sourcesByType.keys()].sort().map((type) => [type, true]))
500
+ };
501
+ const liveConnections = /* @__PURE__ */ new Map();
502
+ const encoder = new TextEncoder();
503
+ const cleanupLiveConnection = (connectionId) => {
504
+ const connection = liveConnections.get(connectionId);
505
+ if (!connection) return;
506
+ connection.closed = true;
507
+ liveConnections.delete(connectionId);
508
+ for (const subscription of connection.subscriptions.values()) subscription.abort();
509
+ connection.subscriptions.clear();
510
+ if (connection.drainInterval) {
511
+ clearInterval(connection.drainInterval);
512
+ connection.drainInterval = void 0;
513
+ }
514
+ if (connection.drainTimeout) {
515
+ clearTimeout(connection.drainTimeout);
516
+ connection.drainTimeout = void 0;
517
+ }
518
+ if (connection.heartbeat) {
519
+ clearInterval(connection.heartbeat);
520
+ connection.heartbeat = void 0;
521
+ }
522
+ try {
523
+ connection.controller?.close();
524
+ } catch {}
525
+ connection.abort();
526
+ };
527
+ const sendLiveMessage = (connection, message, eventId) => {
528
+ if (connection.closed || !connection.controller) return;
529
+ try {
530
+ connection.controller.enqueue(encoder.encode(sse(message, eventId)));
531
+ } catch {
532
+ connection.closed = true;
533
+ }
534
+ };
535
+ const sendLiveComment = (connection, message) => {
536
+ if (connection.closed || !connection.controller) return;
537
+ try {
538
+ connection.controller.enqueue(encoder.encode(sseComment(message)));
539
+ } catch {
540
+ connection.closed = true;
541
+ }
542
+ };
543
+ const drainLiveConnection = async (connection) => {
544
+ if (connection.closed || connection.draining) return;
545
+ connection.draining = true;
546
+ let activeOperationId = null;
547
+ try {
548
+ while (!connection.closed && connection.queue.length > 0) {
549
+ const item = connection.queue.shift();
550
+ activeOperationId = item.operation.id;
551
+ if (!item.subscription.active || connection.subscriptions.get(item.operation.id) !== item.subscription) continue;
552
+ if (item.kind === "entity") {
553
+ const { event, operation, source } = item;
554
+ const select = filterLiveSelection(source, operation.select, event.changed);
555
+ if (select?.length === 0) continue;
556
+ const input = {
557
+ args: operation.args,
558
+ select: select ?? operation.select
559
+ };
560
+ sendLiveMessage(connection, {
561
+ event: livePayload(event, event.type === "delete" ? null : await resolveLiveSourceData({
562
+ ctx: connection.ctx,
563
+ data: event.data,
564
+ id: event.id,
565
+ input,
566
+ registry: sources.registry,
567
+ source
568
+ }), select ?? void 0),
569
+ id: operation.id,
570
+ kind: "next"
571
+ }, event.eventId);
572
+ continue;
573
+ }
574
+ const { event, operation } = item;
575
+ const nodeType = event.nodeType ?? operation.type;
576
+ const source = nodeType ? sourcesByType.get(nodeType) : null;
577
+ const node = event.type === "deleteEdge" || event.type === "invalidate" ? null : source ? await resolveLiveSourceData({
578
+ ctx: connection.ctx,
579
+ data: event.node,
580
+ id: event.id,
581
+ input: {
582
+ args: operation.selectionArgs,
583
+ select: operation.select
584
+ },
585
+ registry: sources.registry,
586
+ source
587
+ }) : null;
588
+ if (event.type !== "invalidate" && (!nodeType || event.type !== "deleteEdge" && node == null)) throw new FateRequestError("NOT_FOUND", `No source registered for live connection node type '${nodeType}'.`);
589
+ sendLiveMessage(connection, {
590
+ event: liveConnectionPayload(event, node),
591
+ id: operation.id,
592
+ kind: "connection"
593
+ }, event.eventId);
594
+ }
595
+ const now = Date.now();
596
+ if (!connection.closed && now - connection.lastHeartbeat >= 25e3) {
597
+ connection.lastHeartbeat = now;
598
+ sendLiveComment(connection, "heartbeat");
599
+ }
600
+ } catch (error) {
601
+ sendLiveMessage(connection, {
602
+ error: toProtocolError(error),
603
+ id: activeOperationId ?? "live",
604
+ kind: "error"
605
+ });
606
+ } finally {
607
+ connection.draining = false;
403
608
  }
404
- if (current && typeof current === "object") {
405
- const resolved = await resolveNode({
406
- item: current,
407
- node: relationNode,
408
- options: resolverOptions
609
+ };
610
+ const scheduleLiveDrain = (connection) => {
611
+ if (connection.closed || connection.drainTimeout) return;
612
+ connection.drainTimeout = setTimeout(() => {
613
+ connection.drainTimeout = void 0;
614
+ drainLiveConnection(connection);
615
+ }, 0);
616
+ };
617
+ const subscribeLiveOperation = (connection, operation, waitUntil) => {
618
+ const source = sourcesByType.get(operation.type);
619
+ if (!source) throw new FateRequestError("NOT_FOUND", `No source registered for '${operation.type}'.`);
620
+ connection.subscriptions.get(operation.id)?.abort();
621
+ const subscriptionController = new AbortController();
622
+ const subscription = {
623
+ abort: () => {
624
+ subscription.active = false;
625
+ subscriptionController.abort();
626
+ },
627
+ active: true
628
+ };
629
+ connection.subscriptions.set(operation.id, subscription);
630
+ const handleEvent = async (event) => {
631
+ try {
632
+ const select = filterLiveSelection(source, operation.select, event.changed);
633
+ if (select?.length === 0) return;
634
+ const input = {
635
+ args: operation.args,
636
+ select: select ?? operation.select
637
+ };
638
+ sendLiveMessage(connection, {
639
+ event: livePayload(event, event.type === "delete" ? null : await resolveLiveSourceData({
640
+ ctx: connection.ctx,
641
+ data: event.data,
642
+ id: event.id,
643
+ input,
644
+ registry: sources.registry,
645
+ source
646
+ }), select ?? void 0),
647
+ id: operation.id,
648
+ kind: "next"
649
+ }, event.eventId);
650
+ } catch (error) {
651
+ if (!subscriptionController.signal.aborted) sendLiveMessage(connection, {
652
+ error: toProtocolError(error),
653
+ id: operation.id,
654
+ kind: "error"
655
+ });
656
+ }
657
+ };
658
+ if (liveBus.listen) {
659
+ const unsubscribe = liveBus.listen(operation.type, operation.entityId, (event) => {
660
+ connection.queue.push({
661
+ event,
662
+ kind: "entity",
663
+ operation,
664
+ source,
665
+ subscription
666
+ });
667
+ scheduleLiveDrain(connection);
668
+ }, {
669
+ lastEventId: operation.lastEventId,
670
+ signal: subscriptionController.signal
409
671
  });
410
- if (resolved !== current) assign(field, resolved);
672
+ const abort = () => {
673
+ subscription.active = false;
674
+ unsubscribe();
675
+ subscriptionController.abort();
676
+ };
677
+ subscription.abort = abort;
678
+ connection.subscriptions.set(operation.id, subscription);
679
+ return;
411
680
  }
412
- }
413
- return result ?? item;
414
- };
415
- /**
416
- * Builds a resolver that applies a client's selection to a server data view,
417
- * filtering fields, running nested resolvers, and shaping selects.
418
- */
419
- function createResolver({ args: args$1, ctx, select: initialSelect, view }) {
420
- const allowedPaths = /* @__PURE__ */ new Set();
421
- const selectedPaths = /* @__PURE__ */ new Set();
422
- selectedPaths.add("id");
423
- const root = createSelectedNode(view, null);
424
- for (const path of initialSelect) {
425
- if (!path) continue;
426
- assignPath(root, path.split("."), null, selectedPaths, view, allowedPaths);
427
- }
428
- const select = prismaSelect([...allowedPaths], args$1);
429
- collectResolvers(root, select, args$1, ctx);
430
- const resolve = async (item) => toConnectionResult({
431
- args: args$1,
432
- item: filterToViewFields(await resolveNode({
433
- item,
434
- node: root,
435
- options: {
436
- args: args$1,
437
- context: ctx
681
+ const task = (async () => {
682
+ try {
683
+ const iterable = liveBus.subscribe(operation.type, operation.entityId, {
684
+ lastEventId: operation.lastEventId,
685
+ signal: subscriptionController.signal
686
+ });
687
+ for await (const [event] of iterable) await handleEvent(event);
688
+ } catch (error) {
689
+ if (!subscriptionController.signal.aborted) sendLiveMessage(connection, {
690
+ error: toProtocolError(error),
691
+ id: operation.id,
692
+ kind: "error"
693
+ });
694
+ } finally {
695
+ if (connection.subscriptions.get(operation.id) === subscription) connection.subscriptions.delete(operation.id);
438
696
  }
439
- }), root.view, selectedPaths),
440
- view: root.view
441
- });
697
+ })();
698
+ waitUntil?.(task);
699
+ };
700
+ const subscribeLiveConnectionOperation = (connection, operation, waitUntil) => {
701
+ connection.subscriptions.get(operation.id)?.abort();
702
+ const subscriptionController = new AbortController();
703
+ const subscription = {
704
+ abort: () => {
705
+ subscription.active = false;
706
+ subscriptionController.abort();
707
+ },
708
+ active: true
709
+ };
710
+ connection.subscriptions.set(operation.id, subscription);
711
+ const handleEvent = async (event) => {
712
+ try {
713
+ const nodeType = event.nodeType ?? operation.type;
714
+ const source = nodeType ? sourcesByType.get(nodeType) : null;
715
+ const node = event.type === "deleteEdge" || event.type === "invalidate" ? null : source ? await resolveLiveSourceData({
716
+ ctx: connection.ctx,
717
+ data: event.node,
718
+ id: event.id,
719
+ input: {
720
+ args: operation.selectionArgs,
721
+ select: operation.select
722
+ },
723
+ registry: sources.registry,
724
+ source
725
+ }) : null;
726
+ if (event.type !== "invalidate" && (!nodeType || event.type !== "deleteEdge" && node == null)) throw new FateRequestError("NOT_FOUND", `No source registered for live connection node type '${nodeType}'.`);
727
+ sendLiveMessage(connection, {
728
+ event: liveConnectionPayload(event, node),
729
+ id: operation.id,
730
+ kind: "connection"
731
+ }, event.eventId);
732
+ } catch (error) {
733
+ if (!subscriptionController.signal.aborted) sendLiveMessage(connection, {
734
+ error: toProtocolError(error),
735
+ id: operation.id,
736
+ kind: "error"
737
+ });
738
+ }
739
+ };
740
+ if (liveBus.listenConnection) {
741
+ const unsubscribe = liveBus.listenConnection({
742
+ args: operation.args,
743
+ procedure: operation.procedure
744
+ }, (event) => {
745
+ connection.queue.push({
746
+ event,
747
+ kind: "connection",
748
+ operation,
749
+ subscription
750
+ });
751
+ scheduleLiveDrain(connection);
752
+ }, {
753
+ lastEventId: operation.lastEventId,
754
+ signal: subscriptionController.signal
755
+ });
756
+ const abort = () => {
757
+ subscription.active = false;
758
+ unsubscribe();
759
+ subscriptionController.abort();
760
+ };
761
+ subscription.abort = abort;
762
+ connection.subscriptions.set(operation.id, subscription);
763
+ return;
764
+ }
765
+ const task = (async () => {
766
+ try {
767
+ const iterable = liveBus.subscribeConnection({
768
+ args: operation.args,
769
+ procedure: operation.procedure
770
+ }, {
771
+ lastEventId: operation.lastEventId,
772
+ signal: subscriptionController.signal
773
+ });
774
+ for await (const [event] of iterable) await handleEvent(event);
775
+ } catch (error) {
776
+ if (!subscriptionController.signal.aborted) sendLiveMessage(connection, {
777
+ error: toProtocolError(error),
778
+ id: operation.id,
779
+ kind: "error"
780
+ });
781
+ } finally {
782
+ if (connection.subscriptions.get(operation.id) === subscription) connection.subscriptions.delete(operation.id);
783
+ }
784
+ })();
785
+ waitUntil?.(task);
786
+ };
787
+ const controlLiveConnection = async (request, adapterContext) => {
788
+ const body = assertLiveControlRequest(await parseJSON(request));
789
+ const connection = liveConnections.get(body.connectionId);
790
+ if (!connection || connection.closed) throw new FateRequestError("NOT_FOUND", "Live connection not found.");
791
+ const results = [];
792
+ const waitUntil = getWaitUntil(adapterContext);
793
+ for (const operation of body.operations) try {
794
+ if (operation.kind === "subscribe") subscribeLiveOperation(connection, operation, waitUntil);
795
+ else if (operation.kind === "subscribeConnection") subscribeLiveConnectionOperation(connection, operation, waitUntil);
796
+ else {
797
+ connection.subscriptions.get(operation.id)?.abort();
798
+ connection.subscriptions.delete(operation.id);
799
+ }
800
+ results.push({
801
+ data: null,
802
+ id: operation.id,
803
+ ok: true
804
+ });
805
+ } catch (error) {
806
+ results.push({
807
+ error: toProtocolError(error),
808
+ id: operation.id,
809
+ ok: false
810
+ });
811
+ }
812
+ return {
813
+ results,
814
+ version: 1
815
+ };
816
+ };
442
817
  return {
443
- resolve,
444
- resolveMany: (items) => Promise.all(items.map((item) => resolve(item))),
445
- select
818
+ async handleLiveRequest(request, adapterContext) {
819
+ try {
820
+ if (!liveBus) throw new FateRequestError("NOT_FOUND", "Live views are not enabled.");
821
+ if (request.method === "POST") return Response.json(await controlLiveConnection(request, adapterContext), { headers: jsonHeaders });
822
+ if (request.method !== "GET") throw new FateRequestError("BAD_REQUEST", "Invalid Fate live request.");
823
+ const connectionId = new URL(request.url).searchParams.get("connectionId");
824
+ if (!connectionId) throw new FateRequestError("BAD_REQUEST", "Invalid Fate live request.");
825
+ const ctx = await getContext(request, adapterContext);
826
+ const liveController = new AbortController();
827
+ const abortLive = () => liveController.abort();
828
+ if (request.signal.aborted) abortLive();
829
+ else request.signal.addEventListener("abort", abortLive, { once: true });
830
+ cleanupLiveConnection(connectionId);
831
+ const stream = new ReadableStream({
832
+ cancel() {
833
+ cleanupLiveConnection(connectionId);
834
+ },
835
+ pull() {
836
+ const connection = liveConnections.get(connectionId);
837
+ if (connection) drainLiveConnection(connection);
838
+ },
839
+ start(controller) {
840
+ const connection = {
841
+ abort: abortLive,
842
+ closed: false,
843
+ controller,
844
+ ctx,
845
+ lastHeartbeat: Date.now(),
846
+ queue: [],
847
+ subscriptions: /* @__PURE__ */ new Map()
848
+ };
849
+ connection.drainInterval = setInterval(() => void drainLiveConnection(connection), 100);
850
+ liveConnections.set(connectionId, connection);
851
+ controller.enqueue(encoder.encode(sseComment("connected")));
852
+ }
853
+ });
854
+ liveController.signal.addEventListener("abort", () => {
855
+ request.signal.removeEventListener("abort", abortLive);
856
+ cleanupLiveConnection(connectionId);
857
+ }, { once: true });
858
+ return new Response(stream, { headers: sseHeaders });
859
+ } catch (error) {
860
+ const protocolError = toProtocolError(error);
861
+ return Response.json({
862
+ results: [{
863
+ error: protocolError,
864
+ id: "live",
865
+ ok: false
866
+ }],
867
+ version: 1
868
+ }, {
869
+ headers: jsonHeaders,
870
+ status: error instanceof FateRequestError ? error.status : 500
871
+ });
872
+ }
873
+ },
874
+ async handleRequest(request, adapterContext) {
875
+ try {
876
+ const body = assertProtocolRequest(await parseJSON(request));
877
+ const ctx = await getContext(request, adapterContext);
878
+ const results = await Promise.all(body.operations.map((operation) => executeOperation(operation, ctx)));
879
+ return Response.json({
880
+ results,
881
+ version: 1
882
+ }, { headers: jsonHeaders });
883
+ } catch (error) {
884
+ const protocolError = toProtocolError(error);
885
+ return Response.json({
886
+ results: [{
887
+ error: protocolError,
888
+ id: "request",
889
+ ok: false
890
+ }],
891
+ version: 1
892
+ }, {
893
+ headers: jsonHeaders,
894
+ status: error instanceof FateRequestError ? error.status : 500
895
+ });
896
+ }
897
+ },
898
+ manifest
899
+ };
900
+ }
901
+ function createFateFetchHandler(server) {
902
+ return (request, adapterContext) => {
903
+ return new URL(request.url).pathname.endsWith("/live") ? server.handleLiveRequest(request, adapterContext) : server.handleRequest(request, adapterContext);
904
+ };
905
+ }
906
+ function createHonoFateHandler(server) {
907
+ return (context) => {
908
+ return new URL(context.req.raw.url).pathname.endsWith("/live") ? server.handleLiveRequest(context.req.raw, context) : server.handleRequest(context.req.raw, context);
446
909
  };
447
910
  }
448
-
449
- //#endregion
450
- //#region src/server/input.ts
451
- const byIdInput = z.object({
452
- args: connectionArgs,
453
- ids: z.array(z.string().min(1)).nonempty(),
454
- select: z.array(z.string())
455
- });
456
-
457
911
  //#endregion
458
- export { byIdInput, connectionArgs, createResolver, dataView, list, resolver, withConnection };
912
+ export { FateRequestError, bindSourceProcedures, byIdInput, computed, connectionArgs, count, createFateFetchHandler, createFateServer, createHonoFateHandler, createLiveEventBus, createNestedSourcePlan, createResolver, createSourcePlan, createSourceProcedures, dataView, field, getNestedSelection, getScopedArgs, hasNestedSelection, isRecord, list, liveConnectionTopic, liveEntityTopic, liveGlobalConnectionTopic, refetchSourceById, resolveSourceById, resolveSourceByIds, resolveSourceConnection, resolver, toPrismaSelect, withConnection };