-
Notifications
You must be signed in to change notification settings - Fork 86
feat: Added push notifications support #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
swapydapy
merged 23 commits into
a2aproject:main
from
botraunak:feat/push_notifications
Aug 31, 2025
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
37b6cc8
feat: push notifications support added
botraunak 329a8d0
feat: save push notification config is available in message config
botraunak c0c4f96
fix: added abort controller for push notification config
botraunak b3e4d70
fix: Requested changes
botraunak 76f3e66
fix: Test changes
botraunak 58df8a4
Merge branch 'main' into feat/push_notifications
botraunak fed8024
fix: use deepequal to check task objects
botraunak febbb55
refactor: create new mocks folder for resuable server mocks
botraunak fbbf4d3
fix: remove slow push testcase and add error case and deep check body
botraunak 50edcaa
fix: added options for push notification sender and resolve
botraunak 88a18f0
fix: afterEach hook error unit test, close testServer properly
botraunak 1f1ece8
Merge branch 'main' into feat/push_notifications
botraunak 016bc67
fix: remove promise.resolve unwanted
botraunak eee1d24
fix: remove unauthorized scenario for push testing
botraunak 2995a1e
fix: use refactored fake task exection
botraunak ee6831b
fix: remove promise.resolves
botraunak ca33ada
fix: remove dependency on taskStore load
botraunak 4795f85
feat: added readme section
botraunak 11d01c3
Merge branch 'main' into feat/push_notifications
botraunak 2c93805
fix: merge
botraunak 3f00e8d
fix: reset readme to main
botraunak 04e854e
feat: added readme section
botraunak 0d3dd61
fix: update heading styles
botraunak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
src/server/push_notification/default_push_notification_sender.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { Task, PushNotificationConfig } from "../../types.js"; | ||
| import { PushNotificationSender } from "./push_notification_sender.js"; | ||
| import { PushNotificationStore } from "./push_notification_store.js"; | ||
|
|
||
| export interface DefaultPushNotificationSenderOptions { | ||
| /** | ||
| * Timeout in milliseconds for the abort controller. Defaults to 5000ms. | ||
| */ | ||
| timeout?: number; | ||
| /** | ||
| * Custom header name for the token. Defaults to 'X-A2A-Notification-Token'. | ||
| */ | ||
| tokenHeaderName?: string; | ||
| } | ||
|
|
||
| export class DefaultPushNotificationSender implements PushNotificationSender { | ||
|
|
||
| private readonly pushNotificationStore: PushNotificationStore; | ||
| private readonly options: Required<DefaultPushNotificationSenderOptions>; | ||
|
|
||
| constructor(pushNotificationStore: PushNotificationStore, options: DefaultPushNotificationSenderOptions = {}) { | ||
| this.pushNotificationStore = pushNotificationStore; | ||
| this.options = { | ||
| timeout: 5000, | ||
| tokenHeaderName: 'X-A2A-Notification-Token', | ||
| ...options | ||
| }; | ||
| } | ||
|
|
||
| async send(task: Task): Promise<void> { | ||
| const pushConfigs = await this.pushNotificationStore.load(task.id); | ||
| if (!pushConfigs || pushConfigs.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| pushConfigs.forEach(pushConfig => { | ||
| this._dispatchNotification(task, pushConfig) | ||
| .catch(error => { | ||
| console.error(`Error sending push notification for task_id=${task.id} to URL: ${pushConfig.url}. Error:`, error); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| private async _dispatchNotification( | ||
| task: Task, | ||
| pushConfig: PushNotificationConfig | ||
| ): Promise<void> { | ||
| const url = pushConfig.url; | ||
| const controller = new AbortController(); | ||
| // Abort the request if it takes longer than the configured timeout. | ||
| const timeoutId = setTimeout(() => controller.abort(), this.options.timeout); | ||
|
|
||
| try { | ||
| const headers: Record<string, string> = { | ||
| 'Content-Type': 'application/json' | ||
| }; | ||
|
|
||
| if (pushConfig.token) { | ||
| headers[this.options.tokenHeaderName] = pushConfig.token; | ||
| } | ||
|
|
||
| const response = await fetch(url, { | ||
| method: 'POST', | ||
| headers, | ||
| body: JSON.stringify(task), | ||
| signal: controller.signal | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); | ||
| } | ||
|
|
||
| console.info(`Push notification sent for task_id=${task.id} to URL: ${url}`); | ||
| } catch (error) { | ||
| // Ignore errors | ||
| console.error(`Error sending push notification for task_id=${task.id} to URL: ${url}. Error:`, error); | ||
| } finally { | ||
| clearTimeout(timeoutId); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { Task } from "../../types.js"; | ||
|
|
||
| export interface PushNotificationSender { | ||
| send(task: Task): Promise<void>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { PushNotificationConfig } from "../../types.js"; | ||
|
|
||
| export interface PushNotificationStore { | ||
| save(taskId: string, pushNotificationConfig: PushNotificationConfig): Promise<void>; | ||
| load(taskId: string): Promise<PushNotificationConfig[]>; | ||
| delete(taskId: string, configId?: string): Promise<void>; | ||
| } | ||
|
|
||
| export class InMemoryPushNotificationStore implements PushNotificationStore { | ||
| private store: Map<string, PushNotificationConfig[]> = new Map(); | ||
|
|
||
| async save(taskId: string, pushNotificationConfig: PushNotificationConfig): Promise<void> { | ||
| const configs = this.store.get(taskId) || []; | ||
|
|
||
| // Set ID if it's not already set | ||
| if (!pushNotificationConfig.id) { | ||
| pushNotificationConfig.id = taskId; | ||
| } | ||
|
|
||
| // Remove existing config with the same ID if it exists | ||
| const existingIndex = configs.findIndex(config => config.id === pushNotificationConfig.id); | ||
| if (existingIndex !== -1) { | ||
| configs.splice(existingIndex, 1); | ||
| } | ||
|
|
||
| // Add the new/updated config | ||
| configs.push(pushNotificationConfig); | ||
| this.store.set(taskId, configs); | ||
| } | ||
swapydapy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| async load(taskId: string): Promise<PushNotificationConfig[]> { | ||
| const configs = this.store.get(taskId); | ||
| return configs || []; | ||
| } | ||
|
|
||
| async delete(taskId: string, configId?: string): Promise<void> { | ||
| // If no configId is provided, use taskId as the configId (backward compatibility) | ||
| if (configId === undefined) { | ||
| configId = taskId; | ||
| } | ||
|
|
||
| const configs = this.store.get(taskId); | ||
| if (!configs) { | ||
| return; | ||
| } | ||
|
|
||
| const configIndex = configs.findIndex(config => config.id === configId); | ||
| if (configIndex !== -1) { | ||
| configs.splice(configIndex, 1); | ||
| } | ||
|
|
||
| if (configs.length === 0) { | ||
| this.store.delete(taskId); | ||
| } else { | ||
| this.store.set(taskId, configs); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.