@go-avro/avro-js 0.0.44 → 0.0.45

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.
@@ -517,6 +517,11 @@ declare module '../client/QueryClient' {
517
517
  }>>;
518
518
  }
519
519
  }
520
+ export type ListQueryMatch = 'match' | 'nomatch' | 'unknown';
521
+ export declare function matchesEventsListQuery(queryKey: readonly unknown[], item: any): ListQueryMatch;
522
+ export declare function matchesBillsListQuery(queryKey: readonly unknown[], item: any): ListQueryMatch;
523
+ export declare function matchesJobsListQuery(queryKey: readonly unknown[], item: any): ListQueryMatch;
524
+ export declare function matchesRoutesListQuery(queryKey: readonly unknown[], _item: any): ListQueryMatch;
520
525
  export declare class AvroQueryClient {
521
526
  protected config: Required<AvroQueryClientConfig>;
522
527
  readonly socket: Socket;
@@ -618,23 +623,13 @@ export declare class AvroQueryClient {
618
623
  getAuthState(): AuthState;
619
624
  getAuthStateAsync(): Promise<AuthState>;
620
625
  getQueryClient(): QueryClient;
621
- /**
622
- * Fetch an entity from the API, optionally construct it, and surgically
623
- * update all matching React-Query caches (individual + list + infinite).
624
- *
625
- * Shared by socket handlers and mutation `onSuccess` callbacks so the
626
- * sender gets an immediate cache sync and everyone else gets the socket
627
- * update — both use the identical code path.
628
- *
629
- * @returns The fetched (and optionally constructed) item, or `undefined`
630
- * for deletes / entities without a fetchPath.
631
- */
632
626
  _syncEntity(queryClient: QueryClient, params: {
633
627
  action: 'create' | 'update' | 'delete';
634
628
  entityKey: string;
635
629
  id: string;
636
630
  fetchPath?: string;
637
631
  construct?: (raw: any) => any;
632
+ matchesQuery?: (queryKey: readonly unknown[], item: any) => ListQueryMatch;
638
633
  }): Promise<any>;
639
634
  useLogout(): ReturnType<typeof useMutation<void, StandardError, CancelToken | undefined>>;
640
635
  fetchJobs(body?: {
@@ -7,6 +7,91 @@ import { StandardError } from '../types/error';
7
7
  function isBulkEvent(c) {
8
8
  return 'invalidateKeys' in c;
9
9
  }
10
+ export function matchesEventsListQuery(queryKey, item) {
11
+ if (queryKey.length < 11)
12
+ return 'match';
13
+ if (!item)
14
+ return 'match';
15
+ const knownIds = queryKey[3];
16
+ const unknownIds = queryKey[4];
17
+ const query = queryKey[5];
18
+ const includeUnbilled = queryKey[6];
19
+ const includeBilled = queryKey[7];
20
+ const includePaid = queryKey[8];
21
+ const filterJobId = queryKey[9];
22
+ if (knownIds && knownIds.length > 0 && !knownIds.includes(item.id))
23
+ return 'nomatch';
24
+ if (unknownIds && unknownIds.length > 0 && unknownIds.includes(item.id))
25
+ return 'nomatch';
26
+ if (filterJobId && item.job_id !== filterJobId)
27
+ return 'nomatch';
28
+ const status = item.status;
29
+ if (status !== undefined) {
30
+ const isPaid = status === 'PAID' || status === 'PREPAID' || status === 'EXTERNALLY_PAID';
31
+ const isBilled = status === 'BILLED' || status === 'EXTERNALLY_BILLED';
32
+ const isUnbilled = !isPaid && !isBilled;
33
+ if (includePaid === false && isPaid)
34
+ return 'nomatch';
35
+ if (includeBilled === false && isBilled)
36
+ return 'nomatch';
37
+ if (includeUnbilled === false && isUnbilled)
38
+ return 'nomatch';
39
+ }
40
+ if (query && query.length > 0)
41
+ return 'unknown';
42
+ return 'match';
43
+ }
44
+ export function matchesBillsListQuery(queryKey, item) {
45
+ if (queryKey.length < 6)
46
+ return 'match';
47
+ if (!item)
48
+ return 'match';
49
+ const query = queryKey[2];
50
+ const knownIds = queryKey[3];
51
+ const unknownIds = queryKey[4];
52
+ const paidFilter = queryKey[5];
53
+ if (knownIds && knownIds.length > 0 && !knownIds.includes(item.id))
54
+ return 'nomatch';
55
+ if (unknownIds && unknownIds.length > 0 && unknownIds.includes(item.id))
56
+ return 'nomatch';
57
+ const status = item.status;
58
+ if (paidFilter !== undefined && status !== undefined) {
59
+ const isPaid = status === 'PAID' || status === 'MANUALLY_PAID';
60
+ if (paidFilter === true && !isPaid)
61
+ return 'nomatch';
62
+ if (paidFilter === false && isPaid)
63
+ return 'nomatch';
64
+ }
65
+ if (query && query.length > 0)
66
+ return 'unknown';
67
+ return 'match';
68
+ }
69
+ export function matchesJobsListQuery(queryKey, item) {
70
+ if (queryKey[0] !== 'infinite')
71
+ return 'match';
72
+ if (queryKey.length < 6)
73
+ return 'match';
74
+ if (!item)
75
+ return 'match';
76
+ const query = queryKey[4];
77
+ const routeId = queryKey[5];
78
+ if (routeId && routeId.length > 0) {
79
+ const routes = item.routes ?? [];
80
+ if (!routes.some((r) => r?.route_id === routeId))
81
+ return 'nomatch';
82
+ }
83
+ if (query && query.length > 0)
84
+ return 'unknown';
85
+ return 'match';
86
+ }
87
+ export function matchesRoutesListQuery(queryKey, _item) {
88
+ if (queryKey.length < 5)
89
+ return 'match';
90
+ const query = queryKey[3];
91
+ if (query && query.length > 0)
92
+ return 'unknown';
93
+ return 'match';
94
+ }
10
95
  /**
11
96
  * Maps socket event names to cache-update strategies.
12
97
  *
@@ -38,12 +123,14 @@ const SOCKET_EVENT_CONFIG = {
38
123
  action: 'create',
39
124
  fetchPath: (id) => `/job/${id}`,
40
125
  construct: (d) => new Job(d),
126
+ matchesQuery: matchesJobsListQuery,
41
127
  },
42
128
  update_job: {
43
129
  entityKey: 'jobs',
44
130
  action: 'update',
45
131
  fetchPath: (id) => `/job/${id}`,
46
132
  construct: (d) => new Job(d),
133
+ matchesQuery: matchesJobsListQuery,
47
134
  },
48
135
  delete_job: { entityKey: 'jobs', action: 'delete', fetchPath: null },
49
136
  update_jobs: { invalidateKeys: [['jobs'], ['infinite', 'jobs']] },
@@ -54,12 +141,14 @@ const SOCKET_EVENT_CONFIG = {
54
141
  action: 'create',
55
142
  fetchPath: (id) => `/route/${id}`,
56
143
  construct: (d) => new Route(d),
144
+ matchesQuery: matchesRoutesListQuery,
57
145
  },
58
146
  update_route: {
59
147
  entityKey: 'routes',
60
148
  action: 'update',
61
149
  fetchPath: (id) => `/route/${id}`,
62
150
  construct: (d) => new Route(d),
151
+ matchesQuery: matchesRoutesListQuery,
63
152
  },
64
153
  delete_route: { entityKey: 'routes', action: 'delete', fetchPath: null },
65
154
  // ── Events (also refetch parent job — overdueness, last_event, etc.) ──
@@ -68,12 +157,14 @@ const SOCKET_EVENT_CONFIG = {
68
157
  action: 'create',
69
158
  fetchPath: (id) => `/event/${id}`,
70
159
  construct: (d) => new _Event(d),
160
+ matchesQuery: matchesEventsListQuery,
71
161
  relatedRefetch: [
72
162
  {
73
163
  entityKey: 'jobs',
74
164
  idField: 'job_id',
75
165
  fetchPath: (id) => `/job/${id}`,
76
166
  construct: (d) => new Job(d),
167
+ matchesQuery: matchesJobsListQuery,
77
168
  },
78
169
  ],
79
170
  },
@@ -82,12 +173,14 @@ const SOCKET_EVENT_CONFIG = {
82
173
  action: 'update',
83
174
  fetchPath: (id) => `/event/${id}`,
84
175
  construct: (d) => new _Event(d),
176
+ matchesQuery: matchesEventsListQuery,
85
177
  relatedRefetch: [
86
178
  {
87
179
  entityKey: 'jobs',
88
180
  idField: 'job_id',
89
181
  fetchPath: (id) => `/job/${id}`,
90
182
  construct: (d) => new Job(d),
183
+ matchesQuery: matchesJobsListQuery,
91
184
  },
92
185
  ],
93
186
  },
@@ -101,6 +194,7 @@ const SOCKET_EVENT_CONFIG = {
101
194
  idField: 'job_id',
102
195
  fetchPath: (id) => `/job/${id}`,
103
196
  construct: (d) => new Job(d),
197
+ matchesQuery: matchesJobsListQuery,
104
198
  },
105
199
  ],
106
200
  },
@@ -115,6 +209,7 @@ const SOCKET_EVENT_CONFIG = {
115
209
  entityKey: 'bills',
116
210
  action: 'create',
117
211
  fetchPath: (id) => `/bill/${id}`,
212
+ matchesQuery: matchesBillsListQuery,
118
213
  },
119
214
  delete_bill: { entityKey: 'bills', action: 'delete', fetchPath: null },
120
215
  update_bills: { invalidateKeys: [['bills']] },
@@ -348,10 +443,10 @@ export class AvroQueryClient {
348
443
  id,
349
444
  fetchPath: fetchPath ? fetchPath(id) : undefined,
350
445
  construct: config.construct,
446
+ matchesQuery: config.matchesQuery,
351
447
  });
352
448
  // Invalidate any additional keys (e.g. companies → /company/list)
353
449
  alsoInvalidate?.forEach((k) => queryClient.invalidateQueries({ queryKey: k }));
354
- // Refetch & patch related entities (e.g. event → parent job)
355
450
  if (relatedRefetch) {
356
451
  for (const related of relatedRefetch) {
357
452
  const relatedId = data?.[related.idField] ?? fetchedItem?.[related.idField];
@@ -363,6 +458,7 @@ export class AvroQueryClient {
363
458
  id: relatedId,
364
459
  fetchPath: related.fetchPath(relatedId),
365
460
  construct: related.construct,
461
+ matchesQuery: related.matchesQuery,
366
462
  });
367
463
  }
368
464
  }
@@ -615,22 +711,10 @@ export class AvroQueryClient {
615
711
  getQueryClient() {
616
712
  return useQueryClient();
617
713
  }
618
- /**
619
- * Fetch an entity from the API, optionally construct it, and surgically
620
- * update all matching React-Query caches (individual + list + infinite).
621
- *
622
- * Shared by socket handlers and mutation `onSuccess` callbacks so the
623
- * sender gets an immediate cache sync and everyone else gets the socket
624
- * update — both use the identical code path.
625
- *
626
- * @returns The fetched (and optionally constructed) item, or `undefined`
627
- * for deletes / entities without a fetchPath.
628
- */
629
714
  async _syncEntity(queryClient, params) {
630
- const { action, entityKey, id, fetchPath, construct } = params;
715
+ const { action, entityKey, id, fetchPath, construct, matchesQuery } = params;
631
716
  const predicate = (q) => matchesEntityKey(q, entityKey);
632
717
  const invalidate = () => queryClient.invalidateQueries({ predicate });
633
- // ─── DELETE ─────────────────────────────────────────
634
718
  if (action === 'delete') {
635
719
  queryClient.removeQueries({ queryKey: [entityKey, id], exact: true });
636
720
  queryClient.setQueriesData({ predicate }, (old) => {
@@ -649,7 +733,6 @@ export class AvroQueryClient {
649
733
  });
650
734
  return undefined;
651
735
  }
652
- // ─── CREATE / UPDATE ────────────────────────────────
653
736
  if (!fetchPath) {
654
737
  invalidate();
655
738
  return undefined;
@@ -662,42 +745,60 @@ export class AvroQueryClient {
662
745
  });
663
746
  const item = construct ? construct(raw) : raw;
664
747
  queryClient.setQueryData([entityKey, id], item);
665
- if (action === 'create') {
666
- queryClient.setQueriesData({ predicate }, (old) => {
667
- if (!old)
668
- return old;
669
- if (old.pages && Array.isArray(old.pages)) {
670
- if (old.pages.some((p) => p.some((x) => x?.id === id)))
748
+ const matchingQueries = queryClient.getQueryCache().findAll({ predicate });
749
+ for (const query of matchingQueries) {
750
+ const queryKey = query.queryKey;
751
+ if (queryKey.length === 2 && queryKey[0] === entityKey)
752
+ continue;
753
+ const verdict = matchesQuery
754
+ ? matchesQuery(queryKey, item)
755
+ : 'match';
756
+ if (verdict === 'unknown') {
757
+ queryClient.invalidateQueries({ queryKey, exact: true });
758
+ continue;
759
+ }
760
+ if (action === 'create') {
761
+ if (verdict === 'nomatch')
762
+ continue;
763
+ queryClient.setQueryData(queryKey, (old) => {
764
+ if (!old)
671
765
  return old;
672
- return {
673
- ...old,
674
- pages: [[item, ...(old.pages[0] || [])], ...old.pages.slice(1)],
675
- };
676
- }
677
- if (Array.isArray(old)) {
678
- if (old.some((x) => x?.id === id))
766
+ if (old.pages && Array.isArray(old.pages)) {
767
+ if (old.pages.some((p) => p.some((x) => x?.id === id)))
768
+ return old;
769
+ return {
770
+ ...old,
771
+ pages: [[item, ...(old.pages[0] || [])], ...old.pages.slice(1)],
772
+ };
773
+ }
774
+ if (Array.isArray(old)) {
775
+ if (old.some((x) => x?.id === id))
776
+ return old;
777
+ return [...old, item];
778
+ }
779
+ return old;
780
+ });
781
+ }
782
+ else {
783
+ queryClient.setQueryData(queryKey, (old) => {
784
+ if (!old)
679
785
  return old;
680
- return [...old, item];
681
- }
682
- return old;
683
- });
684
- }
685
- else {
686
- // UPDATE — replace in every active list / infinite-query cache
687
- queryClient.setQueriesData({ predicate }, (old) => {
688
- if (!old)
786
+ if (old.pages && Array.isArray(old.pages)) {
787
+ return {
788
+ ...old,
789
+ pages: old.pages.map((page) => verdict === 'match'
790
+ ? page.map((x) => (x?.id === id ? item : x))
791
+ : page.filter((x) => x?.id !== id)),
792
+ };
793
+ }
794
+ if (Array.isArray(old)) {
795
+ return verdict === 'match'
796
+ ? old.map((x) => (x?.id === id ? item : x))
797
+ : old.filter((x) => x?.id !== id);
798
+ }
689
799
  return old;
690
- if (old.pages && Array.isArray(old.pages)) {
691
- return {
692
- ...old,
693
- pages: old.pages.map((page) => page.map((x) => (x?.id === id ? item : x))),
694
- };
695
- }
696
- if (Array.isArray(old)) {
697
- return old.map((x) => (x?.id === id ? item : x));
698
- }
699
- return old;
700
- });
800
+ });
801
+ }
701
802
  }
702
803
  return item;
703
804
  }
@@ -1,10 +1,9 @@
1
1
  import { useInfiniteQuery, useQuery, useMutation } from '@tanstack/react-query';
2
- import { AvroQueryClient } from '../../client/QueryClient';
2
+ import { AvroQueryClient, matchesEventsListQuery } from '../../client/QueryClient';
3
3
  import { _Event, Job, LineItemStatus } from '../../types/api';
4
- /** Predicate that matches all 'jobs' queries (list and individual, but NOT 'infinite'). */
5
4
  const isJobsQuery = (q) => q.queryKey[0] === 'jobs';
6
- /** Predicate that matches all 'events' queries (list and individual). */
7
5
  const isEventsQuery = (q) => q.queryKey[0] === 'events';
6
+ const isEventsQueryFor = (item) => (q) => q.queryKey[0] === 'events' && matchesEventsListQuery(q.queryKey, item) === 'match';
8
7
  AvroQueryClient.prototype.useGetEvents = function (body) {
9
8
  const queryClient = this.getQueryClient();
10
9
  const result = useInfiniteQuery({
@@ -102,8 +101,7 @@ AvroQueryClient.prototype.useCreateEvent = function () {
102
101
  return old;
103
102
  });
104
103
  }
105
- // Add optimistic event to all active events queries
106
- queryClient.setQueriesData({ predicate: isEventsQuery, type: 'active' }, (oldData) => {
104
+ queryClient.setQueriesData({ predicate: isEventsQueryFor(optimisticEvent), type: 'active' }, (oldData) => {
107
105
  if (!oldData)
108
106
  return oldData;
109
107
  if (oldData.pages) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-avro/avro-js",
3
- "version": "0.0.44",
3
+ "version": "0.0.45",
4
4
  "description": "JS client for Avro backend integration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",