@beezping/adapter-prisma 0.7.0 → 0.8.0

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.
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * Pluggable storage for feedback screenshots.
3
3
  *
4
- * `adapter-prisma` accepts an optional `screenshotStorage` config. When
5
- * provided, the adapter forwards the widget-supplied data URL to `upload()`
6
- * and persists the returned URL on `Feedback.screenshotUrl`. When not
7
- * provided, the adapter falls back to inline base64 (with a one-time warn) —
8
- * fine for dev and small deployments, a footgun for production Postgres.
4
+ * `adapter-prisma` and `adapter-drizzle` accept an optional
5
+ * `screenshotStorage` config. When provided, the adapter forwards the
6
+ * widget-supplied data URL to `upload()` and persists the returned URL on
7
+ * `Feedback.screenshotUrl`. When not provided, the adapter falls back to
8
+ * inline base64 (with a one-time warn) — fine for dev and small
9
+ * deployments, a footgun for production Postgres.
9
10
  *
10
- * Implementations typically wrap an object store: S3, Cloudflare R2,
11
- * Backblaze B2, Cloudflare Images, local filesystem, etc. They are
12
- * intentionally not shipped from this package — wire your own based on
13
- * existing infra.
11
+ * `@beezping/screenshot-storage` implements it over any S3-compatible
12
+ * bucket (AWS S3, Cloudflare R2, Backblaze B2, MinIO…), Cloudflare Images,
13
+ * a database table, the local filesystem or memory. Implement it yourself
14
+ * for anything else.
14
15
  *
15
16
  * @example
16
17
  * ```ts
@@ -19,17 +20,17 @@
19
20
  *
20
21
  * const s3 = new S3Client({ region: "eu-west-3" });
21
22
  * const screenshotStorage: ScreenshotStorage = {
22
- * async upload(dataUrl, ctx) {
23
- * const buf = Buffer.from(dataUrl.split(",")[1], "base64");
24
- * const key = `feedback/${ctx.feedbackId}.jpg`;
23
+ * async upload(dataUrl, { mimeType }) {
24
+ * const body = Buffer.from(dataUrl.slice(dataUrl.indexOf(",") + 1), "base64");
25
+ * const key = `beezping/${crypto.randomUUID()}`; // fresh per upload, see URL ownership
25
26
  * await s3.send(new PutObjectCommand({
26
- * Bucket: "my-bucket", Key: key, Body: buf, ContentType: ctx.mimeType,
27
+ * Bucket: "my-bucket", Key: key, Body: body, ContentType: mimeType,
27
28
  * }));
28
29
  * return { url: `https://cdn.example.com/${key}` };
29
30
  * },
30
31
  * };
31
32
  *
