/** * Record-construction helpers and the collection-store engine. * * Every snapshot-style adapter (memory, localStorage, flat file, KV, …) * needs the same three ingredients: turn a `FeedbackCreateInput` into a * `FeedbackRecord` (null-normalizing optional fields, stamping ids and * timestamps), filter/paginate with `applyFeedbackFilters`, and implement * the dedup/update/delete choreography of the `SitepingStore` contract. * * `buildFeedbackRecord` / `buildAnnotationRecord` / `buildCommentRecord` * cover the first part for any adapter. `createCollectionStore` covers all * of it: give it `load`, `persist`, and `generateId`, and it returns a fully * conformant `SitepingStore` — writing a new snapshot adapter is ~20 lines * plus its storage specifics. */ import type { AnnotationCreateInput, AnnotationRecord, CommentCreateInput, CommentRecord, FeedbackCreateInput, FeedbackRecord, SitepingStore } from "./types.js"; /** * Build a persisted `AnnotationRecord` from its create input — normalizes * the optional anchor fields to `null` and stamps identity/timestamp. */ export declare function buildAnnotationRecord(input: AnnotationCreateInput, ctx: { id: string; feedbackId: string; now: Date; }): AnnotationRecord; /** Build a persisted `CommentRecord` from its create input — stamps identity, thread and timestamp. */ export declare function buildCommentRecord(input: CommentCreateInput, ctx: { id: string; feedbackId: string; now?: Date; }): CommentRecord; /** * Build a persisted `FeedbackRecord` (with its annotations) from a create * input — normalizes every optional field to `null` and stamps ids and * timestamps. Adapters without external screenshot storage keep the data * URL inline on `screenshotUrl`, which is what this helper does; adapters * with a `ScreenshotStorage` upload first and override `screenshotUrl`. * It leaves `comments` out, so the record minus its `annotations` stays an * insertable feedback row; a store with threads returns `comments: []` itself. */ export declare function buildFeedbackRecord(input: FeedbackCreateInput, ctx: { id: string; annotationId: () => string; now?: Date; }): FeedbackRecord; /** * Storage primitives behind a collection store. `load`/`persist` may be * sync or async, so in-memory arrays, localStorage and async KV stores all * fit the same three functions. When both are sync, a mutation runs from * `load` to `persist` without yielding, so no other code in the realm can * write in between; an async backend is only serialized against the * engine's own queue. */ export interface CollectionStoreBackend { /** * Return the current full snapshot of feedback records. The engine never * mutates this array — handing out a live cache is safe. */ load(): FeedbackRecord[] | Promise; /** * Persist the full snapshot — always a new array, never the one `load()` * returned. Throw `StorePersistenceError` when the write is lost (quota, * storage disabled, …) — never swallow the failure. Because the loaded * snapshot is left untouched, a throw here leaves a cached `load()` result * consistent with durable storage: no phantom record, no half-applied * update. */ persist(feedbacks: FeedbackRecord[]): void | Promise; /** Generate a unique id for a new feedback, annotation or comment record. */ generateId(): string; /** * Keep discussion threads on the records: the store gains `addComment` and * `deleteComment`, and new records start with `comments: []`. Opt in once * `load` hands back what `persist` wrote for them, each comment's * `createdAt` a `Date` again (a JSON backend revives it like the record's * own dates). Off by default, so an adapter written before threads never * gains them — over storage never written for them — on an engine update. */ comments?: boolean | undefined; } /** * A `SitepingStore` with the optional `verifyProjectOwnership` guaranteed — * what `createCollectionStore` returns, which also guarantees * `createFeedbackIfAbsent`. */ export type CollectionStore = SitepingStore & Required>; /** * Build a fully conformant `SitepingStore` on top of a snapshot backend. * * The engine implements the whole store contract: clientId dedup (idempotent * create, with `createFeedbackIfAbsent` reporting inserts), newest-first * ordering, the standard filter/pagination pipeline, `StoreNotFoundError` on * missing update/delete, project-scoped bulk delete, * `verifyProjectOwnership`, and — with `comments: true` — discussion threads * (`addComment`, `deleteComment`) kept on each record. The snapshot returned by `load` is * never mutated: every write hands `persist` a new array, so a failed write leaves * a cached snapshot exactly as it was. When `persist` fails during `createFeedback` * and the record carries an inline screenshot, the engine retries once * without the screenshot (by far the heaviest field) so the text feedback * survives a storage-quota hit; if that also fails, the error propagates — * returning the record would claim a success that was never persisted. * * Mutations (`createFeedbackIfAbsent`, `createFeedback`, `updateFeedback`, * `deleteFeedback`, `deleteAllFeedbacks`, `addComment`, `deleteComment`) run * one at a time through a queue owned by the returned store, so concurrent * calls — the widget's `Promise.all` bulk resolve/delete, a comment posted * while its feedback is resolved — never start from the same snapshot and * overwrite each other, and `createFeedbackIfAbsent` reports `created: true` * exactly once per `clientId`. A failed mutation rejects with its own error * and does not block the ones queued after it, but `load` and `persist` must * always settle: one that never does stalls every later mutation on the * store, so give network-backed primitives a timeout. Reads are not queued: they see the * last persisted snapshot. The guarantee is scoped to one store instance in * one JS realm — two instances over the same storage (two * `LocalStorageStore`s on one key, two browser tabs, several server * processes sharing a KV or a file) are not coordinated; a backend that * needs that must bring its own atomic primitive (a transaction, a * compare-and-set). * * @example * ```ts * export class MemoryStore implements SitepingStore { * private feedbacks: FeedbackRecord[] = []; * private readonly store = createCollectionStore({ * load: () => this.feedbacks, * persist: (next) => { * this.feedbacks = next; * }, * generateId: () => crypto.randomUUID(), * comments: true, * }); * createFeedback = this.store.createFeedback; * // …delegate the remaining methods the same way * } * ``` */ export declare function createCollectionStore(backend: CollectionStoreBackend & { comments: true; }): CollectionStore & Required>; export declare function createCollectionStore(backend: CollectionStoreBackend): CollectionStore & Required>;