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.
Files changed (61) hide show
  1. package/README.md +8 -3
  2. package/dist/cli/bin.mjs +1372 -492
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-DzcR7spt.js → api-CAgKDij7.js} +38 -2
  5. package/dist/runtime/api-CAgKDij7.js.map +1 -0
  6. package/dist/runtime/{api-DR_8vfKg.d.ts → api-Cv3hMbzo.d.ts} +4 -3
  7. package/dist/runtime/client.d.ts +39 -8
  8. package/dist/runtime/client.js +505 -131
  9. package/dist/runtime/client.js.map +1 -1
  10. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +154 -0
  11. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +1 -0
  12. package/dist/runtime/id-Bt9XWRGL.js +423 -0
  13. package/dist/runtime/id-Bt9XWRGL.js.map +1 -0
  14. package/dist/runtime/{id-Cs82tq9Q-hmYFyqTa.d.ts → id-BzFHf3Wo-DGPCjgrf.d.ts} +13 -13
  15. package/dist/runtime/id-Dz0apuB3.d.ts +1 -0
  16. package/dist/runtime/{index-2q9FwJud.d.ts → index-D54flWtH.d.ts} +136 -76
  17. package/dist/runtime/internal/api.d.ts +1 -1
  18. package/dist/runtime/internal/api.js +1 -1
  19. package/dist/runtime/internal/host.d.ts +21 -17
  20. package/dist/runtime/internal/host.js +22 -8
  21. package/dist/runtime/internal/host.js.map +1 -1
  22. package/dist/runtime/internal/server.d.ts +1 -1
  23. package/dist/runtime/internal/server.js +1 -1
  24. package/dist/runtime/internal/types.d.ts +1 -1
  25. package/dist/runtime/internal/types.js +1 -1
  26. package/dist/runtime/{pagination-Bt3l7QaC-BYEGmLBE.d.ts → pagination-DnKg3dkI-r5ZUxBBx.d.ts} +34 -6
  27. package/dist/runtime/pagination-Dz0apuB3.d.ts +1 -0
  28. package/dist/runtime/result-DKAA4gpS.d.ts +1 -0
  29. package/dist/runtime/{schema-ERFjT8-m.js → schema-1Zs03-iS.js} +135 -17
  30. package/dist/runtime/schema-1Zs03-iS.js.map +1 -0
  31. package/dist/runtime/server.d.ts +9 -3
  32. package/dist/runtime/server.js +5 -3
  33. package/dist/runtime/server.js.map +1 -1
  34. package/dist/runtime/{sync-a2EdGSdY.d.ts → sync-Bs8J3fIr.d.ts} +2 -2
  35. package/package.json +4 -4
  36. package/src/cli/agent-files.ts +8 -0
  37. package/src/cli/auth-client.ts +1 -1
  38. package/src/cli/bin.ts +13 -3
  39. package/src/cli/build/server.ts +102 -13
  40. package/src/cli/codegen.ts +7 -4
  41. package/src/cli/control-client.ts +30 -8
  42. package/src/cli/deploy.ts +7 -2
  43. package/src/cli/environment.ts +133 -0
  44. package/src/client/files.ts +43 -27
  45. package/src/client/hooks.ts +94 -17
  46. package/src/client/id.ts +259 -0
  47. package/src/client/index.ts +5 -2
  48. package/src/client/page-observers.ts +77 -0
  49. package/src/client/sync.ts +214 -69
  50. package/src/dev-runtime/functions.ts +232 -86
  51. package/src/dev-runtime/id.ts +174 -2
  52. package/src/dev-runtime/query-cache.ts +105 -0
  53. package/src/dev-runtime/sync.ts +187 -26
  54. package/src/server/index.ts +13 -1
  55. package/dist/runtime/api-DzcR7spt.js.map +0 -1
  56. package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.js +0 -311
  57. package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.js.map +0 -1
  58. package/dist/runtime/file-C1abuMgd.js +0 -173
  59. package/dist/runtime/file-C1abuMgd.js.map +0 -1
  60. package/dist/runtime/pagination-CFJ3xlAt.d.ts +0 -1
  61. package/dist/runtime/schema-ERFjT8-m.js.map +0 -1
@@ -1,9 +1,10 @@
1
- import { C as RuntimeRequestNonce, T as TableId, _ as AppId, a as RuntimeFileMetadata, b as IdAlphabet, c as fileFormats, d as fileLimits, f as fileMimeTypes, h as makeFileValue, l as fileGrantUrlOf, m as isFileValue, o as RuntimeFileOccurrence, p as grantFileValue, r as FileId, u as fileIdOf, v as DeploymentId, w as SubscriptionId, x as InvocationId, y as GeneratedId } from "./file-C1abuMgd.js";
2
- import { b as decodeTransportValue, c as upgrade, d as Result$1, h as pending, m as inspectResult, o as PaginationPageSize, p as failureFromWire, s as initial, t as ValueDescriptor, x as encodeTransportObject } from "./descriptor-C5VA9qRl-DuQsowaQ.js";
3
- import { a as ErrorCode, c as ServerMessageJson, d as WireSuccess, f as AppStateRevision, i as DeploymentCloseCode, l as WireFailure, m as InvocationKey, n as functionPathOf, o as FunctionAddress, p as DeploymentGeneration, r as ClientMessageJson, s as Operation, u as WireResult } from "./api-DzcR7spt.js";
4
- import { Array as Array$1, Cause, Context, Deferred, Duration, Effect, ErrorReporter, Fiber, HashMap, Layer, ManagedRuntime, Predicate, Schedule, Schema, String } from "effect";
1
+ import { A as RuntimeFileMetadata, B as makeFileValue, D as encodeTransportObject, E as decodeTransportValue, F as fileIdOf, G as IdAlphabet, H as AppId, I as fileLimits, J as SessionEpoch, K as InvocationId, L as fileMimeTypes, N as fileFormats, P as fileGrantUrlOf, R as grantFileValue, U as DeploymentId, W as GeneratedId, X as TableId, Y as SubscriptionId, _ as Result$2, a as SessionResponse, b as inspectResult, d as sessionPath, f as sessionRequestsPath, i as SessionRequest, j as RuntimeFileOccurrence, k as FileId, l as developmentSelectionKey, m as upgrade, n as SessionOutcome, o as SessionState, p as initial, q as RuntimeRequestNonce, r as SessionRedirect, t as DevelopmentSelection, x as pending, y as failureFromWire, z as isFileValue } from "./id-Bt9XWRGL.js";
2
+ import { a as ErrorCode, c as ServerMessageJson, d as WireSuccess, f as AppStateRevision, i as DeploymentCloseCode, l as WireFailure, m as InvocationKey, n as functionPathOf, o as FunctionAddress, p as DeploymentGeneration, r as ClientMessageJson, s as Operation, u as WireResult } from "./api-CAgKDij7.js";
3
+ import { s as PaginationPageSize, t as ValueDescriptor } from "./descriptor-Cyo9FP9n-C0SRVXNW.js";
4
+ import { Array as Array$1, Cause, Context, Deferred, Duration, Effect, ErrorReporter, Fiber, HashMap, Layer, ManagedRuntime, Option, Predicate, Schedule, Schema, Semaphore, String } from "effect";
5
5
  import { customAlphabet } from "nanoid";