32
- * createSitepingHandler({ prisma, screenshotStorage });
33
+ * createBeezpingHandler({ prisma, screenshotStorage });
33
34
  * ```
34
35
  */
35
36
  export interface ScreenshotStorage {
@@ -44,17 +45,19 @@ export interface ScreenshotStorage {
44
45
  * `ctx.feedbackId` identifies the upload. Adapters that upload before the
45
46
  * record exists pass the *client-generated* `clientId` (Prisma); the
46
47
  * Drizzle adapter passes the server-generated id the create attempt will
47
- * insert the record under — unique per attempt, so racing submissions of
48
- * one `clientId` never write the same object key.
48
+ * insert the record under.
49
49
  *
50
- * **URL ownership:** the returned URL must be unique to `ctx.feedbackId` —
51
- * key the object by it (as the example does), never by a content hash or a
52
- * fixed name. Adapters treat each URL as the exclusive property of the one
53
- * record that stores it and may pass it to `delete` once that record is
54
- * deleted or its upload discarded, without coordinating with concurrent
55
- * creates. A URL shared by several records (content-addressed or
56
- * id-ignoring keys) breaks this contract: deleting one record can remove
57
- * the object another record still points at.
50
+ * **URL ownership:** every call must return a URL no other call returns —
51
+ * key the object by a fresh random value (`crypto.randomUUID()`, as the
52
+ * example does), never by `ctx.feedbackId` alone nor by a content hash.
53
+ * Adapters treat each URL as the property of the one record that stores it
54
+ * and may pass it to `delete` once that record is deleted, or once its
55
+ * create lost a race: two submissions of one `clientId` both upload, and
56
+ * the object of the one that is not stored is deleted. Under Prisma,
57
+ * `ctx.feedbackId` is that client-supplied `clientId`, so an object keyed
58
+ * by it is rewritten by every replay — a retry, or anyone who learns the
59
+ * id — and a URL shared by several records lets deleting one remove the
60
+ * object another still points at.
58
61
  *
59
62
  * **Security note:** treat `ctx.feedbackId` as attacker-controlled:
60
63
  * sanitize before using it in filesystem paths or object keys, even though
@@ -67,12 +70,34 @@ export interface ScreenshotStorage {
67
70
  url: string;
68
71
  }>;
69
72
  /**
70
- * Optional cleanup hook called when the feedback is deleted, or when an
71
- * uploaded screenshot is discarded because its record was not stored.
72
- * Receives only URLs returned by {@link ScreenshotStorage.upload}, each
73
- * owned by a single record (see its URL ownership rule). Adapters call
74
- * this best-effort and swallow errors — orphaned objects are preferred
75
- * over failed deletes.
73
+ * Optional cleanup hook called when the feedback is deleted, and for an
74
+ * object uploaded by a create whose record was not stored: one rejected
75
+ * because its `clientId` is already stored, or — in adapters that key
76
+ * uploads per attempt, like Drizzle — one whose insert failed. An object a
77
+ * stored row still references (a deterministic key reused by the replay)
78
+ * is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.
79
+ * Adapters call this best-effort and swallow errors — orphaned objects are
80
+ * preferred over failed deletes.
76
81
  */
77
82
  delete?: (url: string) => Promise<void>;
78
83
  }
84
+ /**
85
+ * Most `ScreenshotStorage.delete` calls one cleanup keeps in flight — a
86
+ * project delete may free thousands of objects, and firing them all at once
87
+ * can exhaust sockets (one per request with `fetch`) or memory and trip
88
+ * object-store rate limits. It stays under the 50 sockets the AWS SDK opens
89
+ * per client by default, yet a project delete, which waits for its cleanup,
90
+ * is not serialised: 1,000 objects take 32 rounds. Adapters run their
91
+ * deletes through `settleWithConcurrencyLimit` with this bound.
92
+ */
93
+ export declare const SCREENSHOT_DELETE_CONCURRENCY = 32;
94
+ /**
95
+ * MIME type an adapter reports to {@link ScreenshotStorage.upload}: the one
96
+ * an image data URL declares (`data:image/png;base64,…` → `image/png`),
97
+ * limited to the JPEG, PNG and WebP the HTTP schema accepts. Stores are
98
+ * public and may be fed unvalidated data URLs, and an `image/svg+xml` label
99
+ * would make the stored object script-capable when served inline. Anything
100
+ * else — including a data URL that declares no type — reports JPEG, the
101
+ * widget's capture format.
102
+ */
103
+ export declare function screenshotMimeType(dataUrl: string): string;
@@ -5,15 +5,15 @@
5
5
  * needs the same three ingredients: turn a `FeedbackCreateInput` into a
6
6
  * `FeedbackRecord` (null-normalizing optional fields, stamping ids and
7
7
  * timestamps), filter/paginate with `applyFeedbackFilters`, and implement
8
- * the dedup/update/delete choreography of the `SitepingStore` contract.
8
+ * the dedup/update/delete choreography of the `BeezpingStore` contract.
9
9
  *
10
- * `buildFeedbackRecord` / `buildAnnotationRecord` cover the first part for
11
- * any adapter. `createCollectionStore` covers all of it: give it `load`,
12
- * `persist`, and `generateId`, and it returns a fully conformant
13
- * `SitepingStore` — writing a new snapshot adapter is ~20 lines plus its
14
- * storage specifics.
10
+ * `buildFeedbackRecord` / `buildAnnotationRecord` / `buildCommentRecord`
11
+ * cover the first part for any adapter. `createCollectionStore` covers all
12
+ * of it: give it `load`, `persist`, and `generateId`, and it returns a fully
13
+ * conformant `BeezpingStore` — writing a new snapshot adapter is ~20 lines
14
+ * plus its storage specifics.
15
15
  */
16
- import type { AnnotationCreateInput, AnnotationRecord, FeedbackCreateInput, FeedbackRecord, SitepingStore } from "./types.cjs";
16
+ import type { AnnotationCreateInput, AnnotationRecord, BeezpingStore, CommentCreateInput, CommentRecord, FeedbackCreateInput, FeedbackRecord } from "./types.cjs";
17
17
  /**
18
18
  * Build a persisted `AnnotationRecord` from its create input — normalizes
19
19
  * the optional anchor fields to `null` and stamps identity/timestamp.
@@ -23,12 +23,20 @@ export declare function buildAnnotationRecord(input: AnnotationCreateInput, ctx:
23
23
  feedbackId: string;
24
24
  now: Date;
25
25
  }): AnnotationRecord;
26
+ /** Build a persisted `CommentRecord` from its create input — stamps identity, thread and timestamp. */
27
+ export declare function buildCommentRecord(input: CommentCreateInput, ctx: {
28
+ id: string;
29
+ feedbackId: string;
30
+ now?: Date;
31
+ }): CommentRecord;
26
32
  /**
27
33
  * Build a persisted `FeedbackRecord` (with its annotations) from a create
28
34
  * input — normalizes every optional field to `null` and stamps ids and
29
35
  * timestamps. Adapters without external screenshot storage keep the data
30
36
  * URL inline on `screenshotUrl`, which is what this helper does; adapters
31
37
  * with a `ScreenshotStorage` upload first and override `screenshotUrl`.
38
+ * It leaves `comments` out, so the record minus its `annotations` stays an
39
+ * insertable feedback row; a store with threads returns `comments: []` itself.
32
40
  */
33
41
  export declare function buildFeedbackRecord(input: FeedbackCreateInput, ctx: {
34
42
  id: string;
@@ -37,8 +45,11 @@ export declare function buildFeedbackRecord(input: FeedbackCreateInput, ctx: {
37
45
  }): FeedbackRecord;
38
46
  /**
39
47
  * Storage primitives behind a collection store. `load`/`persist` may be
40
- * sync or async — the engine awaits both, so in-memory arrays, localStorage
41
- * and async KV stores all fit the same three functions.
48
+ * sync or async, so in-memory arrays, localStorage and async KV stores all
49
+ * fit the same three functions. When both are sync, a mutation runs from
50
+ * `load` to `persist` without yielding, so no other code in the realm can
51
+ * write in between; an async backend is only serialized against the
52
+ * engine's own queue.
42
53
  */
43
54
  export interface CollectionStoreBackend {
44
55
  /**
@@ -55,47 +66,60 @@ export interface CollectionStoreBackend {
55
66
  * update.
56
67
  */
57
68
  persist(feedbacks: FeedbackRecord[]): void | Promise<void>;
58
- /** Generate a unique id for a new feedback or annotation record. */
69
+ /** Generate a unique id for a new feedback, annotation or comment record. */
59
70
  generateId(): string;
71
+ /**
72
+ * Keep discussion threads on the records: the store gains `addComment` and
73
+ * `deleteComment`, and new records start with `comments: []`. Opt in once
74
+ * `load` hands back what `persist` wrote for them, each comment's
75
+ * `createdAt` a `Date` again (a JSON backend revives it like the record's
76
+ * own dates). Off by default, so an adapter written before threads never
77
+ * gains them — over storage never written for them — on an engine update.
78
+ */
79
+ comments?: boolean | undefined;
60
80
  }
61
81
  /**
62
- * A `SitepingStore` with the optional `verifyProjectOwnership` and
63
- * `createFeedbackIfAbsent` guaranteed — what `createCollectionStore` returns.
82
+ * A `BeezpingStore` with the optional `verifyProjectOwnership` guaranteed —
83
+ * what `createCollectionStore` returns, which also guarantees
84
+ * `createFeedbackIfAbsent`.
64
85
  */
65
- export type CollectionStore = SitepingStore & Required<Pick<SitepingStore, "verifyProjectOwnership" | "createFeedbackIfAbsent">>;
86
+ export type CollectionStore = BeezpingStore & Required<Pick<BeezpingStore, "verifyProjectOwnership">>;
66
87
  /**
67
- * Build a fully conformant `SitepingStore` on top of a snapshot backend.
88
+ * Build a fully conformant `BeezpingStore` on top of a snapshot backend.
68
89
  *
69
90
  * The engine implements the whole store contract: clientId dedup (idempotent
70
91
  * create, with `createFeedbackIfAbsent` reporting inserts), newest-first
71
- * ordering, the standard filter/pagination pipeline,
72
- * `StoreNotFoundError` on missing update/delete, project-scoped bulk delete,
73
- * and `verifyProjectOwnership`. The snapshot returned by `load` is never
74
- * mutated: every write hands `persist` a new array, so a failed write leaves
92
+ * ordering, the standard filter/pagination pipeline, `StoreNotFoundError` on
93
+ * missing update/delete, project-scoped bulk delete,
94
+ * `verifyProjectOwnership`, and — with `comments: true` — discussion threads
95
+ * (`addComment`, `deleteComment`) kept on each record. The snapshot returned by `load` is
96
+ * never mutated: every write hands `persist` a new array, so a failed write leaves
75
97
  * a cached snapshot exactly as it was. When `persist` fails during `createFeedback`
76
98
  * and the record carries an inline screenshot, the engine retries once
77
99
  * without the screenshot (by far the heaviest field) so the text feedback
78
100
  * survives a storage-quota hit; if that also fails, the error propagates —
79
101
  * returning the record would claim a success that was never persisted.
80
102
  *
81
- * Every read-modify-write mutation (`createFeedbackIfAbsent`,
82
- * `createFeedback`, `updateFeedback`, `deleteFeedback`,
83
- * `deleteAllFeedbacks`) runs through one promise queue per returned store,
84
- * so its `load` → check → `persist` sequence never interleaves with another
85
- * mutation of the same instance. That makes `createFeedbackIfAbsent` report
86
- * `created: true` exactly once per `clientId` and prevents concurrent writes
87
- * from overwriting each other's snapshot. The guarantee is scoped to one
88
- * store instance in one JS process: two instances over the same storage (two
89
- * server processes on a shared file, two browser tabs on the same
90
- * localStorage key) are not coordinated — backends that need that must
91
- * provide their own atomic primitive (a unique constraint, a transaction, a
92
- * compare-and-set). A failed mutation rejects with its original error and
93
- * does not block the ones queued after it. Reads are not queued: they see the
94
- * last persisted snapshot.
103
+ * Mutations (`createFeedbackIfAbsent`, `createFeedback`, `updateFeedback`,
104
+ * `deleteFeedback`, `deleteAllFeedbacks`, `addComment`, `deleteComment`) run
105
+ * one at a time through a queue owned by the returned store, so concurrent
106
+ * calls — the widget's `Promise.all` bulk resolve/delete, a comment posted
107
+ * while its feedback is resolved — never start from the same snapshot and
108
+ * overwrite each other, and `createFeedbackIfAbsent` reports `created: true`
109
+ * exactly once per `clientId`. A failed mutation rejects with its own error
110
+ * and does not block the ones queued after it, but `load` and `persist` must
111
+ * always settle: one that never does stalls every later mutation on the
112
+ * store, so give network-backed primitives a timeout. Reads are not queued: they see the
113
+ * last persisted snapshot. The guarantee is scoped to one store instance in
114
+ * one JS realm — two instances over the same storage (two
115
+ * `LocalStorageStore`s on one key, two browser tabs, several server
116
+ * processes sharing a KV or a file) are not coordinated; a backend that
117
+ * needs that must bring its own atomic primitive (a transaction, a
118
+ * compare-and-set).
95
119
  *
96
120
  * @example
97
121
  * ```ts
