@unisat/notification-service 1.1.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/LICENSE +21 -0
- package/README.md +99 -0
- package/lib/index.d.mts +27 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +148 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +121 -0
- package/lib/index.mjs.map +1 -0
- package/lib/types.d.mts +6 -0
- package/lib/types.d.ts +6 -0
- package/lib/types.js +19 -0
- package/lib/types.js.map +1 -0
- package/lib/types.mjs +1 -0
- package/lib/types.mjs.map +1 -0
- package/package.json +60 -0
- package/src/index.ts +2 -0
- package/src/notification-service.ts +151 -0
- package/src/types.ts +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 UniSat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @unisat/notification-service
|
|
2
|
+
|
|
3
|
+
Cross-platform notification management service for UniSat Wallet. Handles fetching, storing, reading, and deleting in-app notifications with local persistence and automatic cleanup.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @unisat/notification-service
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { NotificationService } from '@unisat/notification-service'
|
|
15
|
+
|
|
16
|
+
const notificationService = new NotificationService()
|
|
17
|
+
|
|
18
|
+
await notificationService.init({
|
|
19
|
+
storage, // ProxyStorageAdapter
|
|
20
|
+
api, // WalletApiService
|
|
21
|
+
logger, // optional Logger
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
### `init(config)`
|
|
28
|
+
|
|
29
|
+
Initializes the service and loads persisted notifications from storage.
|
|
30
|
+
|
|
31
|
+
| Field | Type | Required | Description |
|
|
32
|
+
| --------- | ---------------------- | -------- | ------------------------ |
|
|
33
|
+
| `storage` | `ProxyStorageAdapter` | Yes | Storage adapter |
|
|
34
|
+
| `api` | `WalletApiService` | Yes | API client |
|
|
35
|
+
| `logger` | `Logger` | No | Custom logger |
|
|
36
|
+
|
|
37
|
+
### `getNotifications()`
|
|
38
|
+
|
|
39
|
+
Fetches the latest notifications from the server, merges them with the local store, and returns a sorted list.
|
|
40
|
+
|
|
41
|
+
- Sorted by `priority` desc, then `publishTime` desc
|
|
42
|
+
- Caps the local store at **20 items** (lowest priority/oldest are dropped)
|
|
43
|
+
- Automatically purges read notifications older than **7 days**
|
|
44
|
+
- Falls back to the local store on network error
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const notifications = await notificationService.getNotifications()
|
|
48
|
+
// => StoredNotification[]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `markAsRead(id)`
|
|
52
|
+
|
|
53
|
+
Marks a notification as read on the server and records the read timestamp locally.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await notificationService.markAsRead('notification-id')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `deleteNotification(id)`
|
|
60
|
+
|
|
61
|
+
Deletes a notification locally. If it has not been read yet, it is first marked as read on the server.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
await notificationService.deleteNotification('notification-id')
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `getUnreadCount()`
|
|
68
|
+
|
|
69
|
+
Returns the number of unread notifications in the local store.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const count = notificationService.getUnreadCount()
|
|
73
|
+
// => number
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `resetAllData()`
|
|
77
|
+
|
|
78
|
+
Clears all notifications from both the local store and storage.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
notificationService.resetAllData()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Types
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
interface StoredNotification {
|
|
88
|
+
id: string
|
|
89
|
+
title: string
|
|
90
|
+
content: string
|
|
91
|
+
type: string
|
|
92
|
+
link?: string
|
|
93
|
+
priority: number
|
|
94
|
+
publishTime: number
|
|
95
|
+
readAt?: number // timestamp when marked as read; undefined means unread
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type NotificationStore = Record<string, StoredNotification>
|
|
99
|
+
```
|
package/lib/index.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WalletApiService } from '@unisat/wallet-api';
|
|
2
|
+
import { StoredNotification, Logger } from '@unisat/wallet-shared';
|
|
3
|
+
import { ProxyStorageAdapter } from '@unisat/wallet-storage';
|
|
4
|
+
export { NotificationStore } from './types.mjs';
|
|
5
|
+
|
|
6
|
+
interface NotificationServiceConfig {
|
|
7
|
+
storage?: ProxyStorageAdapter;
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
api?: WalletApiService;
|
|
10
|
+
}
|
|
11
|
+
declare class NotificationService {
|
|
12
|
+
private storage;
|
|
13
|
+
private logger;
|
|
14
|
+
private storageKey;
|
|
15
|
+
private store;
|
|
16
|
+
private api;
|
|
17
|
+
constructor();
|
|
18
|
+
init(config: NotificationServiceConfig): Promise<void>;
|
|
19
|
+
resetAllData: () => void;
|
|
20
|
+
getNotifications: () => Promise<StoredNotification[]>;
|
|
21
|
+
markAsRead: (id: string) => Promise<void>;
|
|
22
|
+
readAll: () => Promise<void>;
|
|
23
|
+
deleteNotification: (id: string) => Promise<void>;
|
|
24
|
+
getUnreadCount: () => number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { NotificationService };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WalletApiService } from '@unisat/wallet-api';
|
|
2
|
+
import { StoredNotification, Logger } from '@unisat/wallet-shared';
|
|
3
|
+
import { ProxyStorageAdapter } from '@unisat/wallet-storage';
|
|
4
|
+
export { NotificationStore } from './types.js';
|
|
5
|
+
|
|
6
|
+
interface NotificationServiceConfig {
|
|
7
|
+
storage?: ProxyStorageAdapter;
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
api?: WalletApiService;
|
|
10
|
+
}
|
|
11
|
+
declare class NotificationService {
|
|
12
|
+
private storage;
|
|
13
|
+
private logger;
|
|
14
|
+
private storageKey;
|
|
15
|
+
private store;
|
|
16
|
+
private api;
|
|
17
|
+
constructor();
|
|
18
|
+
init(config: NotificationServiceConfig): Promise<void>;
|
|
19
|
+
resetAllData: () => void;
|
|
20
|
+
getNotifications: () => Promise<StoredNotification[]>;
|
|
21
|
+
markAsRead: (id: string) => Promise<void>;
|
|
22
|
+
readAll: () => Promise<void>;
|
|
23
|
+
deleteNotification: (id: string) => Promise<void>;
|
|
24
|
+
getUnreadCount: () => number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { NotificationService };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
NotificationService: () => NotificationService
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(src_exports);
|
|
26
|
+
|
|
27
|
+
// src/notification-service.ts
|
|
28
|
+
var MAX_NOTIFICATIONS = 20;
|
|
29
|
+
var READ_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
30
|
+
var defaultLogger = {
|
|
31
|
+
debug: () => {
|
|
32
|
+
},
|
|
33
|
+
info: () => {
|
|
34
|
+
},
|
|
35
|
+
warn: () => {
|
|
36
|
+
},
|
|
37
|
+
error: () => {
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var NotificationService = class {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.storage = void 0;
|
|
43
|
+
this.logger = defaultLogger;
|
|
44
|
+
this.storageKey = "notifications";
|
|
45
|
+
this.store = {};
|
|
46
|
+
this.api = void 0;
|
|
47
|
+
this.resetAllData = () => {
|
|
48
|
+
this.storage.set(this.storageKey, {});
|
|
49
|
+
this.store = {};
|
|
50
|
+
};
|
|
51
|
+
// Fetch from server, merge into local store.
|
|
52
|
+
// Prune read+expired entries, then cap total at MAX_NOTIFICATIONS (keep highest priority / newest).
|
|
53
|
+
this.getNotifications = async () => {
|
|
54
|
+
try {
|
|
55
|
+
const res = await this.api.notification.getList();
|
|
56
|
+
for (const item of res.list) {
|
|
57
|
+
const existing = this.store[item.id];
|
|
58
|
+
this.store[item.id] = {
|
|
59
|
+
...item,
|
|
60
|
+
// preserve readAt if already marked locally
|
|
61
|
+
readAt: existing?.readAt
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
for (const id of Object.keys(this.store)) {
|
|
66
|
+
const entry = this.store[id];
|
|
67
|
+
if (entry.readAt !== void 0 && now - entry.readAt > READ_EXPIRY_MS) {
|
|
68
|
+
delete this.store[id];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const entries = Object.values(this.store);
|
|
72
|
+
if (entries.length > MAX_NOTIFICATIONS) {
|
|
73
|
+
entries.sort((a, b) => b.priority - a.priority || b.publishTime - a.publishTime);
|
|
74
|
+
const keep = entries.slice(0, MAX_NOTIFICATIONS);
|
|
75
|
+
const keepIds = new Set(keep.map((e) => e.id));
|
|
76
|
+
for (const id of Object.keys(this.store)) {
|
|
77
|
+
if (!keepIds.has(id)) {
|
|
78
|
+
delete this.store[id];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await this.storage.set(this.storageKey, this.store);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.logger.error("Failed to fetch notifications from server:", error);
|
|
85
|
+
}
|
|
86
|
+
return Object.values(this.store).sort(
|
|
87
|
+
(a, b) => b.priority - a.priority || b.publishTime - a.publishTime
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
this.markAsRead = async (id) => {
|
|
91
|
+
if (!this.store[id])
|
|
92
|
+
return;
|
|
93
|
+
await this.api.notification.read(id);
|
|
94
|
+
this.store[id] = { ...this.store[id], readAt: Date.now() };
|
|
95
|
+
await this.storage.set(this.storageKey, this.store);
|
|
96
|
+
};
|
|
97
|
+
this.readAll = async () => {
|
|
98
|
+
const unreadIds = Object.values(this.store).filter((n) => n.readAt === void 0).map((n) => n.id);
|
|
99
|
+
await this.api.notification.readAll(unreadIds);
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
for (const id of unreadIds) {
|
|
102
|
+
this.store[id] = { ...this.store[id], readAt: now };
|
|
103
|
+
}
|
|
104
|
+
await this.storage.set(this.storageKey, this.store);
|
|
105
|
+
};
|
|
106
|
+
this.deleteNotification = async (id) => {
|
|
107
|
+
if (!this.store[id])
|
|
108
|
+
return;
|
|
109
|
+
if (this.store[id].readAt === void 0) {
|
|
110
|
+
try {
|
|
111
|
+
await this.api.notification.read(id);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logger.warn("Failed to mark notification as read before deletion:", error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
delete this.store[id];
|
|
117
|
+
await this.storage.set(this.storageKey, this.store);
|
|
118
|
+
};
|
|
119
|
+
this.getUnreadCount = () => {
|
|
120
|
+
return Object.values(this.store).filter((n) => n.readAt === void 0).length;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async init(config) {
|
|
124
|
+
if (config.storage) {
|
|
125
|
+
this.storage = config.storage;
|
|
126
|
+
}
|
|
127
|
+
if (config.logger) {
|
|
128
|
+
this.logger = config.logger;
|
|
129
|
+
}
|
|
130
|
+
if (config.api) {
|
|
131
|
+
this.api = config.api;
|
|
132
|
+
}
|
|
133
|
+
this.logger.debug("Initializing notification service...");
|
|
134
|
+
try {
|
|
135
|
+
const storedData = await this.storage.get(this.storageKey);
|
|
136
|
+
this.store = storedData || {};
|
|
137
|
+
this.logger.debug("Notification service initialization completed");
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.logger.error("Notification service initialization failed:", error);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
145
|
+
0 && (module.exports = {
|
|
146
|
+
NotificationService
|
|
147
|
+
});
|
|
148
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/notification-service.ts"],"sourcesContent":["export { NotificationService } from './notification-service'\nexport type { NotificationStore } from './types'\n","import { WalletApiService } from '@unisat/wallet-api'\nimport { Logger, StoredNotification } from '@unisat/wallet-shared'\nimport { ProxyStorageAdapter } from '@unisat/wallet-storage'\nimport { NotificationStore } from './types'\n\nconst MAX_NOTIFICATIONS = 20\n// Read notifications are deleted after 7 days\nconst READ_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000\n\n// Default no-op logger\nconst defaultLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n\nexport interface NotificationServiceConfig {\n storage?: ProxyStorageAdapter\n logger?: Logger\n api?: WalletApiService\n}\n\nexport class NotificationService {\n private storage: ProxyStorageAdapter = undefined as any\n private logger: Logger = defaultLogger\n private storageKey: string = 'notifications'\n private store: NotificationStore = {}\n private api: WalletApiService = undefined as any\n\n constructor() {}\n\n async init(config: NotificationServiceConfig): Promise<void> {\n if (config.storage) {\n this.storage = config.storage\n }\n if (config.logger) {\n this.logger = config.logger\n }\n if (config.api) {\n this.api = config.api\n }\n\n this.logger.debug('Initializing notification service...')\n\n try {\n const storedData = await this.storage.get(this.storageKey)\n this.store = storedData || {}\n this.logger.debug('Notification service initialization completed')\n } catch (error) {\n this.logger.error('Notification service initialization failed:', error)\n throw error\n }\n }\n\n resetAllData = () => {\n this.storage.set(this.storageKey, {})\n this.store = {}\n }\n\n // Fetch from server, merge into local store.\n // Prune read+expired entries, then cap total at MAX_NOTIFICATIONS (keep highest priority / newest).\n getNotifications = async (): Promise<StoredNotification[]> => {\n try {\n const res = await this.api.notification.getList()\n\n // Merge server items into local store (server is source of truth for content)\n for (const item of res.list) {\n const existing = this.store[item.id]\n this.store[item.id] = {\n ...item,\n // preserve readAt if already marked locally\n readAt: existing?.readAt,\n }\n }\n\n // Purge read entries that have expired\n const now = Date.now()\n for (const id of Object.keys(this.store)) {\n const entry = this.store[id]\n if (entry.readAt !== undefined && now - entry.readAt > READ_EXPIRY_MS) {\n delete this.store[id]\n }\n }\n\n // Cap at MAX_NOTIFICATIONS: sort by priority desc, then publishTime desc, keep first N\n const entries = Object.values(this.store)\n if (entries.length > MAX_NOTIFICATIONS) {\n entries.sort((a, b) => b.priority - a.priority || b.publishTime - a.publishTime)\n const keep = entries.slice(0, MAX_NOTIFICATIONS)\n const keepIds = new Set(keep.map(e => e.id))\n for (const id of Object.keys(this.store)) {\n if (!keepIds.has(id)) {\n delete this.store[id]\n }\n }\n }\n\n await this.storage.set(this.storageKey, this.store)\n } catch (error) {\n this.logger.error('Failed to fetch notifications from server:', error)\n // Fall back to local store on network error\n }\n\n return Object.values(this.store).sort(\n (a, b) => b.priority - a.priority || b.publishTime - a.publishTime\n )\n }\n\n markAsRead = async (id: string): Promise<void> => {\n if (!this.store[id]) return\n\n await this.api.notification.read(id)\n\n this.store[id] = { ...this.store[id], readAt: Date.now() }\n await this.storage.set(this.storageKey, this.store)\n }\n\n readAll = async (): Promise<void> => {\n const unreadIds = Object.values(this.store)\n .filter(n => n.readAt === undefined)\n .map(n => n.id)\n\n await this.api.notification.readAll(unreadIds)\n const now = Date.now()\n for (const id of unreadIds) {\n this.store[id] = { ...this.store[id], readAt: now }\n }\n await this.storage.set(this.storageKey, this.store)\n }\n\n deleteNotification = async (id: string): Promise<void> => {\n if (!this.store[id]) return\n\n // Mark as read on server before deleting locally if not already read\n if (this.store[id].readAt === undefined) {\n try {\n await this.api.notification.read(id)\n } catch (error) {\n this.logger.warn('Failed to mark notification as read before deletion:', error)\n }\n }\n\n delete this.store[id]\n await this.storage.set(this.storageKey, this.store)\n }\n\n getUnreadCount = (): number => {\n return Object.values(this.store).filter(n => n.readAt === undefined).length\n }\n}\n"],"mappings":"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","names":[]}
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/notification-service.ts
|
|
2
|
+
var MAX_NOTIFICATIONS = 20;
|
|
3
|
+
var READ_EXPIRY_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4
|
+
var defaultLogger = {
|
|
5
|
+
debug: () => {
|
|
6
|
+
},
|
|
7
|
+
info: () => {
|
|
8
|
+
},
|
|
9
|
+
warn: () => {
|
|
10
|
+
},
|
|
11
|
+
error: () => {
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var NotificationService = class {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.storage = void 0;
|
|
17
|
+
this.logger = defaultLogger;
|
|
18
|
+
this.storageKey = "notifications";
|
|
19
|
+
this.store = {};
|
|
20
|
+
this.api = void 0;
|
|
21
|
+
this.resetAllData = () => {
|
|
22
|
+
this.storage.set(this.storageKey, {});
|
|
23
|
+
this.store = {};
|
|
24
|
+
};
|
|
25
|
+
// Fetch from server, merge into local store.
|
|
26
|
+
// Prune read+expired entries, then cap total at MAX_NOTIFICATIONS (keep highest priority / newest).
|
|
27
|
+
this.getNotifications = async () => {
|
|
28
|
+
try {
|
|
29
|
+
const res = await this.api.notification.getList();
|
|
30
|
+
for (const item of res.list) {
|
|
31
|
+
const existing = this.store[item.id];
|
|
32
|
+
this.store[item.id] = {
|
|
33
|
+
...item,
|
|
34
|
+
// preserve readAt if already marked locally
|
|
35
|
+
readAt: existing?.readAt
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
for (const id of Object.keys(this.store)) {
|
|
40
|
+
const entry = this.store[id];
|
|
41
|
+
if (entry.readAt !== void 0 && now - entry.readAt > READ_EXPIRY_MS) {
|
|
42
|
+
delete this.store[id];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const entries = Object.values(this.store);
|
|
46
|
+
if (entries.length > MAX_NOTIFICATIONS) {
|
|
47
|
+
entries.sort((a, b) => b.priority - a.priority || b.publishTime - a.publishTime);
|
|
48
|
+
const keep = entries.slice(0, MAX_NOTIFICATIONS);
|
|
49
|
+
const keepIds = new Set(keep.map((e) => e.id));
|
|
50
|
+
for (const id of Object.keys(this.store)) {
|
|
51
|
+
if (!keepIds.has(id)) {
|
|
52
|
+
delete this.store[id];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
await this.storage.set(this.storageKey, this.store);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.logger.error("Failed to fetch notifications from server:", error);
|
|
59
|
+
}
|
|
60
|
+
return Object.values(this.store).sort(
|
|
61
|
+
(a, b) => b.priority - a.priority || b.publishTime - a.publishTime
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
this.markAsRead = async (id) => {
|
|
65
|
+
if (!this.store[id])
|
|
66
|
+
return;
|
|
67
|
+
await this.api.notification.read(id);
|
|
68
|
+
this.store[id] = { ...this.store[id], readAt: Date.now() };
|
|
69
|
+
await this.storage.set(this.storageKey, this.store);
|
|
70
|
+
};
|
|
71
|
+
this.readAll = async () => {
|
|
72
|
+
const unreadIds = Object.values(this.store).filter((n) => n.readAt === void 0).map((n) => n.id);
|
|
73
|
+
await this.api.notification.readAll(unreadIds);
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (const id of unreadIds) {
|
|
76
|
+
this.store[id] = { ...this.store[id], readAt: now };
|
|
77
|
+
}
|
|
78
|
+
await this.storage.set(this.storageKey, this.store);
|
|
79
|
+
};
|
|
80
|
+
this.deleteNotification = async (id) => {
|
|
81
|
+
if (!this.store[id])
|
|
82
|
+
return;
|
|
83
|
+
if (this.store[id].readAt === void 0) {
|
|
84
|
+
try {
|
|
85
|
+
await this.api.notification.read(id);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.logger.warn("Failed to mark notification as read before deletion:", error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
delete this.store[id];
|
|
91
|
+
await this.storage.set(this.storageKey, this.store);
|
|
92
|
+
};
|
|
93
|
+
this.getUnreadCount = () => {
|
|
94
|
+
return Object.values(this.store).filter((n) => n.readAt === void 0).length;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async init(config) {
|
|
98
|
+
if (config.storage) {
|
|
99
|
+
this.storage = config.storage;
|
|
100
|
+
}
|
|
101
|
+
if (config.logger) {
|
|
102
|
+
this.logger = config.logger;
|
|
103
|
+
}
|
|
104
|
+
if (config.api) {
|
|
105
|
+
this.api = config.api;
|
|
106
|
+
}
|
|
107
|
+
this.logger.debug("Initializing notification service...");
|
|
108
|
+
try {
|
|
109
|
+
const storedData = await this.storage.get(this.storageKey);
|
|
110
|
+
this.store = storedData || {};
|
|
111
|
+
this.logger.debug("Notification service initialization completed");
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logger.error("Notification service initialization failed:", error);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
export {
|
|
119
|
+
NotificationService
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/notification-service.ts"],"sourcesContent":["import { WalletApiService } from '@unisat/wallet-api'\nimport { Logger, StoredNotification } from '@unisat/wallet-shared'\nimport { ProxyStorageAdapter } from '@unisat/wallet-storage'\nimport { NotificationStore } from './types'\n\nconst MAX_NOTIFICATIONS = 20\n// Read notifications are deleted after 7 days\nconst READ_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000\n\n// Default no-op logger\nconst defaultLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n\nexport interface NotificationServiceConfig {\n storage?: ProxyStorageAdapter\n logger?: Logger\n api?: WalletApiService\n}\n\nexport class NotificationService {\n private storage: ProxyStorageAdapter = undefined as any\n private logger: Logger = defaultLogger\n private storageKey: string = 'notifications'\n private store: NotificationStore = {}\n private api: WalletApiService = undefined as any\n\n constructor() {}\n\n async init(config: NotificationServiceConfig): Promise<void> {\n if (config.storage) {\n this.storage = config.storage\n }\n if (config.logger) {\n this.logger = config.logger\n }\n if (config.api) {\n this.api = config.api\n }\n\n this.logger.debug('Initializing notification service...')\n\n try {\n const storedData = await this.storage.get(this.storageKey)\n this.store = storedData || {}\n this.logger.debug('Notification service initialization completed')\n } catch (error) {\n this.logger.error('Notification service initialization failed:', error)\n throw error\n }\n }\n\n resetAllData = () => {\n this.storage.set(this.storageKey, {})\n this.store = {}\n }\n\n // Fetch from server, merge into local store.\n // Prune read+expired entries, then cap total at MAX_NOTIFICATIONS (keep highest priority / newest).\n getNotifications = async (): Promise<StoredNotification[]> => {\n try {\n const res = await this.api.notification.getList()\n\n // Merge server items into local store (server is source of truth for content)\n for (const item of res.list) {\n const existing = this.store[item.id]\n this.store[item.id] = {\n ...item,\n // preserve readAt if already marked locally\n readAt: existing?.readAt,\n }\n }\n\n // Purge read entries that have expired\n const now = Date.now()\n for (const id of Object.keys(this.store)) {\n const entry = this.store[id]\n if (entry.readAt !== undefined && now - entry.readAt > READ_EXPIRY_MS) {\n delete this.store[id]\n }\n }\n\n // Cap at MAX_NOTIFICATIONS: sort by priority desc, then publishTime desc, keep first N\n const entries = Object.values(this.store)\n if (entries.length > MAX_NOTIFICATIONS) {\n entries.sort((a, b) => b.priority - a.priority || b.publishTime - a.publishTime)\n const keep = entries.slice(0, MAX_NOTIFICATIONS)\n const keepIds = new Set(keep.map(e => e.id))\n for (const id of Object.keys(this.store)) {\n if (!keepIds.has(id)) {\n delete this.store[id]\n }\n }\n }\n\n await this.storage.set(this.storageKey, this.store)\n } catch (error) {\n this.logger.error('Failed to fetch notifications from server:', error)\n // Fall back to local store on network error\n }\n\n return Object.values(this.store).sort(\n (a, b) => b.priority - a.priority || b.publishTime - a.publishTime\n )\n }\n\n markAsRead = async (id: string): Promise<void> => {\n if (!this.store[id]) return\n\n await this.api.notification.read(id)\n\n this.store[id] = { ...this.store[id], readAt: Date.now() }\n await this.storage.set(this.storageKey, this.store)\n }\n\n readAll = async (): Promise<void> => {\n const unreadIds = Object.values(this.store)\n .filter(n => n.readAt === undefined)\n .map(n => n.id)\n\n await this.api.notification.readAll(unreadIds)\n const now = Date.now()\n for (const id of unreadIds) {\n this.store[id] = { ...this.store[id], readAt: now }\n }\n await this.storage.set(this.storageKey, this.store)\n }\n\n deleteNotification = async (id: string): Promise<void> => {\n if (!this.store[id]) return\n\n // Mark as read on server before deleting locally if not already read\n if (this.store[id].readAt === undefined) {\n try {\n await this.api.notification.read(id)\n } catch (error) {\n this.logger.warn('Failed to mark notification as read before deletion:', error)\n }\n }\n\n delete this.store[id]\n await this.storage.set(this.storageKey, this.store)\n }\n\n getUnreadCount = (): number => {\n return Object.values(this.store).filter(n => n.readAt === undefined).length\n }\n}\n"],"mappings":"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","names":[]}
|
package/lib/types.d.mts
ADDED
package/lib/types.d.ts
ADDED
package/lib/types.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
|
|
16
|
+
// src/types.ts
|
|
17
|
+
var types_exports = {};
|
|
18
|
+
module.exports = __toCommonJS(types_exports);
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import { StoredNotification } from '@unisat/wallet-shared'\n\nexport type { NotificationListItem } from '@unisat/wallet-shared'\n\nexport type NotificationStore = Record<string, StoredNotification>\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
package/lib/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unisat/notification-service",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Cross-platform permission management service for web3 applications",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"module": "lib/index.mjs",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib/**/*",
|
|
10
|
+
"src/**/*",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./lib/index.d.ts",
|
|
16
|
+
"require": "./lib/index.js",
|
|
17
|
+
"import": "./lib/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./types": {
|
|
20
|
+
"types": "./lib/types.d.ts",
|
|
21
|
+
"require": "./lib/types.js",
|
|
22
|
+
"import": "./lib/types.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"bitcoin",
|
|
27
|
+
"permission",
|
|
28
|
+
"web3",
|
|
29
|
+
"wallet",
|
|
30
|
+
"browser-extension",
|
|
31
|
+
"dapp",
|
|
32
|
+
"connected-sites"
|
|
33
|
+
],
|
|
34
|
+
"author": "UniSat Team",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"eventemitter3": "^5.0.1",
|
|
38
|
+
"@unisat/wallet-shared": "0.2.0",
|
|
39
|
+
"@unisat/wallet-storage": "0.2.1",
|
|
40
|
+
"@unisat/wallet-api": "1.1.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.0.0",
|
|
44
|
+
"tsup": "^7.0.0",
|
|
45
|
+
"typescript": "^5.0.0",
|
|
46
|
+
"vitest": "^0.34.0"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"dev": "tsup --watch",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"clean": "rimraf dist"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { WalletApiService } from '@unisat/wallet-api'
|
|
2
|
+
import { Logger, StoredNotification } from '@unisat/wallet-shared'
|
|
3
|
+
import { ProxyStorageAdapter } from '@unisat/wallet-storage'
|
|
4
|
+
import { NotificationStore } from './types'
|
|
5
|
+
|
|
6
|
+
const MAX_NOTIFICATIONS = 20
|
|
7
|
+
// Read notifications are deleted after 7 days
|
|
8
|
+
const READ_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000
|
|
9
|
+
|
|
10
|
+
// Default no-op logger
|
|
11
|
+
const defaultLogger: Logger = {
|
|
12
|
+
debug: () => {},
|
|
13
|
+
info: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
error: () => {},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NotificationServiceConfig {
|
|
19
|
+
storage?: ProxyStorageAdapter
|
|
20
|
+
logger?: Logger
|
|
21
|
+
api?: WalletApiService
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class NotificationService {
|
|
25
|
+
private storage: ProxyStorageAdapter = undefined as any
|
|
26
|
+
private logger: Logger = defaultLogger
|
|
27
|
+
private storageKey: string = 'notifications'
|
|
28
|
+
private store: NotificationStore = {}
|
|
29
|
+
private api: WalletApiService = undefined as any
|
|
30
|
+
|
|
31
|
+
constructor() {}
|
|
32
|
+
|
|
33
|
+
async init(config: NotificationServiceConfig): Promise<void> {
|
|
34
|
+
if (config.storage) {
|
|
35
|
+
this.storage = config.storage
|
|
36
|
+
}
|
|
37
|
+
if (config.logger) {
|
|
38
|
+
this.logger = config.logger
|
|
39
|
+
}
|
|
40
|
+
if (config.api) {
|
|
41
|
+
this.api = config.api
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.logger.debug('Initializing notification service...')
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const storedData = await this.storage.get(this.storageKey)
|
|
48
|
+
this.store = storedData || {}
|
|
49
|
+
this.logger.debug('Notification service initialization completed')
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.logger.error('Notification service initialization failed:', error)
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
resetAllData = () => {
|
|
57
|
+
this.storage.set(this.storageKey, {})
|
|
58
|
+
this.store = {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fetch from server, merge into local store.
|
|
62
|
+
// Prune read+expired entries, then cap total at MAX_NOTIFICATIONS (keep highest priority / newest).
|
|
63
|
+
getNotifications = async (): Promise<StoredNotification[]> => {
|
|
64
|
+
try {
|
|
65
|
+
const res = await this.api.notification.getList()
|
|
66
|
+
|
|
67
|
+
// Merge server items into local store (server is source of truth for content)
|
|
68
|
+
for (const item of res.list) {
|
|
69
|
+
const existing = this.store[item.id]
|
|
70
|
+
this.store[item.id] = {
|
|
71
|
+
...item,
|
|
72
|
+
// preserve readAt if already marked locally
|
|
73
|
+
readAt: existing?.readAt,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Purge read entries that have expired
|
|
78
|
+
const now = Date.now()
|
|
79
|
+
for (const id of Object.keys(this.store)) {
|
|
80
|
+
const entry = this.store[id]
|
|
81
|
+
if (entry.readAt !== undefined && now - entry.readAt > READ_EXPIRY_MS) {
|
|
82
|
+
delete this.store[id]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Cap at MAX_NOTIFICATIONS: sort by priority desc, then publishTime desc, keep first N
|
|
87
|
+
const entries = Object.values(this.store)
|
|
88
|
+
if (entries.length > MAX_NOTIFICATIONS) {
|
|
89
|
+
entries.sort((a, b) => b.priority - a.priority || b.publishTime - a.publishTime)
|
|
90
|
+
const keep = entries.slice(0, MAX_NOTIFICATIONS)
|
|
91
|
+
const keepIds = new Set(keep.map(e => e.id))
|
|
92
|
+
for (const id of Object.keys(this.store)) {
|
|
93
|
+
if (!keepIds.has(id)) {
|
|
94
|
+
delete this.store[id]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await this.storage.set(this.storageKey, this.store)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.logger.error('Failed to fetch notifications from server:', error)
|
|
102
|
+
// Fall back to local store on network error
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Object.values(this.store).sort(
|
|
106
|
+
(a, b) => b.priority - a.priority || b.publishTime - a.publishTime
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
markAsRead = async (id: string): Promise<void> => {
|
|
111
|
+
if (!this.store[id]) return
|
|
112
|
+
|
|
113
|
+
await this.api.notification.read(id)
|
|
114
|
+
|
|
115
|
+
this.store[id] = { ...this.store[id], readAt: Date.now() }
|
|
116
|
+
await this.storage.set(this.storageKey, this.store)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
readAll = async (): Promise<void> => {
|
|
120
|
+
const unreadIds = Object.values(this.store)
|
|
121
|
+
.filter(n => n.readAt === undefined)
|
|
122
|
+
.map(n => n.id)
|
|
123
|
+
|
|
124
|
+
await this.api.notification.readAll(unreadIds)
|
|
125
|
+
const now = Date.now()
|
|
126
|
+
for (const id of unreadIds) {
|
|
127
|
+
this.store[id] = { ...this.store[id], readAt: now }
|
|
128
|
+
}
|
|
129
|
+
await this.storage.set(this.storageKey, this.store)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
deleteNotification = async (id: string): Promise<void> => {
|
|
133
|
+
if (!this.store[id]) return
|
|
134
|
+
|
|
135
|
+
// Mark as read on server before deleting locally if not already read
|
|
136
|
+
if (this.store[id].readAt === undefined) {
|
|
137
|
+
try {
|
|
138
|
+
await this.api.notification.read(id)
|
|
139
|
+
} catch (error) {
|
|
140
|
+
this.logger.warn('Failed to mark notification as read before deletion:', error)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
delete this.store[id]
|
|
145
|
+
await this.storage.set(this.storageKey, this.store)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getUnreadCount = (): number => {
|
|
149
|
+
return Object.values(this.store).filter(n => n.readAt === undefined).length
|
|
150
|
+
}
|
|
151
|
+
}
|