ignotum 0.0.11 → 0.0.13
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 +8 -3
- package/dist/cli/bin.mjs +1372 -492
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-DzcR7spt.js → api-CAgKDij7.js} +38 -2
- package/dist/runtime/api-CAgKDij7.js.map +1 -0
- package/dist/runtime/{api-DR_8vfKg.d.ts → api-Cv3hMbzo.d.ts} +4 -3
- package/dist/runtime/client.d.ts +39 -8
- package/dist/runtime/client.js +505 -131
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +154 -0
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +1 -0
- package/dist/runtime/id-Bt9XWRGL.js +423 -0
- package/dist/runtime/id-Bt9XWRGL.js.map +1 -0
- package/dist/runtime/{id-Cs82tq9Q-hmYFyqTa.d.ts → id-BzFHf3Wo-DGPCjgrf.d.ts} +13 -13
- package/dist/runtime/id-Dz0apuB3.d.ts +1 -0
- package/dist/runtime/{index-2q9FwJud.d.ts → index-D54flWtH.d.ts} +136 -76
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +21 -17
- package/dist/runtime/internal/host.js +22 -8
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/{pagination-Bt3l7QaC-BYEGmLBE.d.ts → pagination-DnKg3dkI-r5ZUxBBx.d.ts} +34 -6
- package/dist/runtime/pagination-Dz0apuB3.d.ts +1 -0
- package/dist/runtime/result-DKAA4gpS.d.ts +1 -0
- package/dist/runtime/{schema-ERFjT8-m.js → schema-1Zs03-iS.js} +135 -17
- package/dist/runtime/schema-1Zs03-iS.js.map +1 -0
- package/dist/runtime/server.d.ts +9 -3
- package/dist/runtime/server.js +5 -3
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/{sync-a2EdGSdY.d.ts → sync-Bs8J3fIr.d.ts} +2 -2
- package/package.json +4 -4
- package/src/cli/agent-files.ts +8 -0
- package/src/cli/auth-client.ts +1 -1
- package/src/cli/bin.ts +13 -3
- package/src/cli/build/server.ts +102 -13
- package/src/cli/codegen.ts +7 -4
- package/src/cli/control-client.ts +30 -8
- package/src/cli/deploy.ts +7 -2
- package/src/cli/environment.ts +133 -0
- package/src/client/files.ts +43 -27
- package/src/client/hooks.ts +94 -17
- package/src/client/id.ts +259 -0
- package/src/client/index.ts +5 -2
- package/src/client/page-observers.ts +77 -0
- package/src/client/sync.ts +214 -69
- package/src/dev-runtime/functions.ts +232 -86
- package/src/dev-runtime/id.ts +174 -2
- package/src/dev-runtime/query-cache.ts +105 -0
- package/src/dev-runtime/sync.ts +187 -26
- package/src/server/index.ts +13 -1
- package/dist/runtime/api-DzcR7spt.js.map +0 -1
- package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.js +0 -311
- package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.js.map +0 -1
- package/dist/runtime/file-C1abuMgd.js +0 -173
- package/dist/runtime/file-C1abuMgd.js.map +0 -1
- package/dist/runtime/pagination-CFJ3xlAt.d.ts +0 -1
- package/dist/runtime/schema-ERFjT8-m.js.map +0 -1
package/src/client/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type {
|
|
|
16
16
|
FileValue,
|
|
17
17
|
} from "@ignotum/contracts/schema/file";
|
|
18
18
|
export { Files } from "./files.js";
|
|
19
|
-
export { useMutation, usePaginatedQuery, useQuery } from "./hooks.js";
|
|
19
|
+
export { useId, useMutation, usePaginatedQuery, useQuery } from "./hooks.js";
|
|
20
20
|
export type { PaginatedQueryOptions, PaginatedQueryValue, PaginationStatus } from "./hooks.js";
|
|
21
21
|
export { Query } from "./query.js";
|
|
22
22
|
export type { QuerySkip } from "./query.js";
|
|
@@ -89,7 +89,7 @@ export {
|
|
|
89
89
|
useDebugValue,
|
|
90
90
|
useEffect,
|
|
91
91
|
useErrorBoundary,
|
|
92
|
-
useId,
|
|
92
|
+
useId as useElementId,
|
|
93
93
|
useImperativeHandle,
|
|
94
94
|
useLayoutEffect,
|
|
95
95
|
useMemo,
|
|
@@ -98,3 +98,6 @@ export {
|
|
|
98
98
|
useState,
|
|
99
99
|
} from "preact/hooks";
|
|
100
100
|
export type { Dispatch, Reducer, StateUpdater } from "preact/hooks";
|
|
101
|
+
|
|
102
|
+
export type { User, UserId, ProfileField } from "@ignotum/contracts/id";
|
|
103
|
+
export type { IdResult, IdOptions } from "./hooks.js";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { FunctionAddress } from "@ignotum/contracts/runtime/sync";
|
|
4
|
+
import { SyncClient, syncClientInternals } from "./sync.js";
|
|
5
|
+
import type { ClientInfrastructureError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
type QueryObserver = Effect.Success<ReturnType<SyncClient["Service"]["observe"]>>;
|
|
8
|
+
|
|
9
|
+
interface PageEntry {
|
|
10
|
+
readonly acquire: Effect.Effect<QueryObserver | undefined, ClientInfrastructureError, SyncClient>;
|
|
11
|
+
release: (() => void) | undefined;
|
|
12
|
+
released: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// @effect-diagnostics-next-line missingPipeableSignature:off This hook-owned lifecycle takes two callbacks, not a data argument.
|
|
16
|
+
export const makePageObservers = (
|
|
17
|
+
onChange: () => void,
|
|
18
|
+
releaseObserver: (release: Effect.Effect<void>) => void,
|
|
19
|
+
) => {
|
|
20
|
+
const pages = new Map<string, PageEntry>();
|
|
21
|
+
let disposed = false;
|
|
22
|
+
|
|
23
|
+
const releaseEntry = (entry: PageEntry) => {
|
|
24
|
+
entry.released = true;
|
|
25
|
+
entry.release?.();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const observe = Effect.fn("PageObservers.observe")(function* (
|
|
29
|
+
functionAddress: FunctionAddress,
|
|
30
|
+
args: Schema.Json,
|
|
31
|
+
) {
|
|
32
|
+
if (disposed) return;
|
|
33
|
+
const key = syncClientInternals.queryIdentity(functionAddress, args);
|
|
34
|
+
const existing = pages.get(key);
|
|
35
|
+
if (existing !== undefined) return yield* existing.acquire;
|
|
36
|
+
|
|
37
|
+
const entry: PageEntry = {
|
|
38
|
+
acquire: yield* Effect.cached(
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const client = yield* SyncClient;
|
|
41
|
+
const observer = yield* client.observe(functionAddress, args);
|
|
42
|
+
if (entry.released) {
|
|
43
|
+
releaseObserver(observer.release);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const unsubscribe = observer.subscribe(onChange);
|
|
47
|
+
entry.release = () => {
|
|
48
|
+
unsubscribe();
|
|
49
|
+
releaseObserver(observer.release);
|
|
50
|
+
};
|
|
51
|
+
return observer;
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
release: undefined,
|
|
55
|
+
released: false,
|
|
56
|
+
};
|
|
57
|
+
pages.set(key, entry);
|
|
58
|
+
return yield* entry.acquire;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
observe,
|
|
63
|
+
retain: (keys: ReadonlySet<string>) => {
|
|
64
|
+
for (const [key, entry] of pages) {
|
|
65
|
+
if (keys.has(key)) continue;
|
|
66
|
+
pages.delete(key);
|
|
67
|
+
releaseEntry(entry);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
release: () => {
|
|
71
|
+
if (disposed) return;
|
|
72
|
+
disposed = true;
|
|
73
|
+
for (const entry of pages.values()) releaseEntry(entry);
|
|
74
|
+
pages.clear();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
package/src/client/sync.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BrowserSession } from "./id.js";
|
|
1
2
|
import {
|
|
2
3
|
Context,
|
|
3
4
|
Deferred,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
HashMap,
|
|
8
9
|
Layer,
|
|
9
10
|
ManagedRuntime,
|
|
11
|
+
Option,
|
|
10
12
|
Schedule,
|
|
11
13
|
Schema,
|
|
12
14
|
} from "effect";
|
|
@@ -59,6 +61,7 @@ interface QueryEntry {
|
|
|
59
61
|
readonly listeners: Set<Listener>;
|
|
60
62
|
readonly subscriptionId: SubscriptionId;
|
|
61
63
|
generation: number;
|
|
64
|
+
dependsOnId?: boolean;
|
|
62
65
|
revision: AppStateRevision | undefined;
|
|
63
66
|
references: number;
|
|
64
67
|
result: QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
|
|
@@ -73,13 +76,14 @@ interface QueryObserver {
|
|
|
73
76
|
type QueryLifecycleMessage = Extract<ClientMessage, { readonly type: "Subscribe" | "Unsubscribe" }>;
|
|
74
77
|
|
|
75
78
|
interface QueryCache {
|
|
79
|
+
readonly invalidate: (ids?: ReadonlyArray<SubscriptionId>) => void;
|
|
80
|
+
readonly invalidatePrivate: () => void;
|
|
76
81
|
readonly entries: () => Iterable<QueryEntry>;
|
|
77
82
|
readonly getById: (id: SubscriptionId) => QueryEntry | undefined;
|
|
78
|
-
readonly setError: (id: SubscriptionId, error: ClientInfrastructureError) => void;
|
|
79
83
|
readonly setResult: (
|
|
80
84
|
id: SubscriptionId,
|
|
81
|
-
result: QueryResult<unknown, ErrorValue
|
|
82
|
-
revision
|
|
85
|
+
result: QueryResult<unknown, ErrorValue> | ClientInfrastructureError,
|
|
86
|
+
revision?: AppStateRevision,
|
|
83
87
|
) => void;
|
|
84
88
|
readonly observe: (
|
|
85
89
|
functionAddress: FunctionAddress,
|
|
@@ -102,7 +106,7 @@ const makeQueryCache = (
|
|
|
102
106
|
sendLifecycleMessage: (message: QueryLifecycleMessage) => Effect.Effect<void>,
|
|
103
107
|
) => {
|
|
104
108
|
const queries = new Map<string, QueryEntry>();
|
|
105
|
-
const
|
|
109
|
+
const queriesById = new Map<SubscriptionId, QueryEntry>();
|
|
106
110
|
|
|
107
111
|
const observe = Effect.fn("SyncClient.QueryCache.observe")(function* (
|
|
108
112
|
functionAddress: FunctionAddress,
|
|
@@ -123,7 +127,7 @@ const makeQueryCache = (
|
|
|
123
127
|
subscriptionId: yield* allocateId,
|
|
124
128
|
};
|
|
125
129
|
queries.set(key, created);
|
|
126
|
-
|
|
130
|
+
queriesById.set(created.subscriptionId, created);
|
|
127
131
|
yield* sendLifecycleMessage(subscribeMessage(created));
|
|
128
132
|
entry = created;
|
|
129
133
|
}
|
|
@@ -157,7 +161,7 @@ const makeQueryCache = (
|
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
queries.delete(key);
|
|
160
|
-
|
|
164
|
+
queriesById.delete(observed.subscriptionId);
|
|
161
165
|
yield* sendLifecycleMessage({
|
|
162
166
|
type: "Unsubscribe",
|
|
163
167
|
id: observed.subscriptionId,
|
|
@@ -166,30 +170,29 @@ const makeQueryCache = (
|
|
|
166
170
|
} satisfies QueryObserver;
|
|
167
171
|
});
|
|
168
172
|
|
|
173
|
+
const clear = (entry: QueryEntry) => {
|
|
174
|
+
entry.result = pending();
|
|
175
|
+
entry.revision = undefined;
|
|
176
|
+
for (const listener of entry.listeners) listener();
|
|
177
|
+
};
|
|
169
178
|
return {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return key === undefined ? undefined : queries.get(key);
|
|
179
|
+
invalidate: (ids) => {
|
|
180
|
+
for (const entry of queries.values())
|
|
181
|
+
if (ids === undefined || ids.includes(entry.subscriptionId)) clear(entry);
|
|
174
182
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const key = queryKeysById.get(id);
|
|
178
|
-
const entry = key === undefined ? undefined : queries.get(key);
|
|
179
|
-
if (entry === undefined) return;
|
|
180
|
-
entry.result = error;
|
|
181
|
-
for (const listener of entry.listeners) {
|
|
182
|
-
listener();
|
|
183
|
-
}
|
|
183
|
+
invalidatePrivate: () => {
|
|
184
|
+
for (const entry of queries.values()) if (entry.dependsOnId !== false) clear(entry);
|
|
184
185
|
},
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
entries: () => queries.values(),
|
|
187
|
+
getById: (id: SubscriptionId) => queriesById.get(id),
|
|
188
|
+
observe,
|
|
189
|
+
setResult: (id, result, revision?: AppStateRevision) => {
|
|
190
|
+
const entry = queriesById.get(id);
|
|
188
191
|
if (entry === undefined) return;
|
|
189
|
-
if (
|
|
190
|
-
return;
|
|
192
|
+
if (revision !== undefined) {
|
|
193
|
+
if (entry.revision !== undefined && revision < entry.revision) return;
|
|
194
|
+
entry.revision = revision;
|
|
191
195
|
}
|
|
192
|
-
entry.revision = revision;
|
|
193
196
|
entry.result = result;
|
|
194
197
|
for (const listener of entry.listeners) {
|
|
195
198
|
listener();
|
|
@@ -202,15 +205,21 @@ type SnapshotFiles = NonNullable<Extract<ServerMessage, { readonly type: "Snapsh
|
|
|
202
205
|
|
|
203
206
|
const resultFromWire = Effect.fn("SyncClient.resultFromWire")(
|
|
204
207
|
(wire: WireResult, files: SnapshotFiles = []) =>
|
|
205
|
-
Effect.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
Effect.try({
|
|
209
|
+
try: () =>
|
|
210
|
+
wire.type === "Success"
|
|
211
|
+
? Result.succeed(
|
|
212
|
+
wire.value === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: applyFileGrants(decodeTransportValue(wire.value, wire.dates ?? []), files),
|
|
215
|
+
)
|
|
216
|
+
: failureFromWire(wire.error, wire.dates),
|
|
217
|
+
catch: (cause) =>
|
|
218
|
+
InvalidServerMessage.make({
|
|
219
|
+
cause,
|
|
220
|
+
message: "The server returned an invalid function result.",
|
|
221
|
+
}),
|
|
222
|
+
}),
|
|
214
223
|
);
|
|
215
224
|
|
|
216
225
|
export const shouldReloadForCloseCode = (code: number): boolean => code === DeploymentCloseCode;
|
|
@@ -259,6 +268,7 @@ interface SyncClientService {
|
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
interface PendingInvocation {
|
|
271
|
+
readonly sessionEpoch: string;
|
|
262
272
|
readonly deferred: Deferred.Deferred<
|
|
263
273
|
IgnotumResult<unknown, ErrorValue>,
|
|
264
274
|
ClientInfrastructureError
|
|
@@ -278,10 +288,12 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
278
288
|
SyncClient,
|
|
279
289
|
Effect.gen(function* () {
|
|
280
290
|
const ids = yield* IdGenerator;
|
|
291
|
+
const session = yield* BrowserSession;
|
|
281
292
|
let invocations = HashMap.empty<InvocationId, PendingInvocation>();
|
|
282
293
|
let rememberedHandshake: SyncHandshake | undefined;
|
|
283
294
|
let reloadRequested = false;
|
|
284
295
|
let writer: SocketWriter | undefined;
|
|
296
|
+
let sessionSuspended: Deferred.Deferred<void> | undefined;
|
|
285
297
|
|
|
286
298
|
const encode = (message: ClientMessage) =>
|
|
287
299
|
Schema.encodeEffect(ClientMessageJson)(message).pipe(
|
|
@@ -324,8 +336,17 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
324
336
|
),
|
|
325
337
|
);
|
|
326
338
|
|
|
339
|
+
const unsubscribeSession = session.subscribe(() => {
|
|
340
|
+
if (session.current() === undefined) {
|
|
341
|
+
queryCache.invalidatePrivate();
|
|
342
|
+
writer = undefined;
|
|
343
|
+
if (sessionSuspended !== undefined) Deferred.doneUnsafe(sessionSuspended, Effect.void);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
yield* Effect.addFinalizer(() => Effect.sync(unsubscribeSession));
|
|
347
|
+
|
|
327
348
|
const rejectInvocation = (id: InvocationId, error: ClientInfrastructureError) => {
|
|
328
|
-
const pendingInvocation = HashMap.
|
|
349
|
+
const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, id));
|
|
329
350
|
if (pendingInvocation === undefined) {
|
|
330
351
|
return Effect.void;
|
|
331
352
|
}
|
|
@@ -341,33 +362,91 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
341
362
|
message: Exclude<ServerMessage, SyncHandshake | { readonly type: "Deployment" }>,
|
|
342
363
|
) {
|
|
343
364
|
switch (message.type) {
|
|
365
|
+
case "Session": {
|
|
366
|
+
if (message.event === "Updated") {
|
|
367
|
+
const previous = session.current();
|
|
368
|
+
if (previous !== undefined && previous.sessionEpoch !== message.session.sessionEpoch)
|
|
369
|
+
return;
|
|
370
|
+
if (previous?.viewRevision !== message.session.viewRevision)
|
|
371
|
+
queryCache.invalidatePrivate();
|
|
372
|
+
session.accept(message.session, message.serverTime);
|
|
373
|
+
} else {
|
|
374
|
+
if (rememberedHandshake?.session?.sessionEpoch !== message.sessionEpoch) return;
|
|
375
|
+
session.suspend(
|
|
376
|
+
message.event === "Unavailable"
|
|
377
|
+
? "Your ID session is temporarily unavailable."
|
|
378
|
+
: undefined,
|
|
379
|
+
);
|
|
380
|
+
if (message.event !== "Unavailable") {
|
|
381
|
+
yield* Effect.forEach(HashMap.keys(invocations), (id) =>
|
|
382
|
+
rejectInvocation(
|
|
383
|
+
id,
|
|
384
|
+
ServerError.make({
|
|
385
|
+
code: "SessionChanged",
|
|
386
|
+
message: "The session changed. This operation may already have completed.",
|
|
387
|
+
}),
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
case "Invalidate": {
|
|
395
|
+
if (message.sessionEpoch === session.current()?.sessionEpoch)
|
|
396
|
+
queryCache.invalidate(message.ids);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
344
399
|
case "Snapshot": {
|
|
400
|
+
const current = session.current();
|
|
401
|
+
if (current === undefined || message.identity?.sessionEpoch !== current.sessionEpoch)
|
|
402
|
+
return;
|
|
403
|
+
if (
|
|
404
|
+
message.identity.dependsOnId &&
|
|
405
|
+
message.identity.viewRevision !== current.viewRevision
|
|
406
|
+
)
|
|
407
|
+
return;
|
|
345
408
|
const entry = queryCache.getById(message.id);
|
|
346
409
|
if (entry === undefined) return;
|
|
410
|
+
entry.dependsOnId = message.identity.dependsOnId;
|
|
411
|
+
if (entry.revision !== undefined && message.revision < entry.revision) return;
|
|
347
412
|
yield* resultFromWire(message.result, message.files ?? []).pipe(
|
|
348
413
|
Effect.tap((result) =>
|
|
349
414
|
Effect.sync(() => queryCache.setResult(message.id, result, message.revision)),
|
|
350
415
|
),
|
|
416
|
+
Effect.catchTags({
|
|
417
|
+
InvalidServerMessage: (error) => {
|
|
418
|
+
queryCache.setResult(message.id, error, message.revision);
|
|
419
|
+
return reportClientError(error);
|
|
420
|
+
},
|
|
421
|
+
}),
|
|
351
422
|
);
|
|
352
423
|
return;
|
|
353
424
|
}
|
|
354
425
|
case "Preparation": {
|
|
355
|
-
|
|
426
|
+
if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
|
|
427
|
+
const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, message.id));
|
|
356
428
|
if (pendingInvocation !== undefined) {
|
|
357
429
|
yield* Deferred.succeed(pendingInvocation.prepared, message.uploads);
|
|
358
430
|
}
|
|
359
431
|
return;
|
|
360
432
|
}
|
|
361
433
|
case "Result": {
|
|
362
|
-
|
|
434
|
+
if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
|
|
435
|
+
const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, message.id));
|
|
363
436
|
if (pendingInvocation === undefined) return;
|
|
364
|
-
invocations = HashMap.remove(invocations, message.id);
|
|
365
437
|
yield* resultFromWire(message.result).pipe(
|
|
366
|
-
Effect.flatMap((result) =>
|
|
438
|
+
Effect.flatMap((result) => {
|
|
439
|
+
invocations = HashMap.remove(invocations, message.id);
|
|
440
|
+
return Deferred.succeed(pendingInvocation.deferred, result);
|
|
441
|
+
}),
|
|
442
|
+
Effect.catchTags({
|
|
443
|
+
InvalidServerMessage: (error) => rejectInvocation(message.id, error),
|
|
444
|
+
}),
|
|
367
445
|
);
|
|
368
446
|
return;
|
|
369
447
|
}
|
|
370
448
|
case "Error": {
|
|
449
|
+
if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
|
|
371
450
|
const error =
|
|
372
451
|
message.operation === undefined
|
|
373
452
|
? ServerError.make({
|
|
@@ -382,7 +461,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
382
461
|
if (message.operation?.type === "Invocation") {
|
|
383
462
|
yield* rejectInvocation(message.operation.id, error);
|
|
384
463
|
} else if (message.operation?.type === "Subscription") {
|
|
385
|
-
queryCache.
|
|
464
|
+
queryCache.setResult(message.operation.id, error);
|
|
386
465
|
yield* reportClientError(error);
|
|
387
466
|
} else {
|
|
388
467
|
yield* reportClientError(error);
|
|
@@ -406,12 +485,18 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
406
485
|
const connect = Effect.scoped(
|
|
407
486
|
Effect.gen(function* () {
|
|
408
487
|
if (reloadRequested) return yield* Effect.never;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
488
|
+
yield* session.bootstrap.pipe(
|
|
489
|
+
Effect.mapError((error) => ConnectionUnavailable.make({ message: error.message })),
|
|
490
|
+
);
|
|
491
|
+
const socket = yield* Socket.makeWebSocket(
|
|
492
|
+
`${socketUrl()}${session.socketParameters()}`,
|
|
493
|
+
{
|
|
494
|
+
closeCodeIsError: (code) => {
|
|
495
|
+
if (shouldReloadForCloseCode(code)) requestReload();
|
|
496
|
+
return false;
|
|
497
|
+
},
|
|
413
498
|
},
|
|
414
|
-
|
|
499
|
+
);
|
|
415
500
|
const write = yield* socket.writer;
|
|
416
501
|
const handshakeReceived = yield* Deferred.make<void>();
|
|
417
502
|
let handshakeAccepted = false;
|
|
@@ -424,6 +509,41 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
424
509
|
return;
|
|
425
510
|
}
|
|
426
511
|
if (decision === "Ignore") return;
|
|
512
|
+
if (handshake.session === undefined || handshake.serverTime === undefined) {
|
|
513
|
+
requestReload();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (
|
|
517
|
+
rememberedHandshake?.session !== undefined &&
|
|
518
|
+
rememberedHandshake.session.sessionEpoch !== handshake.session.sessionEpoch
|
|
519
|
+
) {
|
|
520
|
+
queryCache.invalidatePrivate();
|
|
521
|
+
yield* Effect.forEach(HashMap.keys(invocations), (id) =>
|
|
522
|
+
rejectInvocation(
|
|
523
|
+
id,
|
|
524
|
+
ServerError.make({
|
|
525
|
+
code: "SessionChanged",
|
|
526
|
+
message: "The session changed. This operation may already have completed.",
|
|
527
|
+
}),
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
} else if (
|
|
531
|
+
rememberedHandshake?.session?.viewRevision !== handshake.session.viewRevision
|
|
532
|
+
) {
|
|
533
|
+
queryCache.invalidatePrivate();
|
|
534
|
+
}
|
|
535
|
+
yield* Effect.forEach(HashMap.entries(invocations), ([id, pending]) =>
|
|
536
|
+
pending.sessionEpoch === handshake.session!.sessionEpoch
|
|
537
|
+
? Effect.void
|
|
538
|
+
: rejectInvocation(
|
|
539
|
+
id,
|
|
540
|
+
ServerError.make({
|
|
541
|
+
code: "SessionChanged",
|
|
542
|
+
message: "The session changed. This operation may already have completed.",
|
|
543
|
+
}),
|
|
544
|
+
),
|
|
545
|
+
);
|
|
546
|
+
session.accept(handshake.session, handshake.serverTime);
|
|
427
547
|
rememberedHandshake = handshake;
|
|
428
548
|
handshakeAccepted = true;
|
|
429
549
|
writer = write;
|
|
@@ -461,12 +581,19 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
461
581
|
return yield* handleMessage(message);
|
|
462
582
|
});
|
|
463
583
|
|
|
584
|
+
const suspended = yield* Deferred.make<void>();
|
|
585
|
+
sessionSuspended = suspended;
|
|
586
|
+
yield* Effect.addFinalizer(() =>
|
|
587
|
+
Effect.sync(() => {
|
|
588
|
+
if (sessionSuspended === suspended) sessionSuspended = undefined;
|
|
589
|
+
}),
|
|
590
|
+
);
|
|
464
591
|
const run = yield* socket.runString(handleText).pipe(Effect.forkChild);
|
|
465
592
|
yield* Effect.raceFirst(
|
|
466
593
|
Deferred.await(handshakeReceived).pipe(Effect.timeout("10 seconds")),
|
|
467
594
|
Fiber.join(run),
|
|
468
595
|
);
|
|
469
|
-
yield* Fiber.join(run);
|
|
596
|
+
yield* Effect.raceFirst(Fiber.join(run), Deferred.await(suspended));
|
|
470
597
|
}),
|
|
471
598
|
).pipe(
|
|
472
599
|
Effect.mapError((cause) =>
|
|
@@ -507,6 +634,12 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
507
634
|
|
|
508
635
|
const mutate: SyncClientService["mutate"] = Effect.fn("SyncClient.mutate")(
|
|
509
636
|
function* (functionAddress, args) {
|
|
637
|
+
const admittedSession =
|
|
638
|
+
session.current() ??
|
|
639
|
+
rememberedHandshake?.session ??
|
|
640
|
+
(yield* session.bootstrap.pipe(
|
|
641
|
+
Effect.mapError((error) => ConnectionUnavailable.make({ message: error.message })),
|
|
642
|
+
));
|
|
510
643
|
const invocationId = yield* ids.generate(InvocationId);
|
|
511
644
|
const preparedArguments = yield* prepareMutationArguments(args).pipe(
|
|
512
645
|
Effect.provideService(IdGenerator, ids),
|
|
@@ -555,6 +688,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
555
688
|
files: preparedArguments.fileIds,
|
|
556
689
|
};
|
|
557
690
|
invocations = HashMap.set(invocations, invocationId, {
|
|
691
|
+
sessionEpoch: admittedSession.sessionEpoch,
|
|
558
692
|
deferred,
|
|
559
693
|
function: functionAddress,
|
|
560
694
|
message,
|
|
@@ -573,6 +707,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
573
707
|
const uploads = yield* Deferred.await(prepared);
|
|
574
708
|
const expected = new Set(message.files);
|
|
575
709
|
const received = new Set<string>();
|
|
710
|
+
const transfers: Array<{ readonly file: File; readonly url: string }> = [];
|
|
576
711
|
for (const upload of uploads) {
|
|
577
712
|
if (!expected.has(upload.id) || received.has(upload.id)) {
|
|
578
713
|
return yield* InvalidServerMessage.make({
|
|
@@ -589,28 +724,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
589
724
|
});
|
|
590
725
|
}
|
|
591
726
|
if (upload.url === undefined) continue;
|
|
592
|
-
|
|
593
|
-
const response = yield* Effect.tryPromise({
|
|
594
|
-
try: () =>
|
|
595
|
-
fetch(uploadUrl, {
|
|
596
|
-
method: "PUT",
|
|
597
|
-
body: file,
|
|
598
|
-
headers: { "content-type": file.type },
|
|
599
|
-
}),
|
|
600
|
-
catch: (cause) =>
|
|
601
|
-
ConnectionUnavailable.make({
|
|
602
|
-
cause,
|
|
603
|
-
message: `File '${file.name}' could not be uploaded.`,
|
|
604
|
-
}),
|
|
605
|
-
});
|
|
606
|
-
if (!response.ok) {
|
|
607
|
-
const detail = yield* Effect.promise(() => response.text());
|
|
608
|
-
return yield* InvalidMutationArguments.make({
|
|
609
|
-
cause: new Error(detail),
|
|
610
|
-
function: functionAddress,
|
|
611
|
-
message: `File '${file.name}' was rejected by application storage.`,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
727
|
+
transfers.push({ file, url: upload.url });
|
|
614
728
|
}
|
|
615
729
|
if (received.size !== expected.size) {
|
|
616
730
|
return yield* InvalidServerMessage.make({
|
|
@@ -618,7 +732,37 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
618
732
|
message: "The server returned incomplete application file preparation data.",
|
|
619
733
|
});
|
|
620
734
|
}
|
|
621
|
-
|
|
735
|
+
yield* Effect.forEach(
|
|
736
|
+
transfers,
|
|
737
|
+
Effect.fn("SyncClient.uploadFile")(function* ({ file, url }) {
|
|
738
|
+
const response = yield* Effect.tryPromise({
|
|
739
|
+
try: (signal) =>
|
|
740
|
+
fetch(url, {
|
|
741
|
+
method: "PUT",
|
|
742
|
+
body: file,
|
|
743
|
+
headers: { "content-type": file.type },
|
|
744
|
+
signal,
|
|
745
|
+
}),
|
|
746
|
+
catch: (cause) =>
|
|
747
|
+
ConnectionUnavailable.make({
|
|
748
|
+
cause,
|
|
749
|
+
message: `File '${file.name}' could not be uploaded.`,
|
|
750
|
+
}),
|
|
751
|
+
});
|
|
752
|
+
if (!response.ok) {
|
|
753
|
+
const detail = yield* Effect.promise(() => response.text());
|
|
754
|
+
return yield* InvalidMutationArguments.make({
|
|
755
|
+
cause: new Error(detail),
|
|
756
|
+
function: functionAddress,
|
|
757
|
+
message: `File '${file.name}' was rejected by application storage.`,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}),
|
|
761
|
+
{ concurrency: 3, discard: true },
|
|
762
|
+
);
|
|
763
|
+
const pendingInvocation = Option.getOrUndefined(
|
|
764
|
+
HashMap.get(invocations, invocationId),
|
|
765
|
+
);
|
|
622
766
|
if (pendingInvocation === undefined) return yield* Deferred.await(deferred);
|
|
623
767
|
invocations = HashMap.set(invocations, invocationId, {
|
|
624
768
|
...pendingInvocation,
|
|
@@ -651,6 +795,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
651
795
|
|
|
652
796
|
export const syncRuntime = ManagedRuntime.make(
|
|
653
797
|
SyncClient.layer.pipe(
|
|
798
|
+
Layer.provideMerge(BrowserSession.layer),
|
|
654
799
|
Layer.provideMerge(clientErrorReporterLayer),
|
|
655
800
|
Layer.provideMerge(IdGenerator.layer),
|
|
656
801
|
),
|