@lunora/notify 1.0.0-alpha.1 → 1.0.0-alpha.11

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.
@@ -1,128 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
- import { buildEngine } from './buildEngine-DlmjvnNk.mjs';
3
- import { memorySubscriptionStore } from './memorySubscriptionStore-DhS-YnLe.mjs';
4
- import { normalizeRegisterInput, targetOf, isGoneError } from './fcmId-B-YPgHi7.mjs';
5
-
6
- const resolveMaybeFactory = (value, env) => typeof value === "function" ? value(env) : value;
7
- const receiptError = (receipt) => receipt.successful ? void 0 : receipt.errorMessages.join("; ");
8
- const mapWithConcurrency = async (items, limit, task) => {
9
- const results = Array.from({ length: items.length });
10
- let cursor = 0;
11
- const worker = async () => {
12
- while (cursor < items.length) {
13
- const index = cursor;
14
- cursor += 1;
15
- results[index] = await task(items[index]);
16
- }
17
- };
18
- await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
19
- return results;
20
- };
21
- const resolveProviders = (definition, env) => {
22
- return {
23
- chat: resolveMaybeFactory(definition.chat, env),
24
- fcm: resolveMaybeFactory(definition.fcm, env),
25
- inApp: resolveMaybeFactory(definition.inApp, env),
26
- webhook: resolveMaybeFactory(definition.webhook, env),
27
- webPush: resolveMaybeFactory(definition.webPush, env)
28
- };
29
- };
30
- const runtimeCache = /* @__PURE__ */ new WeakMap();
31
- const runtimeFor = (definition, env) => {
32
- let byEnv = runtimeCache.get(definition);
33
- if (byEnv === void 0) {
34
- byEnv = /* @__PURE__ */ new WeakMap();
35
- runtimeCache.set(definition, byEnv);
36
- }
37
- let runtime = byEnv.get(env);
38
- if (runtime === void 0) {
39
- runtime = { warnedNoStore: false };
40
- byEnv.set(env, runtime);
41
- }
42
- return runtime;
43
- };
44
- const createNotify = (definition, env, options = {}) => {
45
- const runtime = runtimeFor(definition, env);
46
- let engine;
47
- if (options.engine === void 0) {
48
- runtime.engine ??= buildEngine(resolveProviders(definition, env));
49
- engine = runtime.engine;
50
- } else {
51
- engine = options.engine;
52
- }
53
- let store = definition.store?.(env);
54
- if (store === void 0) {
55
- runtime.fallbackStore ??= memorySubscriptionStore();
56
- if (!options.silent && !runtime.warnedNoStore) {
57
- runtime.warnedNoStore = true;
58
- console.warn(
59
- "@lunora/notify: no `store` configured — using a non-durable in-memory subscription store. Configure `store: (env) => d1SubscriptionStore(env.DB)` for production."
60
- );
61
- }
62
- store = runtime.fallbackStore;
63
- }
64
- const subscriptionStore = store;
65
- const concurrency = Math.max(1, options.concurrency ?? 10);
66
- const resolveSubscription = async (target) => {
67
- if (typeof target !== "string") {
68
- return target;
69
- }
70
- const found = await subscriptionStore.get(target);
71
- if (found === void 0) {
72
- throw new LunoraError("BAD_REQUEST", `@lunora/notify: no registered subscription with id "${target}"`);
73
- }
74
- return found;
75
- };
76
- const deliver = async (subscription, payload) => {
77
- const receipt = await engine.sendToChannel("push", { ...payload, to: targetOf(subscription) });
78
- const error = receiptError(receipt);
79
- if (receipt.successful) {
80
- await subscriptionStore.markStatus(subscription.id, "ok");
81
- } else if (isGoneError(error)) {
82
- await subscriptionStore.delete(subscription.id);
83
- } else {
84
- await subscriptionStore.markStatus(subscription.id, "failed", error);
85
- }
86
- return receipt;
87
- };
88
- const push = {
89
- broadcast: async (payload, filter) => {
90
- const subscriptions = await subscriptionStore.list(filter);
91
- const outcomes = await mapWithConcurrency(subscriptions, concurrency, async (subscription) => {
92
- const receipt = await deliver(subscription, payload);
93
- if (receipt.successful) {
94
- return { id: subscription.id, status: "ok" };
95
- }
96
- const error = receiptError(receipt);
97
- return isGoneError(error) ? { error, id: subscription.id, status: "expired" } : { error, id: subscription.id, status: "failed" };
98
- });
99
- return {
100
- failed: outcomes.filter((outcome) => outcome.status === "failed").length,
101
- outcomes,
102
- pruned: outcomes.filter((outcome) => outcome.status === "expired").length,
103
- sent: outcomes.filter((outcome) => outcome.status === "ok").length,
104
- total: outcomes.length
105
- };
106
- },
107
- list: (filter) => subscriptionStore.list(filter),
108
- register: (input) => subscriptionStore.put(normalizeRegisterInput(input)),
109
- send: async (target, payload) => deliver(await resolveSubscription(target), payload),
110
- unregister: (id) => subscriptionStore.delete(id)
111
- };
112
- const sendToChannel = async (channel, payload) => {
113
- if (engine.getProvider(channel) === void 0) {
114
- throw new LunoraError("BAD_REQUEST", `@lunora/notify: the "${channel}" channel is not configured in defineNotify(...)`);
115
- }
116
- return engine.sendToChannel(channel, payload);
117
- };
118
- const notify = {
119
- chat: (payload) => sendToChannel("chat", payload),
120
- inApp: (payload) => sendToChannel("inapp", payload),
121
- push,
122
- send: (message) => engine.send(message),
123
- webhook: (payload) => sendToChannel("webhook", payload)
124
- };
125
- return { notify, push };
126
- };
127
-
128
- export { createNotify };
@@ -1,107 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
-
3
- const rowToSubscription = (row) => {
4
- const subscription = {
5
- createdAt: row.created_at,
6
- id: row.id,
7
- kind: row.kind,
8
- lastSeenAt: row.last_seen_at,
9
- userId: row.user_id
10
- };
11
- if (row.endpoint !== null) {
12
- subscription.endpoint = row.endpoint;
13
- }
14
- if (row.p256dh !== null && row.auth !== null) {
15
- subscription.keys = { auth: row.auth, p256dh: row.p256dh };
16
- }
17
- if (row.token !== null) {
18
- subscription.token = row.token;
19
- }
20
- if (row.last_status !== null) {
21
- subscription.lastStatus = row.last_status;
22
- }
23
- if (row.last_error !== null) {
24
- subscription.lastError = row.last_error;
25
- }
26
- if (row.metadata !== null) {
27
- try {
28
- subscription.metadata = JSON.parse(row.metadata);
29
- } catch {
30
- }
31
- }
32
- return subscription;
33
- };
34
- const IDENTIFIER_PATTERN = /^[A-Za-z_]\w*$/u;
35
- const d1SubscriptionStore = (database, options = {}) => {
36
- const table = options.tableName ?? "lunora_push_subscriptions";
37
- if (!IDENTIFIER_PATTERN.test(table)) {
38
- throw new LunoraError("BAD_REQUEST", `@lunora/notify: d1SubscriptionStore tableName "${table}" is not a bare SQL identifier`);
39
- }
40
- let schemaReady;
41
- const ensureSchema = () => {
42
- if (schemaReady === void 0) {
43
- schemaReady = database.prepare(
44
- `CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, kind TEXT NOT NULL, endpoint TEXT, p256dh TEXT, auth TEXT, token TEXT, user_id TEXT, metadata TEXT, created_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL, last_status TEXT, last_error TEXT)`
45
- ).run().then(() => void 0);
46
- schemaReady.catch(() => {
47
- schemaReady = void 0;
48
- });
49
- }
50
- return schemaReady;
51
- };
52
- const put = async (subscription) => {
53
- await ensureSchema();
54
- await database.prepare(
55
- `INSERT INTO ${table} (id, kind, endpoint, p256dh, auth, token, user_id, metadata, created_at, last_seen_at, last_status, last_error) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) ON CONFLICT(id) DO UPDATE SET kind = ?2, endpoint = ?3, p256dh = ?4, auth = ?5, token = ?6, user_id = ?7, metadata = ?8, last_seen_at = ?10, last_status = ?11, last_error = ?12`
56
- ).bind(
57
- subscription.id,
58
- subscription.kind,
59
- subscription.endpoint ?? null,
60
- subscription.keys?.p256dh ?? null,
61
- subscription.keys?.auth ?? null,
62
- subscription.token ?? null,
63
- subscription.userId ?? null,
64
- subscription.metadata === void 0 ? null : JSON.stringify(subscription.metadata),
65
- subscription.createdAt,
66
- subscription.lastSeenAt,
67
- subscription.lastStatus ?? null,
68
- subscription.lastError ?? null
69
- ).run();
70
- return subscription;
71
- };
72
- const get = async (id) => {
73
- await ensureSchema();
74
- const row = await database.prepare(`SELECT * FROM ${table} WHERE id = ?1`).bind(id).first();
75
- return row === null ? void 0 : rowToSubscription(row);
76
- };
77
- const remove = async (id) => {
78
- await ensureSchema();
79
- await database.prepare(`DELETE FROM ${table} WHERE id = ?1`).bind(id).run();
80
- };
81
- const list = async (filter) => {
82
- await ensureSchema();
83
- const clauses = [];
84
- const bindings = [];
85
- if (filter?.kind !== void 0) {
86
- bindings.push(filter.kind);
87
- clauses.push(`kind = ?${bindings.length.toString()}`);
88
- }
89
- if (filter?.userId !== void 0) {
90
- bindings.push(filter.userId);
91
- clauses.push(filter.userId === null ? "user_id IS NULL" : `user_id = ?${bindings.length.toString()}`);
92
- if (filter.userId === null) {
93
- bindings.pop();
94
- }
95
- }
96
- const where = clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
97
- const { results } = await database.prepare(`SELECT * FROM ${table}${where}`).bind(...bindings).all();
98
- return results.map((row) => rowToSubscription(row));
99
- };
100
- const markStatus = async (id, status, error) => {
101
- await ensureSchema();
102
- await database.prepare(`UPDATE ${table} SET last_status = ?2, last_error = ?3, last_seen_at = ?4 WHERE id = ?1`).bind(id, status, error ?? null, Date.now()).run();
103
- };
104
- return { delete: remove, get, list, markStatus, put };
105
- };
106
-
107
- export { d1SubscriptionStore };
@@ -1,18 +0,0 @@
1
- const defineNotify = (config) => {
2
- if (config.webPush !== void 0 && typeof config.webPush !== "function" && typeof config.webPush !== "object") {
3
- throw new TypeError("defineNotify: `webPush` must be a WebPushConfig object or an `(env) => WebPushConfig` function");
4
- }
5
- if (config.fcm !== void 0 && typeof config.fcm !== "function" && typeof config.fcm !== "object") {
6
- throw new TypeError("defineNotify: `fcm` must be an FcmConfig object or an `(env) => FcmConfig` function");
7
- }
8
- if (config.store !== void 0 && typeof config.store !== "function") {
9
- throw new TypeError("defineNotify: `store` must be a function `(env) => SubscriptionStore` when provided");
10
- }
11
- if (config.webPush === void 0 && config.fcm === void 0) {
12
- throw new TypeError("defineNotify: configure at least one push channel — `webPush` and/or `fcm`");
13
- }
14
- return { ...config, isLunoraNotify: true };
15
- };
16
- const isNotifyDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraNotify === true;
17
-
18
- export { defineNotify, isNotifyDefinition };
@@ -1,4 +0,0 @@
1
- const enqueuePushBroadcast = (queue, job) => queue.send({ ...job, type: "lunora.push.broadcast" });
2
- const runPushBroadcastJob = (push, job) => push.broadcast(job.payload, job.filter);
3
-
4
- export { enqueuePushBroadcast, runPushBroadcastJob };
@@ -1,68 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
-
3
- const fnv1a = (input) => {
4
- let hash = 2166136261;
5
- for (let index = 0; index < input.length; index += 1) {
6
- hash ^= input.codePointAt(index) ?? 0;
7
- hash = Math.imul(hash, 16777619);
8
- }
9
- return (hash >>> 0).toString(16).padStart(8, "0");
10
- };
11
- const webPushId = (endpoint) => `wp_${fnv1a(endpoint)}`;
12
- const fcmId = (token) => `fcm_${fnv1a(token)}`;
13
- const parseSubscription = (subscription) => {
14
- if (typeof subscription !== "string") {
15
- return subscription ?? {};
16
- }
17
- try {
18
- return JSON.parse(subscription);
19
- } catch (error) {
20
- throw new LunoraError(
21
- "BAD_REQUEST",
22
- `@lunora/notify: register() web-push subscription is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
23
- );
24
- }
25
- };
26
- const normalizeRegisterInput = (input, now = Date.now()) => {
27
- if ("token" in input) {
28
- const { token } = input;
29
- if (typeof token !== "string" || token === "") {
30
- throw new LunoraError("BAD_REQUEST", "@lunora/notify: register() fcm input requires a non-empty `token`");
31
- }
32
- return { createdAt: now, id: fcmId(token), kind: "fcm", lastSeenAt: now, metadata: input.metadata, token, userId: input.userId ?? null };
33
- }
34
- const subscription = parseSubscription(input.subscription);
35
- const { endpoint } = subscription;
36
- const p256dh = subscription.keys?.p256dh;
37
- const auth = subscription.keys?.auth;
38
- if (typeof endpoint !== "string" || endpoint === "" || typeof p256dh !== "string" || typeof auth !== "string") {
39
- throw new LunoraError("BAD_REQUEST", "@lunora/notify: register() web-push subscription requires `endpoint` and `keys.{p256dh, auth}`");
40
- }
41
- return {
42
- createdAt: now,
43
- endpoint,
44
- id: webPushId(endpoint),
45
- keys: { auth, p256dh },
46
- kind: "web-push",
47
- lastSeenAt: now,
48
- metadata: input.metadata,
49
- userId: input.userId ?? null
50
- };
51
- };
52
- const targetOf = (subscription) => {
53
- if (subscription.kind === "fcm") {
54
- return subscription.token ?? "";
55
- }
56
- return JSON.stringify({ endpoint: subscription.endpoint, keys: subscription.keys });
57
- };
58
- const WEB_PUSH_GONE_PATTERN = /\bhttp\s*4(?:04|10)\b/iu;
59
- const FCM_GONE_PATTERN = /\b(?:unregistered|not[\s-]?registered|registration-token-not-registered)\b/iu;
60
- const GONE_TEXT_FALLBACK = /\bsubscription (?:is )?(?:gone|expired|no longer valid)\b/iu;
61
- const isGoneError = (message) => {
62
- if (message === void 0) {
63
- return false;
64
- }
65
- return WEB_PUSH_GONE_PATTERN.test(message) || FCM_GONE_PATTERN.test(message) || GONE_TEXT_FALLBACK.test(message);
66
- };
67
-
68
- export { fcmId, isGoneError, normalizeRegisterInput, targetOf, webPushId };
@@ -1,46 +0,0 @@
1
- const matches = (subscription, filter) => {
2
- if (filter === void 0) {
3
- return true;
4
- }
5
- if (filter.kind !== void 0 && subscription.kind !== filter.kind) {
6
- return false;
7
- }
8
- if (filter.userId !== void 0 && (subscription.userId ?? null) !== filter.userId) {
9
- return false;
10
- }
11
- return true;
12
- };
13
- const memorySubscriptionStore = () => {
14
- const map = /* @__PURE__ */ new Map();
15
- return {
16
- delete: (id) => {
17
- map.delete(id);
18
- return Promise.resolve();
19
- },
20
- get: (id) => Promise.resolve(map.get(id)),
21
- list: (filter) => {
22
- const result = [];
23
- for (const subscription of map.values()) {
24
- if (matches(subscription, filter)) {
25
- result.push(subscription);
26
- }
27
- }
28
- return Promise.resolve(result);
29
- },
30
- markStatus: (id, status, error) => {
31
- const existing = map.get(id);
32
- if (existing !== void 0) {
33
- map.set(id, { ...existing, lastError: error, lastSeenAt: Date.now(), lastStatus: status });
34
- }
35
- return Promise.resolve();
36
- },
37
- put: (subscription) => {
38
- const existing = map.get(subscription.id);
39
- const merged = existing === void 0 ? subscription : { ...existing, ...subscription, createdAt: existing.createdAt };
40
- map.set(merged.id, merged);
41
- return Promise.resolve(merged);
42
- }
43
- };
44
- };
45
-
46
- export { memorySubscriptionStore };