6
- import { useCallback, useCallback as useCallback$1, useContext, useDebugValue, useDebugValue as useDebugValue$1, useEffect, useEffect as useEffect$1, useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useMemo as useMemo$1, useReducer, useRef, useState, useState as useState$1 } from "preact/hooks";
6
+ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
7
+ import { useCallback, useCallback as useCallback$1, useContext, useDebugValue, useDebugValue as useDebugValue$1, useEffect, useEffect as useEffect$1, useErrorBoundary, useId as useElementId, useImperativeHandle, useLayoutEffect, useMemo, useMemo as useMemo$1, useReducer, useRef, useState, useState as useState$1 } from "preact/hooks";
7
8
  import * as Socket from "effect/unstable/socket/Socket";
8
9
  import * as BrowserCrypto from "@effect/platform-browser/BrowserCrypto";
9
10
  import { Component, Fragment, cloneElement, createContext, createElement, createRef, h, isValidElement, toChildArray } from "preact";
@@ -32,9 +33,10 @@ const Files = { url: (file) => {
32
33
  } };
33
34
  const formatByMime = {};
34
35
  for (const format of fileFormats) formatByMime[fileMimeTypes[format]] = format;
36
+ const MutationObject = Schema.ObjectKeyword.check(Schema.makeFilter((value) => Object.prototype.toString.call(value) === "[object Object]", { message: "Mutation objects must contain ordinary fields; built-in collections are unsupported." }));
35
37
  const collectNativeFiles = (value, files) => {
36
38
  if (value instanceof File) {
37
- files.push(value);
39
+ files.add(value);
38
40
  return;
39
41
  }
40
42
  if (isFileValue(value) || Predicate.isDate(value)) return;
@@ -43,7 +45,7 @@ const collectNativeFiles = (value, files) => {
43
45
  return;
44
46
  }
45
47
  if (!Predicate.isObject(value)) return;
46
- for (const child of Object.values(value)) collectNativeFiles(child, files);
48
+ for (const child of Object.values(Schema.decodeSync(MutationObject)(value))) collectNativeFiles(child, files);
47
49
  };
