easy-ping 0.1.0

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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/dist/adapter-f0OR2DhY.d.cts +200 -0
  4. package/dist/adapter-f0OR2DhY.d.ts +200 -0
  5. package/dist/adapters/drizzle.cjs +440 -0
  6. package/dist/adapters/drizzle.cjs.map +1 -0
  7. package/dist/adapters/drizzle.d.cts +991 -0
  8. package/dist/adapters/drizzle.d.ts +991 -0
  9. package/dist/adapters/drizzle.js +409 -0
  10. package/dist/adapters/drizzle.js.map +1 -0
  11. package/dist/adapters/mongodb.cjs +458 -0
  12. package/dist/adapters/mongodb.cjs.map +1 -0
  13. package/dist/adapters/mongodb.d.cts +32 -0
  14. package/dist/adapters/mongodb.d.ts +32 -0
  15. package/dist/adapters/mongodb.js +376 -0
  16. package/dist/adapters/mongodb.js.map +1 -0
  17. package/dist/browser.cjs +96 -0
  18. package/dist/browser.cjs.map +1 -0
  19. package/dist/browser.d.cts +45 -0
  20. package/dist/browser.d.ts +45 -0
  21. package/dist/browser.js +89 -0
  22. package/dist/browser.js.map +1 -0
  23. package/dist/chunk-BV3U542P.js +11 -0
  24. package/dist/chunk-BV3U542P.js.map +1 -0
  25. package/dist/chunk-NYDVNMKG.js +62 -0
  26. package/dist/chunk-NYDVNMKG.js.map +1 -0
  27. package/dist/chunk-OZSOIX23.js +120 -0
  28. package/dist/chunk-OZSOIX23.js.map +1 -0
  29. package/dist/chunk-PTGHMCCG.js +23 -0
  30. package/dist/chunk-PTGHMCCG.js.map +1 -0
  31. package/dist/chunk-W3BMEPPQ.js +50 -0
  32. package/dist/chunk-W3BMEPPQ.js.map +1 -0
  33. package/dist/client.cjs +160 -0
  34. package/dist/client.cjs.map +1 -0
  35. package/dist/client.d.cts +42 -0
  36. package/dist/client.d.ts +42 -0
  37. package/dist/client.js +158 -0
  38. package/dist/client.js.map +1 -0
  39. package/dist/index.cjs +920 -0
  40. package/dist/index.cjs.map +1 -0
  41. package/dist/index.d.cts +203 -0
  42. package/dist/index.d.ts +203 -0
  43. package/dist/index.js +723 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/node.cjs +49 -0
  46. package/dist/node.cjs.map +1 -0
  47. package/dist/node.d.cts +8 -0
  48. package/dist/node.d.ts +8 -0
  49. package/dist/node.js +46 -0
  50. package/dist/node.js.map +1 -0
  51. package/dist/plugin-Bqj9xAOS.d.cts +217 -0
  52. package/dist/plugin-bTJKmnlq.d.ts +217 -0
  53. package/dist/plugins/digests.cjs +206 -0
  54. package/dist/plugins/digests.cjs.map +1 -0
  55. package/dist/plugins/digests.d.cts +68 -0
  56. package/dist/plugins/digests.d.ts +68 -0
  57. package/dist/plugins/digests.js +201 -0
  58. package/dist/plugins/digests.js.map +1 -0
  59. package/dist/plugins/preferences-client.cjs +23 -0
  60. package/dist/plugins/preferences-client.cjs.map +1 -0
  61. package/dist/plugins/preferences-client.d.cts +29 -0
  62. package/dist/plugins/preferences-client.d.ts +29 -0
  63. package/dist/plugins/preferences-client.js +18 -0
  64. package/dist/plugins/preferences-client.js.map +1 -0
  65. package/dist/plugins/preferences-react.cjs +125 -0
  66. package/dist/plugins/preferences-react.cjs.map +1 -0
  67. package/dist/plugins/preferences-react.d.cts +40 -0
  68. package/dist/plugins/preferences-react.d.ts +40 -0
  69. package/dist/plugins/preferences-react.js +101 -0
  70. package/dist/plugins/preferences-react.js.map +1 -0
  71. package/dist/plugins/preferences.cjs +147 -0
  72. package/dist/plugins/preferences.cjs.map +1 -0
  73. package/dist/plugins/preferences.d.cts +22 -0
  74. package/dist/plugins/preferences.d.ts +22 -0
  75. package/dist/plugins/preferences.js +119 -0
  76. package/dist/plugins/preferences.js.map +1 -0
  77. package/dist/plugins/push.cjs +146 -0
  78. package/dist/plugins/push.cjs.map +1 -0
  79. package/dist/plugins/push.d.cts +60 -0
  80. package/dist/plugins/push.d.ts +60 -0
  81. package/dist/plugins/push.js +144 -0
  82. package/dist/plugins/push.js.map +1 -0
  83. package/dist/provider-CA45WVqt.d.cts +23 -0
  84. package/dist/provider-CA45WVqt.d.ts +23 -0
  85. package/dist/providers/resend.cjs +69 -0
  86. package/dist/providers/resend.cjs.map +1 -0
  87. package/dist/providers/resend.d.cts +20 -0
  88. package/dist/providers/resend.d.ts +20 -0
  89. package/dist/providers/resend.js +66 -0
  90. package/dist/providers/resend.js.map +1 -0
  91. package/dist/providers/web-push.cjs +211 -0
  92. package/dist/providers/web-push.cjs.map +1 -0
  93. package/dist/providers/web-push.d.cts +33 -0
  94. package/dist/providers/web-push.d.ts +33 -0
  95. package/dist/providers/web-push.js +193 -0
  96. package/dist/providers/web-push.js.map +1 -0
  97. package/dist/react.cjs +218 -0
  98. package/dist/react.cjs.map +1 -0
  99. package/dist/react.d.cts +57 -0
  100. package/dist/react.d.ts +57 -0
  101. package/dist/react.js +193 -0
  102. package/dist/react.js.map +1 -0
  103. package/dist/schema.cjs +263 -0
  104. package/dist/schema.cjs.map +1 -0
  105. package/dist/schema.d.cts +180 -0
  106. package/dist/schema.d.ts +180 -0
  107. package/dist/schema.js +200 -0
  108. package/dist/schema.js.map +1 -0
  109. package/dist/testing.cjs +342 -0
  110. package/dist/testing.cjs.map +1 -0
  111. package/dist/testing.d.cts +38 -0
  112. package/dist/testing.d.ts +38 -0
  113. package/dist/testing.js +340 -0
  114. package/dist/testing.js.map +1 -0
  115. package/package.json +168 -0
