@krizaka/notifications 0.2.0 → 0.2.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.
@@ -2,7 +2,7 @@ import {
2
2
  CHANNELS,
3
3
  NOTIFICATION_CODE,
4
4
  NotificationError
5
- } from "./chunk-FQYNUK64.js";
5
+ } from "./chunk-RTHXH2BK.js";
6
6
 
7
7
  // src/rules.ts
8
8
  import { createHash } from "crypto";
@@ -85,4 +85,4 @@ export {
85
85
  viewOf,
86
86
  checkInput
87
87
  };
88
- //# sourceMappingURL=chunk-2MH7PZDT.js.map
88
+ //# sourceMappingURL=chunk-AQI4ZDWW.js.map
@@ -26,4 +26,4 @@ export {
26
26
  NOTIFICATION_CODE,
27
27
  NotificationError
28
28
  };
29
- //# sourceMappingURL=chunk-FQYNUK64.js.map
29
+ //# sourceMappingURL=chunk-RTHXH2BK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/port.ts"],"sourcesContent":["/**\n * The ports of « notifications »: the bell (an item per notification), the account's preferences (a matrix of categories by\n * channels), the phones registered for push, and one outbox message per enabled channel. Generic: no push or realtime import (the\n * workers that deliver a channel are the product's, or the Java service's for e-mail), no database client, no wall clock.\n */\nexport type { Clock, Query } from \"@krizaka/db\";\n\n/**\n * The channels an account switches. `app` is the bell and the live event of the open pages (one switch);\n * `push` goes to the account's registered phones; `email` to its address.\n */\nexport const CHANNEL = { app: \"app\", push: \"push\", email: \"email\" } as const;\nexport type Channel = (typeof CHANNEL)[keyof typeof CHANNEL];\nexport const CHANNELS: readonly Channel[] = [CHANNEL.app, CHANNEL.push, CHANNEL.email];\n\n/** What the outbox carries: the `channel` of Java's `evt.notification.requested.v1` (EMAIL) and two more for the web (PUSH, LIVE). */\nexport const DELIVERY = { email: \"EMAIL\", push: \"PUSH\", live: \"LIVE\" } as const;\nexport type Delivery = (typeof DELIVERY)[keyof typeof DELIVERY];\n\nexport const PLATFORM = { ios: \"ios\", android: \"android\", web: \"web\" } as const;\nexport type Platform = (typeof PLATFORM)[keyof typeof PLATFORM];\n\n/** A category as the product declares it. */\nexport interface CategoryDefaults {\n /** Cannot be switched off (security, the account): its channels follow `defaults`, whatever the account chose. */\n locked: boolean;\n /** Which channels are on until the account chooses. */\n defaults: Record<Channel, boolean>;\n /** After `burstMax` notifications of this category to one account within `burstWindowSeconds`, the next ones are bell-only. */\n burstMax: number;\n burstWindowSeconds: number;\n}\n\n/** The categories (declared by the product, tuned by the admin) and the retention. Read from the settings at each call. */\nexport interface NotificationConfig {\n categories: Record<string, CategoryDefaults>;\n /** Days an item is kept before `purge`. */\n retentionDays: number;\n}\n\n/** One line of the bell. */\nexport interface Item {\n id: string;\n accountId: string;\n category: string;\n template: string;\n payload: Record<string, unknown>;\n dedupeKey: string | null;\n /** Shown in the bell. An item whose app channel was off is kept (the burst count and the history) but not shown. */\n inBell: boolean;\n createdAt: Date;\n readAt: Date | null;\n}\n\nexport interface Device {\n id: string;\n accountId: string;\n platform: Platform;\n token: string;\n createdAt: Date;\n lastSeenAt: Date;\n}\n\n/** Where the state lives. Every adapter runs `notificationStoreContract` (`@krizaka/notifications/testkit`). */\nexport interface NotificationStore {\n /**\n * Writes the item and says whether it exceeds the burst cap, ATOMICALLY per account and category (the database serialises\n * concurrent callers with a transaction-level advisory lock, so the cap holds under concurrency: pass the caller's `tx`).\n * `duplicate` when the account already has an item with this `dedupeKey`: nothing is written.\n */\n record(item: Item, burst: { max: number; since: Date }): Promise<{ duplicate: boolean; capped: boolean }>;\n /** The account's explicit choices. */\n preferences(accountId: string): Promise<{ category: string; channel: Channel; enabled: boolean }[]>;\n setPreference(accountId: string, category: string, channel: Channel, enabled: boolean, now: Date): Promise<void>;\n /** Items shown in the bell, newest first; `before` is the cursor (the last item of the previous page). */\n list(accountId: string, options: { unreadOnly: boolean; limit: number; before?: { createdAt: Date; id: string } }): Promise<Item[]>;\n unreadCount(accountId: string): Promise<number>;\n /** Marks read the account's own unread items among `ids` (all of them when `ids` is null). The first `read_at` stays. Returns how many. */\n markRead(accountId: string, ids: readonly string[] | null, now: Date): Promise<number>;\n /** A token belongs to one account: registering it again moves it, and brings back a revoked one. */\n upsertDevice(device: Device): Promise<void>;\n devices(accountId: string): Promise<Device[]>;\n /** The push service said the token is dead. */\n revokeDevice(token: string, now: Date): Promise<boolean>;\n purgeBefore(before: Date): Promise<number>;\n /** Removes everything held about the account. Returns how many rows. */\n erase(accountId: string): Promise<number>;\n}\n\nexport const NOTIFICATION_CODE = {\n badCategory: \"NOTIFICATIONS_BAD_CATEGORY\",\n badInput: \"NOTIFICATIONS_BAD_INPUT\",\n locked: \"NOTIFICATIONS_LOCKED\",\n badConfig: \"NOTIFICATIONS_BAD_CONFIG\",\n} as const;\nexport type NotificationCode = (typeof NOTIFICATION_CODE)[keyof typeof NOTIFICATION_CODE];\n\nexport class NotificationError extends Error {\n constructor(\n readonly code: NotificationCode,\n message: string,\n ) {\n super(message);\n this.name = \"NotificationError\";\n }\n}\n"],"mappings":";AAWO,IAAM,UAAU,EAAE,KAAK,OAAO,MAAM,QAAQ,OAAO,QAAQ;AAE3D,IAAM,WAA+B,CAAC,QAAQ,KAAK,QAAQ,MAAM,QAAQ,KAAK;AAG9E,IAAM,WAAW,EAAE,OAAO,SAAS,MAAM,QAAQ,MAAM,OAAO;AAG9D,IAAM,WAAW,EAAE,KAAK,OAAO,SAAS,WAAW,KAAK,MAAM;AAsE9D,IAAM,oBAAoB;AAAA,EAC/B,aAAa;AAAA,EACb,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AACb;AAGO,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YACW,MACT,SACA;AACA,UAAM,OAAO;AAHJ;AAIT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAMb;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { N as NotificationStore, C as Channel, a as NotificationConfig, b as CategoryDefaults, D as Delivery, P as Platform, I as Item, c as Device } from './port-DptCmYwy.js';
2
- export { d as CHANNEL, e as CHANNELS, f as DELIVERY, g as NOTIFICATION_CODE, h as NotificationCode, i as NotificationError, j as PLATFORM } from './port-DptCmYwy.js';
1
+ import { N as NotificationStore, C as Channel, a as NotificationConfig, b as CategoryDefaults, D as Delivery, P as Platform, I as Item, c as Device } from './port-B-JRUQH3.js';
2
+ export { d as CHANNEL, e as CHANNELS, f as DELIVERY, g as NOTIFICATION_CODE, h as NotificationCode, i as NotificationError, j as PLATFORM } from './port-B-JRUQH3.js';
3
3
  import * as _krizaka_db from '@krizaka/db';
