@opengeni/api-router 0.12.5 → 0.12.12

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.
@@ -38,6 +38,97 @@ export type CaptureStoragePort = {
38
38
  }) => Promise<{ url: string; expiresAt: Date }>;
39
39
  };
40
40
 
41
+ type LoadedManifest = {
42
+ manifest: WorkspaceCaptureManifest;
43
+ byteLength: number;
44
+ stats: WorkspaceCaptureStats;
45
+ };
46
+
47
+ type ManifestCacheEntry = LoadedManifest & {
48
+ identity: string;
49
+ inlineResponse?: Extract<GetWorkspaceCaptureResponse, { available: true }>;
50
+ };
51
+
52
+ /**
53
+ * Process-local cache for immutable capture manifests. Capture rows point at a
54
+ * revision-specific object key, so a successfully validated blob is safe to
55
+ * reuse as long as the row identity still matches. Both entry count and bytes
56
+ * are bounded; large signed-manifest responses bypass the cache.
57
+ */
58
+ export class WorkspaceCaptureManifestCache {
59
+ readonly #entries = new Map<string, ManifestCacheEntry>();
60
+ #retainedBytes = 0;
61
+
62
+ constructor(
63
+ readonly maxEntries = 64,
64
+ readonly maxBytes = 16 * 1024 * 1024,
65
+ ) {}
66
+
67
+ get(row: WorkspaceCaptureRow): LoadedManifest | null {
68
+ const key = row.manifestKey;
69
+ if (!key) return null;
70
+ const entry = this.#entries.get(key);
71
+ if (!entry || entry.identity !== manifestIdentity(row)) return null;
72
+ this.#entries.delete(key);
73
+ this.#entries.set(key, entry);
74
+ return entry;
75
+ }
76
+
77
+ getInlineResponse(
78
+ row: WorkspaceCaptureRow,
79
+ ): Extract<GetWorkspaceCaptureResponse, { available: true }> | null {
80
+ const key = row.manifestKey;
81
+ if (!key) return null;
82
+ const entry = this.#entries.get(key);
83
+ if (!entry || entry.identity !== manifestIdentity(row) || !entry.inlineResponse) return null;
84
+ this.#entries.delete(key);
85
+ this.#entries.set(key, entry);
86
+ return entry.inlineResponse;
87
+ }
88
+
89
+ setInlineResponse(
90
+ row: WorkspaceCaptureRow,
91
+ response: Extract<GetWorkspaceCaptureResponse, { available: true }>,
92
+ ): void {
93
+ const key = row.manifestKey;
94
+ if (!key) return;
95
+ const entry = this.#entries.get(key);
96
+ if (!entry || entry.identity !== manifestIdentity(row)) return;
97
+ entry.inlineResponse = response;
98
+ }
99
+
100
+ set(row: WorkspaceCaptureRow, loaded: LoadedManifest): void {
101
+ const key = row.manifestKey;
102
+ if (
103
+ !key ||
104
+ this.maxEntries <= 0 ||
105
+ this.maxBytes <= 0 ||
106
+ loaded.byteLength > CAPTURE_INLINE_MANIFEST_MAX_BYTES ||
107
+ loaded.byteLength > this.maxBytes
108
+ ) {
109
+ return;
110
+ }
111
+ const previous = this.#entries.get(key);
112
+ if (previous) {
113
+ this.#retainedBytes -= previous.byteLength;
114
+ this.#entries.delete(key);
115
+ }
116
+ this.#entries.set(key, { ...loaded, identity: manifestIdentity(row) });
117
+ this.#retainedBytes += loaded.byteLength;
118
+ while (this.#entries.size > this.maxEntries || this.#retainedBytes > this.maxBytes) {
119
+ const oldestKey = this.#entries.keys().next().value;
120
+ if (oldestKey === undefined) break;
121
+ const oldest = this.#entries.get(oldestKey);
122
+ this.#entries.delete(oldestKey);
123
+ if (oldest) this.#retainedBytes -= oldest.byteLength;
124
+ }
125
+ }
126
+ }
127
+
128
+ function manifestIdentity(row: WorkspaceCaptureRow): string {
129
+ return `${row.sessionId}\0${row.turnId}\0${row.revision}\0${row.leaseEpoch}\0${row.capturedAt}`;
130
+ }
131
+
41
132
  function signedUrl(signed: { url: string; expiresAt: Date }): { url: string; expiresAt: string } {
42
133
  return { url: signed.url, expiresAt: signed.expiresAt.toISOString() };
43
134
  }