@@ -0,0 +1,376 @@
1
+ import { isOperator } from '../chunk-OZSOIX23.js';
2
+ import { decodeBase64Url, encodeBase64Url } from '../chunk-PTGHMCCG.js';
3
+ import { coreSchema } from '../chunk-NYDVNMKG.js';
4
+
5
+ // src/adapters/mongodb/adapter.ts
6
+ var str = (value) => String(value);
7
+ var nullableStr = (value) => value == null ? null : String(value);
8
+ var num = (value) => Number(value);
9
+ var date = (value) => value instanceof Date ? value : new Date(String(value));
10
+ var nullableDate = (value) => value == null ? null : value instanceof Date ? value : new Date(String(value));
11
+ var encodeCursor = (createdAt, id) => encodeBase64Url(`${createdAt.toISOString()}|${id}`);
12
+ function decodeCursor(cursor) {
13
+ const decoded = decodeBase64Url(cursor);
14
+ if (!decoded) return null;
15
+ const [iso, id] = decoded.split("|");
16
+ if (!iso || !id) return null;
17
+ const createdAt = new Date(iso);
18
+ return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };
19
+ }
20
+ function toNotification(doc) {
21
+ return {
22
+ id: str(doc._id),
23
+ userId: str(doc.userId),
24
+ type: str(doc.type),
25
+ payload: doc.payload,
26
+ actorId: nullableStr(doc.actorId),
27
+ groupKey: nullableStr(doc.groupKey),
28
+ dedupeKey: nullableStr(doc.dedupeKey),
29
+ seenAt: nullableDate(doc.seenAt),
30
+ readAt: nullableDate(doc.readAt),
31
+ archivedAt: nullableDate(doc.archivedAt),
32
+ createdAt: date(doc.createdAt)
33
+ };
34
+ }
35
+ function toDelivery(doc) {
36
+ return {
37
+ id: str(doc._id),
38
+ notificationId: str(doc.notificationId),
39
+ channel: str(doc.channel),
40
+ status: str(doc.status),
41
+ attempts: num(doc.attempts),
42
+ maxAttempts: num(doc.maxAttempts),
43
+ notBefore: date(doc.notBefore),
44
+ claimedAt: nullableDate(doc.claimedAt),
45
+ claimedBy: nullableStr(doc.claimedBy),
46
+ lastError: nullableStr(doc.lastError),
47
+ updatedAt: date(doc.updatedAt)
48
+ };
49
+ }
50
+ var toMongoField = (field) => field === "id" ? "_id" : field;
51
+ function toFilter(where) {
52
+ const filter = {};
53
+ for (const [field, condition] of Object.entries(where)) {
54
+ const key = toMongoField(field);
55
+ if (condition === null) {
56
+ filter[key] = null;
57
+ continue;
58
+ }
59
+ if (!isOperator(condition)) {
60
+ filter[key] = condition;
61
+ continue;
62
+ }
63
+ const operator = condition;
64
+ if ("in" in operator) filter[key] = { $in: operator.in };
65
+ else if ("lt" in operator) filter[key] = { $lt: operator.lt };
66
+ else if ("lte" in operator) filter[key] = { $lte: operator.lte };
67
+ else if ("gt" in operator) filter[key] = { $gt: operator.gt };
68
+ else if ("gte" in operator) filter[key] = { $gte: operator.gte };
69
+ else if ("not" in operator) {
70
+ filter[key] = { $ne: operator.not };
71
+ }
72
+ }
73
+ return filter;
74
+ }
75
+ var isDuplicateKey = (error) => {
76
+ const code = error?.code;
77
+ if (code === 11e3) return true;
78
+ const writeErrors = error?.writeErrors;
79
+ return Array.isArray(writeErrors) && writeErrors.some((entry) => entry?.code === 11e3);
80
+ };
81
+ function mongoAdapter(db, options = {}) {
82
+ const database = db;
83
+ const prefix = options.prefix ?? "";
84
+ const client = options.client;
85
+ const notifications = () => database.collection(`${prefix}notification`);
86
+ const deliveries = () => database.collection(`${prefix}notification_delivery`);
87
+ async function atomically(fn) {
88
+ if (!client) return fn();
89
+ const session = client.startSession();
90
+ try {
91
+ return await session.withTransaction(() => fn(session));
92
+ } finally {
93
+ await session.endSession();
94
+ }
95
+ }
96
+ return {
97
+ name: "mongodb",
98
+ // Documents keep the declared camelCase, and store objects natively.
99
+ naming: "preserve",
100
+ serializesJson: false,
101
+ async createNotifications(input) {
102
+ if (input.length === 0) return { created: [], deduped: [] };
103
+ const created = [];
104
+ const deduped = [];
105
+ for (const row of input) {
106
+ const now = /* @__PURE__ */ new Date();
107
+ try {
108
+ await atomically(async (session) => {
109
+ const options2 = session ? { session } : void 0;
110
+ await notifications().insertOne(
111
+ {
112
+ _id: row.id,
113
+ userId: row.userId,
114
+ type: row.type,
115
+ payload: row.payload ?? null,
116
+ actorId: row.actorId ?? null,
117
+ groupKey: row.groupKey ?? null,
118
+ dedupeKey: row.dedupeKey ?? null,
119
+ seenAt: null,
120
+ readAt: null,
121
+ archivedAt: null,
122
+ createdAt: now
123
+ },
124
+ options2
125
+ );
126
+ if (row.deliveries.length > 0) {
127
+ await deliveries().insertMany(
128
+ row.deliveries.map((delivery) => ({
129
+ _id: delivery.id,
130
+ notificationId: row.id,
131
+ channel: delivery.channel,
132
+ status: "pending",
133
+ attempts: 0,
134
+ maxAttempts: delivery.maxAttempts,
135
+ notBefore: delivery.notBefore,
136
+ claimedAt: null,
137
+ claimedBy: null,
138
+ lastError: null,
139
+ updatedAt: now
140
+ })),
141
+ options2
142
+ );
143
+ }
144
+ });
145
+ created.push(row.id);
146
+ } catch (error) {
147
+ if (!isDuplicateKey(error)) throw error;
148
+ deduped.push(row.id);
149
+ }
150
+ }
151
+ return { created, deduped };
152
+ },
153
+ async claimPendingDeliveries(args) {
154
+ const now = args.now ?? /* @__PURE__ */ new Date();
155
+ const staleBefore = new Date(now.getTime() - args.leaseMs);
156
+ const eligible = {
157
+ $and: [
158
+ {
159
+ $or: [{ status: "pending" }, { status: "claimed", claimedAt: { $lt: staleBefore } }]
160
+ },
161
+ { notBefore: { $lte: now } },
162
+ { $expr: { $lt: ["$attempts", "$maxAttempts"] } }
163
+ ]
164
+ };
165
+ if (args.channels && args.channels.length > 0) {
166
+ eligible.$and.push({ channel: { $in: [...args.channels] } });
167
+ }
168
+ if (args.ids && args.ids.length > 0) {
169
+ eligible.$and.push({ _id: { $in: [...args.ids] } });
170
+ }
171
+ const claimed = [];
172
+ for (let taken = 0; taken < args.limit; taken += 1) {
173
+ const doc = await deliveries().findOneAndUpdate(
174
+ eligible,
175
+ { $set: { status: "claimed", claimedAt: now, claimedBy: args.claimToken } },
176
+ { sort: { notBefore: 1, _id: 1 }, returnDocument: "after" }
177
+ );
178
+ if (!doc) break;
179
+ claimed.push(doc);
180
+ }
181
+ if (claimed.length === 0) return [];
182
+ const parents = await notifications().find({ _id: { $in: claimed.map((doc) => doc.notificationId) } }).toArray();
183
+ const byId = new Map(parents.map((doc) => [str(doc._id), doc]));
184
+ return claimed.flatMap((doc) => {
185
+ const parent = byId.get(str(doc.notificationId));
186
+ if (!parent) return [];
187
+ return [
188
+ {
189
+ id: str(doc._id),
190
+ notificationId: str(doc.notificationId),
191
+ channel: str(doc.channel),
192
+ attempts: num(doc.attempts),
193
+ maxAttempts: num(doc.maxAttempts),
194
+ notification: {
195
+ userId: str(parent.userId),
196
+ type: str(parent.type),
197
+ payload: parent.payload,
198
+ actorId: nullableStr(parent.actorId)
199
+ }
200
+ }
201
+ ];
202
+ });
203
+ },
204
+ async releaseDeliveries(releases) {
205
+ if (releases.length === 0) return;
206
+ const now = /* @__PURE__ */ new Date();
207
+ for (const { id, outcome, nextAttemptAt } of releases) {
208
+ const failed = outcome.result === "failed";
209
+ const retryable = failed && outcome.retryable;
210
+ await deliveries().updateOne({ _id: id }, [
211
+ {
212
+ $set: {
213
+ attempts: failed ? { $add: ["$attempts", 1] } : "$attempts",
214
+ status: failed ? {
215
+ $cond: [
216
+ { $and: [retryable, { $lt: [{ $add: ["$attempts", 1] }, "$maxAttempts"] }] },
217
+ "pending",
218
+ "failed"
219
+ ]
220
+ } : "sent",
221
+ // $literal: an error message starting with "$" would otherwise
222
+ // be read as a field path.
223
+ lastError: failed ? { $literal: outcome.error.slice(0, 2e3) } : null,
224
+ notBefore: nextAttemptAt ?? "$notBefore",
225
+ // Written unconditionally: if the lease expired and another
226
+ // worker re-sent, the duplicate already happened and the true
227
+ // terminal state beats leaving the row wedged in "claimed".
228
+ claimedAt: null,
229
+ claimedBy: null,
230
+ updatedAt: now
231
+ }
232
+ }
233
+ ]);
234
+ }
235
+ },
236
+ async listNotifications(query) {
237
+ const cursor = query.cursor ? decodeCursor(query.cursor) : null;
238
+ const filter = { userId: query.userId, archivedAt: null };
239
+ if (query.unreadOnly) filter.readAt = null;
240
+ if (cursor) {
241
+ filter.$or = [
242
+ { createdAt: { $lt: cursor.createdAt } },
243
+ { createdAt: cursor.createdAt, _id: { $lt: cursor.id } }
244
+ ];
245
+ }
246
+ const docs = await notifications().find(filter, { sort: { createdAt: -1, _id: -1 }, limit: query.limit + 1 }).toArray();
247
+ const page = docs.slice(0, query.limit).map(toNotification);
248
+ const last = page.at(-1);
249
+ return {
250
+ notifications: page,
251
+ nextCursor: docs.length > query.limit && last ? encodeCursor(last.createdAt, last.id) : null
252
+ };
253
+ },
254
+ async countUnseen(userId) {
255
+ return notifications().countDocuments({ userId, seenAt: null, archivedAt: null });
256
+ },
257
+ async markSeen(userId, before) {
258
+ await notifications().updateMany(
259
+ { userId, seenAt: null, createdAt: { $lte: before } },
260
+ { $set: { seenAt: /* @__PURE__ */ new Date() } }
261
+ );
262
+ },
263
+ async markRead(userId, notificationIds) {
264
+ if (notificationIds.length === 0) return 0;
265
+ const filter = { userId, _id: { $in: [...notificationIds] } };
266
+ await notifications().updateMany(
267
+ { ...filter, readAt: null },
268
+ {
269
+ $set: { readAt: /* @__PURE__ */ new Date() }
270
+ }
271
+ );
272
+ return notifications().countDocuments(filter);
273
+ },
274
+ async markAllRead(userId) {
275
+ const result = await notifications().updateMany(
276
+ { userId, readAt: null },
277
+ { $set: { readAt: /* @__PURE__ */ new Date() } }
278
+ );
279
+ return result.modifiedCount;
280
+ },
281
+ async getFailedDeliveries(args) {
282
+ const docs = await deliveries().find(
283
+ { status: "failed", updatedAt: { $gte: args.since } },
284
+ { sort: { updatedAt: -1 }, limit: args.limit }
285
+ ).toArray();
286
+ return docs.map(toDelivery);
287
+ },
288
+ async queryTable(table, where, options2) {
289
+ const find = {};
290
+ if (options2.limit) find.limit = options2.limit;
291
+ if (options2.orderBy) {
292
+ const direction = options2.orderBy.direction === "desc" ? -1 : 1;
293
+ find.sort = { [toMongoField(options2.orderBy.field)]: direction };
294
+ }
295
+ const docs = await database.collection(table).find(toFilter(where), find).toArray();
296
+ return docs.map(({ _id, ...rest }) => _id === void 0 ? rest : { id: _id, ...rest });
297
+ },
298
+ async insertRows(table, rows, onConflict) {
299
+ if (rows.length === 0) return 0;
300
+ const documents = rows.map(
301
+ ({ id, ...rest }) => id === void 0 ? { ...rest } : { _id: id, ...rest }
302
+ );
303
+ if (!onConflict || onConflict.length === 0) {
304
+ await database.collection(table).insertMany(documents);
305
+ return documents.length;
306
+ }
307
+ let written = 0;
308
+ for (const document of documents) {
309
+ const key = {};
310
+ for (const field of onConflict) key[toMongoField(field)] = document[toMongoField(field)];
311
+ const { _id, ...updatable } = document;
312
+ const update = { $set: updatable };
313
+ if (_id !== void 0) update.$setOnInsert = { _id };
314
+ const result = await database.collection(table).updateOne(key, update, { upsert: true });
315
+ written += result.matchedCount + (result.upsertedCount ?? 0);
316
+ }
317
+ return written;
318
+ },
319
+ async updateRows(table, where, set) {
320
+ const assignments = {};
321
+ for (const [field, value] of Object.entries(set)) assignments[toMongoField(field)] = value;
322
+ if (Object.keys(assignments).length === 0) return 0;
323
+ const result = await database.collection(table).updateMany(toFilter(where), { $set: assignments });
324
+ return result.matchedCount;
325
+ },
326
+ async deleteRows(table, where) {
327
+ const result = await database.collection(table).deleteMany(toFilter(where));
328
+ return result.deletedCount;
329
+ }
330
+ };
331
+ }
332
+
333
+ // src/adapters/mongodb/indexes.ts
334
+ var BSON_TYPE = {
335
+ string: "string",
336
+ number: "number",
337
+ boolean: "bool",
338
+ date: "date",
339
+ json: "object"
340
+ };
341
+ async function createPluginIndexes(db, schema, prefix = "") {
342
+ const database = db;
343
+ for (const table of Object.values(schema)) {
344
+ const collection = database.collection(prefix + table.tableName);
345
+ if (table.primaryKey && table.primaryKey.length > 0 && table.primaryKey[0] !== "id") {
346
+ const spec = {};
347
+ for (const field of table.primaryKey) spec[field] = 1;
348
+ await collection.createIndex(spec, {
349
+ name: `${table.tableName}_pk`,
350
+ unique: true
351
+ });
352
+ }
353
+ for (const declaration of table.indexes ?? []) {
354
+ const spec = {};
355
+ for (const field of declaration.on) spec[field] = 1;
356
+ const options = {
357
+ name: prefix + (declaration.name ?? `${table.tableName}_${declaration.on.join("_")}_idx`)
358
+ };
359
+ if (declaration.unique) {
360
+ options.unique = true;
361
+ const partial = {};
362
+ for (const field of declaration.on) {
363
+ const spec2 = table.fields[field];
364
+ if (spec2 && !spec2.required) partial[field] = { $type: BSON_TYPE[spec2.type] };
365
+ }
366
+ if (Object.keys(partial).length > 0) options.partialFilterExpression = partial;
367
+ }
368
+ await collection.createIndex(spec, options);
369
+ }
370
+ }
371
+ }
372
+ var createMongoIndexes = (db, options = {}) => createPluginIndexes(db, coreSchema, options.prefix ?? "");
373
+
374
+ export { createMongoIndexes, createPluginIndexes, mongoAdapter };
375
+ //# sourceMappingURL=mongodb.js.map
376
+ //# sourceMappingURL=mongodb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/mongodb/adapter.ts","../../src/adapters/mongodb/indexes.ts"],"names":["options","spec"],"mappings":";;;;;AA+EA,IAAM,GAAA,GAAM,CAAC,KAAA,KAA2B,MAAA,CAAO,KAAK,CAAA;AACpD,IAAM,cAAc,CAAC,KAAA,KAAmC,SAAS,IAAA,GAAO,IAAA,GAAO,OAAO,KAAK,CAAA;AAC3F,IAAM,GAAA,GAAM,CAAC,KAAA,KAA2B,MAAA,CAAO,KAAK,CAAA;AACpD,IAAM,IAAA,GAAO,CAAC,KAAA,KAA0B,KAAA,YAAiB,IAAA,GAAO,QAAQ,IAAI,IAAA,CAAK,MAAA,CAAO,KAAK,CAAC,CAAA;AAC9F,IAAM,YAAA,GAAe,CAAC,KAAA,KACpB,KAAA,IAAS,IAAA,GAAO,IAAA,GAAO,KAAA,YAAiB,IAAA,GAAO,KAAA,GAAQ,IAAI,IAAA,CAAK,MAAA,CAAO,KAAK,CAAC,CAAA;AAE/E,IAAM,YAAA,GAAe,CAAC,SAAA,EAAiB,EAAA,KACrC,eAAA,CAAgB,CAAA,EAAG,SAAA,CAAU,WAAA,EAAa,CAAA,CAAA,EAAI,EAAE,CAAA,CAAE,CAAA;AAEpD,SAAS,aAAa,MAAA,EAAwD;AAC5E,EAAA,MAAM,OAAA,GAAU,gBAAgB,MAAM,CAAA;AACtC,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AAErB,EAAA,MAAM,CAAC,GAAA,EAAK,EAAE,CAAA,GAAI,OAAA,CAAQ,MAAM,GAAG,CAAA;AACnC,EAAA,IAAI,CAAC,GAAA,IAAO,CAAC,EAAA,EAAI,OAAO,IAAA;AAExB,EAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,GAAG,CAAA;AAC9B,EAAA,OAAO,MAAA,CAAO,MAAM,SAAA,CAAU,OAAA,EAAS,CAAA,GAAI,IAAA,GAAO,EAAE,SAAA,EAAW,EAAA,EAAG;AACpE;AAGA,SAAS,eAAe,GAAA,EAAmC;AACzD,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAAA,IACf,MAAA,EAAQ,GAAA,CAAI,GAAA,CAAI,MAAM,CAAA;AAAA,IACtB,IAAA,EAAM,GAAA,CAAI,GAAA,CAAI,IAAI,CAAA;AAAA,IAClB,SAAS,GAAA,CAAI,OAAA;AAAA,IACb,OAAA,EAAS,WAAA,CAAY,GAAA,CAAI,OAAO,CAAA;AAAA,IAChC,QAAA,EAAU,WAAA,CAAY,GAAA,CAAI,QAAQ,CAAA;AAAA,IAClC,SAAA,EAAW,WAAA,CAAY,GAAA,CAAI,SAAS,CAAA;AAAA,IACpC,MAAA,EAAQ,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AAAA,IAC/B,MAAA,EAAQ,YAAA,CAAa,GAAA,CAAI,MAAM,CAAA;AAAA,IAC/B,UAAA,EAAY,YAAA,CAAa,GAAA,CAAI,UAAU,CAAA;AAAA,IACvC,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,SAAS;AAAA,GAC/B;AACF;AAEA,SAAS,WAAW,GAAA,EAA+B;AACjD,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAAA,IACf,cAAA,EAAgB,GAAA,CAAI,GAAA,CAAI,cAAc,CAAA;AAAA,IACtC,OAAA,EAAS,GAAA,CAAI,GAAA,CAAI,OAAO,CAAA;AAAA,IACxB,MAAA,EAAQ,GAAA,CAAI,GAAA,CAAI,MAAM,CAAA;AAAA,IACtB,QAAA,EAAU,GAAA,CAAI,GAAA,CAAI,QAAQ,CAAA;AAAA,IAC1B,WAAA,EAAa,GAAA,CAAI,GAAA,CAAI,WAAW,CAAA;AAAA,IAChC,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA;AAAA,IAC7B,SAAA,EAAW,YAAA,CAAa,GAAA,CAAI,SAAS,CAAA;AAAA,IACrC,SAAA,EAAW,WAAA,CAAY,GAAA,CAAI,SAAS,CAAA;AAAA,IACpC,SAAA,EAAW,WAAA,CAAY,GAAA,CAAI,SAAS,CAAA;AAAA,IACpC,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,SAAS;AAAA,GAC/B;AACF;AAGA,IAAM,YAAA,GAAe,CAAC,KAAA,KAAmB,KAAA,KAAU,OAAO,KAAA,GAAQ,KAAA;AAGlE,SAAS,SAAS,KAAA,EAA8B;AAC9C,EAAA,MAAM,SAAmB,EAAC;AAE1B,EAAA,KAAA,MAAW,CAAC,KAAA,EAAO,SAAS,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtD,IAAA,MAAM,GAAA,GAAM,aAAa,KAAK,CAAA;AAE9B,IAAA,IAAI,cAAc,IAAA,EAAM;AACtB,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,IAAA;AACd,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,UAAA,CAAW,SAAS,CAAA,EAAG;AAC1B,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,SAAA;AACd,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,QAAA,GAAW,SAAA;AACjB,IAAA,IAAI,IAAA,IAAQ,UAAU,MAAA,CAAO,GAAG,IAAI,EAAE,GAAA,EAAK,SAAS,EAAA,EAAG;AAAA,SAAA,IAC9C,IAAA,IAAQ,UAAU,MAAA,CAAO,GAAG,IAAI,EAAE,GAAA,EAAK,SAAS,EAAA,EAAG;AAAA,SAAA,IACnD,KAAA,IAAS,UAAU,MAAA,CAAO,GAAG,IAAI,EAAE,IAAA,EAAM,SAAS,GAAA,EAAI;AAAA,SAAA,IACtD,IAAA,IAAQ,UAAU,MAAA,CAAO,GAAG,IAAI,EAAE,GAAA,EAAK,SAAS,EAAA,EAAG;AAAA,SAAA,IACnD,KAAA,IAAS,UAAU,MAAA,CAAO,GAAG,IAAI,EAAE,IAAA,EAAM,SAAS,GAAA,EAAI;AAAA,SAAA,IACtD,SAAS,QAAA,EAAU;AAE1B,MAAA,MAAA,CAAO,GAAG,CAAA,GAAI,EAAE,GAAA,EAAK,SAAS,GAAA,EAAI;AAAA,IACpC;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;AAGA,IAAM,cAAA,GAAiB,CAAC,KAAA,KAA4B;AAClD,EAAA,MAAM,OAAQ,KAAA,EAAgE,IAAA;AAC9E,EAAA,IAAI,IAAA,KAAS,MAAO,OAAO,IAAA;AAC3B,EAAA,MAAM,cAAe,KAAA,EAAiD,WAAA;AACtE,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,WAAW,CAAA,IAAK,WAAA,CAAY,KAAK,CAAC,KAAA,KAAU,KAAA,EAAO,IAAA,KAAS,IAAK,CAAA;AACxF,CAAA;AAEO,SAAS,YAAA,CAAa,EAAA,EAAa,OAAA,GAA+B,EAAC,EAAoB;AAC5F,EAAA,MAAM,QAAA,GAAW,EAAA;AACjB,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,EAAA;AAEjC,EAAA,MAAM,SAAS,OAAA,CAAQ,MAAA;AAEvB,EAAA,MAAM,gBAAgB,MAAM,QAAA,CAAS,UAAA,CAAW,CAAA,EAAG,MAAM,CAAA,YAAA,CAAc,CAAA;AACvE,EAAA,MAAM,aAAa,MAAM,QAAA,CAAS,UAAA,CAAW,CAAA,EAAG,MAAM,CAAA,qBAAA,CAAuB,CAAA;AAW7E,EAAA,eAAe,WAAc,EAAA,EAAwD;AACnF,IAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,EAAA,EAAG;AAEvB,IAAA,MAAM,OAAA,GAAU,OAAO,YAAA,EAAa;AACpC,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,OAAA,CAAQ,eAAA,CAAgB,MAAM,EAAA,CAAG,OAAO,CAAC,CAAA;AAAA,IACxD,CAAA,SAAE;AACA,MAAA,MAAM,QAAQ,UAAA,EAAW;AAAA,IAC3B;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,SAAA;AAAA;AAAA,IAEN,MAAA,EAAQ,UAAA;AAAA,IACR,cAAA,EAAgB,KAAA;AAAA,IAEhB,MAAM,oBAAoB,KAAA,EAAsC;AAC9D,MAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,EAAE,SAAS,EAAC,EAAG,OAAA,EAAS,EAAC,EAAE;AAE1D,MAAA,MAAM,UAAoB,EAAC;AAC3B,MAAA,MAAM,UAAoB,EAAC;AAE3B,MAAA,KAAA,MAAW,OAAO,KAAA,EAAO;AACvB,QAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AAErB,QAAA,IAAI;AACF,UAAA,MAAM,UAAA,CAAW,OAAO,OAAA,KAAY;AAClC,YAAA,MAAMA,QAAAA,GAAU,OAAA,GAAU,EAAE,OAAA,EAAQ,GAAI,KAAA,CAAA;AAIxC,YAAA,MAAM,eAAc,CAAE,SAAA;AAAA,cACpB;AAAA,gBACE,KAAK,GAAA,CAAI,EAAA;AAAA,gBACT,QAAQ,GAAA,CAAI,MAAA;AAAA,gBACZ,MAAM,GAAA,CAAI,IAAA;AAAA,gBACV,OAAA,EAAS,IAAI,OAAA,IAAW,IAAA;AAAA,gBACxB,OAAA,EAAS,IAAI,OAAA,IAAW,IAAA;AAAA,gBACxB,QAAA,EAAU,IAAI,QAAA,IAAY,IAAA;AAAA,gBAC1B,SAAA,EAAW,IAAI,SAAA,IAAa,IAAA;AAAA,gBAC5B,MAAA,EAAQ,IAAA;AAAA,gBACR,MAAA,EAAQ,IAAA;AAAA,gBACR,UAAA,EAAY,IAAA;AAAA,gBACZ,SAAA,EAAW;AAAA,eACb;AAAA,cACAA;AAAA,aACF;AAEA,YAAA,IAAI,GAAA,CAAI,UAAA,CAAW,MAAA,GAAS,CAAA,EAAG;AAC7B,cAAA,MAAM,YAAW,CAAE,UAAA;AAAA,gBACjB,GAAA,CAAI,UAAA,CAAW,GAAA,CAAI,CAAC,QAAA,MAAc;AAAA,kBAChC,KAAK,QAAA,CAAS,EAAA;AAAA,kBACd,gBAAgB,GAAA,CAAI,EAAA;AAAA,kBACpB,SAAS,QAAA,CAAS,OAAA;AAAA,kBAClB,MAAA,EAAQ,SAAA;AAAA,kBACR,QAAA,EAAU,CAAA;AAAA,kBACV,aAAa,QAAA,CAAS,WAAA;AAAA,kBACtB,WAAW,QAAA,CAAS,SAAA;AAAA,kBACpB,SAAA,EAAW,IAAA;AAAA,kBACX,SAAA,EAAW,IAAA;AAAA,kBACX,SAAA,EAAW,IAAA;AAAA,kBACX,SAAA,EAAW;AAAA,iBACb,CAAE,CAAA;AAAA,gBACFA;AAAA,eACF;AAAA,YACF;AAAA,UACF,CAAC,CAAA;AAED,UAAA,OAAA,CAAQ,IAAA,CAAK,IAAI,EAAE,CAAA;AAAA,QACrB,SAAS,KAAA,EAAO;AACd,UAAA,IAAI,CAAC,cAAA,CAAe,KAAK,CAAA,EAAG,MAAM,KAAA;AAClC,UAAA,OAAA,CAAQ,IAAA,CAAK,IAAI,EAAE,CAAA;AAAA,QACrB;AAAA,MACF;AAEA,MAAA,OAAO,EAAE,SAAS,OAAA,EAAQ;AAAA,IAC5B,CAAA;AAAA,IAEA,MAAM,uBAAuB,IAAA,EAAsD;AACjF,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,oBAAO,IAAI,IAAA,EAAK;AACjC,MAAA,MAAM,cAAc,IAAI,IAAA,CAAK,IAAI,OAAA,EAAQ,GAAI,KAAK,OAAO,CAAA;AAEzD,MAAA,MAAM,QAAA,GAAqB;AAAA,QACzB,IAAA,EAAM;AAAA,UACJ;AAAA,YACE,GAAA,EAAK,CAAC,EAAE,MAAA,EAAQ,WAAU,EAAG,EAAE,MAAA,EAAQ,SAAA,EAAW,SAAA,EAAW,EAAE,GAAA,EAAK,WAAA,IAAe;AAAA,WACrF;AAAA,UACA,EAAE,SAAA,EAAW,EAAE,IAAA,EAAM,KAAI,EAAE;AAAA,UAC3B,EAAE,OAAO,EAAE,GAAA,EAAK,CAAC,WAAA,EAAa,cAAc,GAAE;AAAE;AAClD,OACF;AAEA,MAAA,IAAI,IAAA,CAAK,QAAA,IAAY,IAAA,CAAK,QAAA,CAAS,SAAS,CAAA,EAAG;AAC7C,QAAC,QAAA,CAAS,IAAA,CAAoB,IAAA,CAAK,EAAE,OAAA,EAAS,EAAE,GAAA,EAAK,CAAC,GAAG,IAAA,CAAK,QAAQ,CAAA,IAAK,CAAA;AAAA,MAC7E;AACA,MAAA,IAAI,IAAA,CAAK,GAAA,IAAO,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA,EAAG;AACnC,QAAC,QAAA,CAAS,IAAA,CAAoB,IAAA,CAAK,EAAE,GAAA,EAAK,EAAE,GAAA,EAAK,CAAC,GAAG,IAAA,CAAK,GAAG,CAAA,IAAK,CAAA;AAAA,MACpE;AAKA,MAAA,MAAM,UAAsB,EAAC;AAC7B,MAAA,KAAA,IAAS,QAAQ,CAAA,EAAG,KAAA,GAAQ,IAAA,CAAK,KAAA,EAAO,SAAS,CAAA,EAAG;AAClD,QAAA,MAAM,GAAA,GAAM,MAAM,UAAA,EAAW,CAAE,gBAAA;AAAA,UAC7B,QAAA;AAAA,UACA,EAAE,IAAA,EAAM,EAAE,MAAA,EAAQ,SAAA,EAAW,WAAW,GAAA,EAAK,SAAA,EAAW,IAAA,CAAK,UAAA,EAAW,EAAE;AAAA,UAC1E,EAAE,MAAM,EAAE,SAAA,EAAW,GAAG,GAAA,EAAK,CAAA,EAAE,EAAG,cAAA,EAAgB,OAAA;AAAQ,SAC5D;AACA,QAAA,IAAI,CAAC,GAAA,EAAK;AACV,QAAA,OAAA,CAAQ,KAAK,GAAG,CAAA;AAAA,MAClB;AAEA,MAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,EAAC;AAGlC,MAAA,MAAM,UAAU,MAAM,aAAA,GACnB,IAAA,CAAK,EAAE,KAAK,EAAE,GAAA,EAAK,QAAQ,GAAA,CAAI,CAAC,QAAQ,GAAA,CAAI,cAAc,GAAE,EAAG,EAC/D,OAAA,EAAQ;AACX,MAAA,MAAM,IAAA,GAAO,IAAI,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAC,GAAA,KAAQ,CAAC,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,EAAG,GAAG,CAAC,CAAC,CAAA;AAE9D,MAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,CAAC,GAAA,KAAQ;AAC9B,QAAA,MAAM,SAAS,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAA,CAAI,cAAc,CAAC,CAAA;AAC/C,QAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,EAAC;AAErB,QAAA,OAAO;AAAA,UACL;AAAA,YACE,EAAA,EAAI,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAAA,YACf,cAAA,EAAgB,GAAA,CAAI,GAAA,CAAI,cAAc,CAAA;AAAA,YACtC,OAAA,EAAS,GAAA,CAAI,GAAA,CAAI,OAAO,CAAA;AAAA,YACxB,QAAA,EAAU,GAAA,CAAI,GAAA,CAAI,QAAQ,CAAA;AAAA,YAC1B,WAAA,EAAa,GAAA,CAAI,GAAA,CAAI,WAAW,CAAA;AAAA,YAChC,YAAA,EAAc;AAAA,cACZ,MAAA,EAAQ,GAAA,CAAI,MAAA,CAAO,MAAM,CAAA;AAAA,cACzB,IAAA,EAAM,GAAA,CAAI,MAAA,CAAO,IAAI,CAAA;AAAA,cACrB,SAAS,MAAA,CAAO,OAAA;AAAA,cAChB,OAAA,EAAS,WAAA,CAAY,MAAA,CAAO,OAAO;AAAA;AACrC;AACF,SACF;AAAA,MACF,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IAEA,MAAM,kBAAkB,QAAA,EAAsC;AAC5D,MAAA,IAAI,QAAA,CAAS,WAAW,CAAA,EAAG;AAC3B,MAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AAErB,MAAA,KAAA,MAAW,EAAE,EAAA,EAAI,OAAA,EAAS,aAAA,MAAmB,QAAA,EAAU;AACrD,QAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,KAAW,QAAA;AAClC,QAAA,MAAM,SAAA,GAAY,UAAU,OAAA,CAAQ,SAAA;AAIpC,QAAA,MAAM,YAAW,CAAE,SAAA,CAAU,EAAE,GAAA,EAAK,IAAG,EAAG;AAAA,UACxC;AAAA,YACE,IAAA,EAAM;AAAA,cACJ,QAAA,EAAU,SAAS,EAAE,IAAA,EAAM,CAAC,WAAA,EAAa,CAAC,GAAE,GAAI,WAAA;AAAA,cAChD,QAAQ,MAAA,GACJ;AAAA,gBACE,KAAA,EAAO;AAAA,kBACL,EAAE,IAAA,EAAM,CAAC,SAAA,EAAW,EAAE,KAAK,CAAC,EAAE,IAAA,EAAM,CAAC,aAAa,CAAC,CAAA,IAAK,cAAc,CAAA,EAAG,CAAA,EAAE;AAAA,kBAC3E,SAAA;AAAA,kBACA;AAAA;AACF,eACF,GACA,MAAA;AAAA;AAAA;AAAA,cAGJ,SAAA,EAAW,MAAA,GAAS,EAAE,QAAA,EAAU,OAAA,CAAQ,MAAM,KAAA,CAAM,CAAA,EAAG,GAAI,CAAA,EAAE,GAAI,IAAA;AAAA,cACjE,WAAW,aAAA,IAAiB,YAAA;AAAA;AAAA;AAAA;AAAA,cAI5B,SAAA,EAAW,IAAA;AAAA,cACX,SAAA,EAAW,IAAA;AAAA,cACX,SAAA,EAAW;AAAA;AACb;AACF,SACD,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,kBAAkB,KAAA,EAAqC;AAC3D,MAAA,MAAM,SAAS,KAAA,CAAM,MAAA,GAAS,YAAA,CAAa,KAAA,CAAM,MAAM,CAAA,GAAI,IAAA;AAE3D,MAAA,MAAM,SAAmB,EAAE,MAAA,EAAQ,KAAA,CAAM,MAAA,EAAQ,YAAY,IAAA,EAAK;AAClE,MAAA,IAAI,KAAA,CAAM,UAAA,EAAY,MAAA,CAAO,MAAA,GAAS,IAAA;AAEtC,MAAA,IAAI,MAAA,EAAQ;AAEV,QAAA,MAAA,CAAO,GAAA,GAAM;AAAA,UACX,EAAE,SAAA,EAAW,EAAE,GAAA,EAAK,MAAA,CAAO,WAAU,EAAE;AAAA,UACvC,EAAE,WAAW,MAAA,CAAO,SAAA,EAAW,KAAK,EAAE,GAAA,EAAK,MAAA,CAAO,EAAA,EAAG;AAAE,SACzD;AAAA,MACF;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,aAAA,EAAc,CAC9B,KAAK,MAAA,EAAQ,EAAE,MAAM,EAAE,SAAA,EAAW,IAAI,GAAA,EAAK,EAAA,IAAM,KAAA,EAAO,KAAA,CAAM,QAAQ,CAAA,EAAG,EACzE,OAAA,EAAQ;AAEX,MAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,CAAA,EAAG,MAAM,KAAK,CAAA,CAAE,IAAI,cAAc,CAAA;AAC1D,MAAA,MAAM,IAAA,GAAO,IAAA,CAAK,EAAA,CAAG,EAAE,CAAA;AAEvB,MAAA,OAAO;AAAA,QACL,aAAA,EAAe,IAAA;AAAA,QACf,UAAA,EACE,IAAA,CAAK,MAAA,GAAS,KAAA,CAAM,KAAA,IAAS,IAAA,GAAO,YAAA,CAAa,IAAA,CAAK,SAAA,EAAW,IAAA,CAAK,EAAE,CAAA,GAAI;AAAA,OAChF;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,YAAY,MAAA,EAAgB;AAChC,MAAA,OAAO,aAAA,GAAgB,cAAA,CAAe,EAAE,QAAQ,MAAA,EAAQ,IAAA,EAAM,UAAA,EAAY,IAAA,EAAM,CAAA;AAAA,IAClF,CAAA;AAAA,IAEA,MAAM,QAAA,CAAS,MAAA,EAAgB,MAAA,EAAc;AAC3C,MAAA,MAAM,eAAc,CAAE,UAAA;AAAA,QACpB,EAAE,QAAQ,MAAA,EAAQ,IAAA,EAAM,WAAW,EAAE,IAAA,EAAM,QAAO,EAAE;AAAA,QACpD,EAAE,IAAA,EAAM,EAAE,wBAAQ,IAAI,IAAA,IAAO;AAAE,OACjC;AAAA,IACF,CAAA;AAAA,IAEA,MAAM,QAAA,CAAS,MAAA,EAAgB,eAAA,EAAoC;AACjE,MAAA,IAAI,eAAA,CAAgB,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AAIzC,MAAA,MAAM,MAAA,GAAS,EAAE,MAAA,EAAQ,GAAA,EAAK,EAAE,KAAK,CAAC,GAAG,eAAe,CAAA,EAAE,EAAE;AAC5D,MAAA,MAAM,eAAc,CAAE,UAAA;AAAA,QACpB,EAAE,GAAG,MAAA,EAAQ,MAAA,EAAQ,IAAA,EAAK;AAAA,QAC1B;AAAA,UACE,IAAA,EAAM,EAAE,MAAA,kBAAQ,IAAI,MAAK;AAAE;AAC7B,OACF;AAEA,MAAA,OAAO,aAAA,EAAc,CAAE,cAAA,CAAe,MAAM,CAAA;AAAA,IAC9C,CAAA;AAAA,IAEA,MAAM,YAAY,MAAA,EAAgB;AAChC,MAAA,MAAM,MAAA,GAAS,MAAM,aAAA,EAAc,CAAE,UAAA;AAAA,QACnC,EAAE,MAAA,EAAQ,MAAA,EAAQ,IAAA,EAAK;AAAA,QACvB,EAAE,IAAA,EAAM,EAAE,wBAAQ,IAAI,IAAA,IAAO;AAAE,OACjC;AACA,MAAA,OAAO,MAAA,CAAO,aAAA;AAAA,IAChB,CAAA;AAAA,IAEA,MAAM,oBAAoB,IAAA,EAAsC;AAC9D,MAAA,MAAM,IAAA,GAAO,MAAM,UAAA,EAAW,CAC3B,IAAA;AAAA,QACC,EAAE,QAAQ,QAAA,EAAU,SAAA,EAAW,EAAE,IAAA,EAAM,IAAA,CAAK,OAAM,EAAE;AAAA,QACpD,EAAE,MAAM,EAAE,SAAA,EAAW,IAAG,EAAG,KAAA,EAAO,KAAK,KAAA;AAAM,QAE9C,OAAA,EAAQ;AAEX,MAAA,OAAO,IAAA,CAAK,IAAI,UAAU,CAAA;AAAA,IAC5B,CAAA;AAAA,IAEA,MAAM,UAAA,CAAW,KAAA,EAAe,KAAA,EAAoBA,QAAAA,EAAuB;AACzE,MAAA,MAAM,OAAiB,EAAC;AACxB,MAAA,IAAIA,QAAAA,CAAQ,KAAA,EAAO,IAAA,CAAK,KAAA,GAAQA,QAAAA,CAAQ,KAAA;AACxC,MAAA,IAAIA,SAAQ,OAAA,EAAS;AACnB,QAAA,MAAM,SAAA,GAAYA,QAAAA,CAAQ,OAAA,CAAQ,SAAA,KAAc,SAAS,EAAA,GAAK,CAAA;AAC9D,QAAA,IAAA,CAAK,IAAA,GAAO,EAAE,CAAC,YAAA,CAAaA,SAAQ,OAAA,CAAQ,KAAK,CAAC,GAAG,SAAA,EAAU;AAAA,MACjE;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,UAAA,CAAW,KAAK,CAAA,CAAE,IAAA,CAAK,QAAA,CAAS,KAAK,CAAA,EAAG,IAAI,CAAA,CAAE,OAAA,EAAQ;AAGlF,MAAA,OAAO,KAAK,GAAA,CAAI,CAAC,EAAE,GAAA,EAAK,GAAG,IAAA,EAAK,KAAO,GAAA,KAAQ,MAAA,GAAY,OAAO,EAAE,EAAA,EAAI,GAAA,EAAK,GAAG,MAAO,CAAA;AAAA,IACzF,CAAA;AAAA,IAEA,MAAM,UAAA,CACJ,KAAA,EACA,IAAA,EACA,UAAA,EACA;AACA,MAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AAE9B,MAAA,MAAM,YAAY,IAAA,CAAK,GAAA;AAAA,QAAI,CAAC,EAAE,EAAA,EAAI,GAAG,IAAA,OACnC,EAAA,KAAO,MAAA,GAAY,EAAE,GAAG,MAAK,GAAI,EAAE,GAAA,EAAK,EAAA,EAAI,GAAG,IAAA;AAAK,OACtD;AAEA,MAAA,IAAI,CAAC,UAAA,IAAc,UAAA,CAAW,MAAA,KAAW,CAAA,EAAG;AAC1C,QAAA,MAAM,QAAA,CAAS,UAAA,CAAW,KAAK,CAAA,CAAE,WAAW,SAAuB,CAAA;AACnE,QAAA,OAAO,SAAA,CAAU,MAAA;AAAA,MACnB;AAGA,MAAA,IAAI,OAAA,GAAU,CAAA;AACd,MAAA,KAAA,MAAW,YAAY,SAAA,EAAyB;AAC9C,QAAA,MAAM,MAAgB,EAAC;AACvB,QAAA,KAAA,MAAW,KAAA,IAAS,UAAA,EAAY,GAAA,CAAI,YAAA,CAAa,KAAK,CAAC,CAAA,GAAI,QAAA,CAAS,YAAA,CAAa,KAAK,CAAC,CAAA;AAIvF,QAAA,MAAM,EAAE,GAAA,EAAK,GAAG,SAAA,EAAU,GAAI,QAAA;AAC9B,QAAA,MAAM,MAAA,GAAmB,EAAE,IAAA,EAAM,SAAA,EAAU;AAC3C,QAAA,IAAI,GAAA,KAAQ,MAAA,EAAW,MAAA,CAAO,YAAA,GAAe,EAAE,GAAA,EAAI;AAEnD,QAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,UAAA,CAAW,KAAK,CAAA,CAAE,SAAA,CAAU,GAAA,EAAK,MAAA,EAAQ,EAAE,MAAA,EAAQ,IAAA,EAAM,CAAA;AACvF,QAAA,OAAA,IAAW,MAAA,CAAO,YAAA,IAAgB,MAAA,CAAO,aAAA,IAAiB,CAAA,CAAA;AAAA,MAC5D;AACA,MAAA,OAAO,OAAA;AAAA,IACT,CAAA;AAAA,IAEA,MAAM,UAAA,CAAW,KAAA,EAAe,KAAA,EAAoB,GAAA,EAA8B;AAChF,MAAA,MAAM,cAAwB,EAAC;AAC/B,MAAA,KAAA,MAAW,CAAC,KAAA,EAAO,KAAK,CAAA,IAAK,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA,EAAG,WAAA,CAAY,YAAA,CAAa,KAAK,CAAC,CAAA,GAAI,KAAA;AACrF,MAAA,IAAI,OAAO,IAAA,CAAK,WAAW,CAAA,CAAE,MAAA,KAAW,GAAG,OAAO,CAAA;AAElD,MAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAClB,UAAA,CAAW,KAAK,CAAA,CAChB,UAAA,CAAW,QAAA,CAAS,KAAK,CAAA,EAAG,EAAE,IAAA,EAAM,aAAa,CAAA;AACpD,MAAA,OAAO,MAAA,CAAO,YAAA;AAAA,IAChB,CAAA;AAAA,IAEA,MAAM,UAAA,CAAW,KAAA,EAAe,KAAA,EAAoB;AAClD,MAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,UAAA,CAAW,KAAK,CAAA,CAAE,UAAA,CAAW,QAAA,CAAS,KAAK,CAAC,CAAA;AAC1E,MAAA,OAAO,MAAA,CAAO,YAAA;AAAA,IAChB;AAAA,GACF;AACF;;;AClfA,IAAM,SAAA,GAAsD;AAAA,EAC1D,MAAA,EAAQ,QAAA;AAAA,EACR,MAAA,EAAQ,QAAA;AAAA,EACR,OAAA,EAAS,MAAA;AAAA,EACT,IAAA,EAAM,MAAA;AAAA,EACN,IAAA,EAAM;AACR,CAAA;AAWA,eAAsB,mBAAA,CACpB,EAAA,EACA,MAAA,EACA,MAAA,GAAS,EAAA,EACM;AACf,EAAA,MAAM,QAAA,GAAW,EAAA;AAEjB,EAAA,KAAA,MAAW,KAAA,IAAS,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,EAAG;AACzC,IAAA,MAAM,UAAA,GAAa,QAAA,CAAS,UAAA,CAAW,MAAA,GAAS,MAAM,SAAS,CAAA;AAG/D,IAAA,IAAI,KAAA,CAAM,UAAA,IAAc,KAAA,CAAM,UAAA,CAAW,MAAA,GAAS,KAAK,KAAA,CAAM,UAAA,CAAW,CAAC,CAAA,KAAM,IAAA,EAAM;AACnF,MAAA,MAAM,OAAkB,EAAC;AACzB,MAAA,KAAA,MAAW,KAAA,IAAS,KAAA,CAAM,UAAA,EAAY,IAAA,CAAK,KAAK,CAAA,GAAI,CAAA;AACpD,MAAA,MAAM,UAAA,CAAW,YAAY,IAAA,EAAM;AAAA,QACjC,IAAA,EAAM,CAAA,EAAG,KAAA,CAAM,SAAS,CAAA,GAAA,CAAA;AAAA,QACxB,MAAA,EAAQ;AAAA,OACT,CAAA;AAAA,IACH;AAEA,IAAA,KAAA,MAAW,WAAA,IAAe,KAAA,CAAM,OAAA,IAAW,EAAC,EAAG;AAC7C,MAAA,MAAM,OAAkB,EAAC;AACzB,MAAA,KAAA,MAAW,KAAA,IAAS,WAAA,CAAY,EAAA,EAAI,IAAA,CAAK,KAAK,CAAA,GAAI,CAAA;AAElD,MAAA,MAAM,OAAA,GAAwB;AAAA,QAC5B,IAAA,EAAM,MAAA,IAAU,WAAA,CAAY,IAAA,IAAQ,CAAA,EAAG,KAAA,CAAM,SAAS,CAAA,CAAA,EAAI,WAAA,CAAY,EAAA,CAAG,IAAA,CAAK,GAAG,CAAC,CAAA,IAAA,CAAA;AAAA,OACpF;AAEA,MAAA,IAAI,YAAY,MAAA,EAAQ;AACtB,QAAA,OAAA,CAAQ,MAAA,GAAS,IAAA;AAEjB,QAAA,MAAM,UAAmC,EAAC;AAC1C,QAAA,KAAA,MAAW,KAAA,IAAS,YAAY,EAAA,EAAI;AAClC,UAAA,MAAMC,KAAAA,GAAO,KAAA,CAAM,MAAA,CAAO,KAAK,CAAA;AAC/B,UAAA,IAAIA,KAAAA,IAAQ,CAACA,KAAAA,CAAK,QAAA,EAAU,OAAA,CAAQ,KAAK,CAAA,GAAI,EAAE,KAAA,EAAO,SAAA,CAAUA,KAAAA,CAAK,IAAI,CAAA,EAAE;AAAA,QAC7E;AACA,QAAA,IAAI,OAAO,IAAA,CAAK,OAAO,EAAE,MAAA,GAAS,CAAA,UAAW,uBAAA,GAA0B,OAAA;AAAA,MACzE;AAEA,MAAA,MAAM,UAAA,CAAW,WAAA,CAAY,IAAA,EAAM,OAAO,CAAA;AAAA,IAC5C;AAAA,EACF;AACF;AAGO,IAAM,kBAAA,GAAqB,CAAC,EAAA,EAAa,OAAA,GAA+B,EAAC,KAC9E,mBAAA,CAAoB,EAAA,EAAI,UAAA,EAAY,OAAA,CAAQ,MAAA,IAAU,EAAE","file":"mongodb.js","sourcesContent":["import type {\n ClaimArgs,\n ClaimedDelivery,\n DatabaseAdapter,\n DeliveryRelease,\n FeedPage,\n FeedQuery,\n InsertNotification,\n} from \"../../core/adapter\";\nimport { decodeBase64Url, encodeBase64Url } from \"../../core/base64url\";\nimport { isOperator, type QueryOptions, type WhereClause } from \"../../core/store\";\nimport type { Channel, DeliveryRecord, DeliveryStatus, NotificationRecord } from \"../../core/types\";\n\ntype Document = Record<string, unknown>;\n\n/**\n * Minimal structural view of the pieces of the Mongo driver this uses.\n *\n * Typed structurally rather than against the real `Db` so `mongodb` stays an\n * optional peer dependency: an app on Postgres never installs it, and its\n * types are not needed to typecheck this file.\n */\ntype MongoCollection = {\n insertMany(docs: Document[], options?: unknown): Promise<unknown>;\n insertOne(doc: Document, options?: unknown): Promise<unknown>;\n find(filter: Document, options?: Document): { toArray(): Promise<Document[]> };\n findOne(filter: Document, options?: Document): Promise<Document | null>;\n findOneAndUpdate(\n filter: Document,\n update: Document | Document[],\n options?: Document,\n ): Promise<Document | null>;\n updateOne(\n filter: Document,\n update: Document | Document[],\n options?: Document,\n ): Promise<{\n matchedCount: number;\n modifiedCount: number;\n upsertedCount?: number;\n }>;\n updateMany(\n filter: Document,\n update: Document | Document[],\n options?: Document,\n ): Promise<{\n matchedCount: number;\n modifiedCount: number;\n }>;\n deleteMany(filter: Document, options?: Document): Promise<{ deletedCount: number }>;\n countDocuments(filter: Document, options?: Document): Promise<number>;\n createIndex(spec: Document, options?: Document): Promise<string>;\n};\n\ntype MongoDb = {\n collection(name: string): MongoCollection;\n};\n\ntype MongoSession = {\n withTransaction<T>(fn: () => Promise<T>): Promise<T>;\n endSession(): Promise<void>;\n};\n\ntype MongoClient = {\n startSession(): MongoSession;\n};\n\nexport type MongoAdapterOptions = {\n /** Prefixes every collection name, matching the SQL adapter's table prefix. */\n prefix?: string;\n /**\n * The MongoClient the db came from. Supplying it makes createNotifications\n * atomic; without it a crash between the two writes can leave a notification\n * with no deliveries. Transactions need a replica set, so it is opt-in — a\n * standalone mongod rejects the session outright.\n */\n client?: unknown;\n};\n\nconst str = (value: unknown): string => String(value);\nconst nullableStr = (value: unknown): string | null => (value == null ? null : String(value));\nconst num = (value: unknown): number => Number(value);\nconst date = (value: unknown): Date => (value instanceof Date ? value : new Date(String(value)));\nconst nullableDate = (value: unknown): Date | null =>\n value == null ? null : value instanceof Date ? value : new Date(String(value));\n\nconst encodeCursor = (createdAt: Date, id: string) =>\n encodeBase64Url(`${createdAt.toISOString()}|${id}`);\n\nfunction decodeCursor(cursor: string): { createdAt: Date; id: string } | null {\n const decoded = decodeBase64Url(cursor);\n if (!decoded) return null;\n\n const [iso, id] = decoded.split(\"|\");\n if (!iso || !id) return null;\n\n const createdAt = new Date(iso);\n return Number.isNaN(createdAt.getTime()) ? null : { createdAt, id };\n}\n\n/** Documents carry the id as `_id`; the rest of the library never sees that. */\nfunction toNotification(doc: Document): NotificationRecord {\n return {\n id: str(doc._id),\n userId: str(doc.userId),\n type: str(doc.type),\n payload: doc.payload,\n actorId: nullableStr(doc.actorId),\n groupKey: nullableStr(doc.groupKey),\n dedupeKey: nullableStr(doc.dedupeKey),\n seenAt: nullableDate(doc.seenAt),\n readAt: nullableDate(doc.readAt),\n archivedAt: nullableDate(doc.archivedAt),\n createdAt: date(doc.createdAt),\n };\n}\n\nfunction toDelivery(doc: Document): DeliveryRecord {\n return {\n id: str(doc._id),\n notificationId: str(doc.notificationId),\n channel: str(doc.channel) as Channel,\n status: str(doc.status) as DeliveryStatus,\n attempts: num(doc.attempts),\n maxAttempts: num(doc.maxAttempts),\n notBefore: date(doc.notBefore),\n claimedAt: nullableDate(doc.claimedAt),\n claimedBy: nullableStr(doc.claimedBy),\n lastError: nullableStr(doc.lastError),\n updatedAt: date(doc.updatedAt),\n };\n}\n\n/** The store hands `id`; documents key on `_id`. */\nconst toMongoField = (field: string) => (field === \"id\" ? \"_id\" : field);\n\n/** Translates a WhereClause into a Mongo filter. */\nfunction toFilter(where: WhereClause): Document {\n const filter: Document = {};\n\n for (const [field, condition] of Object.entries(where)) {\n const key = toMongoField(field);\n\n if (condition === null) {\n filter[key] = null;\n continue;\n }\n\n if (!isOperator(condition)) {\n filter[key] = condition;\n continue;\n }\n\n const operator = condition as Record<string, unknown>;\n if (\"in\" in operator) filter[key] = { $in: operator.in };\n else if (\"lt\" in operator) filter[key] = { $lt: operator.lt };\n else if (\"lte\" in operator) filter[key] = { $lte: operator.lte };\n else if (\"gt\" in operator) filter[key] = { $gt: operator.gt };\n else if (\"gte\" in operator) filter[key] = { $gte: operator.gte };\n else if (\"not\" in operator) {\n // $ne null also matches a missing field, which is what \"not null\" means.\n filter[key] = { $ne: operator.not };\n }\n }\n\n return filter;\n}\n\n/** A duplicate on a unique index, rather than a real write failure. */\nconst isDuplicateKey = (error: unknown): boolean => {\n const code = (error as { code?: number; writeErrors?: { code?: number }[] })?.code;\n if (code === 11000) return true;\n const writeErrors = (error as { writeErrors?: { code?: number }[] })?.writeErrors;\n return Array.isArray(writeErrors) && writeErrors.some((entry) => entry?.code === 11000);\n};\n\nexport function mongoAdapter(db: unknown, options: MongoAdapterOptions = {}): DatabaseAdapter {\n const database = db as MongoDb;\n const prefix = options.prefix ?? \"\";\n\n const client = options.client as MongoClient | undefined;\n\n const notifications = () => database.collection(`${prefix}notification`);\n const deliveries = () => database.collection(`${prefix}notification_delivery`);\n\n /**\n * Runs `fn` in a transaction when a client was supplied, and plainly\n * otherwise. The session must reach every command inside, so `fn` receives\n * it — a write that omits it silently runs outside the transaction.\n *\n * One transaction per notification, not per batch: a duplicate key aborts\n * the transaction it happens in, and dedupe is the expected path here, so a\n * batch-wide transaction would throw away the notifications around it.\n */\n async function atomically<T>(fn: (session?: MongoSession) => Promise<T>): Promise<T> {\n if (!client) return fn();\n\n const session = client.startSession();\n try {\n return await session.withTransaction(() => fn(session));\n } finally {\n await session.endSession();\n }\n }\n\n return {\n name: \"mongodb\",\n // Documents keep the declared camelCase, and store objects natively.\n naming: \"preserve\",\n serializesJson: false,\n\n async createNotifications(input: readonly InsertNotification[]) {\n if (input.length === 0) return { created: [], deduped: [] };\n\n const created: string[] = [];\n const deduped: string[] = [];\n\n for (const row of input) {\n const now = new Date();\n\n try {\n await atomically(async (session) => {\n const options = session ? { session } : undefined;\n\n // The notification goes in first: it carries the unique dedupe\n // index, so a duplicate is rejected before any delivery is written.\n await notifications().insertOne(\n {\n _id: row.id,\n userId: row.userId,\n type: row.type,\n payload: row.payload ?? null,\n actorId: row.actorId ?? null,\n groupKey: row.groupKey ?? null,\n dedupeKey: row.dedupeKey ?? null,\n seenAt: null,\n readAt: null,\n archivedAt: null,\n createdAt: now,\n },\n options,\n );\n\n if (row.deliveries.length > 0) {\n await deliveries().insertMany(\n row.deliveries.map((delivery) => ({\n _id: delivery.id,\n notificationId: row.id,\n channel: delivery.channel,\n status: \"pending\",\n attempts: 0,\n maxAttempts: delivery.maxAttempts,\n notBefore: delivery.notBefore,\n claimedAt: null,\n claimedBy: null,\n lastError: null,\n updatedAt: now,\n })),\n options,\n );\n }\n });\n\n created.push(row.id);\n } catch (error) {\n if (!isDuplicateKey(error)) throw error;\n deduped.push(row.id);\n }\n }\n\n return { created, deduped };\n },\n\n async claimPendingDeliveries(args: ClaimArgs): Promise<readonly ClaimedDelivery[]> {\n const now = args.now ?? new Date();\n const staleBefore = new Date(now.getTime() - args.leaseMs);\n\n const eligible: Document = {\n $and: [\n {\n $or: [{ status: \"pending\" }, { status: \"claimed\", claimedAt: { $lt: staleBefore } }],\n },\n { notBefore: { $lte: now } },\n { $expr: { $lt: [\"$attempts\", \"$maxAttempts\"] } },\n ],\n };\n\n if (args.channels && args.channels.length > 0) {\n (eligible.$and as Document[]).push({ channel: { $in: [...args.channels] } });\n }\n if (args.ids && args.ids.length > 0) {\n (eligible.$and as Document[]).push({ _id: { $in: [...args.ids] } });\n }\n\n // findOneAndUpdate is atomic per document, so two concurrent callers\n // cannot be handed the same delivery. updateMany would be one round trip\n // but gives no way to learn which documents this call actually won.\n const claimed: Document[] = [];\n for (let taken = 0; taken < args.limit; taken += 1) {\n const doc = await deliveries().findOneAndUpdate(\n eligible,\n { $set: { status: \"claimed\", claimedAt: now, claimedBy: args.claimToken } },\n { sort: { notBefore: 1, _id: 1 }, returnDocument: \"after\" },\n );\n if (!doc) break;\n claimed.push(doc);\n }\n\n if (claimed.length === 0) return [];\n\n // One lookup for the batch rather than one per delivery.\n const parents = await notifications()\n .find({ _id: { $in: claimed.map((doc) => doc.notificationId) } })\n .toArray();\n const byId = new Map(parents.map((doc) => [str(doc._id), doc]));\n\n return claimed.flatMap((doc) => {\n const parent = byId.get(str(doc.notificationId));\n if (!parent) return [];\n\n return [\n {\n id: str(doc._id),\n notificationId: str(doc.notificationId),\n channel: str(doc.channel) as Channel,\n attempts: num(doc.attempts),\n maxAttempts: num(doc.maxAttempts),\n notification: {\n userId: str(parent.userId),\n type: str(parent.type),\n payload: parent.payload,\n actorId: nullableStr(parent.actorId),\n },\n },\n ];\n });\n },\n\n async releaseDeliveries(releases: readonly DeliveryRelease[]) {\n if (releases.length === 0) return;\n const now = new Date();\n\n for (const { id, outcome, nextAttemptAt } of releases) {\n const failed = outcome.result === \"failed\";\n const retryable = failed && outcome.retryable;\n\n // An aggregation-pipeline update so the terminal-vs-retry decision\n // reads maxAttempts from the document itself, as the SQL CASE does.\n await deliveries().updateOne({ _id: id }, [\n {\n $set: {\n attempts: failed ? { $add: [\"$attempts\", 1] } : \"$attempts\",\n status: failed\n ? {\n $cond: [\n { $and: [retryable, { $lt: [{ $add: [\"$attempts\", 1] }, \"$maxAttempts\"] }] },\n \"pending\",\n \"failed\",\n ],\n }\n : \"sent\",\n // $literal: an error message starting with \"$\" would otherwise\n // be read as a field path.\n lastError: failed ? { $literal: outcome.error.slice(0, 2000) } : null,\n notBefore: nextAttemptAt ?? \"$notBefore\",\n // Written unconditionally: if the lease expired and another\n // worker re-sent, the duplicate already happened and the true\n // terminal state beats leaving the row wedged in \"claimed\".\n claimedAt: null,\n claimedBy: null,\n updatedAt: now,\n },\n },\n ]);\n }\n },\n\n async listNotifications(query: FeedQuery): Promise<FeedPage> {\n const cursor = query.cursor ? decodeCursor(query.cursor) : null;\n\n const filter: Document = { userId: query.userId, archivedAt: null };\n if (query.unreadOnly) filter.readAt = null;\n\n if (cursor) {\n // Matches the SQL row comparison (createdAt, id) < (cursor, cursorId).\n filter.$or = [\n { createdAt: { $lt: cursor.createdAt } },\n { createdAt: cursor.createdAt, _id: { $lt: cursor.id } },\n ];\n }\n\n const docs = await notifications()\n .find(filter, { sort: { createdAt: -1, _id: -1 }, limit: query.limit + 1 })\n .toArray();\n\n const page = docs.slice(0, query.limit).map(toNotification);\n const last = page.at(-1);\n\n return {\n notifications: page,\n nextCursor:\n docs.length > query.limit && last ? encodeCursor(last.createdAt, last.id) : null,\n };\n },\n\n async countUnseen(userId: string) {\n return notifications().countDocuments({ userId, seenAt: null, archivedAt: null });\n },\n\n async markSeen(userId: string, before: Date) {\n await notifications().updateMany(\n { userId, seenAt: null, createdAt: { $lte: before } },\n { $set: { seenAt: new Date() } },\n );\n },\n\n async markRead(userId: string, notificationIds: readonly string[]) {\n if (notificationIds.length === 0) return 0;\n\n // Scoped by userId as well as id, and idempotent: re-marking a read\n // notification reports success rather than a 404.\n const filter = { userId, _id: { $in: [...notificationIds] } };\n await notifications().updateMany(\n { ...filter, readAt: null },\n {\n $set: { readAt: new Date() },\n },\n );\n\n return notifications().countDocuments(filter);\n },\n\n async markAllRead(userId: string) {\n const result = await notifications().updateMany(\n { userId, readAt: null },\n { $set: { readAt: new Date() } },\n );\n return result.modifiedCount;\n },\n\n async getFailedDeliveries(args: { since: Date; limit: number }) {\n const docs = await deliveries()\n .find(\n { status: \"failed\", updatedAt: { $gte: args.since } },\n { sort: { updatedAt: -1 }, limit: args.limit },\n )\n .toArray();\n\n return docs.map(toDelivery);\n },\n\n async queryTable(table: string, where: WhereClause, options: QueryOptions) {\n const find: Document = {};\n if (options.limit) find.limit = options.limit;\n if (options.orderBy) {\n const direction = options.orderBy.direction === \"desc\" ? -1 : 1;\n find.sort = { [toMongoField(options.orderBy.field)]: direction };\n }\n\n const docs = await database.collection(table).find(toFilter(where), find).toArray();\n\n // `_id` is a storage detail; plugins declared the field as `id`.\n return docs.map(({ _id, ...rest }) => (_id === undefined ? rest : { id: _id, ...rest }));\n },\n\n async insertRows(\n table: string,\n rows: readonly Record<string, unknown>[],\n onConflict?: readonly string[],\n ) {\n if (rows.length === 0) return 0;\n\n const documents = rows.map(({ id, ...rest }) =>\n id === undefined ? { ...rest } : { _id: id, ...rest },\n );\n\n if (!onConflict || onConflict.length === 0) {\n await database.collection(table).insertMany(documents as Document[]);\n return documents.length;\n }\n\n // Upsert keyed on the conflict fields, matching ON CONFLICT DO UPDATE.\n let written = 0;\n for (const document of documents as Document[]) {\n const key: Document = {};\n for (const field of onConflict) key[toMongoField(field)] = document[toMongoField(field)];\n\n // _id goes in $setOnInsert, never $set: Mongo rejects an update that\n // would modify it, so a conflicting upsert must leave it alone.\n const { _id, ...updatable } = document;\n const update: Document = { $set: updatable };\n if (_id !== undefined) update.$setOnInsert = { _id };\n\n const result = await database.collection(table).updateOne(key, update, { upsert: true });\n written += result.matchedCount + (result.upsertedCount ?? 0);\n }\n return written;\n },\n\n async updateRows(table: string, where: WhereClause, set: Record<string, unknown>) {\n const assignments: Document = {};\n for (const [field, value] of Object.entries(set)) assignments[toMongoField(field)] = value;\n if (Object.keys(assignments).length === 0) return 0;\n\n const result = await database\n .collection(table)\n .updateMany(toFilter(where), { $set: assignments });\n return result.matchedCount;\n },\n\n async deleteRows(table: string, where: WhereClause) {\n const result = await database.collection(table).deleteMany(toFilter(where));\n return result.deletedCount;\n },\n };\n}\n","import type { FieldDeclaration, SchemaDeclaration } from \"../../core/plugin\";\nimport { coreSchema } from \"../../schema/declaration\";\n\ntype IndexSpec = Record<string, 1 | -1>;\n\ntype IndexOptions = {\n name: string;\n unique?: boolean;\n partialFilterExpression?: Record<string, unknown>;\n};\n\ntype IndexableDb = {\n collection(name: string): {\n createIndex(spec: IndexSpec, options: IndexOptions): Promise<string>;\n };\n};\n\nconst BSON_TYPE: Record<FieldDeclaration[\"type\"], string> = {\n string: \"string\",\n number: \"number\",\n boolean: \"bool\",\n date: \"date\",\n json: \"object\",\n};\n\n/**\n * Applies a SchemaDeclaration's indexes to Mongo. The document counterpart of\n * renderPostgresDdl — there are no tables to create, only indexes, and the\n * declaration is the same source of truth.\n *\n * A unique index over a nullable field gets a partial filter. Postgres treats\n * every NULL as distinct, so unlimited rows may leave dedupeKey unset; a plain\n * Mongo unique index treats them as one value and would allow exactly one.\n */\nexport async function createPluginIndexes(\n db: unknown,\n schema: SchemaDeclaration,\n prefix = \"\",\n): Promise<void> {\n const database = db as IndexableDb;\n\n for (const table of Object.values(schema)) {\n const collection = database.collection(prefix + table.tableName);\n\n // A single \"id\" key is stored as _id, which is already unique.\n if (table.primaryKey && table.primaryKey.length > 0 && table.primaryKey[0] !== \"id\") {\n const spec: IndexSpec = {};\n for (const field of table.primaryKey) spec[field] = 1;\n await collection.createIndex(spec, {\n name: `${table.tableName}_pk`,\n unique: true,\n });\n }\n\n for (const declaration of table.indexes ?? []) {\n const spec: IndexSpec = {};\n for (const field of declaration.on) spec[field] = 1;\n\n const options: IndexOptions = {\n name: prefix + (declaration.name ?? `${table.tableName}_${declaration.on.join(\"_\")}_idx`),\n };\n\n if (declaration.unique) {\n options.unique = true;\n\n const partial: Record<string, unknown> = {};\n for (const field of declaration.on) {\n const spec = table.fields[field];\n if (spec && !spec.required) partial[field] = { $type: BSON_TYPE[spec.type] };\n }\n if (Object.keys(partial).length > 0) options.partialFilterExpression = partial;\n }\n\n await collection.createIndex(spec, options);\n }\n }\n}\n\n/** Creates the indexes the core tables need. Call once at startup. */\nexport const createMongoIndexes = (db: unknown, options: { prefix?: string } = {}) =>\n createPluginIndexes(db, coreSchema, options.prefix ?? \"\");\n"]}
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ // src/browser/index.ts
4
+ var PushUnsupportedError = class extends Error {
5
+ constructor(reason) {
6
+ super(`web push is unavailable: ${reason}`);
7
+ this.name = "PushUnsupportedError";
8
+ }
9
+ };
10
+ var PushPermissionError = class extends Error {
11
+ constructor(permission) {
12
+ super(`notification permission was ${permission}`);
13
+ this.permission = permission;
14
+ this.name = "PushPermissionError";
15
+ }
16
+ permission;
17
+ };
18
+ function isPushSupported() {
19
+ return typeof navigator !== "undefined" && "serviceWorker" in navigator && typeof PushManager !== "undefined" && typeof Notification !== "undefined";
20
+ }
21
+ function decodeVapidKey(publicKey) {
22
+ const padded = publicKey.padEnd(publicKey.length + (4 - publicKey.length % 4) % 4, "=");
23
+ let binary;
24
+ try {
25
+ binary = atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
26
+ } catch {
27
+ throw new Error("VAPID publicKey is not valid base64url");
28
+ }
29
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
30
+ if (bytes.length !== 65 || bytes[0] !== 4) {
31
+ throw new Error(
32
+ `VAPID publicKey must be a 65-byte uncompressed EC point, got ${bytes.length} bytes`
33
+ );
34
+ }
35
+ return bytes;
36
+ }
37
+ async function resolveRegistration(options) {
38
+ if (options.registration) return options.registration;
39
+ if (!isPushSupported()) throw new PushUnsupportedError("this browser has no PushManager");
40
+ const path = options.serviceWorkerPath ?? "/sw.js";
41
+ const registration = await navigator.serviceWorker.register(
42
+ path,
43
+ options.scope ? { scope: options.scope } : void 0
44
+ );
45
+ await navigator.serviceWorker.ready;
46
+ return registration;
47
+ }
48
+ async function subscribeToPush(options) {
49
+ const registration = await resolveRegistration(options);
50
+ if (typeof Notification !== "undefined") {
51
+ const permission = Notification.permission === "default" ? await Notification.requestPermission() : Notification.permission;
52
+ if (permission !== "granted") throw new PushPermissionError(permission);
53
+ }
54
+ const existing = await registration.pushManager.getSubscription();
55
+ const subscription = existing ?? await registration.pushManager.subscribe({
56
+ // Required by Chrome; a silent push is rejected outright.
57
+ userVisibleOnly: true,
58
+ applicationServerKey: decodeVapidKey(options.publicKey)
59
+ });
60
+ const payload = subscription.toJSON();
61
+ const doFetch = options.fetch ?? globalThis.fetch;
62
+ const baseUrl = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
63
+ const response = await doFetch(`${baseUrl}/push/devices`, {
64
+ method: "POST",
65
+ headers: { "content-type": "application/json" },
66
+ credentials: "same-origin",
67
+ body: JSON.stringify({ endpoint: payload.endpoint, keys: payload.keys })
68
+ });
69
+ if (!response.ok) {
70
+ throw new Error(`registering the push device failed: ${response.status}`);
71
+ }
72
+ return payload;
73
+ }
74
+ async function unsubscribeFromPush(options) {
75
+ const registration = await resolveRegistration({ ...options});
76
+ const subscription = await registration.pushManager.getSubscription();
77
+ if (!subscription) return false;
78
+ const doFetch = options.fetch ?? globalThis.fetch;
79
+ const baseUrl = (options.baseUrl ?? "/api/notifications").replace(/\/$/, "");
80
+ await doFetch(`${baseUrl}/push/devices/remove`, {
81
+ method: "POST",
82
+ headers: { "content-type": "application/json" },
83
+ credentials: "same-origin",
84
+ body: JSON.stringify({ endpoint: subscription.endpoint })
85
+ }).catch(() => void 0);
86
+ return subscription.unsubscribe();
87
+ }
88
+
89
+ exports.PushPermissionError = PushPermissionError;
90
+ exports.PushUnsupportedError = PushUnsupportedError;
91
+ exports.decodeVapidKey = decodeVapidKey;
92
+ exports.isPushSupported = isPushSupported;
93
+ exports.subscribeToPush = subscribeToPush;
94
+ exports.unsubscribeFromPush = unsubscribeFromPush;
95
+ //# sourceMappingURL=browser.cjs.map
96
+ //# sourceMappingURL=browser.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/browser/index.ts"],"names":[],"mappings":"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","file":"browser.cjs","sourcesContent":["/**\n * The browser half of web push.\n *\n * Everything here is injectable so it can be tested in Node: the real\n * navigator, Notification and fetch are only reached through the defaults.\n */\n\nexport type PushSubscriptionPayload = {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n};\n\nexport type SubscribeOptions = {\n /** VAPID public key, base64url — the same one the server signs with. */\n publicKey: string;\n baseUrl?: string;\n serviceWorkerPath?: string;\n scope?: string;\n fetch?: typeof globalThis.fetch;\n /** Pre-resolved registration, so tests need no service worker. */\n registration?: ServiceWorkerRegistration;\n};\n\nexport class PushUnsupportedError extends Error {\n constructor(reason: string) {\n super(`web push is unavailable: ${reason}`);\n this.name = \"PushUnsupportedError\";\n }\n}\n\nexport class PushPermissionError extends Error {\n constructor(readonly permission: NotificationPermission) {\n super(`notification permission was ${permission}`);\n this.name = \"PushPermissionError\";\n }\n}\n\nexport function isPushSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n typeof PushManager !== \"undefined\" &&\n typeof Notification !== \"undefined\"\n );\n}\n\n/**\n * applicationServerKey must be raw bytes; handing the browser the base64url\n * string it came in as fails with an opaque InvalidCharacterError.\n */\nexport function decodeVapidKey(publicKey: string): Uint8Array<ArrayBuffer> {\n const padded = publicKey.padEnd(publicKey.length + ((4 - (publicKey.length % 4)) % 4), \"=\");\n\n let binary: string;\n try {\n binary = atob(padded.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n } catch {\n // atob throws a bare \"Invalid character\", which says nothing about which\n // of the several keys in a push setup is wrong.\n throw new Error(\"VAPID publicKey is not valid base64url\");\n }\n\n const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n if (bytes.length !== 65 || bytes[0] !== 0x04) {\n throw new Error(\n `VAPID publicKey must be a 65-byte uncompressed EC point, got ${bytes.length} bytes`,\n );\n }\n return bytes;\n}\n\nasync function resolveRegistration(options: SubscribeOptions): Promise<ServiceWorkerRegistration> {\n if (options.registration) return options.registration;\n if (!isPushSupported()) throw new PushUnsupportedError(\"this browser has no PushManager\");\n\n const path = options.serviceWorkerPath ?? \"/sw.js\";\n const registration = await navigator.serviceWorker.register(\n path,\n options.scope ? { scope: options.scope } : undefined,\n );\n\n // subscribe() before the worker is active rejects.\n await navigator.serviceWorker.ready;\n return registration;\n}\n\n/**\n * Registers the worker, asks permission, subscribes, and hands the\n * subscription to the server. Returns what was registered.\n */\nexport async function subscribeToPush(options: SubscribeOptions): Promise<PushSubscriptionPayload> {\n const registration = await resolveRegistration(options);\n\n if (typeof Notification !== \"undefined\") {\n const permission =\n Notification.permission === \"default\"\n ? await Notification.requestPermission()\n : Notification.permission;\n\n if (permission !== \"granted\") throw new PushPermissionError(permission);\n }\n\n const existing = await registration.pushManager.getSubscription();\n const subscription =\n existing ??\n (await registration.pushManager.subscribe({\n // Required by Chrome; a silent push is rejected outright.\n userVisibleOnly: true,\n applicationServerKey: decodeVapidKey(options.publicKey),\n }));\n\n const payload = subscription.toJSON() as PushSubscriptionPayload;\n const doFetch = options.fetch ?? globalThis.fetch;\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n\n const response = await doFetch(`${baseUrl}/push/devices`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n credentials: \"same-origin\",\n body: JSON.stringify({ endpoint: payload.endpoint, keys: payload.keys }),\n });\n\n if (!response.ok) {\n throw new Error(`registering the push device failed: ${response.status}`);\n }\n\n return payload;\n}\n\n/** Tells the server first: a browser-only unsubscribe leaves a dead row behind. */\nexport async function unsubscribeFromPush(\n options: Omit<SubscribeOptions, \"publicKey\">,\n): Promise<boolean> {\n const registration = await resolveRegistration({ ...options, publicKey: \"\" });\n const subscription = await registration.pushManager.getSubscription();\n if (!subscription) return false;\n\n const doFetch = options.fetch ?? globalThis.fetch;\n const baseUrl = (options.baseUrl ?? \"/api/notifications\").replace(/\\/$/, \"\");\n\n await doFetch(`${baseUrl}/push/devices/remove`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n credentials: \"same-origin\",\n body: JSON.stringify({ endpoint: subscription.endpoint }),\n }).catch(() => undefined);\n\n return subscription.unsubscribe();\n}\n"]}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The browser half of web push.
3
+ *
4
+ * Everything here is injectable so it can be tested in Node: the real
5
+ * navigator, Notification and fetch are only reached through the defaults.
6
+ */
7
+ type PushSubscriptionPayload = {
8
+ endpoint: string;
9
+ keys: {
10
+ p256dh: string;
11
+ auth: string;
12
+ };
13
+ };
14
+ type SubscribeOptions = {
15
+ /** VAPID public key, base64url — the same one the server signs with. */
16
+ publicKey: string;
17
+ baseUrl?: string;
18
+ serviceWorkerPath?: string;
19
+ scope?: string;
20
+ fetch?: typeof globalThis.fetch;
21
+ /** Pre-resolved registration, so tests need no service worker. */
22
+ registration?: ServiceWorkerRegistration;
23
+ };
24
+ declare class PushUnsupportedError extends Error {
25
+ constructor(reason: string);
26
+ }
27
+ declare class PushPermissionError extends Error {
28
+ readonly permission: NotificationPermission;
29
+ constructor(permission: NotificationPermission);
30
+ }
31
+ declare function isPushSupported(): boolean;
32
+ /**
33
+ * applicationServerKey must be raw bytes; handing the browser the base64url
34
+ * string it came in as fails with an opaque InvalidCharacterError.
35
+ */
36
+ declare function decodeVapidKey(publicKey: string): Uint8Array<ArrayBuffer>;
37
+ /**
38
+ * Registers the worker, asks permission, subscribes, and hands the
39
+ * subscription to the server. Returns what was registered.
40
+ */
41
+ declare function subscribeToPush(options: SubscribeOptions): Promise<PushSubscriptionPayload>;
42
+ /** Tells the server first: a browser-only unsubscribe leaves a dead row behind. */
43
+ declare function unsubscribeFromPush(options: Omit<SubscribeOptions, "publicKey">): Promise<boolean>;
44
+
45
+ export { PushPermissionError, type PushSubscriptionPayload, PushUnsupportedError, type SubscribeOptions, decodeVapidKey, isPushSupported, subscribeToPush, unsubscribeFromPush };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The browser half of web push.
3
+ *
4
+ * Everything here is injectable so it can be tested in Node: the real
5
+ * navigator, Notification and fetch are only reached through the defaults.
6
+ */
7
+ type PushSubscriptionPayload = {
8
+ endpoint: string;
9
+ keys: {
10
+ p256dh: string;
11
+ auth: string;
12
+ };
13
+ };
14
+ type SubscribeOptions = {
15
+ /** VAPID public key, base64url — the same one the server signs with. */
16
+ publicKey: string;
17
+ baseUrl?: string;
18
+ serviceWorkerPath?: string;
19
+ scope?: string;
20
+ fetch?: typeof globalThis.fetch;
21
+ /** Pre-resolved registration, so tests need no service worker. */
22
+ registration?: ServiceWorkerRegistration;
23
+ };
24
+ declare class PushUnsupportedError extends Error {
25
+ constructor(reason: string);
26
+ }
27
+ declare class PushPermissionError extends Error {
28
+ readonly permission: NotificationPermission;
29
+ constructor(permission: NotificationPermission);
30
+ }
31
+ declare function isPushSupported(): boolean;
32
+ /**
33
+ * applicationServerKey must be raw bytes; handing the browser the base64url
34
+ * string it came in as fails with an opaque InvalidCharacterError.
35
+ */
36
+ declare function decodeVapidKey(publicKey: string): Uint8Array<ArrayBuffer>;
37
+ /**
38
+ * Registers the worker, asks permission, subscribes, and hands the
39
+ * subscription to the server. Returns what was registered.
40
+ */
41
+ declare function subscribeToPush(options: SubscribeOptions): Promise<PushSubscriptionPayload>;
42
+ /** Tells the server first: a browser-only unsubscribe leaves a dead row behind. */
43
+ declare function unsubscribeFromPush(options: Omit<SubscribeOptions, "publicKey">): Promise<boolean>;
44
+
45
+ export { PushPermissionError, type PushSubscriptionPayload, PushUnsupportedError, type SubscribeOptions, decodeVapidKey, isPushSupported, subscribeToPush, unsubscribeFromPush };