98
- * export class MemoryStore implements SitepingStore {
122
+ * export class MemoryStore implements BeezpingStore {
99
123
  * private feedbacks: FeedbackRecord[] = [];
100
124
  * private readonly store = createCollectionStore({
101
125
  * load: () => this.feedbacks,
@@ -103,10 +127,14 @@ export type CollectionStore = SitepingStore & Required<Pick<SitepingStore, "veri
103
127
  * this.feedbacks = next;
104
128
  * },
105
129
  * generateId: () => crypto.randomUUID(),
130
+ * comments: true,
106
131
  * });
107
132
  * createFeedback = this.store.createFeedback;
108
133
  * // …delegate the remaining methods the same way
109
134
  * }
110
135
  * ```
111
136
  */
112
- export declare function createCollectionStore(backend: CollectionStoreBackend): CollectionStore;
137
+ export declare function createCollectionStore(backend: CollectionStoreBackend & {
138
+ comments: true;
139
+ }): CollectionStore & Required<Pick<BeezpingStore, "createFeedbackIfAbsent" | "addComment" | "deleteComment">>;
140
+ export declare function createCollectionStore(backend: CollectionStoreBackend): CollectionStore & Required<Pick<BeezpingStore, "createFeedbackIfAbsent">>;
@@ -5,15 +5,15 @@
5
5
  * needs the same three ingredients: turn a `FeedbackCreateInput` into a
6
6
  * `FeedbackRecord` (null-normalizing optional fields, stamping ids and
7
7
  * timestamps), filter/paginate with `applyFeedbackFilters`, and implement
8
- * the dedup/update/delete choreography of the `SitepingStore` contract.
8
+ * the dedup/update/delete choreography of the `BeezpingStore` contract.
9
9
  *
10
- * `buildFeedbackRecord` / `buildAnnotationRecord` cover the first part for
11
- * any adapter. `createCollectionStore` covers all of it: give it `load`,
12
- * `persist`, and `generateId`, and it returns a fully conformant
13
- * `SitepingStore` — writing a new snapshot adapter is ~20 lines plus its
14
- * storage specifics.
10
+ * `buildFeedbackRecord` / `buildAnnotationRecord` / `buildCommentRecord`
11
+ * cover the first part for any adapter. `createCollectionStore` covers all
12
+ * of it: give it `load`, `persist`, and `generateId`, and it returns a fully
13
+ * conformant `BeezpingStore` — writing a new snapshot adapter is ~20 lines
14
+ * plus its storage specifics.
15
15
  */
16
- import type { AnnotationCreateInput, AnnotationRecord, FeedbackCreateInput, FeedbackRecord, SitepingStore } from "./types.js";
16
+ import type { AnnotationCreateInput, AnnotationRecord, BeezpingStore, CommentCreateInput, CommentRecord, FeedbackCreateInput, FeedbackRecord } from "./types.js";
17
17
  /**
18
18
  * Build a persisted `AnnotationRecord` from its create input — normalizes
19
19
  * the optional anchor fields to `null` and stamps identity/timestamp.
@@ -23,12 +23,20 @@ export declare function buildAnnotationRecord(input: AnnotationCreateInput, ctx:
23
23
  feedbackId: string;
24
24
  now: Date;
25
25
  }): AnnotationRecord;
26
+ /** Build a persisted `CommentRecord` from its create input — stamps identity, thread and timestamp. */
27
+ export declare function buildCommentRecord(input: CommentCreateInput, ctx: {
28
+ id: string;
29
+ feedbackId: string;
30
+ now?: Date;
31
+ }): CommentRecord;
26
32
  /**
27
33
  * Build a persisted `FeedbackRecord` (with its annotations) from a create
28
34
  * input — normalizes every optional field to `null` and stamps ids and
29
35
  * timestamps. Adapters without external screenshot storage keep the data
30
36
  * URL inline on `screenshotUrl`, which is what this helper does; adapters
31
37
  * with a `ScreenshotStorage` upload first and override `screenshotUrl`.
38
+ * It leaves `comments` out, so the record minus its `annotations` stays an
39
+ * insertable feedback row; a store with threads returns `comments: []` itself.
32
40
  */
33
41
  export declare function buildFeedbackRecord(input: FeedbackCreateInput, ctx: {
34
42
  id: string;
@@ -37,8 +45,11 @@ export declare function buildFeedbackRecord(input: FeedbackCreateInput, ctx: {
37
45
  }): FeedbackRecord;
38
46
  /**
39
47
  * Storage primitives behind a collection store. `load`/`persist` may be
40
- * sync or async — the engine awaits both, so in-memory arrays, localStorage
41
- * and async KV stores all fit the same three functions.
48
+ * sync or async, so in-memory arrays, localStorage and async KV stores all
49
+ * fit the same three functions. When both are sync, a mutation runs from
50
+ * `load` to `persist` without yielding, so no other code in the realm can
51
+ * write in between; an async backend is only serialized against the
52
+ * engine's own queue.
42
53
  */
43
54
  export interface CollectionStoreBackend {
44
55
  /**
@@ -55,47 +66,60 @@ export interface CollectionStoreBackend {
55
66
  * update.
56
67
  */
57
68
  persist(feedbacks: FeedbackRecord[]): void | Promise<void>;
58
- /** Generate a unique id for a new feedback or annotation record. */
69
+ /** Generate a unique id for a new feedback, annotation or comment record. */
59
70
  generateId(): string;
71
+ /**
72
+ * Keep discussion threads on the records: the store gains `addComment` and
73
+ * `deleteComment`, and new records start with `comments: []`. Opt in once
74
+ * `load` hands back what `persist` wrote for them, each comment's
75
+ * `createdAt` a `Date` again (a JSON backend revives it like the record's
76
+ * own dates). Off by default, so an adapter written before threads never
77
+ * gains them — over storage never written for them — on an engine update.
78
+ */
79
+ comments?: boolean | undefined;
60
80
  }
61
81
  /**
62
- * A `SitepingStore` with the optional `verifyProjectOwnership` and
63
- * `createFeedbackIfAbsent` guaranteed — what `createCollectionStore` returns.
82
+ * A `BeezpingStore` with the optional `verifyProjectOwnership` guaranteed —
83
+ * what `createCollectionStore` returns, which also guarantees
84
+ * `createFeedbackIfAbsent`.
64
85
  */
65
- export type CollectionStore = SitepingStore & Required<Pick<SitepingStore, "verifyProjectOwnership" | "createFeedbackIfAbsent">>;
86
+ export type CollectionStore = BeezpingStore & Required<Pick<BeezpingStore, "verifyProjectOwnership">>;
66
87
  /**
67
- * Build a fully conformant `SitepingStore` on top of a snapshot backend.
88
+ * Build a fully conformant `BeezpingStore` on top of a snapshot backend.
68
89
  *
69
90
  * The engine implements the whole store contract: clientId dedup (idempotent
70
91
  * create, with `createFeedbackIfAbsent` reporting inserts), newest-first
71
- * ordering, the standard filter/pagination pipeline,
72
- * `StoreNotFoundError` on missing update/delete, project-scoped bulk delete,
73
- * and `verifyProjectOwnership`. The snapshot returned by `load` is never
74
- * mutated: every write hands `persist` a new array, so a failed write leaves
92
+ * ordering, the standard filter/pagination pipeline, `StoreNotFoundError` on
93
+ * missing update/delete, project-scoped bulk delete,
94
+ * `verifyProjectOwnership`, and — with `comments: true` — discussion threads
95
+ * (`addComment`, `deleteComment`) kept on each record. The snapshot returned by `load` is
96
+ * never mutated: every write hands `persist` a new array, so a failed write leaves
75
97
  * a cached snapshot exactly as it was. When `persist` fails during `createFeedback`
76
98
  * and the record carries an inline screenshot, the engine retries once
77
99
  * without the screenshot (by far the heaviest field) so the text feedback
78
100
  * survives a storage-quota hit; if that also fails, the error propagates —
79
101
  * returning the record would claim a success that was never persisted.
80
102
  *
81
- * Every read-modify-write mutation (`createFeedbackIfAbsent`,
82
- * `createFeedback`, `updateFeedback`, `deleteFeedback`,
83
- * `deleteAllFeedbacks`) runs through one promise queue per returned store,
84
- * so its `load` → check → `persist` sequence never interleaves with another
85
- * mutation of the same instance. That makes `createFeedbackIfAbsent` report
86
- * `created: true` exactly once per `clientId` and prevents concurrent writes
87
- * from overwriting each other's snapshot. The guarantee is scoped to one
88
- * store instance in one JS process: two instances over the same storage (two
89
- * server processes on a shared file, two browser tabs on the same
90
- * localStorage key) are not coordinated — backends that need that must
91
- * provide their own atomic primitive (a unique constraint, a transaction, a
92
- * compare-and-set). A failed mutation rejects with its original error and
93
- * does not block the ones queued after it. Reads are not queued: they see the
94
- * last persisted snapshot.
103
+ * Mutations (`createFeedbackIfAbsent`, `createFeedback`, `updateFeedback`,
104
+ * `deleteFeedback`, `deleteAllFeedbacks`, `addComment`, `deleteComment`) run
105
+ * one at a time through a queue owned by the returned store, so concurrent
106
+ * calls — the widget's `Promise.all` bulk resolve/delete, a comment posted
107
+ * while its feedback is resolved — never start from the same snapshot and
108
+ * overwrite each other, and `createFeedbackIfAbsent` reports `created: true`
109
+ * exactly once per `clientId`. A failed mutation rejects with its own error
110
+ * and does not block the ones queued after it, but `load` and `persist` must
111
+ * always settle: one that never does stalls every later mutation on the
112
+ * store, so give network-backed primitives a timeout. Reads are not queued: they see the
113
+ * last persisted snapshot. The guarantee is scoped to one store instance in
114
+ * one JS realm — two instances over the same storage (two
115
+ * `LocalStorageStore`s on one key, two browser tabs, several server
116
+ * processes sharing a KV or a file) are not coordinated; a backend that
117
+ * needs that must bring its own atomic primitive (a transaction, a
118
+ * compare-and-set).
95
119
  *
96
120
  * @example
97
121
  * ```ts