4
4
  import { Clock, Query } from '@krizaka/db';
5
5
  export { Clock, Query } from '@krizaka/db';
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  resolveChannels,
8
8
  uuidOf,
9
9
  viewOf
10
- } from "./chunk-2MH7PZDT.js";
10
+ } from "./chunk-AQI4ZDWW.js";
11
11
  import {
12
12
  CHANNEL,
13
13
  CHANNELS,
@@ -15,7 +15,7 @@ import {
15
15
  NOTIFICATION_CODE,
16
16
  NotificationError,
17
17
  PLATFORM
18
- } from "./chunk-FQYNUK64.js";
18
+ } from "./chunk-RTHXH2BK.js";
19
19
 
20
20
  // src/memory.ts
21
21
  var copy = (v) => ({ ...v });
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /**
8
- * The channels an account switches. `app` is the bell and the live event of the open pages (one switch, like Orochia's matrix);
8
+ * The channels an account switches. `app` is the bell and the live event of the open pages (one switch);
9
9
  * `push` goes to the account's registered phones; `email` to its address.
10
10
  */
11
11
  declare const CHANNEL: {
@@ -1,5 +1,5 @@
1
1
  import { Registry } from '@krizaka/settings';
2
- import { C as Channel, a as NotificationConfig } from './port-DptCmYwy.js';
2
+ import { C as Channel, a as NotificationConfig } from './port-B-JRUQH3.js';
3
3
 
4
4
  /** What a product declares for a category; the admin tunes the values (except `locked`, which is code). */
5
5
  interface CategoryDeclaration {
package/dist/settings.js CHANGED
@@ -2,12 +2,12 @@ import {
2
2
  CATEGORY_NAME,
3
3
  NOTIFICATION_BOUNDS,
4
4
  NOTIFICATION_DEFAULTS
5
- } from "./chunk-2MH7PZDT.js";
5
+ } from "./chunk-AQI4ZDWW.js";
6
6
  import {
7
7
  CHANNELS,
8
8
  NOTIFICATION_CODE,
9
9
  NotificationError
10
- } from "./chunk-FQYNUK64.js";
10
+ } from "./chunk-RTHXH2BK.js";
11
11
 