@@ -51,12 +142,11 @@ function signedUrl(signed: { url: string; expiresAt: Date }): { url: string; exp
51
142
  async function loadManifest(
52
143
  row: WorkspaceCaptureRow,
53
144
  storage: CaptureStoragePort,
54
- ): Promise<{
55
- manifest: WorkspaceCaptureManifest;
56
- byteLength: number;
57
- stats: WorkspaceCaptureStats;
58
- } | null> {
145
+ cache?: WorkspaceCaptureManifestCache,
146
+ ): Promise<LoadedManifest | null> {
59
147
  if (!row.manifestKey) return null;
148
+ const cached = cache?.get(row);
149
+ if (cached) return cached;
60
150
  const stats = WorkspaceCaptureStats.safeParse(row.stats);
61
151
  if (!stats.success) {
62
152
  console.warn(
@@ -114,7 +204,9 @@ async function loadManifest(
114
204
  );
115
205
  return null;
116
206
  }
117
- return { manifest, byteLength: blob.bytes.byteLength, stats: servedStats };
207
+ const loaded = { manifest, byteLength: blob.bytes.byteLength, stats: servedStats };
208
+ cache?.set(row, loaded);
209
+ return loaded;
118
210
  }
119
211
 
120
212
  /**
@@ -126,6 +218,7 @@ async function loadManifest(
126
218
  export async function serveWorkspaceCapture(
127
219
  row: WorkspaceCaptureRow | null,
128
220
  storage: CaptureStoragePort,
221
+ cache?: WorkspaceCaptureManifestCache,
129
222
  ): Promise<GetWorkspaceCaptureResponse> {
130
223
  if (!row) {
131
224
  return { available: false };
@@ -148,11 +241,13 @@ export async function serveWorkspaceCapture(
148
241
  });
149
242
  }
150
243
  if (row.state !== "available" || !row.manifestKey) return { available: false };
244
+ const cachedResponse = cache?.getInlineResponse(row);
245
+ if (cachedResponse) return cachedResponse;
151
246
  // Validate every manifest before serving it, including the rare >2MB signed
152
247
  // path. Previously that branch signed arbitrary bytes merely because they
153
248
  // exceeded the inline cap, allowing a poison/mis-keyed blob to bypass both the
154
249
  // schema and row-identity checks.
155
- const loaded = await loadManifest(row, storage);
250
+ const loaded = await loadManifest(row, storage, cache);
156
251
  if (!loaded) return { available: false };
157
252
  const meta = {
158
253
  available: true as const,
@@ -164,11 +259,16 @@ export async function serveWorkspaceCapture(
164
259
  stats: loaded.stats,
165
260
  };
166
261
  if (loaded.byteLength <= CAPTURE_INLINE_MANIFEST_MAX_BYTES) {
167
- return GetWorkspaceCaptureResponse.parse({
262
+ const response = GetWorkspaceCaptureResponse.parse({
168
263
  ...meta,
169
264
  manifest: loaded.manifest,
170
265
  manifestUrl: null,
171
266
  });
267
+ if (!response.available) {
268
+ throw new Error("validated inline capture response lost its available discriminator");
269
+ }
270
+ cache?.setInlineResponse(row, response);
271
+ return response;
172
272
  }
173
273
  const signed = await storage.createGetUrl({
174
274
  key: row.manifestKey,