@blocksdiy/blocks-client-sdk 1.8.14 → 1.9.1

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.
package/dist/ClientSdk.js CHANGED
@@ -80,7 +80,7 @@ export class ClientSdk {
80
80
  */
81
81
  entity(entityConfig) {
82
82
  if (!this.entities.has(entityConfig.tableBlockId)) {
83
- this.entities.set(entityConfig.tableBlockId, new Entity(entityConfig, { token: this.token }));
83
+ this.entities.set(entityConfig.tableBlockId, new Entity(entityConfig, { token: this.token, appId: this.appId }));
84
84
  }
85
85
  return this.entities.get(entityConfig.tableBlockId);
86
86
  }
package/dist/Entity.d.ts CHANGED
@@ -58,9 +58,11 @@ export declare class Entity<EC extends EntityConfig = EntityConfig> {
58
58
  * Creates a new Entity instance
59
59
  * @param {EC} config - Configuration for the entity
60
60
  * @param {string} token - The token for the application
61
+ * @param {string} appId - The application id, sent as x-app-id on every request
61
62
  */
62
- constructor(config: EC, { token }?: {
63
+ constructor(config: EC, { token, appId }?: {
63
64
  token?: string;
65
+ appId?: string;
64
66
  });
65
67
  /**
66
68
  * Retrieves a single entity by filters
@@ -1 +1 @@
1
- {"version":3,"file":"Entity.d.ts","sourceRoot":"","sources":["../src/Entity.ts"],"names":[],"mappings":"AAaA;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC;AAE9E;;;GAGG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,YAAY,IAAI,cAAc,GAAG,qBAAqB,CAAC,CAAC,CAAC,CAAC;AAE3F;;;;;;;;GAQG;AACH,qBAAa,MAAM,CAAC,EAAE,SAAS,YAAY,GAAG,YAAY;IACxD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,cAAc,CAAiB;IAEvC;;;;OAIG;gBACS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO;IAK1D;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAU1C;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG;IAY5B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,MAAM,CAAC,IAAI,EAAE,qBAAqB,CAAC,EAAE,CAAC;IAU5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACG,UAAU,CAAC,IAAI,EAAE,qBAAqB,CAAC,EAAE,CAAC,EAAE;IAUlD;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC;IAUvE;;;;;;;;;;;;;;;OAeG;IACG,SAAS,CAAC,EAAE,EAAE,MAAM;IAS1B;;;;;;;;;;;;;;;;;;OAkBG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE;CAS/B"}
1
+ {"version":3,"file":"Entity.d.ts","sourceRoot":"","sources":["../src/Entity.ts"],"names":[],"mappings":"AAaA;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC;AAE9E;;;GAGG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,YAAY,IAAI,cAAc,GAAG,qBAAqB,CAAC,CAAC,CAAC,CAAC;AAE3F;;;;;;;;GAQG;AACH,qBAAa,MAAM,CAAC,EAAE,SAAS,YAAY,GAAG,YAAY;IACxD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,cAAc,CAAiB;IAEvC;;;;;OAKG;gBACS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO;IAKjF;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAU1C;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG;IAY5B;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,MAAM,CAAC,IAAI,EAAE,qBAAqB,CAAC,EAAE,CAAC;IAU5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACG,UAAU,CAAC,IAAI,EAAE,qBAAqB,CAAC,EAAE,CAAC,EAAE;IAUlD;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC;IAUvE;;;;;;;;;;;;;;;OAeG;IACG,SAAS,CAAC,EAAE,EAAE,MAAM;IAS1B;;;;;;;;;;;;;;;;;;OAkBG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE;CAS/B"}
package/dist/Entity.js CHANGED
@@ -23,10 +23,11 @@ export class Entity {
23
23
  * Creates a new Entity instance
24
24
  * @param {EC} config - Configuration for the entity
25
25
  * @param {string} token - The token for the application
26
+ * @param {string} appId - The application id, sent as x-app-id on every request
26
27
  */
27
- constructor(config, { token } = {}) {
28
+ constructor(config, { token, appId } = {}) {
28
29
  this.tableBlockId = config.tableBlockId;
29
- this.dataApiService = new DataApiService({ token });
30
+ this.dataApiService = new DataApiService({ token, appId });
30
31
  }
31
32
  /**
32
33
  * Retrieves a single entity by filters
@@ -0,0 +1,162 @@
1
+ import { QueryClient, QueryKey } from "@tanstack/react-query";
2
+ export declare const ENTITIES_QUERY_KEY = "entities";
3
+ /**
4
+ * Minimal shape of a server row required for cache reconciliation.
5
+ * Server rows always carry an id; updatedAt is used to discard out-of-order writes.
6
+ */
7
+ export interface EntityCacheRow {
8
+ id: string | number;
9
+ updatedAt?: string;
10
+ [key: string]: unknown;
11
+ }
12
+ export interface EntityWritePreparation {
13
+ /**
14
+ * Keys of table queries that must be (re-)invalidated after the cache write:
15
+ * queries with a fetch in flight (their response is of unknowable freshness) and
16
+ * queries already marked invalidated (writing to them would wrongly mark them fresh).
17
+ */
18
+ staleKeys: QueryKey[];
19
+ }
20
+ export type EntityQueriesSnapshot = [QueryKey, unknown][];
21
+ export declare const entityTableKey: (tableBlockId: string) => QueryKey;
22
+ /**
23
+ * Normalizes a mutation response into an array of cacheable rows.
24
+ *
25
+ * Server endpoints are inconsistent: create returns a single row, createMany returns
26
+ * an array, and the update endpoint returns an array of rows at runtime even though
27
+ * it is typed as a single entity. Anything we cannot recognize as row data is dropped,
28
+ * letting callers fall back to plain invalidation.
29
+ */
30
+ export declare const normalizeRowsResult: (result: unknown) => EntityCacheRow[];
31
+ export declare const sameId: (a: unknown, b: unknown) => boolean;
32
+ /**
33
+ * Returns true when the incoming server row should replace the cached one.
34
+ * Rows updated concurrently can resolve out of order; updatedAt decides the winner.
35
+ * When timestamps are missing or unparsable we accept the incoming row (server data
36
+ * is more trustworthy than an undecidable comparison).
37
+ */
38
+ export declare const isIncomingRowNewer: (existing: unknown, incoming: EntityCacheRow) => boolean;
39
+ export declare const isListQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
40
+ export declare const isUnfilteredListQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
41
+ export declare const isFilteredListQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
42
+ export declare const isOneQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
43
+ /** Returns the id of a one-query keyed strictly by { id }, undefined for any other filter shape. */
44
+ export declare const getOneQueryKeyId: (queryKey: QueryKey) => string | number | undefined;
45
+ /**
46
+ * Writes canonical server rows into every cached query of the table.
47
+ *
48
+ * - Rows already present in a list (matched by id) are replaced when newer.
49
+ * - New rows are appended to unfiltered lists only (`appendNewToUnfilteredLists`),
50
+ * matching the server's default `id ASC` ordering. Filtered lists never gain or
51
+ * lose membership here - that is decided by `invalidateEntityQueriesAfterWrite`.
52
+ * - One-queries holding a row with the same id are replaced when newer; one-queries
53
+ * keyed by `{ id }` that cached `null` are filled in with the row.
54
+ */
55
+ export declare const applyEntityRowsUpserted: (queryClient: QueryClient, tableBlockId: string, rows: EntityCacheRow[], { appendNewToUnfilteredLists }: {
56
+ appendNewToUnfilteredLists: boolean;
57
+ }) => void;
58
+ /**
59
+ * Applies an optimistic partial patch to every cached copy of a row (lists and
60
+ * one-queries). The cached id and updatedAt always win over patch values: id so the
61
+ * row identity cannot change, and updatedAt so a caller-supplied timestamp (e.g. a
62
+ * whole row spread into the update payload, with a client clock ahead of the
63
+ * server) can never make `isIncomingRowNewer` reject the canonical server response.
64
+ */
65
+ export declare const applyEntityRowPatch: (queryClient: QueryClient, tableBlockId: string, rowId: string | number, patch: Record<string, unknown>) => void;
66
+ /**
67
+ * Removes rows from every cached list of the table and nulls out one-queries that
68
+ * held a deleted row. Removal by id is exact for filtered lists too - a deleted row
69
+ * can never re-match a filter.
70
+ */
71
+ export declare const applyEntityRowsDeleted: (queryClient: QueryClient, tableBlockId: string, rowIds: (string | number)[]) => void;
72
+ /**
73
+ * Seeds one-query cache entries for freshly written rows so a follow-up
74
+ * `useEntityGetOne(Entity, { id })` (or `useEntityGetOne(Entity, id)`) renders
75
+ * instantly without a fetch. Seeds both the raw id shape and the parseInt-normalized
76
+ * shape `useEntityGetOne` produces for string/number ids.
77
+ */
78
+ export declare const seedEntityOneCaches: (queryClient: QueryClient, tableBlockId: string, rows: EntityCacheRow[]) => void;
79
+ /**
80
+ * Whether the cache holds any query that direct row writes can keep fresh
81
+ * (unfiltered lists or one-queries). When none exist, fetching row data has nothing
82
+ * to update - filtered lists are refetched via invalidation regardless.
83
+ */
84
+ export declare const hasDirectlyMaintainedEntityQueries: (queryClient: QueryClient, tableBlockId: string) => boolean;
85
+ export declare const snapshotEntityQueries: (queryClient: QueryClient, tableBlockId: string) => EntityQueriesSnapshot;
86
+ export declare const restoreEntityQueriesSnapshot: (queryClient: QueryClient, snapshot: EntityQueriesSnapshot) => void;
87
+ /**
88
+ * Prepares the table cache for a direct write:
89
+ * - records which queries have a fetch in flight or are already invalidated (a
90
+ * `setQueryData` write would mark them fresh, so they must be re-invalidated
91
+ * afterwards via `invalidateEntityQueriesAfterWrite`),
92
+ * - cancels in-flight fetches of queries that already hold data, so a response
93
+ * snapshotted before the mutation cannot overwrite the rows we are about to write.
94
+ *
95
+ * Fetches of queries without data (initial loads) are left running: cancelling them
96
+ * would strand the query in a non-fetching pending state.
97
+ */
98
+ export declare const prepareEntityCacheWrite: (queryClient: QueryClient, tableBlockId: string) => Promise<EntityWritePreparation>;
99
+ /**
100
+ * Single invalidation pass that re-syncs everything a direct cache write cannot
101
+ * decide on its own:
102
+ * - filtered lists (membership/order/limit semantics live on the server),
103
+ * - one-queries keyed by non-id filters (an update/create may change which row matches),
104
+ * - queries that had a fetch in flight while we wrote (their in-flight response is
105
+ * of unknowable freshness and may carry an external change we cancelled).
106
+ *
107
+ * Unfiltered lists and id-keyed one-queries are NOT invalidated - they were fully
108
+ * reconciled from the server response, which is what saves the refetch round trips.
109
+ */
110
+ export declare const invalidateEntityQueriesAfterWrite: (queryClient: QueryClient, tableBlockId: string, { staleKeys, filteredLists, nonIdOneQueries, deletedRowIds, }: {
111
+ staleKeys?: QueryKey[];
112
+ filteredLists?: boolean;
113
+ nonIdOneQueries?: boolean;
114
+ /**
115
+ * When set (delete flow), non-id one-queries are only invalidated if they cached
116
+ * one of these rows or currently hold null (which may be the optimistic removal
117
+ * of a deleted row, and another row may match the filter on the server).
118
+ */
119
+ deletedRowIds?: (string | number)[];
120
+ }) => void;
121
+ /** Full-table invalidation - the pre-optimistic behavior, kept as the safety fallback. */
122
+ export declare const invalidateEntityTable: (queryClient: QueryClient, tableBlockId: string) => void;
123
+ export interface EntityOptimisticContext {
124
+ snapshot: EntityQueriesSnapshot;
125
+ staleKeys: QueryKey[];
126
+ }
127
+ /**
128
+ * Optimistic phase of an update/delete mutation: cancel clobbering fetches, snapshot
129
+ * the table cache for rollback, then apply the optimistic change. Never throws - on
130
+ * any failure the optimistic change is rolled back and skipped, and the mutation
131
+ * proceeds (the success/error handlers still reconcile the cache).
132
+ */
133
+ export declare const beginEntityOptimisticWrite: (queryClient: QueryClient, tableBlockId: string, applyOptimistic: () => void) => Promise<EntityOptimisticContext | undefined>;
134
+ /**
135
+ * Error phase: restore the snapshot taken in `beginEntityOptimisticWrite`, then
136
+ * refetch the table since the server state is unknown after a failed request.
137
+ * `skipInvalidation` is for mutations rejected before any request was sent
138
+ * (e.g. client-side rate limiting) where the restored snapshot is already correct.
139
+ */
140
+ export declare const rollbackEntityOptimisticWrite: (queryClient: QueryClient, tableBlockId: string, context: EntityOptimisticContext | undefined, { skipInvalidation }?: {
141
+ skipInvalidation?: boolean;
142
+ }) => void;
143
+ /**
144
+ * Success phase of a create/update mutation: writes the canonical rows returned by
145
+ * the server into the cache and invalidates only what a direct write cannot decide
146
+ * (filtered lists, non-id one-queries, queries that were mid-fetch). Falls back to
147
+ * full-table invalidation whenever the response is unusable or anything throws, so
148
+ * the worst case is exactly the previous always-refetch behavior.
149
+ */
150
+ export declare const applyEntityCreateResult: (queryClient: QueryClient, tableBlockId: string, result: unknown, { appendNewToUnfilteredLists, extraStaleKeys, }: {
151
+ appendNewToUnfilteredLists: boolean;
152
+ extraStaleKeys?: QueryKey[];
153
+ }) => Promise<void>;
154
+ /**
155
+ * Success phase of a delete mutation: removes the rows from every cached query.
156
+ * Filtered lists are still refetched because server-side filters may involve
157
+ * limits/pagination that removal alone cannot reconcile.
158
+ */
159
+ export declare const applyEntityDeleteResult: (queryClient: QueryClient, tableBlockId: string, rowIds: (string | number)[], { extraStaleKeys }?: {
160
+ extraStaleKeys?: QueryKey[];
161
+ }) => Promise<void>;
162
+ //# sourceMappingURL=EntityCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityCache.d.ts","sourceRoot":"","sources":["../src/EntityCache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,377 @@
1
+ import { hashKey } from "@tanstack/react-query";
2
+ export const ENTITIES_QUERY_KEY = "entities";
3
+ const ONE_SEGMENT = "one";
4
+ export const entityTableKey = (tableBlockId) => [ENTITIES_QUERY_KEY, tableBlockId];
5
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6
+ const isValidRow = (value) => isRecord(value) && (typeof value.id === "string" || typeof value.id === "number");
7
+ /**
8
+ * Normalizes a mutation response into an array of cacheable rows.
9
+ *
10
+ * Server endpoints are inconsistent: create returns a single row, createMany returns
11
+ * an array, and the update endpoint returns an array of rows at runtime even though
12
+ * it is typed as a single entity. Anything we cannot recognize as row data is dropped,
13
+ * letting callers fall back to plain invalidation.
14
+ */
15
+ export const normalizeRowsResult = (result) => {
16
+ if (Array.isArray(result)) {
17
+ return result.filter(isValidRow);
18
+ }
19
+ return isValidRow(result) ? [result] : [];
20
+ };
21
+ export const sameId = (a, b) => (typeof a === "string" || typeof a === "number") &&
22
+ (typeof b === "string" || typeof b === "number") &&
23
+ String(a) === String(b);
24
+ /**
25
+ * Returns true when the incoming server row should replace the cached one.
26
+ * Rows updated concurrently can resolve out of order; updatedAt decides the winner.
27
+ * When timestamps are missing or unparsable we accept the incoming row (server data
28
+ * is more trustworthy than an undecidable comparison).
29
+ */
30
+ export const isIncomingRowNewer = (existing, incoming) => {
31
+ if (!isRecord(existing) || typeof existing.updatedAt !== "string" || typeof incoming.updatedAt !== "string") {
32
+ return true;
33
+ }
34
+ const existingTime = Date.parse(existing.updatedAt);
35
+ const incomingTime = Date.parse(incoming.updatedAt);
36
+ if (Number.isNaN(existingTime) || Number.isNaN(incomingTime)) {
37
+ return true;
38
+ }
39
+ return incomingTime >= existingTime;
40
+ };
41
+ /* Query key classification.
42
+ * List queries: [ENTITIES_QUERY_KEY, tableBlockId, filters?] (length 3, filters may be undefined)
43
+ * One queries: [ENTITIES_QUERY_KEY, tableBlockId, "one", filters] (length 4)
44
+ */
45
+ const isTableKey = (queryKey, tableBlockId) => Array.isArray(queryKey) && queryKey[0] === ENTITIES_QUERY_KEY && queryKey[1] === tableBlockId;
46
+ export const isListQueryKey = (queryKey, tableBlockId) => isTableKey(queryKey, tableBlockId) && queryKey.length === 3 && queryKey[2] !== ONE_SEGMENT;
47
+ export const isUnfilteredListQueryKey = (queryKey, tableBlockId) => isListQueryKey(queryKey, tableBlockId) && queryKey[2] == null;
48
+ export const isFilteredListQueryKey = (queryKey, tableBlockId) => isListQueryKey(queryKey, tableBlockId) && queryKey[2] != null;
49
+ export const isOneQueryKey = (queryKey, tableBlockId) => isTableKey(queryKey, tableBlockId) && queryKey.length === 4 && queryKey[2] === ONE_SEGMENT;
50
+ /** Returns the id of a one-query keyed strictly by { id }, undefined for any other filter shape. */
51
+ export const getOneQueryKeyId = (queryKey) => {
52
+ const filters = queryKey[3];
53
+ if (!isRecord(filters)) {
54
+ return undefined;
55
+ }
56
+ const keys = Object.keys(filters);
57
+ if (keys.length !== 1 || keys[0] !== "id") {
58
+ return undefined;
59
+ }
60
+ const id = filters.id;
61
+ return typeof id === "string" || typeof id === "number" ? id : undefined;
62
+ };
63
+ const updateListData = (oldData, transform) => {
64
+ if (!Array.isArray(oldData)) {
65
+ // Bail out (undefined keeps the cache untouched) instead of corrupting non-list data.
66
+ return undefined;
67
+ }
68
+ return transform(oldData);
69
+ };
70
+ /**
71
+ * Writes canonical server rows into every cached query of the table.
72
+ *
73
+ * - Rows already present in a list (matched by id) are replaced when newer.
74
+ * - New rows are appended to unfiltered lists only (`appendNewToUnfilteredLists`),
75
+ * matching the server's default `id ASC` ordering. Filtered lists never gain or
76
+ * lose membership here - that is decided by `invalidateEntityQueriesAfterWrite`.
77
+ * - One-queries holding a row with the same id are replaced when newer; one-queries
78
+ * keyed by `{ id }` that cached `null` are filled in with the row.
79
+ */
80
+ export const applyEntityRowsUpserted = (queryClient, tableBlockId, rows, { appendNewToUnfilteredLists }) => {
81
+ if (rows.length === 0) {
82
+ return;
83
+ }
84
+ queryClient.setQueriesData({
85
+ queryKey: entityTableKey(tableBlockId),
86
+ predicate: (query) => isListQueryKey(query.queryKey, tableBlockId),
87
+ }, (oldData) => updateListData(oldData, (oldRows) => {
88
+ let changed = false;
89
+ const nextRows = oldRows.map((oldRow) => {
90
+ if (!isValidRow(oldRow)) {
91
+ return oldRow;
92
+ }
93
+ const incoming = rows.find((row) => sameId(row.id, oldRow.id));
94
+ if (!incoming || !isIncomingRowNewer(oldRow, incoming)) {
95
+ return oldRow;
96
+ }
97
+ changed = true;
98
+ return incoming;
99
+ });
100
+ return changed ? nextRows : undefined;
101
+ }));
102
+ if (appendNewToUnfilteredLists) {
103
+ queryClient.setQueriesData({
104
+ queryKey: entityTableKey(tableBlockId),
105
+ predicate: (query) => isUnfilteredListQueryKey(query.queryKey, tableBlockId),
106
+ }, (oldData) => updateListData(oldData, (oldRows) => {
107
+ const missingRows = rows.filter((row) => !oldRows.some((oldRow) => isValidRow(oldRow) && sameId(oldRow.id, row.id)));
108
+ return missingRows.length > 0 ? [...oldRows, ...missingRows] : undefined;
109
+ }));
110
+ }
111
+ queryClient
112
+ .getQueryCache()
113
+ .findAll({
114
+ queryKey: entityTableKey(tableBlockId),
115
+ predicate: (query) => isOneQueryKey(query.queryKey, tableBlockId),
116
+ })
117
+ .forEach((query) => {
118
+ queryClient.setQueryData(query.queryKey, (oldData) => {
119
+ if (isValidRow(oldData)) {
120
+ const incoming = rows.find((row) => sameId(row.id, oldData.id));
121
+ return incoming && isIncomingRowNewer(oldData, incoming) ? incoming : undefined;
122
+ }
123
+ if (oldData === null) {
124
+ const keyId = getOneQueryKeyId(query.queryKey);
125
+ const incoming = keyId === undefined ? undefined : rows.find((row) => sameId(row.id, keyId));
126
+ return incoming ?? undefined;
127
+ }
128
+ return undefined;
129
+ });
130
+ });
131
+ };
132
+ /**
133
+ * Applies an optimistic partial patch to every cached copy of a row (lists and
134
+ * one-queries). The cached id and updatedAt always win over patch values: id so the
135
+ * row identity cannot change, and updatedAt so a caller-supplied timestamp (e.g. a
136
+ * whole row spread into the update payload, with a client clock ahead of the
137
+ * server) can never make `isIncomingRowNewer` reject the canonical server response.
138
+ */
139
+ export const applyEntityRowPatch = (queryClient, tableBlockId, rowId, patch) => {
140
+ const mergePatch = (row) => ({
141
+ ...row,
142
+ ...patch,
143
+ id: row.id,
144
+ updatedAt: row.updatedAt,
145
+ });
146
+ queryClient.setQueriesData({
147
+ queryKey: entityTableKey(tableBlockId),
148
+ predicate: (query) => isListQueryKey(query.queryKey, tableBlockId),
149
+ }, (oldData) => updateListData(oldData, (oldRows) => {
150
+ const hasMatch = oldRows.some((row) => isValidRow(row) && sameId(row.id, rowId));
151
+ if (!hasMatch) {
152
+ return undefined;
153
+ }
154
+ return oldRows.map((row) => (isValidRow(row) && sameId(row.id, rowId) ? mergePatch(row) : row));
155
+ }));
156
+ queryClient.setQueriesData({
157
+ queryKey: entityTableKey(tableBlockId),
158
+ predicate: (query) => isOneQueryKey(query.queryKey, tableBlockId),
159
+ }, (oldData) => (isValidRow(oldData) && sameId(oldData.id, rowId) ? mergePatch(oldData) : undefined));
160
+ };
161
+ /**
162
+ * Removes rows from every cached list of the table and nulls out one-queries that
163
+ * held a deleted row. Removal by id is exact for filtered lists too - a deleted row
164
+ * can never re-match a filter.
165
+ */
166
+ export const applyEntityRowsDeleted = (queryClient, tableBlockId, rowIds) => {
167
+ if (rowIds.length === 0) {
168
+ return;
169
+ }
170
+ const isDeleted = (id) => rowIds.some((rowId) => sameId(rowId, id));
171
+ queryClient.setQueriesData({
172
+ queryKey: entityTableKey(tableBlockId),
173
+ predicate: (query) => isListQueryKey(query.queryKey, tableBlockId),
174
+ }, (oldData) => updateListData(oldData, (oldRows) => {
175
+ const nextRows = oldRows.filter((row) => !(isValidRow(row) && isDeleted(row.id)));
176
+ return nextRows.length !== oldRows.length ? nextRows : undefined;
177
+ }));
178
+ queryClient.setQueriesData({
179
+ queryKey: entityTableKey(tableBlockId),
180
+ predicate: (query) => isOneQueryKey(query.queryKey, tableBlockId),
181
+ }, (oldData) => {
182
+ if (isValidRow(oldData) && isDeleted(oldData.id)) {
183
+ return null;
184
+ }
185
+ return undefined;
186
+ });
187
+ };
188
+ /**
189
+ * Seeds one-query cache entries for freshly written rows so a follow-up
190
+ * `useEntityGetOne(Entity, { id })` (or `useEntityGetOne(Entity, id)`) renders
191
+ * instantly without a fetch. Seeds both the raw id shape and the parseInt-normalized
192
+ * shape `useEntityGetOne` produces for string/number ids.
193
+ */
194
+ export const seedEntityOneCaches = (queryClient, tableBlockId, rows) => {
195
+ rows.forEach((row) => {
196
+ const idVariants = new Set([row.id]);
197
+ const numericId = typeof row.id === "string" && /^\d+$/.test(row.id) ? parseInt(row.id, 10) : row.id;
198
+ idVariants.add(numericId);
199
+ idVariants.forEach((id) => {
200
+ queryClient.setQueryData([ENTITIES_QUERY_KEY, tableBlockId, ONE_SEGMENT, { id }], (oldData) => oldData === undefined || oldData === null || (isValidRow(oldData) && isIncomingRowNewer(oldData, row))
201
+ ? row
202
+ : undefined);
203
+ });
204
+ });
205
+ };
206
+ /**
207
+ * Whether the cache holds any query that direct row writes can keep fresh
208
+ * (unfiltered lists or one-queries). When none exist, fetching row data has nothing
209
+ * to update - filtered lists are refetched via invalidation regardless.
210
+ */
211
+ export const hasDirectlyMaintainedEntityQueries = (queryClient, tableBlockId) => queryClient.getQueryCache().findAll({
212
+ queryKey: entityTableKey(tableBlockId),
213
+ predicate: (query) => isUnfilteredListQueryKey(query.queryKey, tableBlockId) || isOneQueryKey(query.queryKey, tableBlockId),
214
+ }).length > 0;
215
+ export const snapshotEntityQueries = (queryClient, tableBlockId) => queryClient.getQueriesData({ queryKey: entityTableKey(tableBlockId) });
216
+ export const restoreEntityQueriesSnapshot = (queryClient, snapshot) => {
217
+ snapshot.forEach(([queryKey, data]) => {
218
+ if (data !== undefined) {
219
+ queryClient.setQueryData(queryKey, data);
220
+ }
221
+ });
222
+ };
223
+ /**
224
+ * Prepares the table cache for a direct write:
225
+ * - records which queries have a fetch in flight or are already invalidated (a
226
+ * `setQueryData` write would mark them fresh, so they must be re-invalidated
227
+ * afterwards via `invalidateEntityQueriesAfterWrite`),
228
+ * - cancels in-flight fetches of queries that already hold data, so a response
229
+ * snapshotted before the mutation cannot overwrite the rows we are about to write.
230
+ *
231
+ * Fetches of queries without data (initial loads) are left running: cancelling them
232
+ * would strand the query in a non-fetching pending state.
233
+ */
234
+ export const prepareEntityCacheWrite = async (queryClient, tableBlockId) => {
235
+ const tableKey = entityTableKey(tableBlockId);
236
+ const staleKeys = queryClient
237
+ .getQueryCache()
238
+ .findAll({
239
+ queryKey: tableKey,
240
+ predicate: (query) => query.state.fetchStatus === "fetching" || query.state.isInvalidated,
241
+ })
242
+ .map((query) => query.queryKey);
243
+ await queryClient.cancelQueries({
244
+ queryKey: tableKey,
245
+ predicate: (query) => query.state.data !== undefined,
246
+ });
247
+ return { staleKeys };
248
+ };
249
+ /**
250
+ * Single invalidation pass that re-syncs everything a direct cache write cannot
251
+ * decide on its own:
252
+ * - filtered lists (membership/order/limit semantics live on the server),
253
+ * - one-queries keyed by non-id filters (an update/create may change which row matches),
254
+ * - queries that had a fetch in flight while we wrote (their in-flight response is
255
+ * of unknowable freshness and may carry an external change we cancelled).
256
+ *
257
+ * Unfiltered lists and id-keyed one-queries are NOT invalidated - they were fully
258
+ * reconciled from the server response, which is what saves the refetch round trips.
259
+ */
260
+ export const invalidateEntityQueriesAfterWrite = (queryClient, tableBlockId, { staleKeys = [], filteredLists = false, nonIdOneQueries = false, deletedRowIds, }) => {
261
+ const staleHashes = new Set(staleKeys.map((queryKey) => hashKey(queryKey)));
262
+ void queryClient.invalidateQueries({
263
+ queryKey: entityTableKey(tableBlockId),
264
+ predicate: (query) => {
265
+ if (staleHashes.has(hashKey(query.queryKey))) {
266
+ return true;
267
+ }
268
+ if (filteredLists && isFilteredListQueryKey(query.queryKey, tableBlockId)) {
269
+ return true;
270
+ }
271
+ if (nonIdOneQueries &&
272
+ isOneQueryKey(query.queryKey, tableBlockId) &&
273
+ getOneQueryKeyId(query.queryKey) === undefined) {
274
+ if (!deletedRowIds) {
275
+ return true;
276
+ }
277
+ const data = query.state.data;
278
+ return data === null || (isValidRow(data) && deletedRowIds.some((rowId) => sameId(rowId, data.id)));
279
+ }
280
+ return false;
281
+ },
282
+ });
283
+ };
284
+ /** Full-table invalidation - the pre-optimistic behavior, kept as the safety fallback. */
285
+ export const invalidateEntityTable = (queryClient, tableBlockId) => {
286
+ void queryClient.invalidateQueries({ queryKey: entityTableKey(tableBlockId) });
287
+ };
288
+ /**
289
+ * Optimistic phase of an update/delete mutation: cancel clobbering fetches, snapshot
290
+ * the table cache for rollback, then apply the optimistic change. Never throws - on
291
+ * any failure the optimistic change is rolled back and skipped, and the mutation
292
+ * proceeds (the success/error handlers still reconcile the cache).
293
+ */
294
+ export const beginEntityOptimisticWrite = async (queryClient, tableBlockId, applyOptimistic) => {
295
+ try {
296
+ const { staleKeys } = await prepareEntityCacheWrite(queryClient, tableBlockId);
297
+ const snapshot = snapshotEntityQueries(queryClient, tableBlockId);
298
+ try {
299
+ applyOptimistic();
300
+ }
301
+ catch {
302
+ // A partially applied optimistic change must not linger in the cache.
303
+ restoreEntityQueriesSnapshot(queryClient, snapshot);
304
+ }
305
+ return { snapshot, staleKeys };
306
+ }
307
+ catch {
308
+ return undefined;
309
+ }
310
+ };
311
+ /**
312
+ * Error phase: restore the snapshot taken in `beginEntityOptimisticWrite`, then
313
+ * refetch the table since the server state is unknown after a failed request.
314
+ * `skipInvalidation` is for mutations rejected before any request was sent
315
+ * (e.g. client-side rate limiting) where the restored snapshot is already correct.
316
+ */
317
+ export const rollbackEntityOptimisticWrite = (queryClient, tableBlockId, context, { skipInvalidation = false } = {}) => {
318
+ try {
319
+ if (context) {
320
+ restoreEntityQueriesSnapshot(queryClient, context.snapshot);
321
+ }
322
+ }
323
+ catch {
324
+ invalidateEntityTable(queryClient, tableBlockId);
325
+ return;
326
+ }
327
+ if (!skipInvalidation) {
328
+ invalidateEntityTable(queryClient, tableBlockId);
329
+ }
330
+ };
331
+ /**
332
+ * Success phase of a create/update mutation: writes the canonical rows returned by
333
+ * the server into the cache and invalidates only what a direct write cannot decide
334
+ * (filtered lists, non-id one-queries, queries that were mid-fetch). Falls back to
335
+ * full-table invalidation whenever the response is unusable or anything throws, so
336
+ * the worst case is exactly the previous always-refetch behavior.
337
+ */
338
+ export const applyEntityCreateResult = async (queryClient, tableBlockId, result, { appendNewToUnfilteredLists, extraStaleKeys = [], }) => {
339
+ try {
340
+ const rows = normalizeRowsResult(result);
341
+ if (rows.length === 0) {
342
+ invalidateEntityTable(queryClient, tableBlockId);
343
+ return;
344
+ }
345
+ const { staleKeys } = await prepareEntityCacheWrite(queryClient, tableBlockId);
346
+ applyEntityRowsUpserted(queryClient, tableBlockId, rows, { appendNewToUnfilteredLists });
347
+ seedEntityOneCaches(queryClient, tableBlockId, rows);
348
+ invalidateEntityQueriesAfterWrite(queryClient, tableBlockId, {
349
+ staleKeys: [...extraStaleKeys, ...staleKeys],
350
+ filteredLists: true,
351
+ nonIdOneQueries: true,
352
+ });
353
+ }
354
+ catch {
355
+ invalidateEntityTable(queryClient, tableBlockId);
356
+ }
357
+ };
358
+ /**
359
+ * Success phase of a delete mutation: removes the rows from every cached query.
360
+ * Filtered lists are still refetched because server-side filters may involve
361
+ * limits/pagination that removal alone cannot reconcile.
362
+ */
363
+ export const applyEntityDeleteResult = async (queryClient, tableBlockId, rowIds, { extraStaleKeys = [] } = {}) => {
364
+ try {
365
+ const { staleKeys } = await prepareEntityCacheWrite(queryClient, tableBlockId);
366
+ applyEntityRowsDeleted(queryClient, tableBlockId, rowIds);
367
+ invalidateEntityQueriesAfterWrite(queryClient, tableBlockId, {
368
+ staleKeys: [...extraStaleKeys, ...staleKeys],
369
+ filteredLists: true,
370
+ nonIdOneQueries: true,
371
+ deletedRowIds: rowIds,
372
+ });
373
+ }
374
+ catch {
375
+ invalidateEntityTable(queryClient, tableBlockId);
376
+ }
377
+ };
@@ -0,0 +1,57 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+ export type EntityDataOperation = "insert" | "update" | "delete";
3
+ /** Fetches the current server rows for the given ids (typically `findMany({ id: ids })`). */
4
+ export type EntityRowsByIdsFetcher = (tableBlockId: string, ids: (string | number)[]) => Promise<unknown>;
5
+ export interface EntityDataEventPayload {
6
+ table?: string;
7
+ affectedRowsData?: {
8
+ id?: unknown;
9
+ }[];
10
+ }
11
+ export interface EntityLiveSyncOptions {
12
+ queryClient: QueryClient;
13
+ fetchRowsByIds: EntityRowsByIdsFetcher;
14
+ /** How long to coalesce a burst of events for the same table before syncing once. */
15
+ batchDelayMs?: number;
16
+ /** Above this many affected rows, refetching the queries is cheaper than fetching rows one batch. */
17
+ maxRowFetchCount?: number;
18
+ }
19
+ /**
20
+ * Pulls the canonical state of specific rows from the server and reconciles the
21
+ * cache with it - the "fetch a little data instead of refetching everything" path.
22
+ *
23
+ * - `deleteIds` are removed from every cached query without any fetch.
24
+ * - `insertIds`/`updateIds` are fetched in a single by-id request; the returned rows
25
+ * are written into every cached query (insert-origin rows are appended to
26
+ * unfiltered lists, update-origin rows are only replaced in place).
27
+ * - The fetch is skipped entirely when the cache holds no unfiltered lists or
28
+ * one-queries - there would be nothing for the row data to update.
29
+ * - Requested ids the server no longer returns are treated as deleted (the by-id
30
+ * fetch runs through the same pipeline and policies as lists, so a missing row is
31
+ * either gone or was never visible to this session in the first place).
32
+ * - Filtered lists and non-id one-queries are still invalidated, since membership
33
+ * can only be decided server-side.
34
+ *
35
+ * Any failure falls back to invalidating the whole table, so the worst case is the
36
+ * previous refetch-everything behavior.
37
+ */
38
+ export declare const syncEntityRowsFromServer: (queryClient: QueryClient, tableBlockId: string, fetchRowsByIds: EntityRowsByIdsFetcher, { insertIds, updateIds, deleteIds, }: {
39
+ insertIds?: (string | number)[];
40
+ updateIds?: (string | number)[];
41
+ deleteIds?: (string | number)[];
42
+ }) => Promise<void>;
43
+ /**
44
+ * Turns websocket data events into minimal cache syncs.
45
+ *
46
+ * Events for the same table are coalesced for `batchDelayMs` and then processed in
47
+ * one pass: one small by-id fetch for inserted/updated rows, zero fetches for
48
+ * deletes, instead of invalidating (and refetching) every query of the table.
49
+ * Events without row ids and oversized batches keep the old full-invalidation
50
+ * behavior.
51
+ */
52
+ export declare const createEntityLiveSync: ({ queryClient, fetchRowsByIds, batchDelayMs, maxRowFetchCount, }: EntityLiveSyncOptions) => {
53
+ handleDataEvent: (operation: EntityDataOperation, payload: EntityDataEventPayload) => void;
54
+ dispose: () => void;
55
+ };
56
+ export type EntityLiveSync = ReturnType<typeof createEntityLiveSync>;
57
+ //# sourceMappingURL=EntityLiveSync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityLiveSync.d.ts","sourceRoot":"","sources":["../src/EntityLiveSync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAapD,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjE,6FAA6F;AAC7F,MAAM,MAAM,sBAAsB,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE1G,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,WAAW,CAAC;IACzB,cAAc,EAAE,sBAAsB,CAAC;IACvC,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qGAAqG;IACrG,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAiBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,wBAAwB,GACnC,aAAa,WAAW,EACxB,cAAc,MAAM,EACpB,gBAAgB,sBAAsB,EACtC,sCAIG;IACD,SAAS,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CACjC,KACA,OAAO,CAAC,IAAI,CA6Cd,CAAC;AAkBF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,kEAKlC,qBAAqB;iCA4Ec,mBAAmB,WAAW,sBAAsB,KAAG,IAAI;mBA2B3E,IAAI;CAiBzB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -0,0 +1,182 @@
1
+ import { applyEntityRowsDeleted, applyEntityRowsUpserted, hasDirectlyMaintainedEntityQueries, invalidateEntityQueriesAfterWrite, invalidateEntityTable, normalizeRowsResult, prepareEntityCacheWrite, sameId, } from "./EntityCache.js";
2
+ const DEFAULT_BATCH_DELAY_MS = 150;
3
+ const DEFAULT_MAX_ROW_FETCH_COUNT = 100;
4
+ const isIdValue = (value) => (typeof value === "string" && value.length > 0) || (typeof value === "number" && Number.isFinite(value));
5
+ const extractEventRowIds = (payload) => (payload.affectedRowsData ?? []).map((row) => row?.id).filter(isIdValue);
6
+ const dedupeIds = (ids) => {
7
+ const byKey = new Map();
8
+ ids.forEach((id) => byKey.set(String(id), id));
9
+ return [...byKey.values()];
10
+ };
11
+ /**
12
+ * Pulls the canonical state of specific rows from the server and reconciles the
13
+ * cache with it - the "fetch a little data instead of refetching everything" path.
14
+ *
15
+ * - `deleteIds` are removed from every cached query without any fetch.
16
+ * - `insertIds`/`updateIds` are fetched in a single by-id request; the returned rows
17
+ * are written into every cached query (insert-origin rows are appended to
18
+ * unfiltered lists, update-origin rows are only replaced in place).
19
+ * - The fetch is skipped entirely when the cache holds no unfiltered lists or
20
+ * one-queries - there would be nothing for the row data to update.
21
+ * - Requested ids the server no longer returns are treated as deleted (the by-id
22
+ * fetch runs through the same pipeline and policies as lists, so a missing row is
23
+ * either gone or was never visible to this session in the first place).
24
+ * - Filtered lists and non-id one-queries are still invalidated, since membership
25
+ * can only be decided server-side.
26
+ *
27
+ * Any failure falls back to invalidating the whole table, so the worst case is the
28
+ * previous refetch-everything behavior.
29
+ */
30
+ export const syncEntityRowsFromServer = async (queryClient, tableBlockId, fetchRowsByIds, { insertIds = [], updateIds = [], deleteIds = [], }) => {
31
+ try {
32
+ const { staleKeys } = await prepareEntityCacheWrite(queryClient, tableBlockId);
33
+ if (deleteIds.length > 0) {
34
+ applyEntityRowsDeleted(queryClient, tableBlockId, deleteIds);
35
+ }
36
+ // A row that was also deleted in the same batch is gone - no point fetching it.
37
+ const wantedIds = dedupeIds([...insertIds, ...updateIds].filter((id) => !deleteIds.some((deletedId) => sameId(deletedId, id))));
38
+ let missingIds = [];
39
+ // Only fetch row data when the cache holds queries that direct writes can keep
40
+ // fresh - otherwise the invalidation below already covers everything cached.
41
+ if (wantedIds.length > 0 && hasDirectlyMaintainedEntityQueries(queryClient, tableBlockId)) {
42
+ const fetched = normalizeRowsResult(await fetchRowsByIds(tableBlockId, wantedIds));
43
+ // Only trust rows we actually asked for, in case the fetcher over-returns.
44
+ const rows = fetched.filter((row) => wantedIds.some((id) => sameId(id, row.id)));
45
+ const insertRows = rows.filter((row) => insertIds.some((id) => sameId(id, row.id)));
46
+ const updateRows = rows.filter((row) => !insertRows.includes(row));
47
+ applyEntityRowsUpserted(queryClient, tableBlockId, updateRows, { appendNewToUnfilteredLists: false });
48
+ applyEntityRowsUpserted(queryClient, tableBlockId, insertRows, { appendNewToUnfilteredLists: true });
49
+ // Ids the server did not return are gone (deleted or no longer accessible):
50
+ // the by-id fetch runs through the same query pipeline and policies as lists.
51
+ missingIds = wantedIds.filter((id) => !rows.some((row) => sameId(row.id, id)));
52
+ if (missingIds.length > 0) {
53
+ applyEntityRowsDeleted(queryClient, tableBlockId, missingIds);
54
+ }
55
+ }
56
+ const allDeletedIds = [...deleteIds, ...missingIds];
57
+ invalidateEntityQueriesAfterWrite(queryClient, tableBlockId, {
58
+ staleKeys,
59
+ filteredLists: true,
60
+ nonIdOneQueries: true,
61
+ deletedRowIds: wantedIds.length === 0 && allDeletedIds.length > 0 ? allDeletedIds : undefined,
62
+ });
63
+ }
64
+ catch {
65
+ invalidateEntityTable(queryClient, tableBlockId);
66
+ }
67
+ };
68
+ /**
69
+ * Turns websocket data events into minimal cache syncs.
70
+ *
71
+ * Events for the same table are coalesced for `batchDelayMs` and then processed in
72
+ * one pass: one small by-id fetch for inserted/updated rows, zero fetches for
73
+ * deletes, instead of invalidating (and refetching) every query of the table.
74
+ * Events without row ids and oversized batches keep the old full-invalidation
75
+ * behavior.
76
+ */
77
+ export const createEntityLiveSync = ({ queryClient, fetchRowsByIds, batchDelayMs = DEFAULT_BATCH_DELAY_MS, maxRowFetchCount = DEFAULT_MAX_ROW_FETCH_COUNT, }) => {
78
+ const tables = new Map();
79
+ let disposed = false;
80
+ const getOrCreateState = (table) => {
81
+ const existing = tables.get(table);
82
+ if (existing) {
83
+ return existing;
84
+ }
85
+ const created = {
86
+ insertIds: new Map(),
87
+ updateIds: new Map(),
88
+ deleteIds: new Map(),
89
+ fullInvalidate: false,
90
+ flushing: false,
91
+ };
92
+ tables.set(table, created);
93
+ return created;
94
+ };
95
+ const hasPendingEvents = (state) => state.fullInvalidate || state.insertIds.size > 0 || state.updateIds.size > 0 || state.deleteIds.size > 0;
96
+ const takeBatch = (state) => {
97
+ const batch = {
98
+ insertIds: [...state.insertIds.values()],
99
+ updateIds: [...state.updateIds.values()],
100
+ deleteIds: [...state.deleteIds.values()],
101
+ fullInvalidate: state.fullInvalidate,
102
+ };
103
+ state.insertIds = new Map();
104
+ state.updateIds = new Map();
105
+ state.deleteIds = new Map();
106
+ state.fullInvalidate = false;
107
+ return batch;
108
+ };
109
+ const scheduleFlush = (table, state) => {
110
+ state.timer = setTimeout(() => {
111
+ void flush(table);
112
+ }, batchDelayMs);
113
+ };
114
+ const flush = async (table) => {
115
+ const state = tables.get(table);
116
+ if (!state || state.flushing) {
117
+ return;
118
+ }
119
+ state.timer = undefined;
120
+ state.flushing = true;
121
+ const batch = takeBatch(state);
122
+ try {
123
+ const rowFetchCount = batch.insertIds.length + batch.updateIds.length;
124
+ if (batch.fullInvalidate || rowFetchCount > maxRowFetchCount) {
125
+ invalidateEntityTable(queryClient, table);
126
+ }
127
+ else {
128
+ await syncEntityRowsFromServer(queryClient, table, fetchRowsByIds, batch);
129
+ }
130
+ }
131
+ catch {
132
+ invalidateEntityTable(queryClient, table);
133
+ }
134
+ finally {
135
+ state.flushing = false;
136
+ if (disposed || !hasPendingEvents(state)) {
137
+ tables.delete(table);
138
+ }
139
+ else {
140
+ // Events arrived while we were syncing - process them in the next window.
141
+ scheduleFlush(table, state);
142
+ }
143
+ }
144
+ };
145
+ const handleDataEvent = (operation, payload) => {
146
+ if (disposed) {
147
+ return;
148
+ }
149
+ const table = payload?.table;
150
+ if (!table) {
151
+ return;
152
+ }
153
+ const state = getOrCreateState(table);
154
+ const ids = extractEventRowIds(payload);
155
+ if (ids.length === 0) {
156
+ // No row ids in the event - we cannot sync selectively.
157
+ state.fullInvalidate = true;
158
+ }
159
+ else {
160
+ const target = operation === "insert" ? state.insertIds : operation === "update" ? state.updateIds : state.deleteIds;
161
+ ids.forEach((id) => target.set(String(id), id));
162
+ }
163
+ if (!state.timer && !state.flushing) {
164
+ scheduleFlush(table, state);
165
+ }
166
+ };
167
+ const dispose = () => {
168
+ disposed = true;
169
+ tables.forEach((state, table) => {
170
+ if (state.timer) {
171
+ clearTimeout(state.timer);
172
+ state.timer = undefined;
173
+ }
174
+ // Pending events cannot be synced during teardown - fall back to marking stale.
175
+ if (hasPendingEvents(state)) {
176
+ invalidateEntityTable(queryClient, table);
177
+ }
178
+ });
179
+ tables.clear();
180
+ };
181
+ return { handleDataEvent, dispose };
182
+ };
@@ -86,12 +86,12 @@ export declare const useClient: () => ReactClientSdk;
86
86
  * @returns {EntityType<E>[] | undefined} returns.data - The fetched entities
87
87
  * @returns {boolean} returns.isLoading - Whether the query is loading
88
88
  * @returns {Error | null} returns.error - Any error that occurred
89
- * @returns {Function} returns.refetch - Function to manually refetch the data
89
+ * @returns {Function} returns.refetch - Intentional no-op kept for API compatibility; returns the current state without fetching. Data is kept fresh automatically (mutation cache writes + websocket live sync), so manual refetching is unsupported.
90
90
  * @returns {boolean} returns.isError - Whether the query resulted in an error
91
91
  * @returns {boolean} returns.isFetched - Whether the query has been fetched
92
92
  * @returns {boolean} returns.isFetching - Whether the query is currently fetching
93
93
  * @returns {boolean} returns.isSuccess - Whether the query was successful
94
- * @returns {string} returns.status - Current status of the query: 'idle', 'loading', 'success', 'error', or 'pending'
94
+ * @returns {string} returns.status - Current status of the query: 'pending', 'error', or 'success'
95
95
  * @example
96
96
  * ```tsx
97
97
  * import { ItemEntity } from '@/product-types';
@@ -121,7 +121,7 @@ export declare const useEntityGetAll: <E extends EntityConfig>(entityConfig: E,
121
121
  isFetched: boolean;
122
122
  isFetching: boolean;
123
123
  isSuccess: boolean;
124
- status: "idle" | "loading" | "success" | "error" | "pending";
124
+ status: "pending" | "error" | "success";
125
125
  };
126
126
  /**
127
127
  * Hook to fetch a single entity by filters
@@ -139,12 +139,12 @@ export declare const useEntityGetAll: <E extends EntityConfig>(entityConfig: E,
139
139
  * @returns {EntityType<EC> | undefined | null} returns.data - The fetched entity
140
140
  * @returns {boolean} returns.isLoading - Whether the query is loading
141
141
  * @returns {Error | null} returns.error - Any error that occurred
142
- * @returns {Function} returns.refetch - Function to manually refetch the data
142
+ * @returns {Function} returns.refetch - Intentional no-op kept for API compatibility; returns the current state without fetching. Data is kept fresh automatically (mutation cache writes + websocket live sync), so manual refetching is unsupported.
143
143
  * @returns {boolean} returns.isError - Whether the query resulted in an error
144
144
  * @returns {boolean} returns.isFetched - Whether the query has been fetched
145
145
  * @returns {boolean} returns.isFetching - Whether the query is currently fetching
146
146
  * @returns {boolean} returns.isSuccess - Whether the query was successful
147
- * @returns {string} returns.status - Current status of the query: 'idle', 'loading', 'success', 'error', or 'pending'
147
+ * @returns {string} returns.status - Current status of the query: 'pending', 'error', or 'success'
148
148
  * @example
149
149
  * ```tsx
150
150
  * import { UserEntity } from '@/product-types';
@@ -171,13 +171,15 @@ export declare const useEntityGetOne: <EC extends EntityConfig = EntityConfig>(e
171
171
  isFetched: boolean;
172
172
  isFetching: boolean;
173
173
  isSuccess: boolean;
174
- status: "idle" | "loading" | "success" | "error" | "pending";
174
+ status: "pending" | "error" | "success";
175
175
  };
176
176
  /**
177
177
  * Hook to create a new entity
178
178
  *
179
179
  * Creates a new entity instance in the data store.
180
180
  * EntityTypeOnlyMutable<EC> represents the writable properties of the entity.
181
+ * The created entity returned by the server is written directly into the query cache,
182
+ * so `useEntityGetAll`/`useEntityGetOne` consumers update instantly without refetching.
181
183
  *
182
184
  * @template EC - Entity configuration type
183
185
  * @param {EC} entityConfig - Configuration for the entity type
@@ -226,7 +228,8 @@ export declare const useEntityCreate: <EC extends EntityConfig = EntityConfig>(e
226
228
  * Creates multiple entity instances in the data store in one batch operation.
227
229
  * This is more efficient than calling useEntityCreate multiple times when you need
228
230
  * to create several entities at once. EntityTypeOnlyMutable<EC> represents the
229
- * writable properties of the entity.
231
+ * writable properties of the entity. The created entities returned by the server are
232
+ * written directly into the query cache, so consumers update without refetching.
230
233
  *
231
234
  * @template EC - Entity configuration type
232
235
  * @param {EC} entityConfig - Configuration for the entity type
@@ -269,6 +272,8 @@ export declare const useEntityCreateMany: <EC extends EntityConfig = EntityConfi
269
272
  *
270
273
  * Updates an existing entity with the provided partial data.
271
274
  * Only the properties included in the data object will be updated.
275
+ * The change is applied to the query cache optimistically (and rolled back on error),
276
+ * then confirmed with the canonical row returned by the server - no refetch needed.
272
277
  *
273
278
  * @template EC - Entity configuration type
274
279
  * @param {EC} entityConfig - Configuration for the entity type
@@ -310,7 +315,8 @@ export declare const useEntityUpdate: <EC extends EntityConfig = EntityConfig>(e
310
315
  * Hook to delete an entity
311
316
  *
312
317
  * Permanently removes an entity from the data store by its ID.
313
- * Automatically invalidates relevant queries after deletion.
318
+ * The row is removed from the query cache optimistically (and restored on error),
319
+ * so dependent queries update instantly without refetching.
314
320
  *
315
321
  * @template EC - Entity configuration type
316
322
  * @param {EC} entityConfig - Configuration for the entity type
@@ -347,7 +353,8 @@ export declare const useEntityDelete: <EC extends EntityConfig = EntityConfig>(e
347
353
  * Hook to delete multiple entities at once
348
354
  *
349
355
  * Permanently removes multiple entities from the data store by their IDs.
350
- * Automatically invalidates relevant queries after deletion.
356
+ * The rows are removed from the query cache optimistically (and restored on error),
357
+ * so dependent queries update instantly without refetching.
351
358
  *
352
359
  * @template EC - Entity configuration type
353
360
  * @param {EC} entityConfig - Configuration for the entity type
@@ -1 +1 @@
1
- {"version":3,"file":"ReactClientSdk.d.ts","sourceRoot":"","sources":["../src/ReactClientSdk.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ReactClientSdk.d.ts","sourceRoot":"","sources":["../src/ReactClientSdk.tsx"],"names":[],"mappings":"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"}
@@ -5,10 +5,13 @@ import { useAiStreamChunks } from "@blocksdiy/react-common/useAiStreamChunks";
5
5
  import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
6
6
  import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
7
7
  import { ClientSdk } from "./ClientSdk.js";
8
+ import { applyEntityCreateResult, applyEntityDeleteResult, applyEntityRowPatch, applyEntityRowsDeleted, beginEntityOptimisticWrite, ENTITIES_QUERY_KEY, rollbackEntityOptimisticWrite, } from "./EntityCache.js";
9
+ import { createEntityLiveSync } from "./EntityLiveSync.js";
8
10
  export class ReactClientSdk extends ClientSdk {
9
11
  }
10
- const ENTITIES_QUERY_KEY = "entities";
11
12
  const CURRENT_USER_QUERY_KEY = "currentUser";
13
+ // Avoids redundant refetches on quick remounts and window focus flaps without meaningfully delaying anything.
14
+ const ENTITIES_STALE_TIME = 5_000;
12
15
  const ClientContext = createContext(null);
13
16
  const ThemeModeContext = createContext({
14
17
  themeMode: "system",
@@ -157,7 +160,17 @@ const useEntityRowMutationRateLimiting = (entityConfig, functionName) => {
157
160
  * ```
158
161
  */
159
162
  export const ClientProvider = ({ client, children, themeMode, setThemeMode, }) => {
160
- const [queryClient] = useState(() => new QueryClient());
163
+ const [queryClient] = useState(() => new QueryClient({
164
+ defaultOptions: {
165
+ queries: {
166
+ staleTime: ENTITIES_STALE_TIME,
167
+ // TEMP: disable React Query's default refetch-on-window-focus so switching
168
+ // browser tabs never triggers fetches. Remove this line to restore the
169
+ // default focus revalidation (refetch stale queries on refocus).
170
+ refetchOnWindowFocus: false,
171
+ },
172
+ },
173
+ }));
161
174
  const user = client.getUser();
162
175
  const userIdInNumber = useMemo(() => {
163
176
  if (user?.id) {
@@ -171,22 +184,25 @@ export const ClientProvider = ({ client, children, themeMode, setThemeMode, }) =
171
184
  }, [user?.id]);
172
185
  useWebsockets({ userId: userIdInNumber, token: client.token });
173
186
  useWebsocketsAppSubscribe({ appId: client.appId });
174
- const dataChangeCallback = useCallback(async (data) => {
175
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, data.table] });
176
- data.affectRows?.forEach((row) => {
177
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, data.table, "one", { id: row.id }] });
178
- });
179
- }, [queryClient]);
187
+ // Websocket events update cache for specific rows after external changes
180
188
  useEffect(() => {
181
- websocketsService.listen(AppDataEventTypes.APP_DATA_INSERT, dataChangeCallback);
182
- websocketsService.listen(AppDataEventTypes.APP_DATA_UPDATE, dataChangeCallback);
183
- websocketsService.listen(AppDataEventTypes.APP_DATA_DELETE, dataChangeCallback);
189
+ const liveSync = createEntityLiveSync({
190
+ queryClient,
191
+ fetchRowsByIds: (tableBlockId, ids) => client.entity({ tableBlockId, instanceType: {} }).findMany({ id: ids }),
192
+ });
193
+ const onInsert = async (data) => liveSync.handleDataEvent("insert", data);
194
+ const onUpdate = async (data) => liveSync.handleDataEvent("update", data);
195
+ const onDelete = async (data) => liveSync.handleDataEvent("delete", data);
196
+ websocketsService.listen(AppDataEventTypes.APP_DATA_INSERT, onInsert);
197
+ websocketsService.listen(AppDataEventTypes.APP_DATA_UPDATE, onUpdate);
198
+ websocketsService.listen(AppDataEventTypes.APP_DATA_DELETE, onDelete);
184
199
  return () => {
185
- websocketsService.unlisten(AppDataEventTypes.APP_DATA_INSERT, dataChangeCallback);
186
- websocketsService.unlisten(AppDataEventTypes.APP_DATA_UPDATE, dataChangeCallback);
187
- websocketsService.unlisten(AppDataEventTypes.APP_DATA_DELETE, dataChangeCallback);
200
+ websocketsService.unlisten(AppDataEventTypes.APP_DATA_INSERT, onInsert);
201
+ websocketsService.unlisten(AppDataEventTypes.APP_DATA_UPDATE, onUpdate);
202
+ websocketsService.unlisten(AppDataEventTypes.APP_DATA_DELETE, onDelete);
203
+ liveSync.dispose();
188
204
  };
189
- }, [dataChangeCallback]);
205
+ }, [queryClient, client]);
190
206
  return (<ClientContext value={client}>
191
207
  <ThemeModeContext value={{ themeMode, setThemeMode }}>
192
208
  {/* <QueryClientProvider client={queryClient}>
@@ -244,12 +260,12 @@ export const useClient = () => {
244
260
  * @returns {EntityType<E>[] | undefined} returns.data - The fetched entities
245
261
  * @returns {boolean} returns.isLoading - Whether the query is loading
246
262
  * @returns {Error | null} returns.error - Any error that occurred
247
- * @returns {Function} returns.refetch - Function to manually refetch the data
263
+ * @returns {Function} returns.refetch - Intentional no-op kept for API compatibility; returns the current state without fetching. Data is kept fresh automatically (mutation cache writes + websocket live sync), so manual refetching is unsupported.
248
264
  * @returns {boolean} returns.isError - Whether the query resulted in an error
249
265
  * @returns {boolean} returns.isFetched - Whether the query has been fetched
250
266
  * @returns {boolean} returns.isFetching - Whether the query is currently fetching
251
267
  * @returns {boolean} returns.isSuccess - Whether the query was successful
252
- * @returns {string} returns.status - Current status of the query: 'idle', 'loading', 'success', 'error', or 'pending'
268
+ * @returns {string} returns.status - Current status of the query: 'pending', 'error', or 'success'
253
269
  * @example
254
270
  * ```tsx
255
271
  * import { ItemEntity } from '@/product-types';
@@ -280,6 +296,8 @@ export const useEntityGetAll = (entityConfig, filters, queryOptions = {}) => {
280
296
  data,
281
297
  isLoading,
282
298
  error,
299
+ // Deliberately NOT React Query's refetch: app code must not be able to force
300
+ // refetches - the cache stays fresh via mutation writes and websocket live sync.
283
301
  refetch: () => ({ data, isLoading, error, isError, isFetched, isFetching, isSuccess, status }),
284
302
  isError,
285
303
  isFetched,
@@ -304,12 +322,12 @@ export const useEntityGetAll = (entityConfig, filters, queryOptions = {}) => {
304
322
  * @returns {EntityType<EC> | undefined | null} returns.data - The fetched entity
305
323
  * @returns {boolean} returns.isLoading - Whether the query is loading
306
324
  * @returns {Error | null} returns.error - Any error that occurred
307
- * @returns {Function} returns.refetch - Function to manually refetch the data
325
+ * @returns {Function} returns.refetch - Intentional no-op kept for API compatibility; returns the current state without fetching. Data is kept fresh automatically (mutation cache writes + websocket live sync), so manual refetching is unsupported.
308
326
  * @returns {boolean} returns.isError - Whether the query resulted in an error
309
327
  * @returns {boolean} returns.isFetched - Whether the query has been fetched
310
328
  * @returns {boolean} returns.isFetching - Whether the query is currently fetching
311
329
  * @returns {boolean} returns.isSuccess - Whether the query was successful
312
- * @returns {string} returns.status - Current status of the query: 'idle', 'loading', 'success', 'error', or 'pending'
330
+ * @returns {string} returns.status - Current status of the query: 'pending', 'error', or 'success'
313
331
  * @example
314
332
  * ```tsx
315
333
  * import { UserEntity } from '@/product-types';
@@ -344,6 +362,8 @@ export const useEntityGetOne = (entityConfig, filters, queryOptions = {}) => {
344
362
  data,
345
363
  isLoading,
346
364
  error,
365
+ // Deliberately NOT React Query's refetch: app code must not be able to force
366
+ // refetches - the cache stays fresh via mutation writes and websocket live sync.
347
367
  refetch: () => ({ data, isLoading, error, isError, isFetched, isFetching, isSuccess, status }),
348
368
  isError,
349
369
  isFetched,
@@ -357,6 +377,8 @@ export const useEntityGetOne = (entityConfig, filters, queryOptions = {}) => {
357
377
  *
358
378
  * Creates a new entity instance in the data store.
359
379
  * EntityTypeOnlyMutable<EC> represents the writable properties of the entity.
380
+ * The created entity returned by the server is written directly into the query cache,
381
+ * so `useEntityGetAll`/`useEntityGetOne` consumers update instantly without refetching.
360
382
  *
361
383
  * @template EC - Entity configuration type
362
384
  * @param {EC} entityConfig - Configuration for the entity type
@@ -401,9 +423,11 @@ export const useEntityCreate = (entityConfig) => {
401
423
  validateExceededMaxCount({ data });
402
424
  return client.entity(entityConfig).create(data);
403
425
  },
404
- onSuccess: () => {
405
- // Invalidate the list so we refetch it with the newly created item
406
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
426
+ onSuccess: async (result) => {
427
+ // The server returns the canonical created row, so write it into the cache
428
+ await applyEntityCreateResult(queryClient, entityConfig.tableBlockId, result, {
429
+ appendNewToUnfilteredLists: true,
430
+ });
407
431
  },
408
432
  });
409
433
  if (error instanceof FatalAppError) {
@@ -417,7 +441,8 @@ export const useEntityCreate = (entityConfig) => {
417
441
  * Creates multiple entity instances in the data store in one batch operation.
418
442
  * This is more efficient than calling useEntityCreate multiple times when you need
419
443
  * to create several entities at once. EntityTypeOnlyMutable<EC> represents the
420
- * writable properties of the entity.
444
+ * writable properties of the entity. The created entities returned by the server are
445
+ * written directly into the query cache, so consumers update without refetching.
421
446
  *
422
447
  * @template EC - Entity configuration type
423
448
  * @param {EC} entityConfig - Configuration for the entity type
@@ -457,9 +482,11 @@ export const useEntityCreateMany = (entityConfig) => {
457
482
  validateExceededMaxCount({ data });
458
483
  return client.entity(entityConfig).createMany(data);
459
484
  },
460
- onSuccess: () => {
461
- // Invalidate the list so we refetch it with the newly created item
462
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
485
+ onSuccess: async (result) => {
486
+ // The server returns the canonical created rows, so write them into the cache
487
+ await applyEntityCreateResult(queryClient, entityConfig.tableBlockId, result, {
488
+ appendNewToUnfilteredLists: true,
489
+ });
463
490
  },
464
491
  });
465
492
  if (error instanceof FatalAppError) {
@@ -472,6 +499,8 @@ export const useEntityCreateMany = (entityConfig) => {
472
499
  *
473
500
  * Updates an existing entity with the provided partial data.
474
501
  * Only the properties included in the data object will be updated.
502
+ * The change is applied to the query cache optimistically (and rolled back on error),
503
+ * then confirmed with the canonical row returned by the server - no refetch needed.
475
504
  *
476
505
  * @template EC - Entity configuration type
477
506
  * @param {EC} entityConfig - Configuration for the entity type
@@ -506,14 +535,28 @@ export const useEntityUpdate = (entityConfig) => {
506
535
  const queryClient = useQueryClient();
507
536
  const { validateExceededMaxCount } = useEntityRowMutationRateLimiting(entityConfig, "useEntityUpdate");
508
537
  const { mutateAsync: updateFunction, isPending: isLoading, error, } = useMutation({
538
+ onMutate: ({ id, data }) =>
539
+ // Optimistically patch every cached copy of the row for instant UI feedback.
540
+ beginEntityOptimisticWrite(queryClient, entityConfig.tableBlockId, () => {
541
+ applyEntityRowPatch(queryClient, entityConfig.tableBlockId, id, data);
542
+ }),
509
543
  mutationFn: ({ id, data }) => {
510
544
  validateExceededMaxCount({ rowId: id, data });
511
545
  return client.entity(entityConfig).updateOne(id, data);
512
546
  },
513
- onSuccess: (_result, { id }) => {
514
- // Invalidate both the list and the specific item's query
515
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
516
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
547
+ onSuccess: async (result, _variables, context) => {
548
+ // Replace the optimistic patch with the canonical updated row from the server
549
+ await applyEntityCreateResult(queryClient, entityConfig.tableBlockId, result, {
550
+ appendNewToUnfilteredLists: false,
551
+ extraStaleKeys: context?.staleKeys,
552
+ });
553
+ },
554
+ onError: (mutationError, _variables, context) => {
555
+ rollbackEntityOptimisticWrite(queryClient, entityConfig.tableBlockId, context, {
556
+ // Rate-limit rejections happen before the request is sent, so the restored
557
+ // snapshot is already the correct server state.
558
+ skipInvalidation: mutationError instanceof FatalAppError,
559
+ });
517
560
  },
518
561
  });
519
562
  if (error instanceof FatalAppError) {
@@ -525,7 +568,8 @@ export const useEntityUpdate = (entityConfig) => {
525
568
  * Hook to delete an entity
526
569
  *
527
570
  * Permanently removes an entity from the data store by its ID.
528
- * Automatically invalidates relevant queries after deletion.
571
+ * The row is removed from the query cache optimistically (and restored on error),
572
+ * so dependent queries update instantly without refetching.
529
573
  *
530
574
  * @template EC - Entity configuration type
531
575
  * @param {EC} entityConfig - Configuration for the entity type
@@ -556,14 +600,25 @@ export const useEntityDelete = (entityConfig) => {
556
600
  const queryClient = useQueryClient();
557
601
  const { validateExceededMaxCount } = useEntityRowMutationRateLimiting(entityConfig, "useEntityDelete");
558
602
  const { mutateAsync: deleteFunction, isPending: isLoading, error, } = useMutation({
603
+ onMutate: ({ id }) =>
604
+ // Optimistically remove the row from every cached query for instant UI feedback.
605
+ beginEntityOptimisticWrite(queryClient, entityConfig.tableBlockId, () => {
606
+ applyEntityRowsDeleted(queryClient, entityConfig.tableBlockId, [id]);
607
+ }),
559
608
  mutationFn: ({ id }) => {
560
609
  validateExceededMaxCount({ rowId: id });
561
610
  return client.entity(entityConfig).deleteOne(id);
562
611
  },
563
- onSuccess: (_result, { id }) => {
564
- // Invalidate both the list and the specific item's query
565
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
566
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
612
+ onSuccess: async (_result, { id }, context) => {
613
+ // Confirm the removal directly in the cache instead of refetching every query.
614
+ await applyEntityDeleteResult(queryClient, entityConfig.tableBlockId, [id], {
615
+ extraStaleKeys: context?.staleKeys,
616
+ });
617
+ },
618
+ onError: (mutationError, _variables, context) => {
619
+ rollbackEntityOptimisticWrite(queryClient, entityConfig.tableBlockId, context, {
620
+ skipInvalidation: mutationError instanceof FatalAppError,
621
+ });
567
622
  },
568
623
  });
569
624
  if (error instanceof FatalAppError) {
@@ -575,7 +630,8 @@ export const useEntityDelete = (entityConfig) => {
575
630
  * Hook to delete multiple entities at once
576
631
  *
577
632
  * Permanently removes multiple entities from the data store by their IDs.
578
- * Automatically invalidates relevant queries after deletion.
633
+ * The rows are removed from the query cache optimistically (and restored on error),
634
+ * so dependent queries update instantly without refetching.
579
635
  *
580
636
  * @template EC - Entity configuration type
581
637
  * @param {EC} entityConfig - Configuration for the entity type
@@ -606,14 +662,24 @@ export const useEntityDeleteMany = (entityConfig) => {
606
662
  const queryClient = useQueryClient();
607
663
  const { validateExceededMaxCount } = useEntityMutationRateLimiting(entityConfig, "useEntityDeleteMany");
608
664
  const { mutateAsync: deleteManyFunction, isPending: isLoading, error, } = useMutation({
665
+ onMutate: ({ ids }) =>
666
+ // Optimistically remove the rows from every cached query for instant UI feedback.
667
+ beginEntityOptimisticWrite(queryClient, entityConfig.tableBlockId, () => {
668
+ applyEntityRowsDeleted(queryClient, entityConfig.tableBlockId, ids);
669
+ }),
609
670
  mutationFn: ({ ids }) => {
610
671
  validateExceededMaxCount({ data: ids });
611
672
  return client.entity(entityConfig).deleteMany(ids);
612
673
  },
613
- onSuccess: (_result, { ids }) => {
614
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId] });
615
- ids.forEach((id) => {
616
- queryClient.invalidateQueries({ queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, "one", { id }] });
674
+ onSuccess: async (_result, { ids }, context) => {
675
+ // Confirm the removal directly in the cache instead of refetching every query.
676
+ await applyEntityDeleteResult(queryClient, entityConfig.tableBlockId, ids, {
677
+ extraStaleKeys: context?.staleKeys,
678
+ });
679
+ },
680
+ onError: (mutationError, _variables, context) => {
681
+ rollbackEntityOptimisticWrite(queryClient, entityConfig.tableBlockId, context, {
682
+ skipInvalidation: mutationError instanceof FatalAppError,
617
683
  });
618
684
  },
619
685
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocksdiy/blocks-client-sdk",
3
- "version": "1.8.14",
3
+ "version": "1.9.1",
4
4
  "type": "module",
5
5
  "description": "Blocks client sdk",
6
6
  "keywords": [],
@@ -46,9 +46,9 @@
46
46
  ]
47
47
  },
48
48
  "dependencies": {
49
- "@tanstack/react-query": "^5.90.21",
50
- "@blocksdiy/blocks-client-api": "1.12.0",
51
- "@blocksdiy/react-common": "1.30.2"
49
+ "@tanstack/react-query": "^5.101.0",
50
+ "@blocksdiy/blocks-client-api": "1.13.0",
51
+ "@blocksdiy/react-common": "1.31.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsc-alias": "^1.8.10",