48
50
  const replaceNativeFiles = (value, replacements) => {
49
51
  if (value instanceof File) return replacements.get(value);
@@ -54,18 +56,23 @@ const replaceNativeFiles = (value, replacements) => {
54
56
  };
55
57
  const prepareMutationArguments = Effect.fn("SyncClient.prepareMutationArguments")(function* (input) {
56
58
  const ids = yield* IdGenerator;
57
- const native = [];
58
- collectNativeFiles(input, native);
59
- const unique = new Set(native);
60
- if (unique.size > fileLimits.filesPerMutation) throw new Error(`A mutation may upload at most ${fileLimits.filesPerMutation} files.`);
61
- if ([...unique].reduce((total, file) => total + file.size, 0) > fileLimits.mutationBytes) throw new Error(`Mutation uploads may total at most ${fileLimits.mutationBytes} bytes.`);
59
+ const unique = yield* Effect.try(() => {
60
+ const files = /* @__PURE__ */ new Set();
61
+ collectNativeFiles(input, files);
62
+ if (files.size > fileLimits.filesPerMutation) throw new Error(`A mutation may upload at most ${fileLimits.filesPerMutation} files.`);
63
+ if ([...files].reduce((total, file) => total + file.size, 0) > fileLimits.mutationBytes) throw new Error(`Mutation uploads may total at most ${fileLimits.mutationBytes} bytes.`);
64
+ return files;
65
+ });
62
66
  const replacements = /* @__PURE__ */ new Map();
63
67
  const nativeFiles = /* @__PURE__ */ new Map();
64
68
  for (const file of unique) {
65
- const format = formatByMime[file.type.toLowerCase()];
66
- if (format === void 0) throw new Error(`File '${file.name}' has unsupported media type '${file.type || "unknown"}'.`);
67
- if (file.size === 0 || file.size > fileLimits.fileBytes) throw new Error(`File '${file.name}' must be between 1 and ${fileLimits.fileBytes} bytes.`);
68
- if (new TextEncoder().encode(file.name).byteLength > fileLimits.filenameBytes) throw new Error(`File '${file.name}' has a filename that is too long.`);
69
+ const format = yield* Effect.try(() => {
70
+ const format = formatByMime[file.type.toLowerCase()];
71
+ if (format === void 0) throw new Error(`File '${file.name}' has unsupported media type '${file.type || "unknown"}'.`);
72
+ if (file.size === 0 || file.size > fileLimits.fileBytes) throw new Error(`File '${file.name}' must be between 1 and ${fileLimits.fileBytes} bytes.`);
73
+ if (new TextEncoder().encode(file.name).byteLength > fileLimits.filenameBytes) throw new Error(`File '${file.name}' has a filename that is too long.`);
74
+ return format;
75
+ });
69
76
  const id = yield* ids.generate(FileId);
70
77
  replacements.set(file, makeFileValue(id, {
71
78
  format,
@@ -75,12 +82,13 @@ const prepareMutationArguments = Effect.fn("SyncClient.prepareMutationArguments"
75
82
  nativeFiles.set(id, file);
76
83
  }
77
84
  return {
78
- value: replaceNativeFiles(input, replacements),
85
+ value: unique.size === 0 ? input : yield* Effect.try(() => replaceNativeFiles(input, replacements)),
79
86
  nativeFiles,
80
87
  fileIds: Array.from(nativeFiles.keys())
81
88
  };
82
89
  });
83
90
  const applyFileGrants = (value, grants) => {
91
+ if (grants.length === 0) return value;
84
92
  const byId = /* @__PURE__ */ new Map();
85
93
  for (const grant of grants) {
86
94
  if (byId.has(grant.id)) throw new Error("A query file grant ID is duplicated.");
@@ -105,14 +113,182 @@ const applyFileGrants = (value, grants) => {
105
113
  };
106
114
  //#endregion
107
115
  //#region ../contracts/dist/json.js
108
- const encodeScalar = (value) => Schema.decodeSync(Schema.String)(JSON.stringify(value));
109
- const encodeCanonicalJson = (value) => {
110
- if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return encodeScalar(value);
111
- if (Predicate.isObject(value)) return `{${Array$1.map(Array$1.sort(String.Order)(Object.keys(value)), (key) => {
112
- const field = Schema.decodeUnknownSync(Schema.Json)(value[key]);
113
- return `${encodeScalar(key)}:${encodeCanonicalJson(field)}`;
114
- }).join(",")}}`;
115
- return `[${Schema.decodeUnknownSync(Schema.Array(Schema.Json))(value).map(encodeCanonicalJson).join(",")}]`;
116
+ const encodeJson = (value) => {
117
+ if (Array$1.isArray(value)) return `[${value.map(encodeJson).join(",")}]`;
118
+ if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return JSON.stringify(value);
119
+ return `{${Array$1.sortWith(Object.entries(value), ([key]) => key, String.Order).map(([key, field]) => `${JSON.stringify(key)}:${encodeJson(field)}`).join(",")}}`;
120
+ };
121
+ const encodeCanonicalJson = (value) => encodeJson(Schema.decodeSync(Schema.Json)(value));
122
+ //#endregion
123
+ //#region src/client/id.ts
124
+ var IdRequestError = class extends Schema.TaggedError()("IdRequestError", { message: Schema.String }) {};
125
+ const outcomeKey = "ignotum.id.outcome";
126
+ const selectionJson = Schema.fromJsonString(DevelopmentSelection);
127
+ var BrowserSession = class BrowserSession extends Context.Service()("ignotum/client/id/BrowserSession") {
128
+ static layer = Layer.effect(BrowserSession, Effect.gen(function* () {
129
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
130
+ const ids = yield* IdGenerator;
131
+ const bootstrapLock = yield* Semaphore.make(1);
132
+ const listeners = /* @__PURE__ */ new Set();
133
+ let state = { status: "pending" };
134
+ let current;
135
+ let generation = 0;
136
+ let deadline = Infinity;
137
+ let development = false;
138
+ let selection;
139
+ let expiry;
140
+ const notify = () => {
141
+ for (const listener of listeners) listener();
142
+ };
143
+ const suspend = (error) => {
144
+ generation += 1;
145
+ deadline = Infinity;
146
+ if (expiry !== void 0) clearTimeout(expiry);
147
+ current = void 0;
148
+ state = error === void 0 ? {
149
+ status: "pending",
150
+ outcome: state.outcome
151
+ } : {
152
+ status: "error",
153
+ error,
154
+ outcome: state.outcome
155
+ };
156
+ notify();
157
+ };
158
+ const accept = (session, serverTime) => {
159
+ generation += 1;
160
+ if (expiry !== void 0) clearTimeout(expiry);
161
+ current = session;
162
+ state = session.user === null ? {
163
+ status: "signedOut",
164
+ outcome: state.outcome
165
+ } : {
166
+ status: "signedIn",
167
+ user: session.user,
168
+ outcome: state.outcome
169
+ };
170
+ const delay = Math.max(0, session.validUntil - serverTime);
171
+ deadline = performance.now() + delay;
172
+ if (delay < 2147483647) expiry = setTimeout(() => suspend(), delay);
173
+ notify();
174
+ };
175
+ const checkExpiry = () => {
176
+ if (current !== void 0 && performance.now() >= deadline) suspend();
177
+ };
178
+ globalThis.addEventListener?.("focus", checkExpiry);
179
+ globalThis.document?.addEventListener("visibilitychange", checkExpiry);
180
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
181
+ if (expiry !== void 0) clearTimeout(expiry);
182
+ listeners.clear();
183
+ globalThis.removeEventListener?.("focus", checkExpiry);
184
+ globalThis.document?.removeEventListener("visibilitychange", checkExpiry);
185
+ }));
186
+ const readOutcome = () => {
187
+ const raw = globalThis.sessionStorage.getItem(outcomeKey);
188
+ if (raw !== null) {
189
+ const decoded = Schema.decodeUnknownOption(SessionOutcome)(raw);
190
+ if (Option.isSome(decoded)) state = {
191
+ ...state,
192
+ outcome: decoded.value
193
+ };
194
+ globalThis.sessionStorage.removeItem(outcomeKey);
195
+ }
196
+ };
197
+ const parameters = () => {
198
+ if (!development || selection === void 0) return "";
199
+ const params = new URLSearchParams({ epoch: selection.epoch });
200
+ if (selection.username !== null) params.set("username", selection.username);
201
+ return `?${params}`;
202
+ };
203
+ const readSession = Effect.fn("BrowserSession.read")(function* () {
204
+ return yield* client.get(`${sessionPath}${parameters()}`, { headers: { "x-ignotum-request": "1" } }).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(SessionResponse)));
205
+ });
206
+ let bootstrapGeneration = 0;
207
+ const bootstrap = bootstrapLock.withPermits(1)(Effect.gen(function* () {
208
+ const admittedGeneration = generation;
209
+ bootstrapGeneration = admittedGeneration;
210
+ const startedAt = performance.now();
211
+ let session = yield* readSession();
212
+ development = session.development === true;
213
+ if (development && selection === void 0) {
214
+ const saved = globalThis.sessionStorage.getItem(developmentSelectionKey);
215
+ const decoded = saved === null ? Option.none() : Schema.decodeOption(selectionJson)(saved);
216
+ selection = Option.getOrElse(decoded, () => ({
217
+ formatVersion: 1,
218
+ username: null,
219
+ epoch: ""
220
+ }));
221
+ if (selection.epoch === "") selection = {
222
+ ...selection,
223
+ epoch: yield* ids.generate(SessionEpoch)
224
+ };
225
+ globalThis.sessionStorage.setItem(developmentSelectionKey, Schema.encodeSync(selectionJson)(selection));
226
+ session = yield* readSession();
227
+ }
228
+ if (generation !== admittedGeneration) {
229
+ if (current !== void 0) return current;
230
+ return yield* IdRequestError.make({ message: "Your ID session changed while loading." });
231
+ }
232
+ readOutcome();
233
+ if (session.outcome !== void 0) state = {
234
+ ...state,
235
+ outcome: session.outcome
236
+ };
237
+ accept(session, session.serverTime + performance.now() - startedAt);
238
+ return session;
239
+ })).pipe(Effect.mapError(() => IdRequestError.make({ message: "Could not load your ID session." })), Effect.tapError((error) => Effect.sync(() => {
240
+ if (generation === bootstrapGeneration) suspend(error.message);
241
+ })));
242
+ const start = Effect.fn("BrowserSession.start")(function* (intent, profile, returnTo) {
243
+ const session = current ?? (yield* bootstrap);
244
+ if (intent === "requestProfile" && session.user === null) return yield* IdRequestError.make({ message: "Sign in before requesting profile information." });
245
+ const request = yield* Schema.decodeUnknownEffect(SessionRequest)({
246
+ intent,
247
+ profile,
248
+ sessionEpoch: session.sessionEpoch,
249
+ returnTo: returnTo ?? `${globalThis.location.pathname}${globalThis.location.search}${globalThis.location.hash}`
250
+ }).pipe(Effect.mapError(() => IdRequestError.make({ message: "The ID request is invalid." })));
251
+ const redirect = yield* HttpClientRequest.post(sessionRequestsPath).pipe(HttpClientRequest.setHeader("x-ignotum-request", "1"), HttpClientRequest.bodyJsonUnsafe(request), client.execute, Effect.flatMap(HttpClientResponse.schemaBodyJson(SessionRedirect)), Effect.mapError(() => IdRequestError.make({ message: "Could not start the ID request." })));
252
+ if (current?.sessionEpoch !== session.sessionEpoch) return yield* IdRequestError.make({ message: "Your ID session changed. Try again." });
253
+ globalThis.location.assign(redirect.redirectUrl);
254
+ });
255
+ const signOut = Effect.gen(function* () {
256
+ const session = current ?? (yield* bootstrap);
257
+ yield* HttpClientRequest.delete(sessionPath).pipe(HttpClientRequest.setHeader("x-ignotum-request", "1"), HttpClientRequest.bodyJsonUnsafe({ sessionEpoch: session.sessionEpoch }), client.execute, Effect.mapError(() => IdRequestError.make({ message: "Could not sign out." })));
258
+ suspend();
259
+ if (development) {
260
+ selection = {
261
+ formatVersion: 1,
262
+ username: null,
263
+ epoch: yield* ids.generate(SessionEpoch)
264
+ };
265
+ globalThis.sessionStorage.setItem(developmentSelectionKey, Schema.encodeSync(selectionJson)(selection));
266
+ }
267
+ globalThis.location.reload();
268
+ });
269
+ return BrowserSession.of({
270
+ snapshot: () => {
271
+ checkExpiry();
272
+ return state;
273
+ },
274
+ current: () => {
275
+ checkExpiry();
276
+ return current;
277
+ },
278
+ subscribe: (listener) => {
279
+ listeners.add(listener);
280
+ return () => {
281
+ listeners.delete(listener);
282
+ };
283
+ },
284
+ accept,
285
+ suspend,
286
+ bootstrap,
287
+ start,
288
+ signOut,
289
+ socketParameters: parameters
290
+ });
291
+ })).pipe(Layer.provide(FetchHttpClient.layer));
116
292
  };
117
293
  //#endregion
118
294
  //#region src/client/errors.ts
@@ -194,7 +370,9 @@ const IndexPointInvalidation = Schema.Struct({
194
370
  index: IndexIdentity,
195
371
  key: EncodedIndexKey
196
372
  });
373
+ const IdDependency = Schema.Struct({ type: Schema.Literal("Id") });
197
374
  const ReadDependency = Schema.Union([
375
+ IdDependency,
198
376
  TableDependency,
199
377
  DocumentDependency,
200
378
  IndexRangeDependency
@@ -259,7 +437,8 @@ const InvocationBase = {
259
437
  generation: DeploymentGeneration,
260
438
  invocationKey: InvocationKey,
261
439
  function: FunctionAddress,
262
- args: Schema.Json
440
+ args: Schema.Json,
441
+ identity: Schema.optional(SessionState)
263
442
  };
264
443
  const QueryInvocation = Schema.Struct({
265
444
  ...InvocationBase,
@@ -282,6 +461,7 @@ var RuntimeDeploymentUnavailable = class extends Schema.TaggedError()("RuntimeDe
282
461
  message: Schema.String
283
462
  }) {};
284
463
  const RuntimeInvocationRejectionCode = Schema.Literals([
464
+ "SessionExpired",
285
465
  "FunctionUnavailable",
286
466
  "InvalidArguments",
287
467
  "InvocationIdConflict",
@@ -342,6 +522,7 @@ const ArtifactKind = Schema.Literals([
342
522
  "ClientShell",
343
523
  "FunctionBundle",
344
524
  "ServerManifest",
525
+ "ServerEnvironment",
345
526
  "SourceMap",
346
527
  "SchemaSnapshot"
347
528
  ]);
@@ -366,7 +547,14 @@ const DeploymentInventoryV1 = Schema.Struct({
366
547
  formatVersion: Schema.Literal(1),
367
548
  files: Schema.Array(ArtifactFile)
368
549
  });
369
- initial(DeploymentInventoryV1);
550
+ const DeploymentInventoryV2 = Schema.Struct({
551
+ formatVersion: Schema.Literal(2),
552
+ files: Schema.Array(ArtifactFile)
553
+ });
554
+ upgrade(initial(DeploymentInventoryV1), DeploymentInventoryV2, (inventory) => ({
555
+ formatVersion: 2,
556
+ files: inventory.files
557
+ }));
370
558
  const ClientManifestV1 = Schema.Struct({
371
559
  formatVersion: Schema.Literal(1),
372
560
  shell: ArtifactReference,
@@ -387,6 +575,16 @@ const SchemaSnapshotField = Schema.Struct({
387
575
  name: Schema.String,
388
576
  value: ValueDescriptor
389
577
  });
578
+ const EnvironmentArtifactV1 = Schema.Struct({
579
+ formatVersion: Schema.Literal(1),
580
+ nonce: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)),
581
+ values: Schema.Record(Schema.String, Schema.String)
582
+ });
583
+ initial(EnvironmentArtifactV1);
584
+ const ServerEnvironmentArtifact = Schema.Struct({
585
+ artifact: ArtifactReference,
586
+ fields: Schema.Array(SchemaSnapshotField)
587
+ });
390
588
  const SchemaSnapshotIndex = Schema.Struct({
391
589
  name: Schema.String,
392
590
  fields: Schema.Array(Schema.String)
@@ -440,11 +638,22 @@ const ServerBuildManifestV2 = Schema.Struct({
440
638
  schema: ArtifactReference,
441
639
  functions: Schema.Array(ServerFunctionArtifact)
442
640
  });
443
- upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
641
+ const ServerBuildManifestThroughV2 = upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
444
642
  formatVersion: 2,
445
643
  schema: manifest.schema,
446
644
  functions: manifest.functions
447
645
  }));
646
+ const ServerBuildManifestV3 = Schema.Struct({
647
+ formatVersion: Schema.Literal(3),
648
+ schema: ArtifactReference,
649
+ environment: Schema.optional(ServerEnvironmentArtifact),
650
+ functions: Schema.Array(ServerFunctionArtifact)
651
+ });
652
+ upgrade(ServerBuildManifestThroughV2, ServerBuildManifestV3, (manifest) => ({
653
+ formatVersion: 3,
654
+ schema: manifest.schema,
655
+ functions: manifest.functions
656
+ }));
448
657
  ArtifactPath.make("inventory.json");
449
658
  ArtifactPath.make("client/manifest.json");
450
659
  ArtifactPath.make("client/shell.html");
@@ -452,6 +661,7 @@ ArtifactPath.make("client/shell.html");
452
661
  `${ArtifactPath.make("client/routes")}`;
453
662
  ArtifactPath.make("server/manifest.json");
454
663
  ArtifactPath.make("server/schema.json");
664
+ ArtifactPath.make("server/environment.json");
455
665
  //#endregion
456
666
  //#region ../contracts/dist/runtime/transport.js
457
667
  const runtimeInvocationPath = "/v1/invoke";
@@ -519,78 +729,87 @@ const subscribeMessage = (entry) => ({
519
729
  });
520
730
  const makeQueryCache = (allocateId, sendLifecycleMessage) => {
521
731
  const queries = /* @__PURE__ */ new Map();
522
- const queryKeysById = /* @__PURE__ */ new Map();
732
+ const queriesById = /* @__PURE__ */ new Map();
733
+ const observe = Effect.fn("SyncClient.QueryCache.observe")(function* (functionAddress, args) {
734
+ const key = queryKey(functionAddress, args);
735
+ let entry = queries.get(key);
736
+ if (entry === void 0) {
737
+ const created = {
738
+ args,
739
+ function: functionAddress,
740
+ generation: 0,
741
+ listeners: /* @__PURE__ */ new Set(),
742
+ revision: void 0,
743
+ references: 0,
744
+ result: pending(),
745
+ subscriptionId: yield* allocateId
746
+ };
747
+ queries.set(key, created);
748
+ queriesById.set(created.subscriptionId, created);
749
+ yield* sendLifecycleMessage(subscribeMessage(created));
750
+ entry = created;
751
+ }
752
+ const observed = entry;
753
+ observed.references += 1;
754
+ observed.generation += 1;
755
+ let released = false;
756
+ return {
757
+ getSnapshot: () => observed.result,
758
+ subscribe: (listener) => {
759
+ observed.listeners.add(listener);
760
+ return () => observed.listeners.delete(listener);
761
+ },
762
+ release: Effect.gen(function* () {
763
+ if (released) return;
764
+ released = true;
765
+ observed.references -= 1;
766
+ if (observed.references !== 0) return;
767
+ const generation = ++observed.generation;
768
+ yield* Effect.sleep("25 millis");
769
+ if (observed.references !== 0 || observed.generation !== generation || queries.get(key) !== observed) return;
770
+ queries.delete(key);
771
+ queriesById.delete(observed.subscriptionId);
772
+ yield* sendLifecycleMessage({
773
+ type: "Unsubscribe",
774
+ id: observed.subscriptionId
775
+ });
776
+ })
777
+ };
778
+ });
779
+ const clear = (entry) => {
780
+ entry.result = pending();
781
+ entry.revision = void 0;
782
+ for (const listener of entry.listeners) listener();
783
+ };
523
784
  return {
524
- entries: () => queries.values(),
525
- getById: (id) => {
526
- const key = queryKeysById.get(id);
527
- return key === void 0 ? void 0 : queries.get(key);
785
+ invalidate: (ids) => {
786
+ for (const entry of queries.values()) if (ids === void 0 || ids.includes(entry.subscriptionId)) clear(entry);
528
787
  },
529
- observe: Effect.fn("SyncClient.QueryCache.observe")(function* (functionAddress, args) {
530
- const key = queryKey(functionAddress, args);
531
- let entry = queries.get(key);
532
- if (entry === void 0) {
533
- const created = {
534
- args,
535
- function: functionAddress,
536
- generation: 0,
537
- listeners: /* @__PURE__ */ new Set(),
538
- revision: void 0,
539
- references: 0,
540
- result: pending(),
541
- subscriptionId: yield* allocateId
542
- };
543
- queries.set(key, created);
544
- queryKeysById.set(created.subscriptionId, key);
545
- yield* sendLifecycleMessage(subscribeMessage(created));
546
- entry = created;
547
- }
548
- const observed = entry;
549
- observed.references += 1;
550
- observed.generation += 1;
551
- let released = false;
552
- return {
553
- getSnapshot: () => observed.result,
554
- subscribe: (listener) => {
555
- observed.listeners.add(listener);
556
- return () => observed.listeners.delete(listener);
557
- },
558
- release: Effect.gen(function* () {
559
- if (released) return;
560
- released = true;
561
- observed.references -= 1;
562
- if (observed.references !== 0) return;
563
- const generation = ++observed.generation;
564
- yield* Effect.sleep("25 millis");
565
- if (observed.references !== 0 || observed.generation !== generation || queries.get(key) !== observed) return;
566
- queries.delete(key);
567
- queryKeysById.delete(observed.subscriptionId);
568
- yield* sendLifecycleMessage({
569
- type: "Unsubscribe",
570
- id: observed.subscriptionId
571
- });
572
- })
573
- };
574
- }),
575
- setError: (id, error) => {
576
- const key = queryKeysById.get(id);
577
- const entry = key === void 0 ? void 0 : queries.get(key);
578
- if (entry === void 0) return;
579
- entry.result = error;
580
- for (const listener of entry.listeners) listener();
788
+ invalidatePrivate: () => {
789
+ for (const entry of queries.values()) if (entry.dependsOnId !== false) clear(entry);
581
790
  },
791
+ entries: () => queries.values(),
792
+ getById: (id) => queriesById.get(id),
793
+ observe,
582
794
  setResult: (id, result, revision) => {
583
- const key = queryKeysById.get(id);
584
- const entry = key === void 0 ? void 0 : queries.get(key);
795
+ const entry = queriesById.get(id);
585
796
  if (entry === void 0) return;
586
- if (entry.revision !== void 0 && revision < entry.revision) return;
587
- entry.revision = revision;
797
+ if (revision !== void 0) {
798
+ if (entry.revision !== void 0 && revision < entry.revision) return;
799
+ entry.revision = revision;
800
+ }
588
801
  entry.result = result;
589
802
  for (const listener of entry.listeners) listener();
590
803
  }
591
804
  };
592
805
  };
593
- const resultFromWire = Effect.fn("SyncClient.resultFromWire")((wire, files = []) => Effect.succeed(wire.type === "Success" ? Result$1.succeed(wire.value === void 0 ? void 0 : applyFileGrants(decodeTransportValue(wire.value, wire.dates ?? []), files)) : failureFromWire(wire.error, wire.dates)));
806
+ const resultFromWire = Effect.fn("SyncClient.resultFromWire")((wire, files = []) => Effect.try({
807
+ try: () => wire.type === "Success" ? Result$2.succeed(wire.value === void 0 ? void 0 : applyFileGrants(decodeTransportValue(wire.value, wire.dates ?? []), files)) : failureFromWire(wire.error, wire.dates),
808
+ catch: (cause) => InvalidServerMessage.make({
809
+ cause,
810
+ message: "The server returned an invalid function result."
811
+ })
812
+ }));
594
813
  const shouldReloadForCloseCode = (code) => code === DeploymentCloseCode;
595
814
  const handshakesMatch = (first, next) => first.appId === next.appId && first.deploymentId === next.deploymentId && first.generation === next.generation;
596
815
  const handshakeDecision = (remembered, acceptedOnConnection, next) => {
@@ -613,10 +832,12 @@ const socketUrl = () => {
613
832
  var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync/SyncClient") {
614
833
  static layer = Layer.effect(SyncClient, Effect.gen(function* () {
615
834
  const ids = yield* IdGenerator;
835
+ const session = yield* BrowserSession;
616
836
  let invocations = HashMap.empty();
617
837
  let rememberedHandshake;
618
838
  let reloadRequested = false;
619
839
  let writer;
840
+ let sessionSuspended;
620
841
  const encode = (message) => Schema.encodeEffect(ClientMessageJson)(message).pipe(Effect.mapError((cause) => InvalidClientMessage.make({
621
842
  cause,
622
843
  message: "Could not encode an Ignotum sync message."
@@ -630,31 +851,72 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
630
851
  ConnectionUnavailable: reportClientError,
631
852
  InvalidClientMessage: reportClientError
632
853
  })));
854
+ const unsubscribeSession = session.subscribe(() => {
855
+ if (session.current() === void 0) {
856
+ queryCache.invalidatePrivate();
857
+ writer = void 0;
858
+ if (sessionSuspended !== void 0) Deferred.doneUnsafe(sessionSuspended, Effect.void);
859
+ }
860
+ });
861
+ yield* Effect.addFinalizer(() => Effect.sync(unsubscribeSession));
633
862
  const rejectInvocation = (id, error) => {
634
- const pendingInvocation = HashMap.getUnsafe(invocations, id);
863
+ const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, id));
635
864
  if (pendingInvocation === void 0) return Effect.void;
636
865
  invocations = HashMap.remove(invocations, id);
637
866
  return reportClientError(error).pipe(Effect.andThen(Deferred.fail(pendingInvocation.prepared, error)), Effect.andThen(Deferred.fail(pendingInvocation.deferred, error)), Effect.asVoid);
638
867
  };
639
868
  const handleMessage = Effect.fn("SyncClient.handleMessage")(function* (message) {
640
869
  switch (message.type) {
641
- case "Snapshot":
642
- if (queryCache.getById(message.id) === void 0) return;
643
- yield* resultFromWire(message.result, message.files ?? []).pipe(Effect.tap((result) => Effect.sync(() => queryCache.setResult(message.id, result, message.revision))));
870
+ case "Session":
871
+ if (message.event === "Updated") {
872
+ const previous = session.current();
873
+ if (previous !== void 0 && previous.sessionEpoch !== message.session.sessionEpoch) return;
874
+ if (previous?.viewRevision !== message.session.viewRevision) queryCache.invalidatePrivate();
875
+ session.accept(message.session, message.serverTime);
876
+ } else {
877
+ if (rememberedHandshake?.session?.sessionEpoch !== message.sessionEpoch) return;
878
+ session.suspend(message.event === "Unavailable" ? "Your ID session is temporarily unavailable." : void 0);
879
+ if (message.event !== "Unavailable") yield* Effect.forEach(HashMap.keys(invocations), (id) => rejectInvocation(id, ServerError.make({
880
+ code: "SessionChanged",
881
+ message: "The session changed. This operation may already have completed."
882
+ })));
883
+ }
884
+ return;
885
+ case "Invalidate":
886
+ if (message.sessionEpoch === session.current()?.sessionEpoch) queryCache.invalidate(message.ids);
644
887
  return;
888
+ case "Snapshot": {
889
+ const current = session.current();
890
+ if (current === void 0 || message.identity?.sessionEpoch !== current.sessionEpoch) return;
891
+ if (message.identity.dependsOnId && message.identity.viewRevision !== current.viewRevision) return;
892
+ const entry = queryCache.getById(message.id);
893
+ if (entry === void 0) return;
894
+ entry.dependsOnId = message.identity.dependsOnId;
895
+ if (entry.revision !== void 0 && message.revision < entry.revision) return;
896
+ yield* resultFromWire(message.result, message.files ?? []).pipe(Effect.tap((result) => Effect.sync(() => queryCache.setResult(message.id, result, message.revision))), Effect.catchTags({ InvalidServerMessage: (error) => {
897
+ queryCache.setResult(message.id, error, message.revision);
898
+ return reportClientError(error);
899
+ } }));
900
+ return;
901
+ }
645
902
  case "Preparation": {
646
- const pendingInvocation = HashMap.getUnsafe(invocations, message.id);
903
+ if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
904
+ const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, message.id));
647
905
  if (pendingInvocation !== void 0) yield* Deferred.succeed(pendingInvocation.prepared, message.uploads);
648
906
  return;
649
907
  }
650
908
  case "Result": {
651
- const pendingInvocation = HashMap.getUnsafe(invocations, message.id);
909
+ if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
910
+ const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, message.id));
652
911
  if (pendingInvocation === void 0) return;
653
- invocations = HashMap.remove(invocations, message.id);
654
- yield* resultFromWire(message.result).pipe(Effect.flatMap((result) => Deferred.succeed(pendingInvocation.deferred, result)));
912
+ yield* resultFromWire(message.result).pipe(Effect.flatMap((result) => {
913
+ invocations = HashMap.remove(invocations, message.id);
914
+ return Deferred.succeed(pendingInvocation.deferred, result);
915
+ }), Effect.catchTags({ InvalidServerMessage: (error) => rejectInvocation(message.id, error) }));
655
916
  return;
656
917
  }
657
918
  case "Error": {
919
+ if (message.sessionEpoch !== session.current()?.sessionEpoch) return;
658
920
  const error = message.operation === void 0 ? ServerError.make({
659
921
  code: message.code,
660
922
  message: message.message
@@ -665,7 +927,7 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
665
927
  });
666
928
  if (message.operation?.type === "Invocation") yield* rejectInvocation(message.operation.id, error);
667
929
  else if (message.operation?.type === "Subscription") {
668
- queryCache.setError(message.operation.id, error);
930
+ queryCache.setResult(message.operation.id, error);
669
931
  yield* reportClientError(error);
670
932
  } else yield* reportClientError(error);
671
933
  return;
@@ -683,7 +945,8 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
683
945
  });
684
946
  const connect = Effect.scoped(Effect.gen(function* () {
685
947
  if (reloadRequested) return yield* Effect.never;
686
- const socket = yield* Socket.makeWebSocket(socketUrl(), { closeCodeIsError: (code) => {
948
+ yield* session.bootstrap.pipe(Effect.mapError((error) => ConnectionUnavailable.make({ message: error.message })));
949
+ const socket = yield* Socket.makeWebSocket(`${socketUrl()}${session.socketParameters()}`, { closeCodeIsError: (code) => {
687
950
  if (shouldReloadForCloseCode(code)) requestReload();
688
951
  return false;
689
952
  } });
@@ -697,6 +960,22 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
697
960
  return;
698
961
  }
699
962
  if (decision === "Ignore") return;
963
+ if (handshake.session === void 0 || handshake.serverTime === void 0) {
964
+ requestReload();
965
+ return;
966
+ }
967
+ if (rememberedHandshake?.session !== void 0 && rememberedHandshake.session.sessionEpoch !== handshake.session.sessionEpoch) {
968
+ queryCache.invalidatePrivate();
969
+ yield* Effect.forEach(HashMap.keys(invocations), (id) => rejectInvocation(id, ServerError.make({
970
+ code: "SessionChanged",
971
+ message: "The session changed. This operation may already have completed."
972
+ })));
973
+ } else if (rememberedHandshake?.session?.viewRevision !== handshake.session.viewRevision) queryCache.invalidatePrivate();
974
+ yield* Effect.forEach(HashMap.entries(invocations), ([id, pending]) => pending.sessionEpoch === handshake.session.sessionEpoch ? Effect.void : rejectInvocation(id, ServerError.make({
975
+ code: "SessionChanged",
976
+ message: "The session changed. This operation may already have completed."
977
+ })));
978
+ session.accept(handshake.session, handshake.serverTime);
700
979
  rememberedHandshake = handshake;
701
980
  handshakeAccepted = true;
702
981
  writer = write;
@@ -722,9 +1001,14 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
722
1001
  });
723
1002
  return yield* handleMessage(message);
724
1003
  });
1004
+ const suspended = yield* Deferred.make();
1005
+ sessionSuspended = suspended;
1006
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
1007
+ if (sessionSuspended === suspended) sessionSuspended = void 0;
1008
+ }));
725
1009
  const run = yield* socket.runString(handleText).pipe(Effect.forkChild);
726
1010
  yield* Effect.raceFirst(Deferred.await(handshakeReceived).pipe(Effect.timeout("10 seconds")), Fiber.join(run));
727
- yield* Fiber.join(run);
1011
+ yield* Effect.raceFirst(Fiber.join(run), Deferred.await(suspended));
728
1012
  })).pipe(Effect.mapError((cause) => Schema.is(ClientInfrastructureError)(cause) ? cause : ConnectionUnavailable.make({
729
1013
  cause,
730
1014
  message: "The sync connection was lost."
@@ -735,6 +1019,7 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
735
1019
  return yield* queryCache.observe(functionAddress, args);
736
1020
  });
737
1021
  const mutate = Effect.fn("SyncClient.mutate")(function* (functionAddress, args) {
1022
+ const admittedSession = session.current() ?? rememberedHandshake?.session ?? (yield* session.bootstrap.pipe(Effect.mapError((error) => ConnectionUnavailable.make({ message: error.message }))));
738
1023
  const invocationId = yield* ids.generate(InvocationId);
739
1024
  const preparedArguments = yield* prepareMutationArguments(args).pipe(Effect.provideService(IdGenerator, ids), Effect.mapError((cause) => InvalidMutationArguments.make({
740
1025
  cause,
@@ -767,6 +1052,7 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
767
1052
  files: preparedArguments.fileIds
768
1053
  };
769
1054
  invocations = HashMap.set(invocations, invocationId, {
1055
+ sessionEpoch: admittedSession.sessionEpoch,
770
1056
  deferred,
771
1057
  function: functionAddress,
772
1058
  message,
@@ -781,6 +1067,7 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
781
1067
  const uploads = yield* Deferred.await(prepared);
782
1068
  const expected = new Set(message.files);
783
1069
  const received = /* @__PURE__ */ new Set();
1070
+ const transfers = [];
784
1071
  for (const upload of uploads) {
785
1072
  if (!expected.has(upload.id) || received.has(upload.id)) return yield* InvalidServerMessage.make({
786
1073
  cause: /* @__PURE__ */ new Error(`Unexpected prepared file '${upload.id}'.`),
@@ -793,12 +1080,22 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
793
1080
  message: "The server prepared an unknown application file."
794
1081
  });
795
1082
  if (upload.url === void 0) continue;
796
- const uploadUrl = upload.url;
1083
+ transfers.push({
1084
+ file,
1085
+ url: upload.url
1086
+ });
1087
+ }
1088
+ if (received.size !== expected.size) return yield* InvalidServerMessage.make({
1089
+ cause: /* @__PURE__ */ new Error("The server omitted a prepared file."),
1090
+ message: "The server returned incomplete application file preparation data."
1091
+ });
1092
+ yield* Effect.forEach(transfers, Effect.fn("SyncClient.uploadFile")(function* ({ file, url }) {
797
1093
  const response = yield* Effect.tryPromise({
798
- try: () => fetch(uploadUrl, {
1094
+ try: (signal) => fetch(url, {
799
1095
  method: "PUT",
800
1096
  body: file,
801
- headers: { "content-type": file.type }
1097
+ headers: { "content-type": file.type },
1098
+ signal
802
1099
  }),
803
1100
  catch: (cause) => ConnectionUnavailable.make({
804
1101
  cause,
@@ -813,12 +1110,11 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
813
1110
  message: `File '${file.name}' was rejected by application storage.`
814
1111
  });
815
1112
  }
816
- }
817
- if (received.size !== expected.size) return yield* InvalidServerMessage.make({
818
- cause: /* @__PURE__ */ new Error("The server omitted a prepared file."),
819
- message: "The server returned incomplete application file preparation data."
1113
+ }), {
1114
+ concurrency: 3,
1115
+ discard: true
820
1116
  });
821
- const pendingInvocation = HashMap.getUnsafe(invocations, invocationId);
1117
+ const pendingInvocation = Option.getOrUndefined(HashMap.get(invocations, invocationId));
822
1118
  if (pendingInvocation === void 0) return yield* Deferred.await(deferred);
823
1119
  invocations = HashMap.set(invocations, invocationId, {
824
1120
  ...pendingInvocation,
@@ -840,10 +1136,60 @@ var SyncClient = class SyncClient extends Context.Service()("ignotum/client/sync
840
1136
  });
841
1137
  })).pipe(Layer.provide(Layer.merge(Socket.layerWebSocketConstructorGlobal, BrowserCrypto.layer)));
842
1138
  };
843
- const syncRuntime = ManagedRuntime.make(SyncClient.layer.pipe(Layer.provideMerge(clientErrorReporterLayer), Layer.provideMerge(IdGenerator.layer)));
1139
+ const syncRuntime = ManagedRuntime.make(SyncClient.layer.pipe(Layer.provideMerge(BrowserSession.layer), Layer.provideMerge(clientErrorReporterLayer), Layer.provideMerge(IdGenerator.layer)));
844
1140
  const hot = import.meta.hot;
845
1141
  if (hot !== void 0) hot.dispose(() => void syncRuntime.dispose());
846
1142
  //#endregion
1143
+ //#region src/client/page-observers.ts
1144
+ const makePageObservers = (onChange, releaseObserver) => {
1145
+ const pages = /* @__PURE__ */ new Map();
1146
+ let disposed = false;
1147
+ const releaseEntry = (entry) => {
1148
+ entry.released = true;
1149
+ entry.release?.();
1150
+ };
1151
+ return {
1152
+ observe: Effect.fn("PageObservers.observe")(function* (functionAddress, args) {
1153
+ if (disposed) return;
1154
+ const key = syncClientInternals.queryIdentity(functionAddress, args);
1155
+ const existing = pages.get(key);
1156
+ if (existing !== void 0) return yield* existing.acquire;
1157
+ const entry = {
1158
+ acquire: yield* Effect.cached(Effect.gen(function* () {
1159
+ const observer = yield* (yield* SyncClient).observe(functionAddress, args);
1160
+ if (entry.released) {
1161
+ releaseObserver(observer.release);
1162
+ return;
1163
+ }
1164
+ const unsubscribe = observer.subscribe(onChange);
1165
+ entry.release = () => {
1166
+ unsubscribe();
1167
+ releaseObserver(observer.release);
1168
+ };
1169
+ return observer;
1170
+ })),
1171
+ release: void 0,
1172
+ released: false
1173
+ };
1174
+ pages.set(key, entry);
1175
+ return yield* entry.acquire;
1176
+ }),
1177
+ retain: (keys) => {
1178
+ for (const [key, entry] of pages) {
1179
+ if (keys.has(key)) continue;
1180
+ pages.delete(key);
1181
+ releaseEntry(entry);
1182
+ }
1183
+ },
1184
+ release: () => {
1185
+ if (disposed) return;
1186
+ disposed = true;
1187
+ for (const entry of pages.values()) releaseEntry(entry);
1188
+ pages.clear();
1189
+ }
1190
+ };
1191
+ };
1192
+ //#endregion
847
1193
  //#region src/client/hooks.ts
848
1194
  function useQuery(reference, args) {
849
1195
  const functionPath = functionPathOf(reference);
@@ -916,6 +1262,10 @@ function usePaginatedQuery(reference, args, options = {}) {
916
1262
  result: pending()
917
1263
  }));
918
1264
  const loadMore = useCallback$1(() => setRequestedPages((count) => count + 1), []);
1265
+ const pages = useMemo$1(() => makePageObservers(() => setRefresh((version) => version + 1), (release) => {
1266
+ syncRuntime.runFork(release);
1267
+ }), [identity, pageSize]);
1268
+ useEffect$1(() => () => pages.release(), [pages]);
919
1269
  useDebugValue$1({
920
1270
  args: input,
921
1271
  function: functionPath,
@@ -931,9 +1281,8 @@ function usePaginatedQuery(reference, args, options = {}) {
931
1281
  useEffect$1(() => {
932
1282
  if (skipped) return;
933
1283
  let active = true;
934
- const releases = [];
1284
+ const usedPages = /* @__PURE__ */ new Set();
935
1285
  syncRuntime.runPromise(Effect.gen(function* () {
936
- const client = yield* SyncClient;
937
1286
  const items = [];
938
1287
  let cursor = null;
939
1288
  for (let pageNumber = 0; pageNumber < requestedPages; pageNumber += 1) {
@@ -944,18 +1293,9 @@ function usePaginatedQuery(reference, args, options = {}) {
944
1293
  pageSize
945
1294
  }
946
1295
  });
947
- const observer = yield* client.observe(functionPath, pageArgs);
948
- if (!active) {
949
- yield* observer.release;
950
- return;
951
- }
952
- const releaseSubscription = observer.subscribe(() => {
953
- if (active) setRefresh((version) => version + 1);
954
- });
955
- releases.push(() => {
956
- releaseSubscription();
957
- syncRuntime.runFork(observer.release);
958
- });
1296
+ usedPages.add(syncClientInternals.queryIdentity(functionPath, pageArgs));
1297
+ const observer = yield* pages.observe(functionPath, pageArgs);
1298
+ if (!active || observer === void 0) return;
959
1299
  const snapshot = observer.getSnapshot();
960
1300
  if (Schema.is(ClientInfrastructureError)(snapshot)) {
961
1301
  setState({
@@ -968,7 +1308,7 @@ function usePaginatedQuery(reference, args, options = {}) {
968
1308
  if (inspected === void 0) {
969
1309
  setState({
970
1310
  identity,
971
- result: items.length === 0 ? pending() : Result$1.succeed({
1311
+ result: items.length === 0 ? pending() : Result$2.succeed({
972
1312
  items,
973
1313
  loadMore,
974
1314
  status: "LoadingMore"
@@ -989,7 +1329,7 @@ function usePaginatedQuery(reference, args, options = {}) {
989
1329
  if (cursor === null) {
990
1330
  setState({
991
1331
  identity,
992
- result: Result$1.succeed({
1332
+ result: Result$2.succeed({
993
1333
  items,
994
1334
  loadMore,
995
1335
  status: "Exhausted"
@@ -1000,13 +1340,15 @@ function usePaginatedQuery(reference, args, options = {}) {
1000
1340
  }
1001
1341
  setState({
1002
1342
  identity,
1003
- result: Result$1.succeed({
1343
+ result: Result$2.succeed({
1004
1344
  items,
1005
1345
  loadMore,
1006
1346
  status: "CanLoadMore"
1007
1347
  })
1008
1348
  });
1009
- })).catch((error) => {
1349
+ }).pipe(Effect.ensuring(Effect.sync(() => {
1350
+ if (active) pages.retain(usedPages);
1351
+ })))).catch((error) => {
1010
1352
  if (active && Schema.is(ClientInfrastructureError)(error)) setState({
1011
1353
  identity,
1012
1354
  result: error
@@ -1014,13 +1356,13 @@ function usePaginatedQuery(reference, args, options = {}) {
1014
1356
  });
1015
1357
  return () => {
1016
1358
  active = false;
1017
- for (const release of releases) release();
1018
1359
  };
1019
1360
  }, [
1020
1361
  functionPath,
1021
1362
  identity,
1022
1363
  loadMore,
1023
1364
  pageSize,
1365
+ pages,
1024
1366
  refresh,
1025
1367
  requestedPages,
1026
1368
  skipped
@@ -1040,6 +1382,38 @@ const useMutation = (reference) => useMemo$1(() => {
1040
1382
  };
1041
1383
  return mutate;
1042
1384
  }, [reference]);
1385
+ function useId() {
1386
+ const [state, setState] = useState$1({ status: "pending" });
1387
+ useEffect$1(() => {
1388
+ let active = true;
1389
+ let unsubscribe;
1390
+ syncRuntime.runPromise(BrowserSession).then((session) => {
1391
+ if (!active) return;
1392
+ const update = () => setState(session.snapshot());
1393
+ unsubscribe = session.subscribe(update);
1394
+ update();
1395
+ });
1396
+ return () => {
1397
+ active = false;
1398
+ unsubscribe?.();
1399
+ };
1400
+ }, []);
1401
+ const actions = useMemo$1(() => ({
1402
+ signIn: (options) => syncRuntime.runPromise(Effect.gen(function* () {
1403
+ return yield* (yield* BrowserSession).start("signIn", options?.profile ?? [], options?.returnTo);
1404
+ })),
1405
+ requestProfile: (fields, options) => syncRuntime.runPromise(Effect.gen(function* () {
1406
+ return yield* (yield* BrowserSession).start("requestProfile", fields, options?.returnTo);
1407
+ })),
1408
+ signOut: () => syncRuntime.runPromise(Effect.gen(function* () {
1409
+ return yield* (yield* BrowserSession).signOut;
1410
+ }))
1411
+ }), []);
1412
+ return {
1413
+ ...state,
1414
+ ...actions
1415
+ };
1416
+ }
1043
1417
  //#endregion
1044
1418
  //#region src/client/app.ts
1045
1419
  const app = (definition) => {
@@ -1048,8 +1422,8 @@ const app = (definition) => {
1048
1422
  };
1049
1423
  //#endregion
1050
1424
  //#region src/client/index.ts
1051
- const Result = { match: Result$1.match };
1425
+ const Result = { match: Result$2.match };
1052
1426
  //#endregion
1053
- export { Component, Files, Fragment, Query, Result, app, cloneElement, createContext, createElement, createRef, h, isValidElement, toChildArray, useCallback, useContext, useDebugValue, useEffect, useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useMutation, usePaginatedQuery, useQuery, useReducer, useRef, useState };
1427
+ export { Component, Files, Fragment, Query, Result, app, cloneElement, createContext, createElement, createRef, h, isValidElement, toChildArray, useCallback, useContext, useDebugValue, useEffect, useElementId, useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useMutation, usePaginatedQuery, useQuery, useReducer, useRef, useState };
1054
1428
 
1055
1429
  //# sourceMappingURL=client.js.map