98
- * export class MemoryStore implements SitepingStore {
122
+ * export class MemoryStore implements BeezpingStore {
99
123
  * private feedbacks: FeedbackRecord[] = [];
100
124
  * private readonly store = createCollectionStore({
101
125
  * load: () => this.feedbacks,
@@ -103,10 +127,14 @@ export type CollectionStore = SitepingStore & Required<Pick<SitepingStore, "veri
103
127
  * this.feedbacks = next;
104
128
  * },
105
129
  * generateId: () => crypto.randomUUID(),
130
+ * comments: true,
106
131
  * });
107
132
  * createFeedback = this.store.createFeedback;
108
133
  * // …delegate the remaining methods the same way
109
134
  * }
110
135
  * ```
111
136
  */
112
- export declare function createCollectionStore(backend: CollectionStoreBackend): CollectionStore;
137
+ export declare function createCollectionStore(backend: CollectionStoreBackend & {
138
+ comments: true;
139
+ }): CollectionStore & Required<Pick<BeezpingStore, "createFeedbackIfAbsent" | "addComment" | "deleteComment">>;
140
+ export declare function createCollectionStore(backend: CollectionStoreBackend): CollectionStore & Required<Pick<BeezpingStore, "createFeedbackIfAbsent">>;
@@ -41,6 +41,14 @@ export type AssertEqual<Actual, Expected> = IfEquals<Actual, Expected, true, nev
41
41
  export type Serialized<T> = {
42
42
  [K in keyof T]: T[K] extends Date ? string : T[K] extends Date | null ? string | null : T[K] extends (infer U)[] ? Serialized<U>[] : T[K];
43
43
  };
44
+ /**
45
+ * Read-only all the way down, for plain data that is deeply frozen at
46
+ * runtime: nested objects get read-only properties, arrays become
47
+ * `readonly` arrays.
48
+ */
49
+ export type DeepReadonly<T> = T extends readonly (infer U)[] ? readonly DeepReadonly<U>[] : T extends object ? {
50
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
51
+ } : T;
44
52
  /**
45
53
  * Type guard that narrows `value` to a non-null `Record<PropertyKey, unknown>`.
46
54
  * Useful when validating arbitrary inputs before reading fields.
@@ -41,6 +41,14 @@ export type AssertEqual<Actual, Expected> = IfEquals<Actual, Expected, true, nev
41
41
  export type Serialized<T> = {
42
42
  [K in keyof T]: T[K] extends Date ? string : T[K] extends Date | null ? string | null : T[K] extends (infer U)[] ? Serialized<U>[] : T[K];
43
43
  };
44
+ /**
45
+ * Read-only all the way down, for plain data that is deeply frozen at
46
+ * runtime: nested objects get read-only properties, arrays become
47
+ * `readonly` arrays.
48
+ */
49
+ export type DeepReadonly<T> = T extends readonly (infer U)[] ? readonly DeepReadonly<U>[] : T extends object ? {
50
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
51
+ } : T;
44
52
  /**
45
53
  * Type guard that narrows `value` to a non-null `Record<PropertyKey, unknown>`.
46
54
  * Useful when validating arbitrary inputs before reading fields.