Skip to content

Commit 9cefb24

Browse files
mizchiclaude
andcommitted
fix: persist GitServeSession state to durable storage
The DO was losing session state (active, sessionToken, registeredAt) when evicted between poll intervals, causing clone requests to get 404 "session not active". Now persists to storage on register and restores on construction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8d95ff6 commit 9cefb24

1 file changed

Lines changed: 66 additions & 4 deletions

File tree

src/git_serve_session.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,18 @@ function fromBase64(b64: string): Uint8Array {
5757
return bytes;
5858
}
5959

60+
export interface PersistableSessionState {
61+
active: boolean;
62+
sessionToken: string;
63+
registeredAt: number | null;
64+
}
65+
6066
export function createGitServeSession(): {
6167
state: GitServeSessionState;
6268
fetch: (request: Request) => Promise<Response>;
6369
cleanup: () => void;
70+
persistableState: () => PersistableSessionState;
71+
restore: (saved: PersistableSessionState) => void;
6472
} {
6573
const state: GitServeSessionState = {
6674
active: false,
@@ -351,18 +359,72 @@ export function createGitServeSession(): {
351359
return Response.json({ ok: false, error: 'not found' }, { status: 404 });
352360
}
353361

354-
return { state, fetch, cleanup };
362+
function persistableState(): PersistableSessionState {
363+
return {
364+
active: state.active,
365+
sessionToken: state.sessionToken,
366+
registeredAt: state.registeredAt,
367+
};
368+
}
369+
370+
function restore(saved: PersistableSessionState): void {
371+
state.active = saved.active;
372+
state.sessionToken = saved.sessionToken;
373+
state.registeredAt = saved.registeredAt;
374+
if (state.active) {
375+
resetSessionTimer();
376+
}
377+
}
378+
379+
return { state, fetch, cleanup, persistableState, restore };
355380
}
356381

382+
// Durable Object class used by Cloudflare Workers.
383+
// Accepts a state object with storage for persistence.
384+
// Also works without storage (e.g. in tests / Deno).
357385
export class GitServeSession {
358386
private session: ReturnType<typeof createGitServeSession>;
387+
// deno-lint-ignore no-explicit-any
388+
private storage: any;
389+
private ready: Promise<void>;
359390

360-
constructor() {
391+
// deno-lint-ignore no-explicit-any
392+
constructor(state?: any, _env?: any) {
361393
this.session = createGitServeSession();
394+
this.storage = state?.storage ?? null;
395+
396+
const restoreFromStorage = async () => {
397+
if (!this.storage) return;
398+
const saved = await this.storage.get('session_state');
399+
if (saved && typeof saved === 'object') {
400+
this.session.restore(saved as PersistableSessionState);
401+
}
402+
};
403+
404+
if (state && typeof state.blockConcurrencyWhile === 'function') {
405+
this.ready = state.blockConcurrencyWhile(restoreFromStorage);
406+
} else {
407+
this.ready = restoreFromStorage();
408+
}
409+
}
410+
411+
private async persist(): Promise<void> {
412+
if (!this.storage) return;
413+
await this.storage.put('session_state', this.session.persistableState());
362414
}
363415

364-
// deno-lint-ignore require-await
365416
async fetch(request: Request): Promise<Response> {
366-
return this.session.fetch(request);
417+
await this.ready;
418+
const url = new URL(request.url);
419+
const path = url.pathname;
420+
421+
const response = await this.session.fetch(request);
422+
423+
// Persist state after register or cleanup-triggering operations
424+
if (path === '/register' && request.method === 'POST') {
425+
await this.persist();
426+
}
427+
428+
return response;
367429
}
368430
}

0 commit comments

Comments
 (0)