12
12
  // src/settings.ts
13
13
  import { flag, integer } from "@krizaka/settings";
package/dist/testkit.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { I as Item, N as NotificationStore } from './port-DptCmYwy.js';
1
+ import { I as Item, N as NotificationStore } from './port-B-JRUQH3.js';
2
2
 
3
3
  declare const T0: Date;
4
4
  declare const at: (seconds: number) => Date;
package/dist/testkit.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CHANNEL
3
- } from "./chunk-FQYNUK64.js";
3
+ } from "./chunk-RTHXH2BK.js";
4
4
 
5
5
  // src/testkit.ts
6
6
  import { randomUUID } from "crypto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/notifications",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "The bell, channels and preferences for every Krizaka product: one notify() writes the in-app item and appends one outbox message per enabled channel (e-mail, push per device, live), respecting the account's matrix of categories by channels and per-category burst caps that hold under concurrency. The product declares its categories; the admin tunes defaults and caps. No push or realtime import, no wall clock.",
5
5
  "keywords": [
6
6
  "krizaka",
@@ -57,13 +57,13 @@
57
57
  "typescript": "^5.9.3",
58
58
  "vitest": "^4.1.11",
59
59
  "zod": "^4.6.7",
60
- "@krizaka/config": "^0.8.1",
61
- "@krizaka/outbox": "^0.1.1"
60
+ "@krizaka/config": "^0.17.0",
61
+ "@krizaka/outbox": "^0.1.3"
62
62
  },
63
63
  "dependencies": {
64
- "@krizaka/db": "^0.2.0",
65
- "@krizaka/settings": "^0.5.0",
66
- "@krizaka/time": "^0.1.0"
64
+ "@krizaka/settings": "^0.5.2",
65
+ "@krizaka/time": "^0.2.0",
66
+ "@krizaka/db": "^0.3.1"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "vitest": ">=4",
@@ -81,7 +81,7 @@
81
81
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
82
82
  "lint": "eslint . --max-warnings 0 && tsc --noEmit",
83
83
  "typecheck": "tsc --noEmit",
84
- "test": "vitest run",
84
+ "test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=lcov --coverage.reporter=text-summary \"--coverage.include=src/**/*.ts\"",
85
85
  "check": "pnpm run lint && pnpm run test && pnpm run build"
86
86
  }
