@luckydye/calendar 1.1.0 → 1.1.2

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/src/lib.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./CalendarView.js";
2
+ export * from "./InMemorySource.js";
3
+ export * from "./CalendarInternal.js";
4
+ export * from "./CalendarIntegration.js";
@@ -0,0 +1,177 @@
1
+ const SW_VERSION = "1.0.0";
2
+ const DB_NAME = "calendar-notifications";
3
+ const STORE_NAME = "notification-schedule";
4
+ const DB_VERSION = 1;
5
+
6
+ console.log(`Service Worker ${SW_VERSION} installing`);
7
+
8
+ self.addEventListener("install", () => {
9
+ console.log(`Service Worker ${SW_VERSION} installed`);
10
+ self.skipWaiting();
11
+ });
12
+ self.addEventListener("activate", () => {
13
+ console.log(`Service Worker ${SW_VERSION} activated`);
14
+ self.clients.claim();
15
+ });
16
+
17
+ // Message handling from app
18
+ self.addEventListener("message", async (event) => {
19
+ const { type, ...data } = event.data;
20
+
21
+ try {
22
+ if (type === "SCHEDULE_NOTIFICATIONS") {
23
+ await scheduleNotifications(data.notifications);
24
+ event.ports[0].postMessage({
25
+ type: "SUCCESS",
26
+ count: data.notifications.length,
27
+ });
28
+ } else if (type === "CANCEL_EVENT_NOTIFICATIONS") {
29
+ await cancelEventNotifications(data.eventId);
30
+ event.ports[0].postMessage({ type: "SUCCESS" });
31
+ } else if (type === "GET_SCHEDULED_NOTIFICATIONS") {
32
+ const notifications = await getScheduledNotifications();
33
+ event.ports[0].postMessage({ type: "SUCCESS", notifications });
34
+ }
35
+ } catch (error) {
36
+ event.ports[0].postMessage({ type: "ERROR", message: error.message });
37
+ }
38
+ });
39
+
40
+ // Periodic check every minute
41
+ let checkInterval;
42
+ self.addEventListener("activate", () => {
43
+ checkInterval = setInterval(checkAndFireNotifications, 60000);
44
+ checkAndFireNotifications(); // Initial check
45
+ });
46
+
47
+ // IndexedDB operations
48
+ async function openDB() {
49
+ return new Promise((resolve, reject) => {
50
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
51
+ request.onsuccess = () => resolve(request.result);
52
+ request.onerror = () => reject(request.error);
53
+ request.onupgradeneeded = (e) => {
54
+ const db = e.target.result;
55
+ const store = db.createObjectStore(STORE_NAME, {
56
+ keyPath: "notificationId",
57
+ });
58
+ store.createIndex("triggerTime", "triggerTime");
59
+ store.createIndex("eventId", "eventId");
60
+ };
61
+ });
62
+ }
63
+
64
+ async function scheduleNotifications(notifications) {
65
+ const db = await openDB();
66
+ const tx = db.transaction(STORE_NAME, "readwrite");
67
+ const store = tx.objectStore(STORE_NAME);
68
+ const now = new Date();
69
+
70
+ for (const notif of notifications) {
71
+ if (new Date(notif.triggerTime) > now) {
72
+ await store.put(notif);
73
+ }
74
+ }
75
+
76
+ return new Promise((resolve, reject) => {
77
+ tx.oncomplete = () => resolve();
78
+ tx.onerror = () => reject(tx.error);
79
+ });
80
+ }
81
+
82
+ async function cancelEventNotifications(eventId) {
83
+ const db = await openDB();
84
+ const tx = db.transaction(STORE_NAME, "readwrite");
85
+ const index = tx.objectStore(STORE_NAME).index("eventId");
86
+
87
+ return new Promise((resolve, reject) => {
88
+ const request = index.openCursor(IDBKeyRange.only(eventId));
89
+ request.onsuccess = (e) => {
90
+ const cursor = e.target.result;
91
+ if (cursor) {
92
+ cursor.delete();
93
+ cursor.continue();
94
+ }
95
+ };
96
+ tx.oncomplete = () => resolve();
97
+ tx.onerror = () => reject(tx.error);
98
+ });
99
+ }
100
+
101
+ async function getScheduledNotifications() {
102
+ const db = await openDB();
103
+ const tx = db.transaction(STORE_NAME, "readonly");
104
+ const store = tx.objectStore(STORE_NAME);
105
+ const index = store.index("triggerTime");
106
+
107
+ return new Promise((resolve, reject) => {
108
+ const request = index.getAll();
109
+ request.onsuccess = () => {
110
+ const notifications = request.result || [];
111
+ // Sort by trigger time
112
+ notifications.sort((a, b) =>
113
+ new Date(a.triggerTime).getTime() - new Date(b.triggerTime).getTime()
114
+ );
115
+ resolve(notifications);
116
+ };
117
+ request.onerror = () => reject(request.error);
118
+ });
119
+ }
120
+
121
+ async function checkAndFireNotifications() {
122
+ const db = await openDB();
123
+ const tx = db.transaction(STORE_NAME, "readwrite");
124
+ const store = tx.objectStore(STORE_NAME);
125
+ const index = store.index("triggerTime");
126
+
127
+ const now = new Date().toISOString();
128
+ const range = IDBKeyRange.upperBound(now);
129
+
130
+ return new Promise((resolve, reject) => {
131
+ const request = index.openCursor(range);
132
+ const toFire = [];
133
+
134
+ request.onsuccess = (e) => {
135
+ const cursor = e.target.result;
136
+ if (cursor) {
137
+ toFire.push(cursor.value);
138
+ cursor.delete();
139
+ cursor.continue();
140
+ }
141
+ };
142
+
143
+ tx.oncomplete = () => {
144
+ toFire.forEach(fireNotification);
145
+ resolve();
146
+ };
147
+ tx.onerror = () => reject(tx.error);
148
+ });
149
+ }
150
+
151
+ function fireNotification(notif) {
152
+ const eventDate = new Date(notif.eventStart);
153
+ const timeStr = eventDate.toLocaleTimeString([], {
154
+ hour: "numeric",
155
+ minute: "2-digit",
156
+ });
157
+ const dateStr = eventDate.toLocaleDateString([], {
158
+ month: "short",
159
+ day: "numeric",
160
+ });
161
+
162
+ const body = notif.eventLocation
163
+ ? `${dateStr} at ${timeStr}\n${notif.eventLocation}`
164
+ : `${dateStr} at ${timeStr}`;
165
+
166
+ self.registration.showNotification(notif.eventTitle, {
167
+ body,
168
+ icon: "/icon.svg",
169
+ tag: notif.eventId,
170
+ requireInteraction: false,
171
+ });
172
+ }
173
+
174
+ self.addEventListener("notificationclick", (event) => {
175
+ event.notification.close();
176
+ event.waitUntil(clients.openWindow("/"));
177
+ });