@go-avro/avro-js 0.0.29 → 0.0.31
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/client/QueryClient.d.ts +8 -0
- package/dist/client/QueryClient.js +233 -83
- package/dist/types/api/Task.js +1 -1
- package/package.json +1 -1
|
@@ -537,6 +537,14 @@ export declare class AvroQueryClient {
|
|
|
537
537
|
* Called once from AvroQueryClientProvider when the tanstack
|
|
538
538
|
* QueryClient is available. Also handles auto-joining the
|
|
539
539
|
* company room on connect so consumer apps don't have to.
|
|
540
|
+
*
|
|
541
|
+
* Targeted events (single-entity CRUD) receive `{ id }` from the
|
|
542
|
+
* backend and surgically create / update / remove only the
|
|
543
|
+
* affected cache entries. Bulk events fall back to full
|
|
544
|
+
* query-key invalidation.
|
|
545
|
+
*
|
|
546
|
+
* Backward-compatible: if a targeted event arrives without an id
|
|
547
|
+
* (old backend), we fall back to full invalidation.
|
|
540
548
|
*/
|
|
541
549
|
setupSocketInvalidation(queryClient: QueryClient): void;
|
|
542
550
|
/**
|
|
@@ -4,83 +4,98 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
4
4
|
import { LoginResponse } from '../types/api';
|
|
5
5
|
import { AuthState } from '../types/auth';
|
|
6
6
|
import { StandardError } from '../types/error';
|
|
7
|
+
function isBulkEvent(c) {
|
|
8
|
+
return 'invalidateKeys' in c;
|
|
9
|
+
}
|
|
7
10
|
/**
|
|
8
|
-
* Maps socket event names to
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* Maps socket event names to cache-update strategies.
|
|
12
|
+
*
|
|
13
|
+
* *Targeted* events receive `{ id }` from the backend and surgically
|
|
14
|
+
* update / insert / remove only the affected cache entries.
|
|
15
|
+
*
|
|
16
|
+
* *Bulk* events (plural mutations, scheduling, etc.) fall back to
|
|
17
|
+
* full query-key invalidation because the backend doesn't emit per-item ids.
|
|
11
18
|
*/
|
|
12
|
-
const
|
|
13
|
-
// Company
|
|
14
|
-
create_company:
|
|
15
|
-
update_company:
|
|
16
|
-
delete_company:
|
|
17
|
-
// Users
|
|
18
|
-
user_updated: [['users']],
|
|
19
|
-
update_users: [['users']],
|
|
20
|
-
// Jobs
|
|
21
|
-
create_job:
|
|
22
|
-
update_job:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
delete_jobs: [['jobs']],
|
|
26
|
-
// Routes
|
|
27
|
-
create_route:
|
|
28
|
-
update_route:
|
|
29
|
-
delete_route:
|
|
30
|
-
// Events
|
|
31
|
-
create_event:
|
|
32
|
-
update_event:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Teams
|
|
36
|
-
create_team:
|
|
37
|
-
update_team:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Bills
|
|
41
|
-
create_bill:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Sessions
|
|
45
|
-
create_session:
|
|
46
|
-
update_session:
|
|
47
|
-
// Catalog
|
|
48
|
-
create_catalog_item:
|
|
49
|
-
update_catalog_item:
|
|
50
|
-
delete_catalog_item:
|
|
51
|
-
// Groups
|
|
52
|
-
create_group:
|
|
53
|
-
update_group:
|
|
54
|
-
delete_group:
|
|
55
|
-
// Labels
|
|
56
|
-
create_label:
|
|
57
|
-
update_label:
|
|
58
|
-
delete_label:
|
|
59
|
-
// Skills
|
|
60
|
-
create_skill:
|
|
61
|
-
update_skill:
|
|
62
|
-
delete_skill:
|
|
63
|
-
// Proposals
|
|
64
|
-
create_proposal:
|
|
65
|
-
update_proposal:
|
|
66
|
-
delete_proposal:
|
|
67
|
-
// Service Months
|
|
68
|
-
create_month:
|
|
69
|
-
update_months: [['months']],
|
|
70
|
-
delete_months: [['months']],
|
|
71
|
-
// Tasks
|
|
72
|
-
create_task:
|
|
73
|
-
update_task:
|
|
74
|
-
delete_task:
|
|
75
|
-
// Scheduling
|
|
76
|
-
schedule_complete: [['routes'], ['jobs']],
|
|
77
|
-
// Location
|
|
78
|
-
location_update: [['teams']],
|
|
79
|
-
// Prepayments
|
|
80
|
-
update_prepayments: [['prepayments']],
|
|
81
|
-
// Chats
|
|
82
|
-
new_message: [['chats'], ['messages']],
|
|
19
|
+
const SOCKET_EVENT_CONFIG = {
|
|
20
|
+
// ── Company ──
|
|
21
|
+
create_company: { entityKey: 'companies', action: 'create', fetchPath: (id) => `/company/${id}` },
|
|
22
|
+
update_company: { entityKey: 'companies', action: 'update', fetchPath: (id) => `/company/${id}` },
|
|
23
|
+
delete_company: { entityKey: 'companies', action: 'delete', fetchPath: null },
|
|
24
|
+
// ── Users (no single-entity socket events) ──
|
|
25
|
+
user_updated: { invalidateKeys: [['users'], ['user']] },
|
|
26
|
+
update_users: { invalidateKeys: [['users'], ['user']] },
|
|
27
|
+
// ── Jobs ──
|
|
28
|
+
create_job: { entityKey: 'jobs', action: 'create', fetchPath: (id) => `/job/${id}` },
|
|
29
|
+
update_job: { entityKey: 'jobs', action: 'update', fetchPath: (id) => `/job/${id}` },
|
|
30
|
+
delete_job: { entityKey: 'jobs', action: 'delete', fetchPath: null },
|
|
31
|
+
update_jobs: { invalidateKeys: [['jobs'], ['infinite', 'jobs']] },
|
|
32
|
+
delete_jobs: { invalidateKeys: [['jobs'], ['infinite', 'jobs']] },
|
|
33
|
+
// ── Routes ──
|
|
34
|
+
create_route: { entityKey: 'routes', action: 'create', fetchPath: (id) => `/route/${id}` },
|
|
35
|
+
update_route: { entityKey: 'routes', action: 'update', fetchPath: (id) => `/route/${id}` },
|
|
36
|
+
delete_route: { entityKey: 'routes', action: 'delete', fetchPath: null },
|
|
37
|
+
// ── Events ──
|
|
38
|
+
create_event: { entityKey: 'events', action: 'create', fetchPath: (id) => `/event/${id}` },
|
|
39
|
+
update_event: { entityKey: 'events', action: 'update', fetchPath: (id) => `/event/${id}` },
|
|
40
|
+
delete_event: { entityKey: 'events', action: 'delete', fetchPath: null },
|
|
41
|
+
update_events: { invalidateKeys: [['events']] },
|
|
42
|
+
// ── Teams ──
|
|
43
|
+
create_team: { entityKey: 'teams', action: 'create', fetchPath: null },
|
|
44
|
+
update_team: { entityKey: 'teams', action: 'update', fetchPath: null },
|
|
45
|
+
delete_team: { entityKey: 'teams', action: 'delete', fetchPath: null },
|
|
46
|
+
update_teams: { invalidateKeys: [['teams']] },
|
|
47
|
+
// ── Bills ──
|
|
48
|
+
create_bill: { entityKey: 'bills', action: 'create', fetchPath: (id) => `/bill/${id}` },
|
|
49
|
+
delete_bill: { entityKey: 'bills', action: 'delete', fetchPath: null },
|
|
50
|
+
update_bills: { invalidateKeys: [['bills']] },
|
|
51
|
+
// ── Sessions ──
|
|
52
|
+
create_session: { entityKey: 'sessions', action: 'create', fetchPath: null },
|
|
53
|
+
update_session: { entityKey: 'sessions', action: 'update', fetchPath: null },
|
|
54
|
+
// ── Catalog Items ──
|
|
55
|
+
create_catalog_item: { entityKey: 'catalog_items', action: 'create', fetchPath: (id) => `/catalog_item/${id}` },
|
|
56
|
+
update_catalog_item: { entityKey: 'catalog_items', action: 'update', fetchPath: (id) => `/catalog_item/${id}` },
|
|
57
|
+
delete_catalog_item: { entityKey: 'catalog_items', action: 'delete', fetchPath: null },
|
|
58
|
+
// ── Groups ──
|
|
59
|
+
create_group: { entityKey: 'groups', action: 'create', fetchPath: null },
|
|
60
|
+
update_group: { entityKey: 'groups', action: 'update', fetchPath: null },
|
|
61
|
+
delete_group: { entityKey: 'groups', action: 'delete', fetchPath: null },
|
|
62
|
+
// ── Labels ──
|
|
63
|
+
create_label: { entityKey: 'labels', action: 'create', fetchPath: null },
|
|
64
|
+
update_label: { entityKey: 'labels', action: 'update', fetchPath: null },
|
|
65
|
+
delete_label: { entityKey: 'labels', action: 'delete', fetchPath: null },
|
|
66
|
+
// ── Skills ──
|
|
67
|
+
create_skill: { entityKey: 'skills', action: 'create', fetchPath: null },
|
|
68
|
+
update_skill: { entityKey: 'skills', action: 'update', fetchPath: null },
|
|
69
|
+
delete_skill: { entityKey: 'skills', action: 'delete', fetchPath: null },
|
|
70
|
+
// ── Proposals ──
|
|
71
|
+
create_proposal: { entityKey: 'proposals', action: 'create', fetchPath: (id) => `/proposal/${id}` },
|
|
72
|
+
update_proposal: { entityKey: 'proposals', action: 'update', fetchPath: (id) => `/proposal/${id}` },
|
|
73
|
+
delete_proposal: { entityKey: 'proposals', action: 'delete', fetchPath: null },
|
|
74
|
+
// ── Service Months ──
|
|
75
|
+
create_month: { entityKey: 'months', action: 'create', fetchPath: null },
|
|
76
|
+
update_months: { invalidateKeys: [['months']] },
|
|
77
|
+
delete_months: { invalidateKeys: [['months']] },
|
|
78
|
+
// ── Tasks (nested under jobs — target the parent job) ──
|
|
79
|
+
create_task: { entityKey: 'jobs', action: 'update', fetchPath: (id) => `/job/${id}`, idField: 'job_id' },
|
|
80
|
+
update_task: { entityKey: 'jobs', action: 'update', fetchPath: (id) => `/job/${id}`, idField: 'job_id' },
|
|
81
|
+
delete_task: { entityKey: 'jobs', action: 'update', fetchPath: (id) => `/job/${id}`, idField: 'job_id' },
|
|
82
|
+
// ── Scheduling ──
|
|
83
|
+
schedule_complete: { invalidateKeys: [['routes'], ['jobs'], ['infinite', 'jobs']] },
|
|
84
|
+
// ── Location ──
|
|
85
|
+
location_update: { invalidateKeys: [['teams']] },
|
|
86
|
+
// ── Prepayments ──
|
|
87
|
+
update_prepayments: { invalidateKeys: [['prepayments']] },
|
|
88
|
+
// ── Chats ──
|
|
89
|
+
new_message: { invalidateKeys: [['chats'], ['messages']] },
|
|
83
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Returns true when `query.queryKey` belongs to the given entity,
|
|
93
|
+
* including the `['infinite', entityKey, ...]` variant used by jobs.
|
|
94
|
+
*/
|
|
95
|
+
function matchesEntityKey(query, entityKey) {
|
|
96
|
+
const k = query.queryKey;
|
|
97
|
+
return k[0] === entityKey || (k[0] === 'infinite' && k[1] === entityKey);
|
|
98
|
+
}
|
|
84
99
|
export class AvroQueryClient {
|
|
85
100
|
constructor(config) {
|
|
86
101
|
this._authState = AuthState.UNKNOWN;
|
|
@@ -157,6 +172,14 @@ export class AvroQueryClient {
|
|
|
157
172
|
* Called once from AvroQueryClientProvider when the tanstack
|
|
158
173
|
* QueryClient is available. Also handles auto-joining the
|
|
159
174
|
* company room on connect so consumer apps don't have to.
|
|
175
|
+
*
|
|
176
|
+
* Targeted events (single-entity CRUD) receive `{ id }` from the
|
|
177
|
+
* backend and surgically create / update / remove only the
|
|
178
|
+
* affected cache entries. Bulk events fall back to full
|
|
179
|
+
* query-key invalidation.
|
|
180
|
+
*
|
|
181
|
+
* Backward-compatible: if a targeted event arrives without an id
|
|
182
|
+
* (old backend), we fall back to full invalidation.
|
|
160
183
|
*/
|
|
161
184
|
setupSocketInvalidation(queryClient) {
|
|
162
185
|
// Prevent double-setup
|
|
@@ -164,15 +187,142 @@ export class AvroQueryClient {
|
|
|
164
187
|
return;
|
|
165
188
|
this._queryClient = queryClient;
|
|
166
189
|
const handlers = [];
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
const client = this; // stable reference for async closures
|
|
191
|
+
/** Full-invalidate every key that matches the entity (including 'infinite' prefix). */
|
|
192
|
+
const invalidateEntity = (entityKey) => {
|
|
193
|
+
queryClient.invalidateQueries({ predicate: (q) => matchesEntityKey(q, entityKey) });
|
|
194
|
+
};
|
|
195
|
+
for (const [event, config] of Object.entries(SOCKET_EVENT_CONFIG)) {
|
|
196
|
+
if (isBulkEvent(config)) {
|
|
197
|
+
// ── Bulk: invalidate all specified keys ──────────────
|
|
198
|
+
const handler = () => {
|
|
199
|
+
for (const key of config.invalidateKeys) {
|
|
200
|
+
queryClient.invalidateQueries({ queryKey: key });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
this.socket.on(event, handler);
|
|
204
|
+
handlers.push({ event, handler });
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// ── Targeted: surgical cache update ──────────────────
|
|
208
|
+
const { entityKey, action, fetchPath, idField, alsoInvalidate } = config;
|
|
209
|
+
const handler = async (data) => {
|
|
210
|
+
const id = data?.[idField ?? 'id'];
|
|
211
|
+
// No id → old backend or malformed payload → full invalidation
|
|
212
|
+
if (!id || typeof id !== 'string') {
|
|
213
|
+
invalidateEntity(entityKey);
|
|
214
|
+
alsoInvalidate?.forEach((k) => queryClient.invalidateQueries({ queryKey: k }));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const entityPredicate = (q) => matchesEntityKey(q, entityKey);
|
|
218
|
+
switch (action) {
|
|
219
|
+
// ─── CREATE ─────────────────────────────────
|
|
220
|
+
case 'create': {
|
|
221
|
+
if (fetchPath) {
|
|
222
|
+
try {
|
|
223
|
+
const item = await queryClient.fetchQuery({
|
|
224
|
+
queryKey: [entityKey, id],
|
|
225
|
+
queryFn: () => client.get({ path: fetchPath(id) }),
|
|
226
|
+
staleTime: 0,
|
|
227
|
+
});
|
|
228
|
+
queryClient.setQueriesData({ predicate: entityPredicate, type: 'active' }, (old) => {
|
|
229
|
+
if (!old)
|
|
230
|
+
return old;
|
|
231
|
+
if (old.pages && Array.isArray(old.pages)) {
|
|
232
|
+
// Skip if already present (avoids duplicates
|
|
233
|
+
// when the creating user also gets the event)
|
|
234
|
+
if (old.pages.some((p) => p.some((x) => x?.id === id)))
|
|
235
|
+
return old;
|
|
236
|
+
return {
|
|
237
|
+
...old,
|
|
238
|
+
pages: [
|
|
239
|
+
[item, ...(old.pages[0] || [])],
|
|
240
|
+
...old.pages.slice(1),
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (Array.isArray(old)) {
|
|
245
|
+
if (old.some((x) => x?.id === id))
|
|
246
|
+
return old;
|
|
247
|
+
return [...old, item];
|
|
248
|
+
}
|
|
249
|
+
return old;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
invalidateEntity(entityKey);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
// No individual GET endpoint → invalidate lists
|
|
258
|
+
invalidateEntity(entityKey);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
// ─── UPDATE ─────────────────────────────────
|
|
263
|
+
case 'update': {
|
|
264
|
+
if (fetchPath) {
|
|
265
|
+
try {
|
|
266
|
+
// Fetch fresh copy (also updates the [entity, id] cache)
|
|
267
|
+
const item = await queryClient.fetchQuery({
|
|
268
|
+
queryKey: [entityKey, id],
|
|
269
|
+
queryFn: () => client.get({ path: fetchPath(id) }),
|
|
270
|
+
staleTime: 0,
|
|
271
|
+
});
|
|
272
|
+
// Patch it into every active list / infinite-query cache
|
|
273
|
+
queryClient.setQueriesData({ predicate: entityPredicate, type: 'active' }, (old) => {
|
|
274
|
+
if (!old)
|
|
275
|
+
return old;
|
|
276
|
+
if (old.pages && Array.isArray(old.pages)) {
|
|
277
|
+
return {
|
|
278
|
+
...old,
|
|
279
|
+
pages: old.pages.map((page) => page.map((x) => (x?.id === id ? item : x))),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (Array.isArray(old)) {
|
|
283
|
+
return old.map((x) => (x?.id === id ? item : x));
|
|
284
|
+
}
|
|
285
|
+
return old;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
invalidateEntity(entityKey);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// No individual GET → invalidate everything for this entity
|
|
294
|
+
invalidateEntity(entityKey);
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
// ─── DELETE ─────────────────────────────────
|
|
299
|
+
case 'delete': {
|
|
300
|
+
// Remove the individual-item cache entry
|
|
301
|
+
queryClient.removeQueries({ queryKey: [entityKey, id], exact: true });
|
|
302
|
+
// Remove from every active list / infinite-query cache
|
|
303
|
+
queryClient.setQueriesData({ predicate: entityPredicate, type: 'active' }, (old) => {
|
|
304
|
+
if (!old)
|
|
305
|
+
return old;
|
|
306
|
+
if (old.pages && Array.isArray(old.pages)) {
|
|
307
|
+
return {
|
|
308
|
+
...old,
|
|
309
|
+
pages: old.pages.map((page) => page.filter((x) => x?.id !== id)),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (Array.isArray(old)) {
|
|
313
|
+
return old.filter((x) => x?.id !== id);
|
|
314
|
+
}
|
|
315
|
+
return old;
|
|
316
|
+
});
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Invalidate any additional keys (e.g. companies → /company/list)
|
|
321
|
+
alsoInvalidate?.forEach((k) => queryClient.invalidateQueries({ queryKey: k }));
|
|
322
|
+
};
|
|
323
|
+
this.socket.on(event, handler);
|
|
324
|
+
handlers.push({ event, handler });
|
|
325
|
+
}
|
|
176
326
|
}
|
|
177
327
|
// Auto join/leave company room
|
|
178
328
|
const joinCompanyRoom = () => {
|
package/dist/types/api/Task.js
CHANGED