87
87
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/port.ts"],"sourcesContent":["/**\n * The ports of « notifications »: the bell (an item per notification), the account's preferences (a matrix of categories by\n * channels), the phones registered for push, and one outbox message per enabled channel. Generic: no push or realtime import (the\n * workers that deliver a channel are the product's, or the Java service's for e-mail), no database client, no wall clock.\n */\nexport type { Clock, Query } from \"@krizaka/db\";\n\n/**\n * The channels an account switches. `app` is the bell and the live event of the open pages (one switch, like Orochia's matrix);\n * `push` goes to the account's registered phones; `email` to its address.\n */\nexport const CHANNEL = { app: \"app\", push: \"push\", email: \"email\" } as const;\nexport type Channel = (typeof CHANNEL)[keyof typeof CHANNEL];\nexport const CHANNELS: readonly Channel[] = [CHANNEL.app, CHANNEL.push, CHANNEL.email];\n\n/** What the outbox carries: the `channel` of Java's `evt.notification.requested.v1` (EMAIL) and two more for the web (PUSH, LIVE). */\nexport const DELIVERY = { email: \"EMAIL\", push: \"PUSH\", live: \"LIVE\" } as const;\nexport type Delivery = (typeof DELIVERY)[keyof typeof DELIVERY];\n\nexport const PLATFORM = { ios: \"ios\", android: \"android\", web: \"web\" } as const;\nexport type Platform = (typeof PLATFORM)[keyof typeof PLATFORM];\n\n/** A category as the product declares it. */\nexport interface CategoryDefaults {\n /** Cannot be switched off (security, the account): its channels follow `defaults`, whatever the account chose. */\n locked: boolean;\n /** Which channels are on until the account chooses. */\n defaults: Record<Channel, boolean>;\n /** After `burstMax` notifications of this category to one account within `burstWindowSeconds`, the next ones are bell-only. */\n burstMax: number;\n burstWindowSeconds: number;\n}\n\n/** The categories (declared by the product, tuned by the admin) and the retention. Read from the settings at each call. */\nexport interface NotificationConfig {\n categories: Record<string, CategoryDefaults>;\n /** Days an item is kept before `purge`. */\n retentionDays: number;\n}\n\n/** One line of the bell. */\nexport interface Item {\n id: string;\n accountId: string;\n category: string;\n template: string;\n payload: Record<string, unknown>;\n dedupeKey: string | null;\n /** Shown in the bell. An item whose app channel was off is kept (the burst count and the history) but not shown. */\n inBell: boolean;\n createdAt: Date;\n readAt: Date | null;\n}\n\nexport interface Device {\n id: string;\n accountId: string;\n platform: Platform;\n token: string;\n createdAt: Date;\n lastSeenAt: Date;\n}\n\n/** Where the state lives. Every adapter runs `notificationStoreContract` (`@krizaka/notifications/testkit`). */\nexport interface NotificationStore {\n /**\n * Writes the item and says whether it exceeds the burst cap, ATOMICALLY per account and category (the database serialises\n * concurrent callers with a transaction-level advisory lock, so the cap holds under concurrency: pass the caller's `tx`).\n * `duplicate` when the account already has an item with this `dedupeKey`: nothing is written.\n */\n record(item: Item, burst: { max: number; since: Date }): Promise<{ duplicate: boolean; capped: boolean }>;\n /** The account's explicit choices. */\n preferences(accountId: string): Promise<{ category: string; channel: Channel; enabled: boolean }[]>;\n setPreference(accountId: string, category: string, channel: Channel, enabled: boolean, now: Date): Promise<void>;\n /** Items shown in the bell, newest first; `before` is the cursor (the last item of the previous page). */\n list(accountId: string, options: { unreadOnly: boolean; limit: number; before?: { createdAt: Date; id: string } }): Promise<Item[]>;\n unreadCount(accountId: string): Promise<number>;\n /** Marks read the account's own unread items among `ids` (all of them when `ids` is null). The first `read_at` stays. Returns how many. */\n markRead(accountId: string, ids: readonly string[] | null, now: Date): Promise<number>;\n /** A token belongs to one account: registering it again moves it, and brings back a revoked one. */\n upsertDevice(device: Device): Promise<void>;\n devices(accountId: string): Promise<Device[]>;\n /** The push service said the token is dead. */\n revokeDevice(token: string, now: Date): Promise<boolean>;\n purgeBefore(before: Date): Promise<number>;\n /** Removes everything held about the account. Returns how many rows. */\n erase(accountId: string): Promise<number>;\n}\n\nexport const NOTIFICATION_CODE = {\n badCategory: \"NOTIFICATIONS_BAD_CATEGORY\",\n badInput: \"NOTIFICATIONS_BAD_INPUT\",\n locked: \"NOTIFICATIONS_LOCKED\",\n badConfig: \"NOTIFICATIONS_BAD_CONFIG\",\n} as const;\nexport type NotificationCode = (typeof NOTIFICATION_CODE)[keyof typeof NOTIFICATION_CODE];\n\nexport class NotificationError extends Error {\n constructor(\n readonly code: NotificationCode,\n message: string,\n ) {\n super(message);\n this.name = \"NotificationError\";\n }\n}\n"],"mappings":";AAWO,IAAM,UAAU,EAAE,KAAK,OAAO,MAAM,QAAQ,OAAO,QAAQ;AAE3D,IAAM,WAA+B,CAAC,QAAQ,KAAK,QAAQ,MAAM,QAAQ,KAAK;AAG9E,IAAM,WAAW,EAAE,OAAO,SAAS,MAAM,QAAQ,MAAM,OAAO;AAG9D,IAAM,WAAW,EAAE,KAAK,OAAO,SAAS,WAAW,KAAK,MAAM;AAsE9D,IAAM,oBAAoB;AAAA,EAC/B,aAAa;AAAA,EACb,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AACb;AAGO,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YACW,MACT,SACA;AACA,UAAM,OAAO;AAHJ;AAIT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAMb;","names":[]}