/** * Shared wiring and pacing helpers for the Google Sheets API provider * operations. * * The provider class hands one immutable `GoogleSheetsApiProviderDeps` object * to every operation function so the class stays a thin facade. Bounded * request-start admission (independent read/write lanes, per-credential * slots) lives in the kernel (`@hikoutei/ikisaki`); the `runRead`/`runWrite` * wrappers here bind that admission to Sheets telemetry, quota-outcome * feedback, and refusal errors. Route validation against the registered * definition and batchUpdate reply validation live here because every * operation shares them. */ import type { RegisteredSyncProjectionDefinition } from "../../../../contracts/sheets/sheetsProvisioning.js"; import { type Presence } from "../../../../contracts/state/index.js"; import type { GoogleSheetsApiRequestEvent } from "../../../../contracts/sheets/googleSheetsApi.js"; import type { GoogleSheetsApiTransport } from "../transport/googleSheetsApiTransport.js"; import { type BandedGet, type BandEvidence, type EngineRuntime } from "@hikoutei/ikisaki"; import { groupByRouteKey as groupNeutralByRouteKey, refreshFirstRouteContext as refreshNeutralFirstRouteContext, type WriteBatchTelemetry } from "@hikoutei/ikisaki"; import { ReceiptReadCursor, type ReadCalibration } from "@hikoutei/ikisaki"; import type { ParsedGridData, ParsedSheet, PreflightContext, PreflightReceipt } from "../model/preflightTypes.js"; import type { BuiltApplyBatch } from "../model/batchBuilder.js"; import type { ReadQoSScheduler, RequestStartLimiter } from "@hikoutei/ikisaki"; import { type CredentialPacingPool, type RequestStartPacing } from "@hikoutei/ikisaki"; import { type QuotaGovernorTimingDefaults, type QuotaPacingGovernor, type RollingQuotaBudget } from "@hikoutei/ikisaki"; /** * Re-exports of the neutral request-start admission surface (owned by * `@hikoutei/ikisaki` `worker/pacing/requestAdmission.ts`) so operation * modules keep importing the pacing vocabulary from this shared wiring * module. Route validation, batchUpdate reply validation, and * transport-outcome mapping stay Sheets-owned below. */ export { credentialBinding, type CredentialPacingPool, type CredentialPacingSlot, type ReadPacing, type RequestStartAdmissionOutcome, type RequestStartPacing, } from "@hikoutei/ikisaki"; /** * Minimal FIFO promise-tail lock used to serialize shared receipt-tab * initialization. * * The Google Sheets API provider holds one instance per spreadsheet. Two * prepared write batches (possibly on different routes/sheets) that both * preflighted the shared receipt tab absent can race: without a guard, both * re-read and, if the tab still does not exist, both emit a duplicate * `addSheet`, failing the second write with a 400. This serializes the * refresh+write so the first writer creates the tab and later writers append * to it instead. It is a promise gate, not a worker/lease authority; the * durable outbox, leases, and receipts remain the source of truth. */ export declare class PromiseTailLock { private tail; /** Runs `task` after any prior holder completes; never deadlocks on throw. */ run(task: () => Promise): Promise; } /** Immutable wiring every operation function receives from the provider. */ export interface GoogleSheetsApiProviderDeps { readonly spreadsheetId: string; /** * Per-instance nonce bound into every prepared-apply state this provider * produces, so a prepared token from another provider instance (e.g. after * the provider was re-pointed to a new spreadsheet) fails closed before any * write even when the spreadsheetId happens to match. */ readonly providerNonce: string; /** * Identity registry of the exact prepared-apply state objects this provider * produced. `preflightApplyEffects` registers each returned state; the * write+verify stage rejects any state not in this registry, so a forged or * replaced nested plan fails before any remote call. */ readonly preparedStateRegistry: WeakSet; /** * Per-spreadsheet guard for shared receipt-tab initialization. Acquired * only when a prepared write observed the receipt absent; once the tab * exists later writes never touch it, so steady state pays no lock. */ readonly receiptInitLock: PromiseTailLock; readonly definitions: readonly RegisteredSyncProjectionDefinition[]; readonly transport: GoogleSheetsApiTransport; /** * Per-provider receipt READ cursor (neutral `ReceiptReadCursor` over the * Sheets `PreflightReceipt` evidence). Steady state apply/fast-append * preflights AND postcondition probes read only the receipt tail band * this cursor opens; the receipt-refresh path and the probe's * whole-table evidence fallback keep the historical full receipt read. */ readonly receiptReadCursor: ReceiptReadCursor; /** * Per-provider authoritative ROW BOUND cache (sheet title → committed * `gridProperties.rowCount`) for the unified read engine's band planning. * Settled by a range-less metadata enumeration when cold (see * `ensureSheetRowBounds`) and refreshed from EVERY engine response's * sheet properties, so it tracks grid growth without extra calls. A * too-low entry can never truncate coverage: the engine's last band per * column always stays open-ended. */ readonly sheetRowBounds: Map; /** * Per-provider read-size calibration (neutral kernel planner): observed * `responseBytes ÷ cellsRequested` above a class constant inflates future * estimates, shrinking band sizes on the NEXT plan (telemetry-based budget * reduction; never grows one request). */ readonly readCalibration: ReadCalibration; readonly readTimeoutMs: number; readonly maxBatchBytes: number; /** * Internal read QoS scheduler: pacing and weighted fairness for read-class * starts. POLLING (values/observation/safety reads) and PREFLIGHT (outbound * read-ahead reads) share ONE timeline and interval under the 2:1 weighted * policy; the separate WRITE limiter paces writes independently. */ readonly readScheduler: ReadQoSScheduler; /** Write limiter: batchUpdate starts serialize only against writes. */ readonly writeLimiter: RequestStartLimiter; /** * The single quota/backoff marker object for this provider: the SAME * object builds every pooled governor AND feeds both * `isQuotaLimitedOutcome` call sites below, so pacing backoff and * quota-outcome classification can never diverge on the markers. */ readonly timingDefaults: QuotaGovernorTimingDefaults; /** * Sliding-window per-minute request-start budget for the READ lane * (getSpreadsheet starts, paced on either read class). Enforced IN * ADDITION to the interval pacing: a start needs both a budget slot and * an interval slot. Postcondition reads paced on the write lane count * against the write budget instead (they are rare recovery traffic). */ readonly readBudget: RollingQuotaBudget; /** Sliding-window per-minute request-start budget for the WRITE lane. */ readonly writeBudget: RollingQuotaBudget; /** * AIMD pacing feedback: quota-limited (429) responses grow the offending * lane's pacing interval via the limiters' `getIntervalMs`, quiet success * recovers it gradually. Gates request STARTS only; never touches CAS, * prepared state, or result handling. Constructed with the single * `timingDefaults` object above (one per pooled identity). */ readonly quotaGovernor: QuotaPacingGovernor; /** * Maximum admitted wait for ONE request start: a call whose predicted slot * is further out is refused before transport. The per-minute budget gate * and the lane pacing gate SHARE this one bound via a single admission * deadline, so total bounded-admission waiting never exceeds it. On a * pooled run the SAME deadline spans the whole search: each refusal moves * on to the next slot with only the time still remaining, so a busy * identity never blocks a healthy one and one request start still pays at * most one bounded wait. */ readonly maxRequestStartWaitMs: number; /** * Per-credential pacing pool (credential pools with 2+ identities only). * `undefined` — the historical single-credential path — keeps admission * byte-identical: the flat `readScheduler`/`writeLimiter`/budgets/ * `quotaGovernor` fields above ARE the one slot, and transport requests * carry no `credentialIndex`. */ readonly credentialPacing?: CredentialPacingPool; readonly now: () => number; readonly onRequest: ((event: GoogleSheetsApiRequestEvent) => void) | undefined; } /** * Redacted batch metadata attached to one write request event. * * Only counts and byte estimates are exposed; never ids, spreadsheet ids, * URLs, credentials, values, or payloads. All fields are optional so a * caller that lacks a value simply omits it. */ export interface GoogleSheetsApiRequestMeta { /** Pacing wait before the request-start slot was granted (0 when none). */ readonly pacingWaitMs?: number; /** * Pool identity this request was admitted AND signed with (index only, * never any credential material); absent on the single-credential path. */ readonly credentialIndex?: number; /** Number of batchUpdate requests in the written batch. */ readonly requestCount?: number; /** Serialized batchUpdate body-size estimate in bytes. */ readonly bodyBytes?: number; /** Effects requested for this write batch. */ readonly requestedEffects?: number; /** Effects included in the written batch (the budget-fitting prefix). */ readonly includedEffects?: number; /** * Parsed-response size estimate in bytes (see the event field docs). * Computed by the run helpers ONLY while a telemetry sink is attached. */ readonly responseBytes?: number; } /** * Estimates the serialized size of one parsed transport response. * * The transport hands back PARSED JSON (no raw bytes), so the payload size * is a `JSON.stringify().length` estimate. Returns `undefined` when the * value cannot be serialized (e.g. BigInt) instead of throwing: a size * estimate must never change a successful remote result. */ export declare function estimateJsonBytesOrNull(value: unknown): number | undefined; /** * Builds one raw-response measurement carrier: `meta` for `runRead`/`runWrite` * plus the matching `onRawResponse` callback for read helpers that can capture * the RAW transport document before parsing. Both are `undefined`-safe: with * no telemetry sink attached, `onRawResponse` is `undefined` and the meta * carrier stays empty (zero estimate cost without telemetry). */ export declare function createRawResponseMeta(deps: GoogleSheetsApiProviderDeps): { readonly meta: { responseBytes?: number; }; readonly onRawResponse: ((raw: unknown) => void) | undefined; }; /** * Paces ONE `getSpreadsheet` transport call and emits one read event. * * `pacing` selects the request-start lane: the two read classes route through * the shared read QoS scheduler (`polling` for values/observation/safety * reads, `preflight` for outbound read-ahead), while a postcondition read * that verifies a just-written row passes `"write"` so it serializes against * writes instead of competing with the read burst. The telemetry operation * stays `getSpreadsheet` in both cases — it is still a read transport call; * only the pacing lane changes. */ export declare function runRead(deps: GoogleSheetsApiProviderDeps, /** Receives the admitted pool identity; the task MUST bind it into the * transport request via `credentialBinding` when the pool is active * (`undefined` on the single-credential path). */ task: (credentialIndex: number | undefined) => Promise, pacing?: RequestStartPacing, meta?: GoogleSheetsApiRequestMeta): Promise; /** Paces ONE `batchUpdate` transport call and emits one write event. */ export declare function runWrite(deps: GoogleSheetsApiProviderDeps, /** See `runRead`: bind the admitted pool identity into the request. */ task: (credentialIndex: number | undefined) => Promise, meta?: GoogleSheetsApiRequestMeta): Promise; /** * Emits one redacted telemetry event; diagnostics must never throw. * * The code is re-sanitized at the sink as defense in depth: every value * reaching `onRequest` is either an allowlisted stable code or the fixed * `unknown` category, so a future caller can never forward an arbitrary * remote string. */ export declare function emitRequest(deps: GoogleSheetsApiProviderDeps, operation: "getSpreadsheet" | "batchUpdate", pacing: RequestStartPacing, operationCount: number, startedAt: number, ok: boolean, httpStatus: Presence, code: Presence, meta?: GoogleSheetsApiRequestMeta): void; /** Resolves the registered projection definition for one physical sheet. */ export declare function definitionForPhysicalSheet(deps: GoogleSheetsApiProviderDeps, physicalSheetId: string): RegisteredSyncProjectionDefinition; /** * Route validation against the registered definition (mirrors * `validateRoute` in the Apps Script operation provider). */ export declare function validateRoute(request: { readonly sheetName: string; readonly registeredRange: string; readonly projection: string; readonly schemaVersion: number; }, definition: RegisteredSyncProjectionDefinition): void; /** Derives the per-route effect options exactly like the Apps Script provider. */ export declare function effectRouteOptions(definition: RegisteredSyncProjectionDefinition): { readonly identityField: Presence; readonly checkboxHeaders: readonly string[]; }; /** * Validates a batchUpdate reply shape: one reply per request, with the * addSheet reply carrying the created sheet id. A malformed 2xx response must * not close effects, so this throws a delivery-uncertain state error * classified as a `batch_update_reply` / `malformed_reply` invalid state. */ export declare function requireValidBatchUpdateReply(value: unknown, requestCount: number): void; /** * Thin Sheets adapters over the neutral kernel engines. * * These keep the historical operation signatures: they convert the Sheets * grid model to the kernel's neutral descriptors at the call site (sheet * bounds, calibration, receipt evidence), run the neutral iteration, and * map telemetry/validation back into Sheets vocabulary. The band/batch * planning and execution semantics live in `@hikoutei/ikisaki` and are * unchanged here. */ /** Sheets grid documents keyed the way the transport returns them. */ export type SheetsBandedGet = BandedGet; export type SheetsEngineRuntime = EngineRuntime; /** * Builds the executor for ONE logical read: fixed field mask, evidence * class, pacing lane, and telemetry label. The returned closure owns no * state — bounds/calibration updates land on the shared `deps` carriers. */ export declare function createBandedGet(deps: GoogleSheetsApiProviderDeps, pacing: RequestStartPacing, fields: string, evidence: BandEvidence, label: string): SheetsBandedGet; /** * Builds the model-facing engine runtime for one logical read on one lane: * the fields/evidence → executor factory plus the shared bounds cache and * calibration tracker. Model functions receive this instead of a raw * transport, which is what lets a single logical read expand into * sequential paced band requests WITHOUT the model layer importing the * operations layer. */ export declare function createEngineRuntime(deps: GoogleSheetsApiProviderDeps, pacing: RequestStartPacing, label: string): SheetsEngineRuntime; /** * Ensures every listed tab has an authoritative row bound in the * provider-instance cache, settling cold titles with ONE range-less * metadata enumeration (`gridProperties.rowCount` is metadata-only). The * cache is refreshed by every subsequent engine response's sheet * properties, so the enumeration is a once-per-title-per-instance cost — * the polling lane has no per-dispatch enumeration of its own and this is * where its committed upper bound comes from. */ export declare function ensureSheetRowBounds(deps: GoogleSheetsApiProviderDeps, pacing: RequestStartPacing, titles: readonly string[]): Promise; /** * Sends one built batch as ONE paced `batchUpdate` and validates the reply. * * `BuiltApplyBatch` is the shared return shape of every batch builder * (apply, append, and their combined variants), and the batch contents here * are exactly what the caller's builder produced: the engine never rebuilds * or reorders requests. A malformed or short reply throws the existing * delivery-uncertain invalid-state classification, so a 2xx that cannot be * matched request-for-request never closes effects. Zero-request batches * must be skipped by the caller (no transport call and no telemetry event * for an empty batch, exactly like before). */ export declare function executeBatchUpdate(deps: GoogleSheetsApiProviderDeps, batch: BuiltApplyBatch, telemetry: WriteBatchTelemetry): Promise; /** * Runs one prepared write unit behind the receipt-init guard. * * When the unit needs initialization, refresh + write run as ONE atomic * section on the per-spreadsheet `receiptInitLock`; steady state (receipt * present at preflight) never takes the lock. Callers keep their own * eligibility guard: a deterministic no-op batch must not take the refresh, * whose write-lane admission can be refused under saturation and would turn * the no-op into a delivery-uncertain requeue. */ export declare function executePreparedWrite(deps: GoogleSheetsApiProviderDeps, unit: { readonly context: C; readonly needsReceiptInit: boolean; readonly refresh: (context: C) => Promise; readonly write: (context: C) => Promise; }): Promise; /** True when a preflight context observed the shared receipt tab absent. */ export declare function receiptInitNeeded(context: PreflightContext): boolean; /** * Buckets items by their canonical route key (neutral kernel grouping: * first-seen group order and per-group order are preserved). */ export declare const groupByRouteKey: typeof groupNeutralByRouteKey; /** * Refreshes the shared receipt tab through the FIRST route's context and * returns the route list with that context replaced (neutral kernel * first-route replacement; the caller supplies the Sheets refresh). */ export declare const refreshFirstRouteContext: typeof refreshNeutralFirstRouteContext; //# sourceMappingURL=shared.d.ts.map