ignotum 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -0
- package/dist/cli/bin.d.mts +1 -0
- package/dist/cli/bin.mjs +1696 -0
- package/dist/cli/bin.mjs.map +1 -0
- package/dist/runtime/api-BXXIZc_Q.js +119 -0
- package/dist/runtime/api-BXXIZc_Q.js.map +1 -0
- package/dist/runtime/api-Cpx57mk3.d.ts +47 -0
- package/dist/runtime/client/jsx-dev-runtime.d.ts +2 -0
- package/dist/runtime/client/jsx-dev-runtime.js +2 -0
- package/dist/runtime/client/jsx-runtime.d.ts +2 -0
- package/dist/runtime/client/jsx-runtime.js +2 -0
- package/dist/runtime/client.d.ts +29 -0
- package/dist/runtime/client.js +364 -0
- package/dist/runtime/client.js.map +1 -0
- package/dist/runtime/index-BgWROoyk.d.ts +249 -0
- package/dist/runtime/internal/api.d.ts +2 -0
- package/dist/runtime/internal/api.js +2 -0
- package/dist/runtime/internal/server.d.ts +6 -0
- package/dist/runtime/internal/server.js +7 -0
- package/dist/runtime/internal/server.js.map +1 -0
- package/dist/runtime/internal/types.d.ts +2 -0
- package/dist/runtime/internal/types.js +2 -0
- package/dist/runtime/result-B2W-z2wG.js +136 -0
- package/dist/runtime/result-B2W-z2wG.js.map +1 -0
- package/dist/runtime/result-C1ZdsM6Y.d.ts +106 -0
- package/dist/runtime/schema-CNEVLF7D.js +116 -0
- package/dist/runtime/schema-CNEVLF7D.js.map +1 -0
- package/dist/runtime/server.d.ts +9 -0
- package/dist/runtime/server.js +9 -0
- package/dist/runtime/server.js.map +1 -0
- package/package.json +81 -2
- package/src/cli/agent-files.ts +35 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/client-plugin.ts +66 -0
- package/src/cli/codegen.ts +203 -0
- package/src/cli/command.ts +155 -0
- package/src/cli/dev.ts +206 -0
- package/src/cli/new-project.ts +377 -0
- package/src/cli/package-manager.ts +55 -0
- package/src/client/errors.ts +83 -0
- package/src/client/hooks.ts +141 -0
- package/src/client/index.ts +90 -0
- package/src/client/jsx-dev-runtime.ts +2 -0
- package/src/client/jsx-runtime.ts +2 -0
- package/src/client/query.ts +17 -0
- package/src/client/sync.ts +487 -0
- package/src/dev-runtime/database.ts +374 -0
- package/src/dev-runtime/dev-database.ts +199 -0
- package/src/dev-runtime/functions.ts +293 -0
- package/src/dev-runtime/id.ts +11 -0
- package/src/dev-runtime/sync.ts +473 -0
- package/src/internal/api.ts +141 -0
- package/src/internal/http-paths.ts +5 -0
- package/src/internal/server.ts +3 -0
- package/src/internal/types.ts +2 -0
- package/src/raw.d.ts +4 -0
- package/src/server/index.ts +8 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Result as contractResult } from "@ignotum/contracts/runtime/result";
|
|
2
|
+
import type {
|
|
3
|
+
ErrorValue,
|
|
4
|
+
QueryResult as ContractQueryResult,
|
|
5
|
+
SettledResult,
|
|
6
|
+
} from "@ignotum/contracts/runtime/result";
|
|
7
|
+
|
|
8
|
+
export const Result = { match: contractResult.match };
|
|
9
|
+
export type Result<Value, Error extends ErrorValue> = SettledResult<Value, Error>;
|
|
10
|
+
export type QueryResult<Value, Error extends ErrorValue> = ContractQueryResult<Value, Error>;
|
|
11
|
+
export type { InternalServerError } from "@ignotum/contracts/runtime/result";
|
|
12
|
+
export { useMutation, useQuery } from "./hooks.js";
|
|
13
|
+
export { Query } from "./query.js";
|
|
14
|
+
export type { QuerySkip } from "./query.js";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
Component,
|
|
18
|
+
Fragment,
|
|
19
|
+
cloneElement,
|
|
20
|
+
createContext,
|
|
21
|
+
createElement,
|
|
22
|
+
createRef,
|
|
23
|
+
h,
|
|
24
|
+
isValidElement,
|
|
25
|
+
toChildArray,
|
|
26
|
+
} from "preact";
|
|
27
|
+
export type {
|
|
28
|
+
AnyComponent,
|
|
29
|
+
Attributes,
|
|
30
|
+
ClassAttributes,
|
|
31
|
+
ComponentChild,
|
|
32
|
+
ComponentChildren,
|
|
33
|
+
ComponentClass,
|
|
34
|
+
ComponentConstructor,
|
|
35
|
+
ComponentFactory,
|
|
36
|
+
ComponentProps,
|
|
37
|
+
ComponentType,
|
|
38
|
+
Consumer,
|
|
39
|
+
Context,
|
|
40
|
+
ContextType,
|
|
41
|
+
ErrorInfo,
|
|
42
|
+
FunctionComponent,
|
|
43
|
+
FunctionalComponent,
|
|
44
|
+
JSX,
|
|
45
|
+
Key,
|
|
46
|
+
PreactContext,
|
|
47
|
+
PreactConsumer,
|
|
48
|
+
PreactDOMAttributes,
|
|
49
|
+
PreactProvider,
|
|
50
|
+
Provider,
|
|
51
|
+
Ref,
|
|
52
|
+
RefCallback,
|
|
53
|
+
RefObject,
|
|
54
|
+
RenderableProps,
|
|
55
|
+
TargetedAnimationEvent,
|
|
56
|
+
TargetedClipboardEvent,
|
|
57
|
+
TargetedCommandEvent,
|
|
58
|
+
TargetedCompositionEvent,
|
|
59
|
+
TargetedDragEvent,
|
|
60
|
+
TargetedEvent,
|
|
61
|
+
TargetedFocusEvent,
|
|
62
|
+
TargetedInputEvent,
|
|
63
|
+
TargetedKeyboardEvent,
|
|
64
|
+
TargetedMouseEvent,
|
|
65
|
+
TargetedPictureInPictureEvent,
|
|
66
|
+
TargetedPointerEvent,
|
|
67
|
+
TargetedSnapEvent,
|
|
68
|
+
TargetedSubmitEvent,
|
|
69
|
+
TargetedToggleEvent,
|
|
70
|
+
TargetedTouchEvent,
|
|
71
|
+
TargetedTransitionEvent,
|
|
72
|
+
TargetedUIEvent,
|
|
73
|
+
TargetedWheelEvent,
|
|
74
|
+
VNode,
|
|
75
|
+
} from "preact";
|
|
76
|
+
export {
|
|
77
|
+
useCallback,
|
|
78
|
+
useContext,
|
|
79
|
+
useDebugValue,
|
|
80
|
+
useEffect,
|
|
81
|
+
useErrorBoundary,
|
|
82
|
+
useId,
|
|
83
|
+
useImperativeHandle,
|
|
84
|
+
useLayoutEffect,
|
|
85
|
+
useMemo,
|
|
86
|
+
useReducer,
|
|
87
|
+
useRef,
|
|
88
|
+
useState,
|
|
89
|
+
} from "preact/hooks";
|
|
90
|
+
export type { Dispatch, Reducer, StateUpdater } from "preact/hooks";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const QuerySkipTypeId: unique symbol = Symbol.for("ignotum/client/QuerySkip");
|
|
2
|
+
|
|
3
|
+
export interface QuerySkip {
|
|
4
|
+
readonly [QuerySkipTypeId]: typeof QuerySkipTypeId;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
class QuerySkipToken implements QuerySkip {
|
|
8
|
+
readonly [QuerySkipTypeId]: typeof QuerySkipTypeId = QuerySkipTypeId;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const skip: QuerySkip = Object.freeze(new QuerySkipToken());
|
|
12
|
+
|
|
13
|
+
export const Query = { skip };
|
|
14
|
+
|
|
15
|
+
export const isQuerySkip = <Value extends object | undefined>(
|
|
16
|
+
value: Value,
|
|
17
|
+
): value is Value & QuerySkip => value === skip;
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
Deferred,
|
|
4
|
+
Duration,
|
|
5
|
+
Effect,
|
|
6
|
+
HashMap,
|
|
7
|
+
Layer,
|
|
8
|
+
ManagedRuntime,
|
|
9
|
+
Predicate,
|
|
10
|
+
Schedule,
|
|
11
|
+
Schema,
|
|
12
|
+
} from "effect";
|
|
13
|
+
import * as Socket from "effect/unstable/socket/Socket";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ErrorValue,
|
|
17
|
+
QueryResult,
|
|
18
|
+
Result as IgnotumResult,
|
|
19
|
+
} from "@ignotum/contracts/runtime/result";
|
|
20
|
+
import { failureFromWire, pending, Result } from "@ignotum/contracts/runtime/result";
|
|
21
|
+
import {
|
|
22
|
+
ClientMessageJson,
|
|
23
|
+
RequestId,
|
|
24
|
+
ServerMessageJson,
|
|
25
|
+
SubscriptionId,
|
|
26
|
+
decodeTransportValue,
|
|
27
|
+
type ClientMessage,
|
|
28
|
+
type FunctionAddress,
|
|
29
|
+
type WireResult,
|
|
30
|
+
} from "@ignotum/contracts/runtime/sync";
|
|
31
|
+
import {
|
|
32
|
+
ClientInfrastructureError,
|
|
33
|
+
ConnectionUnavailable,
|
|
34
|
+
InvalidClientMessage,
|
|
35
|
+
InvalidServerMessage,
|
|
36
|
+
MutationOutcomeUnknown,
|
|
37
|
+
ServerProtocolError,
|
|
38
|
+
clientErrorReporterLayer,
|
|
39
|
+
reportClientError,
|
|
40
|
+
} from "./errors.js";
|
|
41
|
+
import { syncPath } from "../internal/http-paths.js";
|
|
42
|
+
|
|
43
|
+
type Listener = () => void;
|
|
44
|
+
type SocketWriter = (chunk: string) => Effect.Effect<void, Socket.SocketError>;
|
|
45
|
+
|
|
46
|
+
interface QueryEntry {
|
|
47
|
+
readonly args: Schema.Json;
|
|
48
|
+
readonly function: FunctionAddress;
|
|
49
|
+
readonly listeners: Set<Listener>;
|
|
50
|
+
readonly subscriptionId: SubscriptionId;
|
|
51
|
+
generation: number;
|
|
52
|
+
references: number;
|
|
53
|
+
result: QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface QueryObserver {
|
|
57
|
+
readonly getSnapshot: () => QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
|
|
58
|
+
readonly release: Effect.Effect<void>;
|
|
59
|
+
readonly subscribe: (listener: Listener) => () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type QueryLifecycleMessage = Extract<ClientMessage, { readonly type: "Subscribe" | "Unsubscribe" }>;
|
|
63
|
+
|
|
64
|
+
interface QueryCache {
|
|
65
|
+
readonly entries: () => Iterable<QueryEntry>;
|
|
66
|
+
readonly getById: (id: SubscriptionId) => QueryEntry | undefined;
|
|
67
|
+
readonly setError: (id: SubscriptionId, error: ClientInfrastructureError) => void;
|
|
68
|
+
readonly setResult: (id: SubscriptionId, result: QueryResult<unknown, ErrorValue>) => void;
|
|
69
|
+
readonly observe: (
|
|
70
|
+
functionAddress: FunctionAddress,
|
|
71
|
+
args: Schema.Json,
|
|
72
|
+
) => Effect.Effect<QueryObserver>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const canonicalJson = (value: Schema.Json): string => {
|
|
76
|
+
if (
|
|
77
|
+
Predicate.isNull(value) ||
|
|
78
|
+
Predicate.isString(value) ||
|
|
79
|
+
Predicate.isNumber(value) ||
|
|
80
|
+
Predicate.isBoolean(value)
|
|
81
|
+
) {
|
|
82
|
+
return JSON.stringify(value);
|
|
83
|
+
}
|
|
84
|
+
if (globalThis.Array.isArray(value)) {
|
|
85
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `{${Object.entries(value)
|
|
89
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
90
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${canonicalJson(child)}`)
|
|
91
|
+
.join(",")}}`;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const queryKey = (functionAddress: FunctionAddress, args: Schema.Json): string =>
|
|
95
|
+
`${functionAddress}:${canonicalJson(args)}`;
|
|
96
|
+
|
|
97
|
+
const subscribeMessage = (entry: QueryEntry): QueryLifecycleMessage => ({
|
|
98
|
+
type: "Subscribe",
|
|
99
|
+
id: entry.subscriptionId,
|
|
100
|
+
function: entry.function,
|
|
101
|
+
args: entry.args,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const makeQueryCache = (
|
|
105
|
+
allocateId: () => SubscriptionId,
|
|
106
|
+
sendLifecycleMessage: (message: QueryLifecycleMessage) => Effect.Effect<void>,
|
|
107
|
+
) => {
|
|
108
|
+
const queries = new Map<string, QueryEntry>();
|
|
109
|
+
const queryKeysById = new Map<SubscriptionId, string>();
|
|
110
|
+
|
|
111
|
+
const observe = Effect.fn("SyncClient.QueryCache.observe")(function* (
|
|
112
|
+
functionAddress: FunctionAddress,
|
|
113
|
+
args: Schema.Json,
|
|
114
|
+
) {
|
|
115
|
+
const key = queryKey(functionAddress, args);
|
|
116
|
+
let entry = queries.get(key);
|
|
117
|
+
|
|
118
|
+
if (entry === undefined) {
|
|
119
|
+
const created: QueryEntry = {
|
|
120
|
+
args,
|
|
121
|
+
function: functionAddress,
|
|
122
|
+
generation: 0,
|
|
123
|
+
listeners: new Set(),
|
|
124
|
+
references: 0,
|
|
125
|
+
result: pending(),
|
|
126
|
+
subscriptionId: allocateId(),
|
|
127
|
+
};
|
|
128
|
+
queries.set(key, created);
|
|
129
|
+
queryKeysById.set(created.subscriptionId, key);
|
|
130
|
+
yield* sendLifecycleMessage(subscribeMessage(created));
|
|
131
|
+
entry = created;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const observed = entry;
|
|
135
|
+
observed.references += 1;
|
|
136
|
+
observed.generation += 1;
|
|
137
|
+
let released = false;
|
|
138
|
+
return {
|
|
139
|
+
getSnapshot: () => observed.result,
|
|
140
|
+
subscribe: (listener: Listener) => {
|
|
141
|
+
observed.listeners.add(listener);
|
|
142
|
+
return () => observed.listeners.delete(listener);
|
|
143
|
+
},
|
|
144
|
+
release: Effect.gen(function* () {
|
|
145
|
+
if (released) return;
|
|
146
|
+
released = true;
|
|
147
|
+
observed.references -= 1;
|
|
148
|
+
if (observed.references !== 0) return;
|
|
149
|
+
const generation = ++observed.generation;
|
|
150
|
+
|
|
151
|
+
// Preact and HMR may release and reacquire an observer during one render handoff.
|
|
152
|
+
// Keep the server subscription alive long enough for that handoff to reuse it.
|
|
153
|
+
yield* Effect.sleep("25 millis");
|
|
154
|
+
if (
|
|
155
|
+
observed.references !== 0 ||
|
|
156
|
+
observed.generation !== generation ||
|
|
157
|
+
queries.get(key) !== observed
|
|
158
|
+
) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
queries.delete(key);
|
|
163
|
+
queryKeysById.delete(observed.subscriptionId);
|
|
164
|
+
yield* sendLifecycleMessage({
|
|
165
|
+
type: "Unsubscribe",
|
|
166
|
+
id: observed.subscriptionId,
|
|
167
|
+
});
|
|
168
|
+
}),
|
|
169
|
+
} satisfies QueryObserver;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
entries: () => queries.values(),
|
|
174
|
+
getById: (id: SubscriptionId) => {
|
|
175
|
+
const key = queryKeysById.get(id);
|
|
176
|
+
return key === undefined ? undefined : queries.get(key);
|
|
177
|
+
},
|
|
178
|
+
observe,
|
|
179
|
+
setError: (id, error) => {
|
|
180
|
+
const key = queryKeysById.get(id);
|
|
181
|
+
const entry = key === undefined ? undefined : queries.get(key);
|
|
182
|
+
if (entry === undefined) return;
|
|
183
|
+
entry.result = error;
|
|
184
|
+
for (const listener of entry.listeners) {
|
|
185
|
+
listener();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
setResult: (id, result) => {
|
|
189
|
+
const key = queryKeysById.get(id);
|
|
190
|
+
const entry = key === undefined ? undefined : queries.get(key);
|
|
191
|
+
if (entry === undefined) return;
|
|
192
|
+
entry.result = result;
|
|
193
|
+
for (const listener of entry.listeners) {
|
|
194
|
+
listener();
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
} satisfies QueryCache;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const resultFromWire = Effect.fn("SyncClient.resultFromWire")((wire: WireResult) =>
|
|
201
|
+
Effect.succeed(
|
|
202
|
+
wire.type === "Success"
|
|
203
|
+
? Result.succeed(
|
|
204
|
+
wire.value === undefined ? undefined : decodeTransportValue(wire.value, wire.dates ?? []),
|
|
205
|
+
)
|
|
206
|
+
: failureFromWire(wire.error, wire.dates),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
export const syncClientInternals = { makeQueryCache, queryIdentity: queryKey, resultFromWire };
|
|
211
|
+
|
|
212
|
+
const socketUrl = (): string => {
|
|
213
|
+
const url = new URL(syncPath, globalThis.location.href);
|
|
214
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
215
|
+
return url.href;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
interface SyncClientService {
|
|
219
|
+
readonly mutate: (
|
|
220
|
+
functionAddress: FunctionAddress,
|
|
221
|
+
args: Schema.Json,
|
|
222
|
+
) => Effect.Effect<IgnotumResult<unknown, ErrorValue>, ClientInfrastructureError>;
|
|
223
|
+
readonly observe: (
|
|
224
|
+
functionAddress: FunctionAddress,
|
|
225
|
+
args: Schema.Json,
|
|
226
|
+
) => Effect.Effect<QueryObserver, ClientInfrastructureError>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface PendingMutation {
|
|
230
|
+
readonly deferred: Deferred.Deferred<
|
|
231
|
+
IgnotumResult<unknown, ErrorValue>,
|
|
232
|
+
ClientInfrastructureError
|
|
233
|
+
>;
|
|
234
|
+
readonly function: FunctionAddress;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export class SyncClient extends Context.Service<SyncClient, SyncClientService>()(
|
|
238
|
+
"ignotum/client/sync/SyncClient",
|
|
239
|
+
) {
|
|
240
|
+
static readonly layer = Layer.effect(
|
|
241
|
+
SyncClient,
|
|
242
|
+
Effect.gen(function* () {
|
|
243
|
+
let nextOperationId = 0;
|
|
244
|
+
let mutations = HashMap.empty<RequestId, PendingMutation>();
|
|
245
|
+
let writer: SocketWriter | undefined;
|
|
246
|
+
|
|
247
|
+
const encode = (message: ClientMessage) =>
|
|
248
|
+
Schema.encodeEffect(ClientMessageJson)(message).pipe(
|
|
249
|
+
Effect.mapError((cause) =>
|
|
250
|
+
InvalidClientMessage.make({
|
|
251
|
+
cause,
|
|
252
|
+
message: "Could not encode an Ignotum sync message.",
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const sendWith = (write: SocketWriter, message: ClientMessage) =>
|
|
258
|
+
encode(message).pipe(
|
|
259
|
+
Effect.flatMap(write),
|
|
260
|
+
Effect.mapError((cause) =>
|
|
261
|
+
Schema.is(InvalidClientMessage)(cause)
|
|
262
|
+
? cause
|
|
263
|
+
: ConnectionUnavailable.make({
|
|
264
|
+
cause,
|
|
265
|
+
message: "The sync connection was lost.",
|
|
266
|
+
}),
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const send = (message: ClientMessage) =>
|
|
271
|
+
writer === undefined
|
|
272
|
+
? Effect.fail(
|
|
273
|
+
ConnectionUnavailable.make({ message: "The sync connection is not ready." }),
|
|
274
|
+
)
|
|
275
|
+
: sendWith(writer, message);
|
|
276
|
+
|
|
277
|
+
const queryCache = makeQueryCache(
|
|
278
|
+
() => SubscriptionId.make(nextOperationId++),
|
|
279
|
+
(message) =>
|
|
280
|
+
writer === undefined
|
|
281
|
+
? Effect.void
|
|
282
|
+
: send(message).pipe(
|
|
283
|
+
Effect.catchTags({
|
|
284
|
+
ConnectionUnavailable: reportClientError,
|
|
285
|
+
InvalidClientMessage: reportClientError,
|
|
286
|
+
}),
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const rejectMutation = (requestId: RequestId, error: ClientInfrastructureError) => {
|
|
291
|
+
const pendingMutation = HashMap.getUnsafe(mutations, requestId);
|
|
292
|
+
if (pendingMutation === undefined) {
|
|
293
|
+
return Effect.void;
|
|
294
|
+
}
|
|
295
|
+
mutations = HashMap.remove(mutations, requestId);
|
|
296
|
+
return reportClientError(error).pipe(
|
|
297
|
+
Effect.andThen(Deferred.fail(pendingMutation.deferred, error)),
|
|
298
|
+
Effect.asVoid,
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const handleMessage = Effect.fn("SyncClient.handleMessage")(function* (text: string) {
|
|
303
|
+
const message = yield* Schema.decodeEffect(ServerMessageJson)(text).pipe(
|
|
304
|
+
Effect.mapError((cause) =>
|
|
305
|
+
InvalidServerMessage.make({
|
|
306
|
+
cause,
|
|
307
|
+
message: "The server returned an invalid sync message.",
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
switch (message.type) {
|
|
313
|
+
case "QuerySnapshot": {
|
|
314
|
+
const entry = queryCache.getById(message.id);
|
|
315
|
+
if (entry === undefined) return;
|
|
316
|
+
yield* resultFromWire(message.result).pipe(
|
|
317
|
+
Effect.tap((result) => Effect.sync(() => queryCache.setResult(message.id, result))),
|
|
318
|
+
);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
case "MutationResult": {
|
|
322
|
+
const pendingMutation = HashMap.getUnsafe(mutations, message.id);
|
|
323
|
+
if (pendingMutation === undefined) return;
|
|
324
|
+
mutations = HashMap.remove(mutations, message.id);
|
|
325
|
+
yield* resultFromWire(message.result).pipe(
|
|
326
|
+
Effect.flatMap((result) => Deferred.succeed(pendingMutation.deferred, result)),
|
|
327
|
+
);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
case "ProtocolError": {
|
|
331
|
+
const error =
|
|
332
|
+
message.operation === undefined
|
|
333
|
+
? ServerProtocolError.make({
|
|
334
|
+
code: message.code,
|
|
335
|
+
message: message.message,
|
|
336
|
+
})
|
|
337
|
+
: ServerProtocolError.make({
|
|
338
|
+
code: message.code,
|
|
339
|
+
message: message.message,
|
|
340
|
+
operation: message.operation,
|
|
341
|
+
});
|
|
342
|
+
if (message.operation?.type === "Mutate") {
|
|
343
|
+
yield* rejectMutation(message.operation.id, error);
|
|
344
|
+
} else if (message.operation?.type === "Query") {
|
|
345
|
+
queryCache.setError(message.operation.id, error);
|
|
346
|
+
yield* reportClientError(error);
|
|
347
|
+
} else {
|
|
348
|
+
yield* reportClientError(error);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const disconnect = Effect.gen(function* () {
|
|
356
|
+
writer = undefined;
|
|
357
|
+
const current = mutations;
|
|
358
|
+
mutations = HashMap.empty();
|
|
359
|
+
yield* Effect.forEach(HashMap.entries(current), ([id, pendingMutation]) => {
|
|
360
|
+
const error = MutationOutcomeUnknown.make({
|
|
361
|
+
function: pendingMutation.function,
|
|
362
|
+
id,
|
|
363
|
+
message: "The connection closed before the mutation outcome was known.",
|
|
364
|
+
});
|
|
365
|
+
return reportClientError(error).pipe(
|
|
366
|
+
Effect.andThen(Deferred.fail(pendingMutation.deferred, error)),
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const connect = Effect.scoped(
|
|
372
|
+
Effect.gen(function* () {
|
|
373
|
+
const socket = yield* Socket.makeWebSocket(socketUrl(), {
|
|
374
|
+
closeCodeIsError: () => false,
|
|
375
|
+
});
|
|
376
|
+
const write = yield* socket.writer;
|
|
377
|
+
const onOpen = Effect.gen(function* () {
|
|
378
|
+
writer = write;
|
|
379
|
+
yield* Effect.forEach(queryCache.entries(), (entry) =>
|
|
380
|
+
sendWith(write, subscribeMessage(entry)),
|
|
381
|
+
);
|
|
382
|
+
}).pipe(Effect.tapError(reportClientError), Effect.ignore);
|
|
383
|
+
|
|
384
|
+
yield* socket.runString(handleMessage, { onOpen });
|
|
385
|
+
}),
|
|
386
|
+
).pipe(
|
|
387
|
+
Effect.mapError((cause) =>
|
|
388
|
+
Schema.is(ClientInfrastructureError)(cause)
|
|
389
|
+
? cause
|
|
390
|
+
: ConnectionUnavailable.make({
|
|
391
|
+
cause,
|
|
392
|
+
message: "The sync connection was lost.",
|
|
393
|
+
}),
|
|
394
|
+
),
|
|
395
|
+
Effect.ensuring(disconnect),
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const reconnectSchedule = Schedule.exponential("250 millis").pipe(
|
|
399
|
+
Schedule.modifyDelay(({ duration }) =>
|
|
400
|
+
Effect.succeed(Duration.min(duration, Duration.seconds(5))),
|
|
401
|
+
),
|
|
402
|
+
Schedule.jittered,
|
|
403
|
+
);
|
|
404
|
+
yield* connect.pipe(
|
|
405
|
+
Effect.andThen(
|
|
406
|
+
Effect.fail(
|
|
407
|
+
ConnectionUnavailable.make({
|
|
408
|
+
message: "The sync connection closed; reconnecting.",
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
411
|
+
),
|
|
412
|
+
Effect.tapError(reportClientError),
|
|
413
|
+
Effect.retry(reconnectSchedule),
|
|
414
|
+
Effect.forkScoped,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const observe: SyncClientService["observe"] = Effect.fn("SyncClient.observe")(
|
|
418
|
+
function* (functionAddress, args) {
|
|
419
|
+
return yield* queryCache.observe(functionAddress, args);
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const mutate: SyncClientService["mutate"] = Effect.fn("SyncClient.mutate")(
|
|
424
|
+
function* (functionAddress, args) {
|
|
425
|
+
const requestId = RequestId.make(nextOperationId++);
|
|
426
|
+
const deferred = yield* Deferred.make<
|
|
427
|
+
IgnotumResult<unknown, ErrorValue>,
|
|
428
|
+
ClientInfrastructureError
|
|
429
|
+
>();
|
|
430
|
+
mutations = HashMap.set(mutations, requestId, {
|
|
431
|
+
deferred,
|
|
432
|
+
function: functionAddress,
|
|
433
|
+
});
|
|
434
|
+
const message: ClientMessage = {
|
|
435
|
+
type: "Mutate",
|
|
436
|
+
id: requestId,
|
|
437
|
+
function: functionAddress,
|
|
438
|
+
args,
|
|
439
|
+
};
|
|
440
|
+
const sendMutation = Effect.gen(function* () {
|
|
441
|
+
const mutationWriter = writer;
|
|
442
|
+
if (mutationWriter === undefined) {
|
|
443
|
+
return yield* ConnectionUnavailable.make({
|
|
444
|
+
message: "The sync connection is not ready.",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return yield* sendWith(mutationWriter, message).pipe(
|
|
448
|
+
Effect.mapError((error) =>
|
|
449
|
+
Schema.is(InvalidClientMessage)(error)
|
|
450
|
+
? error
|
|
451
|
+
: MutationOutcomeUnknown.make({
|
|
452
|
+
cause: error,
|
|
453
|
+
function: functionAddress,
|
|
454
|
+
id: requestId,
|
|
455
|
+
message: "The connection failed while sending the mutation.",
|
|
456
|
+
}),
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
yield* sendMutation.pipe(
|
|
461
|
+
Effect.tapError(reportClientError),
|
|
462
|
+
Effect.tapError(() =>
|
|
463
|
+
Effect.sync(() => (mutations = HashMap.remove(mutations, requestId))),
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
return yield* Deferred.await(deferred);
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
return SyncClient.of({ mutate, observe });
|
|
471
|
+
}),
|
|
472
|
+
).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export const syncRuntime = ManagedRuntime.make(
|
|
476
|
+
SyncClient.layer.pipe(Layer.provideMerge(clientErrorReporterLayer)),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// SAFETY: Vite adds this optional property to browser development modules.
|
|
480
|
+
const hot = (
|
|
481
|
+
import.meta as ImportMeta & {
|
|
482
|
+
readonly hot?: { readonly dispose: (cleanup: () => void) => void };
|
|
483
|
+
}
|
|
484
|
+
).hot;
|
|
485
|
+
if (hot !== undefined) {
|
|
486
|
+
hot.dispose(() => void syncRuntime.dispose());
|
|
487
|
+
}
|