@go-avro/avro-js 0.0.24 → 0.0.26
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 +29 -0
- package/dist/client/QueryClient.js +55 -0
- package/dist/client/hooks/bills.js +2 -2
- package/dist/client/hooks/email.d.ts +47 -0
- package/dist/client/hooks/email.js +104 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types/api/Company.d.ts +1 -0
- package/dist/types/api/EmailNotification.d.ts +25 -0
- package/dist/types/api/EmailNotification.js +8 -0
- package/dist/types/api.d.ts +1 -0
- package/dist/types/api.js +1 -0
- package/package.json +4 -2
|
@@ -7,6 +7,16 @@ import { CancelToken, RetryStrategy } from '../types/client';
|
|
|
7
7
|
import { StandardError } from '../types/error';
|
|
8
8
|
import { CacheData } from '../types/cache';
|
|
9
9
|
import { Waiver } from '../types/api/Waiver';
|
|
10
|
+
import type { EmailSucceededPayload, EmailFailedPayload, EmailType } from "../types/api/EmailNotification";
|
|
11
|
+
/** Callbacks for a tracked email request. */
|
|
12
|
+
export interface TrackEmailOptions {
|
|
13
|
+
emailType?: EmailType;
|
|
14
|
+
/** How long to wait before firing onTimeout (ms). Default 30 000. */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
onSuccess?: (data: EmailSucceededPayload) => void;
|
|
17
|
+
onFailure?: (data: EmailFailedPayload) => void;
|
|
18
|
+
onTimeout?: (requestId: string) => void;
|
|
19
|
+
}
|
|
10
20
|
export interface AvroQueryClientConfig {
|
|
11
21
|
baseUrl: string;
|
|
12
22
|
authManager: AuthManager;
|
|
@@ -130,6 +140,7 @@ declare module '../client/QueryClient' {
|
|
|
130
140
|
billId: string;
|
|
131
141
|
subject?: string;
|
|
132
142
|
recipients?: string[][];
|
|
143
|
+
request_id?: string;
|
|
133
144
|
}): Promise<{
|
|
134
145
|
msg: string;
|
|
135
146
|
}>;
|
|
@@ -515,6 +526,8 @@ export declare class AvroQueryClient {
|
|
|
515
526
|
private authStateListeners;
|
|
516
527
|
private _queryClient;
|
|
517
528
|
private _socketInvalidationCleanup;
|
|
529
|
+
private _emailTracking;
|
|
530
|
+
private _emailListenersInit;
|
|
518
531
|
constructor(config: AvroQueryClientConfig);
|
|
519
532
|
emit(eventName: string, data: unknown): void;
|
|
520
533
|
on<T>(eventName: string, callback: (data: T) => void): void;
|
|
@@ -708,10 +721,26 @@ export declare class AvroQueryClient {
|
|
|
708
721
|
query?: string;
|
|
709
722
|
offset?: number;
|
|
710
723
|
}, cancelToken?: CancelToken, headers?: Record<string, string>): Promise<any>;
|
|
724
|
+
/**
|
|
725
|
+
* Lazily register socket listeners for email_succeeded / email_failed.
|
|
726
|
+
* Listeners live on the socket (not in a React effect) so they survive
|
|
727
|
+
* component unmounts.
|
|
728
|
+
*/
|
|
729
|
+
private _initEmailListeners;
|
|
730
|
+
/**
|
|
731
|
+
* Track an outbound email request.
|
|
732
|
+
*
|
|
733
|
+
* Generates a `request_id`, registers socket listeners (once), and returns
|
|
734
|
+
* the ID so callers can pass it in the HTTP body. The backend emits
|
|
735
|
+
* `email_succeeded` / `email_failed` to the user's GUID room; this method
|
|
736
|
+
* correlates the event by `request_id` and fires the appropriate callback.
|
|
737
|
+
*/
|
|
738
|
+
trackEmail(options?: TrackEmailOptions): string;
|
|
711
739
|
sendEmail(emailId: string, formData: FormData, progressUpdateCallback?: (loaded: number, total: number) => void): Promise<void>;
|
|
712
740
|
sendBillEmail(billId: string, body?: {
|
|
713
741
|
subject?: string;
|
|
714
742
|
recipients?: string[][];
|
|
715
743
|
base_url?: string;
|
|
744
|
+
request_id?: string;
|
|
716
745
|
}): Promise<void>;
|
|
717
746
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import io from 'socket.io-client';
|
|
2
2
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
4
|
import { LoginResponse } from '../types/api';
|
|
4
5
|
import { AuthState } from '../types/auth';
|
|
5
6
|
import { StandardError } from '../types/error';
|
|
@@ -88,6 +89,8 @@ export class AvroQueryClient {
|
|
|
88
89
|
this.authStateListeners = [];
|
|
89
90
|
this._queryClient = null;
|
|
90
91
|
this._socketInvalidationCleanup = null;
|
|
92
|
+
this._emailTracking = new Map();
|
|
93
|
+
this._emailListenersInit = false;
|
|
91
94
|
this.config = {
|
|
92
95
|
baseUrl: config.baseUrl,
|
|
93
96
|
authManager: config.authManager,
|
|
@@ -685,6 +688,58 @@ export class AvroQueryClient {
|
|
|
685
688
|
throw new StandardError(500, 'Failed to fetch sessions');
|
|
686
689
|
});
|
|
687
690
|
}
|
|
691
|
+
/* ── Email delivery tracking ──────────────────────────────────────── */
|
|
692
|
+
/**
|
|
693
|
+
* Lazily register socket listeners for email_succeeded / email_failed.
|
|
694
|
+
* Listeners live on the socket (not in a React effect) so they survive
|
|
695
|
+
* component unmounts.
|
|
696
|
+
*/
|
|
697
|
+
_initEmailListeners() {
|
|
698
|
+
if (this._emailListenersInit)
|
|
699
|
+
return;
|
|
700
|
+
this._emailListenersInit = true;
|
|
701
|
+
this.socket.on("email_succeeded", (data) => {
|
|
702
|
+
const entry = this._emailTracking.get(data.request_id);
|
|
703
|
+
if (!entry)
|
|
704
|
+
return;
|
|
705
|
+
clearTimeout(entry.timerId);
|
|
706
|
+
this._emailTracking.delete(data.request_id);
|
|
707
|
+
entry.onSuccess?.(data);
|
|
708
|
+
});
|
|
709
|
+
this.socket.on("email_failed", (data) => {
|
|
710
|
+
const entry = this._emailTracking.get(data.request_id);
|
|
711
|
+
if (!entry)
|
|
712
|
+
return;
|
|
713
|
+
clearTimeout(entry.timerId);
|
|
714
|
+
this._emailTracking.delete(data.request_id);
|
|
715
|
+
entry.onFailure?.(data);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Track an outbound email request.
|
|
720
|
+
*
|
|
721
|
+
* Generates a `request_id`, registers socket listeners (once), and returns
|
|
722
|
+
* the ID so callers can pass it in the HTTP body. The backend emits
|
|
723
|
+
* `email_succeeded` / `email_failed` to the user's GUID room; this method
|
|
724
|
+
* correlates the event by `request_id` and fires the appropriate callback.
|
|
725
|
+
*/
|
|
726
|
+
trackEmail(options = {}) {
|
|
727
|
+
this._initEmailListeners();
|
|
728
|
+
const requestId = uuidv4();
|
|
729
|
+
const { timeout = 30000 } = options;
|
|
730
|
+
const timerId = setTimeout(() => {
|
|
731
|
+
this._emailTracking.delete(requestId);
|
|
732
|
+
options.onTimeout?.(requestId);
|
|
733
|
+
}, timeout);
|
|
734
|
+
this._emailTracking.set(requestId, {
|
|
735
|
+
emailType: options.emailType,
|
|
736
|
+
timerId,
|
|
737
|
+
onSuccess: options.onSuccess,
|
|
738
|
+
onFailure: options.onFailure,
|
|
739
|
+
onTimeout: options.onTimeout,
|
|
740
|
+
});
|
|
741
|
+
return requestId;
|
|
742
|
+
}
|
|
688
743
|
sendEmail(emailId, formData, progressUpdateCallback) {
|
|
689
744
|
try {
|
|
690
745
|
return this.post({
|
|
@@ -193,10 +193,10 @@ AvroQueryClient.prototype.generatePDFFromBackend = function ({ billId }) {
|
|
|
193
193
|
return response;
|
|
194
194
|
});
|
|
195
195
|
};
|
|
196
|
-
AvroQueryClient.prototype.sendBillingEmail = async function ({ subject, billId, recipients, }) {
|
|
196
|
+
AvroQueryClient.prototype.sendBillingEmail = async function ({ subject, billId, recipients, request_id, }) {
|
|
197
197
|
return this.post({
|
|
198
198
|
path: `/bill/${billId}/email`,
|
|
199
|
-
data: JSON.stringify({ recipients, subject }),
|
|
199
|
+
data: JSON.stringify({ recipients, subject, request_id }),
|
|
200
200
|
headers: {
|
|
201
201
|
"Content-Type": "application/json"
|
|
202
202
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EmailFailedPayload, EmailType } from "../../types/api/EmailNotification";
|
|
2
|
+
export type EmailResultStatus = "succeeded" | "failed" | "timeout";
|
|
3
|
+
export interface EmailResult {
|
|
4
|
+
requestId: string;
|
|
5
|
+
status: EmailResultStatus;
|
|
6
|
+
emailType?: EmailType;
|
|
7
|
+
recipient?: string;
|
|
8
|
+
error?: EmailFailedPayload["error"];
|
|
9
|
+
}
|
|
10
|
+
export interface UseEmailStatusOptions {
|
|
11
|
+
/** How long to wait for a socket event before firing `onTimeout` (ms). Default 30 000. */
|
|
12
|
+
timeout?: number;
|
|
13
|
+
/** Called when the backend reports success. */
|
|
14
|
+
onSuccess?: (result: EmailResult) => void;
|
|
15
|
+
/** Called when the backend reports failure. */
|
|
16
|
+
onFailure?: (result: EmailResult) => void;
|
|
17
|
+
/** Called when neither success nor failure arrives within the timeout window. */
|
|
18
|
+
onTimeout?: (requestId: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Subscribe to backend email delivery notifications via Socket.IO.
|
|
22
|
+
*
|
|
23
|
+
* Delegates to `AvroQueryClient.trackEmail()` which registers socket
|
|
24
|
+
* listeners on the client itself (not in a React effect), so
|
|
25
|
+
* notifications survive component unmounts.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* ```ts
|
|
29
|
+
* const { trackEmail, pending, results } = useEmailStatus({
|
|
30
|
+
* onSuccess: (r) => toast.success(`Email sent to ${r.recipient}`),
|
|
31
|
+
* onFailure: (r) => toast.error(`Email failed: ${r.error?.message}`),
|
|
32
|
+
* onTimeout: (id) => toast.warn("Email status unknown"),
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* const requestId = trackEmail("bill");
|
|
36
|
+
* await avroQueryClient.sendBillingEmail({ billId, request_id: requestId });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function useEmailStatus(options?: UseEmailStatusOptions): {
|
|
40
|
+
readonly trackEmail: (emailType?: EmailType) => string;
|
|
41
|
+
readonly pending: Map<string, {
|
|
42
|
+
emailType?: EmailType;
|
|
43
|
+
trackedAt: number;
|
|
44
|
+
}>;
|
|
45
|
+
readonly results: Map<string, EmailResult>;
|
|
46
|
+
readonly clearResult: (requestId: string) => void;
|
|
47
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { useAvroQueryClient } from "../../client/AvroQueryClientProvider";
|
|
3
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
4
|
+
/* Hook */
|
|
5
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
6
|
+
/**
|
|
7
|
+
* Subscribe to backend email delivery notifications via Socket.IO.
|
|
8
|
+
*
|
|
9
|
+
* Delegates to `AvroQueryClient.trackEmail()` which registers socket
|
|
10
|
+
* listeners on the client itself (not in a React effect), so
|
|
11
|
+
* notifications survive component unmounts.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```ts
|
|
15
|
+
* const { trackEmail, pending, results } = useEmailStatus({
|
|
16
|
+
* onSuccess: (r) => toast.success(`Email sent to ${r.recipient}`),
|
|
17
|
+
* onFailure: (r) => toast.error(`Email failed: ${r.error?.message}`),
|
|
18
|
+
* onTimeout: (id) => toast.warn("Email status unknown"),
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* const requestId = trackEmail("bill");
|
|
22
|
+
* await avroQueryClient.sendBillingEmail({ billId, request_id: requestId });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function useEmailStatus(options = {}) {
|
|
26
|
+
const { timeout = 30000 } = options;
|
|
27
|
+
const client = useAvroQueryClient();
|
|
28
|
+
// Use refs for callbacks so the client-level handlers always see the latest
|
|
29
|
+
const onSuccessRef = useRef(options.onSuccess);
|
|
30
|
+
const onFailureRef = useRef(options.onFailure);
|
|
31
|
+
const onTimeoutRef = useRef(options.onTimeout);
|
|
32
|
+
onSuccessRef.current = options.onSuccess;
|
|
33
|
+
onFailureRef.current = options.onFailure;
|
|
34
|
+
onTimeoutRef.current = options.onTimeout;
|
|
35
|
+
// Expose reactive state so the UI can render pending / completed
|
|
36
|
+
const [pending, setPending] = useState(new Map());
|
|
37
|
+
const [results, setResults] = useState(new Map());
|
|
38
|
+
/* ── trackEmail — delegates to client.trackEmail() ──────────────── */
|
|
39
|
+
const trackEmail = useCallback((emailType) => {
|
|
40
|
+
const requestId = client.trackEmail({
|
|
41
|
+
emailType,
|
|
42
|
+
timeout,
|
|
43
|
+
onSuccess: (data) => {
|
|
44
|
+
const result = {
|
|
45
|
+
requestId: data.request_id,
|
|
46
|
+
status: "succeeded",
|
|
47
|
+
emailType: data.email_type,
|
|
48
|
+
recipient: data.recipient,
|
|
49
|
+
};
|
|
50
|
+
setPending((prev) => {
|
|
51
|
+
const next = new Map(prev);
|
|
52
|
+
next.delete(data.request_id);
|
|
53
|
+
return next;
|
|
54
|
+
});
|
|
55
|
+
setResults((prev) => new Map(prev).set(data.request_id, result));
|
|
56
|
+
onSuccessRef.current?.(result);
|
|
57
|
+
},
|
|
58
|
+
onFailure: (data) => {
|
|
59
|
+
const result = {
|
|
60
|
+
requestId: data.request_id,
|
|
61
|
+
status: "failed",
|
|
62
|
+
emailType: data.email_type,
|
|
63
|
+
recipient: data.recipient,
|
|
64
|
+
error: data.error,
|
|
65
|
+
};
|
|
66
|
+
setPending((prev) => {
|
|
67
|
+
const next = new Map(prev);
|
|
68
|
+
next.delete(data.request_id);
|
|
69
|
+
return next;
|
|
70
|
+
});
|
|
71
|
+
setResults((prev) => new Map(prev).set(data.request_id, result));
|
|
72
|
+
onFailureRef.current?.(result);
|
|
73
|
+
},
|
|
74
|
+
onTimeout: (rid) => {
|
|
75
|
+
const result = {
|
|
76
|
+
requestId: rid,
|
|
77
|
+
status: "timeout",
|
|
78
|
+
emailType,
|
|
79
|
+
};
|
|
80
|
+
setPending((prev) => {
|
|
81
|
+
const next = new Map(prev);
|
|
82
|
+
next.delete(rid);
|
|
83
|
+
return next;
|
|
84
|
+
});
|
|
85
|
+
setResults((prev) => new Map(prev).set(rid, result));
|
|
86
|
+
onTimeoutRef.current?.(rid);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
setPending((prev) => new Map(prev).set(requestId, {
|
|
90
|
+
emailType,
|
|
91
|
+
trackedAt: Date.now(),
|
|
92
|
+
}));
|
|
93
|
+
return requestId;
|
|
94
|
+
}, [client, timeout]);
|
|
95
|
+
/* ── clearResult (optional cleanup) ─────────────────────────────── */
|
|
96
|
+
const clearResult = useCallback((requestId) => {
|
|
97
|
+
setResults((prev) => {
|
|
98
|
+
const next = new Map(prev);
|
|
99
|
+
next.delete(requestId);
|
|
100
|
+
return next;
|
|
101
|
+
});
|
|
102
|
+
}, []);
|
|
103
|
+
return { trackEmail, pending, results, clearResult };
|
|
104
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { AvroQueryClientConfig, AvroQueryClient } from './client/QueryClient';
|
|
2
2
|
export { AvroQueryClientProvider, useAvroQueryClient, useSocketEvent } from './client/AvroQueryClientProvider';
|
|
3
|
+
export { useEmailStatus } from './client/hooks/email';
|
|
4
|
+
export type { EmailResult, EmailResultStatus, UseEmailStatusOptions } from './client/hooks/email';
|
|
3
5
|
export { AuthManager } from './auth/AuthManager';
|
|
4
6
|
export { MemoryStorage, LocalStorage } from './auth/storage';
|
|
5
7
|
import './client/core/xhr';
|
|
@@ -28,6 +30,7 @@ import './client/hooks/skills';
|
|
|
28
30
|
import './client/hooks/proposal';
|
|
29
31
|
import './client/hooks/timecards';
|
|
30
32
|
import './client/hooks/waivers';
|
|
33
|
+
import './client/hooks/email';
|
|
31
34
|
export * from './types/api';
|
|
32
35
|
export * from './types/auth';
|
|
33
36
|
export * from './types/cache';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AvroQueryClient } from './client/QueryClient';
|
|
2
2
|
export { AvroQueryClientProvider, useAvroQueryClient, useSocketEvent } from './client/AvroQueryClientProvider';
|
|
3
|
+
export { useEmailStatus } from './client/hooks/email';
|
|
3
4
|
export { AuthManager } from './auth/AuthManager';
|
|
4
5
|
export { MemoryStorage, LocalStorage } from './auth/storage';
|
|
5
6
|
import './client/core/xhr';
|
|
@@ -28,6 +29,7 @@ import './client/hooks/skills';
|
|
|
28
29
|
import './client/hooks/proposal';
|
|
29
30
|
import './client/hooks/timecards';
|
|
30
31
|
import './client/hooks/waivers';
|
|
32
|
+
import './client/hooks/email';
|
|
31
33
|
export * from './types/api';
|
|
32
34
|
export * from './types/auth';
|
|
33
35
|
export * from './types/cache';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket event payloads for email delivery notifications.
|
|
3
|
+
*
|
|
4
|
+
* The backend emits `email_succeeded` or `email_failed` after an email
|
|
5
|
+
* thread finishes. Both events include a `request_id` that the frontend
|
|
6
|
+
* uses to correlate the notification with the original HTTP request.
|
|
7
|
+
*/
|
|
8
|
+
/** Provider-agnostic error shape emitted by the backend. */
|
|
9
|
+
export interface EmailError {
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
provider: "outlook" | "gmail" | "smtp";
|
|
13
|
+
}
|
|
14
|
+
export type EmailType = "bill" | "proposal" | "job" | "generic";
|
|
15
|
+
export interface EmailSucceededPayload {
|
|
16
|
+
request_id: string;
|
|
17
|
+
email_type: EmailType;
|
|
18
|
+
recipient?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface EmailFailedPayload {
|
|
21
|
+
request_id: string;
|
|
22
|
+
email_type: EmailType;
|
|
23
|
+
error: EmailError;
|
|
24
|
+
recipient?: string;
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket event payloads for email delivery notifications.
|
|
3
|
+
*
|
|
4
|
+
* The backend emits `email_succeeded` or `email_failed` after an email
|
|
5
|
+
* thread finishes. Both events include a `request_id` that the frontend
|
|
6
|
+
* uses to correlate the notification with the original HTTP request.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/types/api.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "../types/api/Chat";
|
|
|
11
11
|
export * from "../types/api/Company";
|
|
12
12
|
export * from "../types/api/CustomLineItem";
|
|
13
13
|
export * from "../types/api/Email";
|
|
14
|
+
export * from "../types/api/EmailNotification";
|
|
14
15
|
export * from "../types/api/Friendship";
|
|
15
16
|
export * from "../types/api/Group";
|
|
16
17
|
export * from "../types/api/Job";
|
package/dist/types/api.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from "../types/api/Chat";
|
|
|
10
10
|
export * from "../types/api/Company";
|
|
11
11
|
export * from "../types/api/CustomLineItem";
|
|
12
12
|
export * from "../types/api/Email";
|
|
13
|
+
export * from "../types/api/EmailNotification";
|
|
13
14
|
export * from "../types/api/Friendship";
|
|
14
15
|
export * from "../types/api/Group";
|
|
15
16
|
export * from "../types/api/Job";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-avro/avro-js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"description": "JS client for Avro backend integration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"license": "CC-BY-SA-4.0",
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/react": "^19.2.2",
|
|
36
|
+
"@types/uuid": "^10.0.0",
|
|
36
37
|
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
|
37
38
|
"@typescript-eslint/parser": "^8.38.0",
|
|
38
39
|
"eslint": "^8.57.1",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"@tanstack/react-query": "^5.90.2",
|
|
54
|
-
"socket.io-client": "^4.8.1"
|
|
55
|
+
"socket.io-client": "^4.8.1",
|
|
56
|
+
"uuid": "^14.0.0"
|
|
55
57
|
}
|
|
56
58
|
}
|