Publish Claude Code source snapshot from a single baseline commit
Squash the imported source snapshot and follow-up documentation history into one root commit so the archive starts from a single coherent state. Constraint: Repository intentionally tracks an analyzed Claude Code source snapshot Constraint: Author and committer must be instructkr <no-contact@instruct.kr> Rejected: Preserve the four-step import/docs history | user explicitly requested one squashed commit Confidence: high Scope-risk: broad Reversibility: clean Directive: Keep future analysis and refactor commits separate from this archive baseline Tested: git status clean; local history rewritten to one commit; force-pushed main to origin and instructkr Not-tested: Fresh clone verification after push
This commit is contained in:
131
src/cli/transports/WorkerStateUploader.ts
Normal file
131
src/cli/transports/WorkerStateUploader.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { sleep } from '../../utils/sleep.js'
|
||||
|
||||
/**
|
||||
* Coalescing uploader for PUT /worker (session state + metadata).
|
||||
*
|
||||
* - 1 in-flight PUT + 1 pending patch
|
||||
* - New calls coalesce into pending (never grows beyond 1 slot)
|
||||
* - On success: send pending if exists
|
||||
* - On failure: exponential backoff (clamped), retries indefinitely
|
||||
* until success or close(). Absorbs any pending patches before each retry.
|
||||
* - No backpressure needed — naturally bounded at 2 slots
|
||||
*
|
||||
* Coalescing rules:
|
||||
* - Top-level keys (worker_status, external_metadata) — last value wins
|
||||
* - Inside external_metadata / internal_metadata — RFC 7396 merge:
|
||||
* keys are added/overwritten, null values preserved (server deletes)
|
||||
*/
|
||||
|
||||
type WorkerStateUploaderConfig = {
|
||||
send: (body: Record<string, unknown>) => Promise<boolean>
|
||||
/** Base delay for exponential backoff (ms) */
|
||||
baseDelayMs: number
|
||||
/** Max delay cap (ms) */
|
||||
maxDelayMs: number
|
||||
/** Random jitter range added to retry delay (ms) */
|
||||
jitterMs: number
|
||||
}
|
||||
|
||||
export class WorkerStateUploader {
|
||||
private inflight: Promise<void> | null = null
|
||||
private pending: Record<string, unknown> | null = null
|
||||
private closed = false
|
||||
private readonly config: WorkerStateUploaderConfig
|
||||
|
||||
constructor(config: WorkerStateUploaderConfig) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a patch to PUT /worker. Coalesces with any existing pending
|
||||
* patch. Fire-and-forget — callers don't need to await.
|
||||
*/
|
||||
enqueue(patch: Record<string, unknown>): void {
|
||||
if (this.closed) return
|
||||
this.pending = this.pending ? coalescePatches(this.pending, patch) : patch
|
||||
void this.drain()
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.closed = true
|
||||
this.pending = null
|
||||
}
|
||||
|
||||
private async drain(): Promise<void> {
|
||||
if (this.inflight || this.closed) return
|
||||
if (!this.pending) return
|
||||
|
||||
const payload = this.pending
|
||||
this.pending = null
|
||||
|
||||
this.inflight = this.sendWithRetry(payload).then(() => {
|
||||
this.inflight = null
|
||||
if (this.pending && !this.closed) {
|
||||
void this.drain()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Retries indefinitely with exponential backoff until success or close(). */
|
||||
private async sendWithRetry(payload: Record<string, unknown>): Promise<void> {
|
||||
let current = payload
|
||||
let failures = 0
|
||||
while (!this.closed) {
|
||||
const ok = await this.config.send(current)
|
||||
if (ok) return
|
||||
|
||||
failures++
|
||||
await sleep(this.retryDelay(failures))
|
||||
|
||||
// Absorb any patches that arrived during the retry
|
||||
if (this.pending && !this.closed) {
|
||||
current = coalescePatches(current, this.pending)
|
||||
this.pending = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private retryDelay(failures: number): number {
|
||||
const exponential = Math.min(
|
||||
this.config.baseDelayMs * 2 ** (failures - 1),
|
||||
this.config.maxDelayMs,
|
||||
)
|
||||
const jitter = Math.random() * this.config.jitterMs
|
||||
return exponential + jitter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coalesce two patches for PUT /worker.
|
||||
*
|
||||
* Top-level keys: overlay replaces base (last value wins).
|
||||
* Metadata keys (external_metadata, internal_metadata): RFC 7396 merge
|
||||
* one level deep — overlay keys are added/overwritten, null values
|
||||
* preserved for server-side delete.
|
||||
*/
|
||||
function coalescePatches(
|
||||
base: Record<string, unknown>,
|
||||
overlay: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const merged = { ...base }
|
||||
|
||||
for (const [key, value] of Object.entries(overlay)) {
|
||||
if (
|
||||
(key === 'external_metadata' || key === 'internal_metadata') &&
|
||||
merged[key] &&
|
||||
typeof merged[key] === 'object' &&
|
||||
typeof value === 'object' &&
|
||||
value !== null
|
||||
) {
|
||||
// RFC 7396 merge — overlay keys win, nulls preserved for server
|
||||
merged[key] = {
|
||||
...(merged[key] as Record<string, unknown>),
|
||||
...(value as Record<string, unknown>),
|
||||
}
|
||||
} else {
|
||||
merged[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
Reference in New Issue
Block a user