@baukit/notifications-core 0.5.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/errors.d.ts +8 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +11 -0
  6. package/dist/errors.js.map +1 -0
  7. package/dist/index.d.ts +8 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +8 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/memory.d.ts +45 -0
  12. package/dist/memory.d.ts.map +1 -0
  13. package/dist/memory.js +104 -0
  14. package/dist/memory.js.map +1 -0
  15. package/dist/occurrences.d.ts +34 -0
  16. package/dist/occurrences.d.ts.map +1 -0
  17. package/dist/occurrences.js +84 -0
  18. package/dist/occurrences.js.map +1 -0
  19. package/dist/ownership.d.ts +11 -0
  20. package/dist/ownership.d.ts.map +1 -0
  21. package/dist/ownership.js +49 -0
  22. package/dist/ownership.js.map +1 -0
  23. package/dist/plan-vectors.d.ts +57 -0
  24. package/dist/plan-vectors.d.ts.map +1 -0
  25. package/dist/plan-vectors.js +108 -0
  26. package/dist/plan-vectors.js.map +1 -0
  27. package/dist/replacement-plan.d.ts +11 -0
  28. package/dist/replacement-plan.d.ts.map +1 -0
  29. package/dist/replacement-plan.js +28 -0
  30. package/dist/replacement-plan.js.map +1 -0
  31. package/dist/scheduler.d.ts +46 -0
  32. package/dist/scheduler.d.ts.map +1 -0
  33. package/dist/scheduler.js +143 -0
  34. package/dist/scheduler.js.map +1 -0
  35. package/dist/validation.d.ts +21 -0
  36. package/dist/validation.d.ts.map +1 -0
  37. package/dist/validation.js +62 -0
  38. package/dist/validation.js.map +1 -0
  39. package/dist/vitest.d.ts +13 -0
  40. package/dist/vitest.d.ts.map +1 -0
  41. package/dist/vitest.js +261 -0
  42. package/dist/vitest.js.map +1 -0
  43. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Koss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # `@baukit/notifications-core`
2
+
3
+ `@baukit/notifications-core` plans local notifications and replaces only the ones a product owns.
4
+ It has no runtime dependencies besides `@baukit/localization-core` and never imports a
5
+ notification library. The product decides which occurrences are eligible and what the copy says.
6
+ This package turns them into instants, drops the ones outside the horizon, and computes what to
7
+ keep, cancel, and schedule.
8
+
9
+ ## Planning
10
+
11
+ ```ts
12
+ import {
13
+ planNotificationReplacement,
14
+ resolveNotificationOccurrences,
15
+ } from '@baukit/notifications-core';
16
+
17
+ const resolution = resolveNotificationOccurrences({
18
+ occurrences: [
19
+ {
20
+ logicalId: 'plan:2026-03-29',
21
+ civilDate: '2026-03-29',
22
+ civilTime: '02:30',
23
+ contentDigest: 'v3',
24
+ },
25
+ {
26
+ logicalId: 'plan:2026-03-30',
27
+ civilDate: '2026-03-30',
28
+ civilTime: '02:30',
29
+ contentDigest: 'v3',
30
+ },
31
+ ],
32
+ timeZone: 'Europe/Berlin',
33
+ gap: 'shiftForward',
34
+ fold: 'earlier',
35
+ clock: () => Date.now(),
36
+ horizonDays: 14,
37
+ });
38
+ // resolution.desired: both entries with epochMilliseconds and transition ('gap' for the first)
39
+
40
+ const plan = planNotificationReplacement(current, resolution.desired);
41
+ // plan.keep, plan.cancel, plan.schedule, each sorted by instant, then logical ID
42
+ ```
43
+
44
+ - Each occurrence resolves through `resolveZonedLocalTime` from `@baukit/localization-core`. The
45
+ caller picks the gap and fold policies; neither has a default.
46
+ - The horizon is the civil dates from today in `timeZone` through today plus `horizonDays - 1`.
47
+ `resolution.firstCivilDate` and `resolution.lastCivilDate` report it.
48
+ - `resolution.skipped` lists occurrences with a reason: `past` (the instant is at or before the
49
+ clock), `outside_horizon`, or `nonexistent_local_time` (a gap under `gap: 'reject'`).
50
+ - Extra fields on an occurrence, such as the notification copy, pass through to `desired`.
51
+ - An entry is kept only when its logical ID, instant, and content digest all match. A text change
52
+ under the same digest is kept on purpose. Change the digest, or pass `{ replaceAll: true }`, to
53
+ replace the text.
54
+ - Invalid input throws `NotificationPlanError` with a `code` and, when it applies, the `logicalId`:
55
+ `invalid_civil_date`, `invalid_civil_time`, `invalid_time_zone`, `invalid_horizon`,
56
+ `invalid_clock`, `invalid_logical_id`, `invalid_content_digest`, `invalid_instant`, or
57
+ `duplicate_logical_id`. An invalid date throws even when it falls outside the horizon. An
58
+ unknown policy throws `RangeError`, as in `resolveZonedLocalTime`.
59
+
60
+ The same input and clock give the same result, whatever order the occurrences arrive in. The shared
61
+ vectors in `fixtures/notifications/plan-vectors-v1.json` cover DST gaps and folds, month and year
62
+ changes, travel between zones, duplicates, invalid input, changed content, and horizon boundaries.
63
+ `@baukit/notifications-core/vectors` exports `notificationPlanVectorChecks(fixture)`, the checks the
64
+ Vitest suite runs: each success case yields the expected plan, convergence once the plan is applied,
65
+ and independence from input order. `examples/expo-notifications-conformance` runs them inside
66
+ Hermes.
67
+
68
+ ## Owned replacement
69
+
70
+ `createOwnedNotificationScheduler(platform, { pendingLimit })` runs a plan against a
71
+ `NotificationPlatform<TContent>` port with four calls: `list`, `cancel`, `permission`, and
72
+ `schedule`. `@baukit/notifications-expo` is the Expo implementation.
73
+
74
+ ```ts
75
+ const outcome = await scheduler.replaceOwned({ namespace: 'reminders' }, desired);
76
+ // { status: 'complete' | 'incomplete' | 'superseded', kept, cancelled, scheduled, failures }
77
+ ```
78
+
79
+ - A notification belongs to a namespace when it carries a valid marker for that namespace and its
80
+ identifier is `baukit:<namespace>:<logicalId>`. Everything else is left alone, including other
81
+ Baukit namespaces and requests with a copied marker.
82
+ - The scheduler cancels one request at a time and never cancels everything. An item whose cancel
83
+ failed is not rescheduled in the same run.
84
+ - It checks permission only when something needs scheduling. Without permission it still cancels
85
+ stale entries.
86
+ - It schedules in instant order and stops at `pendingLimit`, which counts every pending request on
87
+ the device, not only this owner's.
88
+ - Failures carry a code and a logical ID, never content: `list_failed`, `cancel_failed`,
89
+ `permission_denied`, `permission_failed`, `schedule_failed`, `schedule_limit`. Any failure makes
90
+ the outcome `incomplete`. Calling `replaceOwned` again with the same set converges.
91
+ - One replacement runs per namespace at a time. While it runs, later calls for that namespace
92
+ collapse to the newest. The ones that never ran resolve as `superseded`. Namespaces do not wait
93
+ for each other. Share one scheduler per device so this works.
94
+ - An invalid namespace (lowercase letters and digits separated by `.` or `-`, at most 64
95
+ characters) or an invalid desired set rejects with `NotificationPlanError` before the platform is
96
+ touched.
97
+
98
+ ## Conformance suite
99
+
100
+ `@baukit/notifications-core/vitest` exports `describeOwnedNotificationSchedulerContract`. Run it
101
+ against any platform implementation; it needs the consumer's Vitest installation.
102
+
103
+ ```ts
104
+ import {
105
+ InMemoryNotificationPlatform,
106
+ createOwnedNotificationScheduler,
107
+ } from '@baukit/notifications-core';
108
+ import { describeOwnedNotificationSchedulerContract } from '@baukit/notifications-core/vitest';
109
+
110
+ describeOwnedNotificationSchedulerContract<string>((options) => {
111
+ const platform = new InMemoryNotificationPlatform<string>();
112
+ return {
113
+ scheduler: createOwnedNotificationScheduler(platform, options),
114
+ faults: platform.faults,
115
+ content: (_logicalId, text) => text,
116
+ addUnrelated: (identifier) => platform.addUnrelated(identifier),
117
+ pending: () => platform.pending(),
118
+ };
119
+ });
120
+ ```
121
+
122
+ `NotificationPlatformFaultState` holds the injected faults (failed list, cancel, schedule and
123
+ permission calls, a revoked permission, a held list), so a mocked native module can reuse it.
124
+
125
+ ## Boundaries
126
+
127
+ The package does not pick reminder times, quiet hours, or copy, and it does not request permission.
128
+ Products compute eligible occurrences and the content digest, ask for permission in their own UX, and
129
+ choose the pending limit for their platform. It plans one time zone per call.
@@ -0,0 +1,8 @@
1
+ export type NotificationPlanErrorCode = 'invalid_civil_date' | 'invalid_civil_time' | 'invalid_time_zone' | 'invalid_horizon' | 'invalid_clock' | 'invalid_namespace' | 'invalid_logical_id' | 'invalid_content_digest' | 'invalid_instant' | 'invalid_pending_limit' | 'duplicate_logical_id' | 'reserved_data_key';
2
+ export declare class NotificationPlanError extends Error {
3
+ readonly name = "NotificationPlanError";
4
+ readonly code: NotificationPlanErrorCode;
5
+ readonly logicalId: string | undefined;
6
+ constructor(code: NotificationPlanErrorCode, logicalId?: string);
7
+ }
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,yBAAyB,GACjC,oBAAoB,GACpB,oBAAoB,GACpB,mBAAmB,GACnB,iBAAiB,GACjB,eAAe,GACf,mBAAmB,GACnB,oBAAoB,GACpB,wBAAwB,GACxB,iBAAiB,GACjB,uBAAuB,GACvB,sBAAsB,GACtB,mBAAmB,CAAC;AAExB,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,2BAA2B;IACjD,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;gBAE3B,IAAI,EAAE,yBAAyB,EAAE,SAAS,CAAC,EAAE,MAAM;CAKhE"}
package/dist/errors.js ADDED
@@ -0,0 +1,11 @@
1
+ export class NotificationPlanError extends Error {
2
+ name = 'NotificationPlanError';
3
+ code;
4
+ logicalId;
5
+ constructor(code, logicalId) {
6
+ super(logicalId === undefined ? code : `${code}: ${logicalId}`);
7
+ this.code = code;
8
+ this.logicalId = logicalId;
9
+ }
10
+ }
11
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAcA,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC5B,IAAI,GAAG,uBAAuB,CAAC;IACxC,IAAI,CAA4B;IAChC,SAAS,CAAqB;IAEvC,YAAY,IAA+B,EAAE,SAAkB;QAC7D,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,SAAS,EAAE,CAAC,CAAC;QAChE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ export { NotificationPlanError, type NotificationPlanErrorCode } from './errors.js';
2
+ export { InMemoryNotificationPlatform, NotificationPlatformFaultState, type NotificationPlatformFaults, type StoredNotification, } from './memory.js';
3
+ export { resolveNotificationOccurrences, type NotificationClock, type NotificationOccurrence, type NotificationOccurrenceInput, type NotificationOccurrenceResolution, type ResolvedNotification, type SkippedOccurrence, type SkippedOccurrenceReason, } from './occurrences.js';
4
+ export { decodeOwnedNotificationMarker, encodeOwnedNotificationMarker, isOwnedBy, OWNED_NOTIFICATION_DATA_KEY, OWNED_NOTIFICATION_MARKER_VERSION, ownedNotificationIdentifier, type OwnedNotificationMarker, } from './ownership.js';
5
+ export { planNotificationReplacement, type NotificationReplacementOptions, type NotificationReplacementPlan, } from './replacement-plan.js';
6
+ export { createOwnedNotificationScheduler, type NotificationOwner, type NotificationPermission, type NotificationPlatform, type OwnedNotification, type OwnedNotificationFailure, type OwnedNotificationFailureCode, type OwnedNotificationReplacementOutcome, type OwnedNotificationReplacementStatus, type OwnedNotificationScheduler, type OwnedNotificationSchedulerOptions, type OwnedNotificationScheduleRequest, type PendingNotification, } from './scheduler.js';
7
+ export { isValidContentDigest, isValidLogicalId, isValidNamespace, MAX_CONTENT_DIGEST_LENGTH, MAX_LOGICAL_ID_LENGTH, MAX_NAMESPACE_LENGTH, type PlannedNotification, } from './validation.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,KAAK,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EACL,4BAA4B,EAC5B,8BAA8B,EAC9B,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,8BAA8B,EAC9B,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,gCAAgC,EACrC,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC7B,SAAS,EACT,2BAA2B,EAC3B,iCAAiC,EACjC,2BAA2B,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,2BAA2B,EAC3B,KAAK,8BAA8B,EACnC,KAAK,2BAA2B,GACjC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,gCAAgC,EAChC,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,4BAA4B,EACjC,KAAK,mCAAmC,EACxC,KAAK,kCAAkC,EACvC,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACtC,KAAK,gCAAgC,EACrC,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,yBAAyB,EACzB,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { NotificationPlanError } from './errors.js';
2
+ export { InMemoryNotificationPlatform, NotificationPlatformFaultState, } from './memory.js';
3
+ export { resolveNotificationOccurrences, } from './occurrences.js';
4
+ export { decodeOwnedNotificationMarker, encodeOwnedNotificationMarker, isOwnedBy, OWNED_NOTIFICATION_DATA_KEY, OWNED_NOTIFICATION_MARKER_VERSION, ownedNotificationIdentifier, } from './ownership.js';
5
+ export { planNotificationReplacement, } from './replacement-plan.js';
6
+ export { createOwnedNotificationScheduler, } from './scheduler.js';
7
+ export { isValidContentDigest, isValidLogicalId, isValidNamespace, MAX_CONTENT_DIGEST_LENGTH, MAX_LOGICAL_ID_LENGTH, MAX_NAMESPACE_LENGTH, } from './validation.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAkC,MAAM,aAAa,CAAC;AACpF,OAAO,EACL,4BAA4B,EAC5B,8BAA8B,GAG/B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,8BAA8B,GAQ/B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC7B,SAAS,EACT,2BAA2B,EAC3B,iCAAiC,EACjC,2BAA2B,GAE5B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,2BAA2B,GAG5B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,gCAAgC,GAajC,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,yBAAyB,EACzB,qBAAqB,EACrB,oBAAoB,GAErB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { NotificationPermission, NotificationPlatform, OwnedNotificationScheduleRequest, PendingNotification } from './scheduler.js';
2
+ export interface NotificationPlatformFaults {
3
+ setPermission(permission: NotificationPermission): void;
4
+ failNextList(): void;
5
+ failNextPermission(): void;
6
+ failCancel(identifier: string): void;
7
+ failSchedule(identifier: string): void;
8
+ holdNextList(): () => void;
9
+ afterNextList(action: () => void): void;
10
+ }
11
+ export interface StoredNotification<TContent> extends PendingNotification {
12
+ readonly content: TContent | null;
13
+ }
14
+ export declare class NotificationPlatformFaultState implements NotificationPlatformFaults {
15
+ permission: NotificationPermission;
16
+ private listFailures;
17
+ private permissionFailures;
18
+ private readonly cancelFailures;
19
+ private readonly scheduleFailures;
20
+ private readonly listHolds;
21
+ private readonly listActions;
22
+ setPermission(permission: NotificationPermission): void;
23
+ failNextList(): void;
24
+ failNextPermission(): void;
25
+ failCancel(identifier: string): void;
26
+ failSchedule(identifier: string): void;
27
+ holdNextList(): () => void;
28
+ afterNextList(action: () => void): void;
29
+ beforeList(): Promise<void>;
30
+ afterList(): void;
31
+ beforePermission(): void;
32
+ beforeCancel(identifier: string): void;
33
+ beforeSchedule(identifier: string): void;
34
+ }
35
+ export declare class InMemoryNotificationPlatform<TContent> implements NotificationPlatform<TContent> {
36
+ readonly faults: NotificationPlatformFaultState;
37
+ private readonly requests;
38
+ addUnrelated(identifier: string): void;
39
+ pending(): readonly StoredNotification<TContent>[];
40
+ list(): Promise<readonly PendingNotification[]>;
41
+ cancel(identifier: string): Promise<void>;
42
+ permission(): Promise<NotificationPermission>;
43
+ schedule(request: OwnedNotificationScheduleRequest<TContent>): Promise<void>;
44
+ }
45
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,gCAAgC,EAChC,mBAAmB,EACpB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,0BAA0B;IACzC,aAAa,CAAC,UAAU,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACxD,YAAY,IAAI,IAAI,CAAC;IACrB,kBAAkB,IAAI,IAAI,CAAC;IAC3B,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,YAAY,IAAI,MAAM,IAAI,CAAC;IAC3B,aAAa,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,kBAAkB,CAAC,QAAQ,CAAE,SAAQ,mBAAmB;IACvE,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;CACnC;AAED,qBAAa,8BAA+B,YAAW,0BAA0B;IAC/E,UAAU,EAAE,sBAAsB,CAAa;IAC/C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuB;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;IAElD,aAAa,CAAC,UAAU,EAAE,sBAAsB,GAAG,IAAI;IAIvD,YAAY,IAAI,IAAI;IAIpB,kBAAkB,IAAI,IAAI;IAI1B,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIpC,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAItC,YAAY,IAAI,MAAM,IAAI;IAU1B,aAAa,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,IAAI;IAIjC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQjC,SAAS,IAAI,IAAI;IAIjB,gBAAgB,IAAI,IAAI;IAOxB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAMtC,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;CAKzC;AAED,qBAAa,4BAA4B,CAAC,QAAQ,CAAE,YAAW,oBAAoB,CAAC,QAAQ,CAAC;IAC3F,QAAQ,CAAC,MAAM,iCAAwC;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmD;IAE5E,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAItC,OAAO,IAAI,SAAS,kBAAkB,CAAC,QAAQ,CAAC,EAAE;IAM5C,IAAI,IAAI,OAAO,CAAC,SAAS,mBAAmB,EAAE,CAAC;IAOrD,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOzC,UAAU,IAAI,OAAO,CAAC,sBAAsB,CAAC;IAO7C,QAAQ,CAAC,OAAO,EAAE,gCAAgC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAU7E"}
package/dist/memory.js ADDED
@@ -0,0 +1,104 @@
1
+ export class NotificationPlatformFaultState {
2
+ permission = 'granted';
3
+ listFailures = 0;
4
+ permissionFailures = 0;
5
+ cancelFailures = new Set();
6
+ scheduleFailures = new Set();
7
+ listHolds = [];
8
+ listActions = [];
9
+ setPermission(permission) {
10
+ this.permission = permission;
11
+ }
12
+ failNextList() {
13
+ this.listFailures += 1;
14
+ }
15
+ failNextPermission() {
16
+ this.permissionFailures += 1;
17
+ }
18
+ failCancel(identifier) {
19
+ this.cancelFailures.add(identifier);
20
+ }
21
+ failSchedule(identifier) {
22
+ this.scheduleFailures.add(identifier);
23
+ }
24
+ holdNextList() {
25
+ let release = () => undefined;
26
+ this.listHolds.push(new Promise((resolve) => {
27
+ release = resolve;
28
+ }));
29
+ return release;
30
+ }
31
+ afterNextList(action) {
32
+ this.listActions.push(action);
33
+ }
34
+ async beforeList() {
35
+ await this.listHolds.shift();
36
+ if (this.listFailures > 0) {
37
+ this.listFailures -= 1;
38
+ throw new Error('list failed');
39
+ }
40
+ }
41
+ afterList() {
42
+ this.listActions.shift()?.();
43
+ }
44
+ beforePermission() {
45
+ if (this.permissionFailures > 0) {
46
+ this.permissionFailures -= 1;
47
+ throw new Error('permission check failed');
48
+ }
49
+ }
50
+ beforeCancel(identifier) {
51
+ if (this.cancelFailures.delete(identifier)) {
52
+ throw new Error('cancel failed');
53
+ }
54
+ }
55
+ beforeSchedule(identifier) {
56
+ if (this.scheduleFailures.delete(identifier)) {
57
+ throw new Error('schedule failed');
58
+ }
59
+ }
60
+ }
61
+ export class InMemoryNotificationPlatform {
62
+ faults = new NotificationPlatformFaultState();
63
+ requests = new Map();
64
+ addUnrelated(identifier) {
65
+ this.requests.set(identifier, { identifier, marker: null, content: null });
66
+ }
67
+ pending() {
68
+ return [...this.requests.values()].sort((left, right) => left.identifier < right.identifier ? -1 : 1);
69
+ }
70
+ async list() {
71
+ await this.faults.beforeList();
72
+ const snapshot = this.pending().map(({ identifier, marker }) => ({ identifier, marker }));
73
+ this.faults.afterList();
74
+ return snapshot;
75
+ }
76
+ cancel(identifier) {
77
+ return settle(() => {
78
+ this.faults.beforeCancel(identifier);
79
+ this.requests.delete(identifier);
80
+ });
81
+ }
82
+ permission() {
83
+ return settle(() => {
84
+ this.faults.beforePermission();
85
+ return this.faults.permission;
86
+ });
87
+ }
88
+ schedule(request) {
89
+ return settle(() => {
90
+ this.faults.beforeSchedule(request.identifier);
91
+ this.requests.set(request.identifier, {
92
+ identifier: request.identifier,
93
+ marker: request.marker,
94
+ content: request.content,
95
+ });
96
+ });
97
+ }
98
+ }
99
+ function settle(action) {
100
+ return new Promise((resolve) => {
101
+ resolve(action());
102
+ });
103
+ }
104
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { type FoldPolicy, type GapPolicy, type LocalTimeTransition } from '@baukit/localization-core';
2
+ export interface NotificationOccurrence {
3
+ readonly logicalId: string;
4
+ readonly civilDate: string;
5
+ readonly civilTime: string;
6
+ readonly contentDigest: string;
7
+ }
8
+ export type NotificationClock = () => number;
9
+ export interface NotificationOccurrenceInput<T extends NotificationOccurrence> {
10
+ readonly occurrences: readonly T[];
11
+ readonly timeZone: string;
12
+ readonly gap: GapPolicy;
13
+ readonly fold: FoldPolicy;
14
+ readonly clock: NotificationClock;
15
+ readonly horizonDays: number;
16
+ }
17
+ export type ResolvedNotification<T extends NotificationOccurrence = NotificationOccurrence> = T & {
18
+ readonly epochMilliseconds: number;
19
+ readonly transition: LocalTimeTransition;
20
+ };
21
+ export type SkippedOccurrenceReason = 'past' | 'outside_horizon' | 'nonexistent_local_time';
22
+ export interface SkippedOccurrence {
23
+ readonly logicalId: string;
24
+ readonly reason: SkippedOccurrenceReason;
25
+ }
26
+ export interface NotificationOccurrenceResolution<T extends NotificationOccurrence> {
27
+ readonly now: number;
28
+ readonly firstCivilDate: string;
29
+ readonly lastCivilDate: string;
30
+ readonly desired: readonly ResolvedNotification<T>[];
31
+ readonly skipped: readonly SkippedOccurrence[];
32
+ }
33
+ export declare function resolveNotificationOccurrences<T extends NotificationOccurrence>(input: NotificationOccurrenceInput<T>): NotificationOccurrenceResolution<T>;
34
+ //# sourceMappingURL=occurrences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"occurrences.d.ts","sourceRoot":"","sources":["../src/occurrences.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,mBAAmB,EAEzB,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,iBAAiB,GAAG,MAAM,MAAM,CAAC;AAE7C,MAAM,WAAW,2BAA2B,CAAC,CAAC,SAAS,sBAAsB;IAC3E,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,sBAAsB,GAAG,sBAAsB,IAAI,CAAC,GAAG;IAChG,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,iBAAiB,GAAG,wBAAwB,CAAC;AAE5F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;CAC1C;AAED,MAAM,WAAW,gCAAgC,CAAC,CAAC,SAAS,sBAAsB;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,SAAS,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAChD;AAWD,wBAAgB,8BAA8B,CAAC,CAAC,SAAS,sBAAsB,EAC7E,KAAK,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACpC,gCAAgC,CAAC,CAAC,CAAC,CAgCrC"}
@@ -0,0 +1,84 @@
1
+ import { addCivilDays, civilDateForInstant, compareCivilDates, INVALID_TIME_ZONE_CODE, NONEXISTENT_LOCAL_TIME_CODE, resolveZonedLocalTime, } from '@baukit/localization-core';
2
+ import { NotificationPlanError } from './errors.js';
3
+ import { assertIdentity, compareByInstant, compareCodeUnits } from './validation.js';
4
+ const TIME_ZONE_PROBE_DATE = '2000-01-01';
5
+ const TIME_ZONE_PROBE_TIME = '12:00';
6
+ export function resolveNotificationOccurrences(input) {
7
+ const window = horizonWindow(input);
8
+ const seen = new Set();
9
+ const desired = [];
10
+ const skipped = [];
11
+ for (const occurrence of input.occurrences) {
12
+ assertIdentity(occurrence, seen);
13
+ const result = resolveZonedLocalTime({
14
+ civilDate: occurrence.civilDate,
15
+ civilTime: occurrence.civilTime,
16
+ timeZone: input.timeZone,
17
+ gap: input.gap,
18
+ fold: input.fold,
19
+ });
20
+ const reason = skipReason(occurrence, result, window);
21
+ if (reason !== null) {
22
+ skipped.push({ logicalId: occurrence.logicalId, reason });
23
+ }
24
+ else if (result.ok) {
25
+ desired.push({
26
+ ...occurrence,
27
+ epochMilliseconds: result.epochMilliseconds,
28
+ transition: result.transition,
29
+ });
30
+ }
31
+ }
32
+ return {
33
+ ...window,
34
+ desired: desired.sort(compareByInstant),
35
+ skipped: skipped.sort((left, right) => compareCodeUnits(left.logicalId, right.logicalId)),
36
+ };
37
+ }
38
+ function horizonWindow(input) {
39
+ assertTimeZoneAndPolicies(input);
40
+ if (!Number.isSafeInteger(input.horizonDays) || input.horizonDays < 1) {
41
+ throw new NotificationPlanError('invalid_horizon');
42
+ }
43
+ const now = input.clock();
44
+ if (!Number.isFinite(now) || Number.isNaN(new Date(now).getTime())) {
45
+ throw new NotificationPlanError('invalid_clock');
46
+ }
47
+ const firstCivilDate = civilDateForInstant(now, input.timeZone);
48
+ try {
49
+ return {
50
+ now,
51
+ firstCivilDate,
52
+ lastCivilDate: addCivilDays(firstCivilDate, input.horizonDays - 1),
53
+ };
54
+ }
55
+ catch {
56
+ throw new NotificationPlanError('invalid_horizon');
57
+ }
58
+ }
59
+ function assertTimeZoneAndPolicies(input) {
60
+ const probe = resolveZonedLocalTime({
61
+ civilDate: TIME_ZONE_PROBE_DATE,
62
+ civilTime: TIME_ZONE_PROBE_TIME,
63
+ timeZone: input.timeZone,
64
+ gap: input.gap,
65
+ fold: input.fold,
66
+ });
67
+ if (!probe.ok && probe.code === INVALID_TIME_ZONE_CODE) {
68
+ throw new NotificationPlanError('invalid_time_zone');
69
+ }
70
+ }
71
+ function skipReason(occurrence, result, window) {
72
+ if (!result.ok && result.code !== NONEXISTENT_LOCAL_TIME_CODE) {
73
+ throw new NotificationPlanError(result.code, occurrence.logicalId);
74
+ }
75
+ if (compareCivilDates(occurrence.civilDate, window.firstCivilDate) < 0 ||
76
+ compareCivilDates(occurrence.civilDate, window.lastCivilDate) > 0) {
77
+ return 'outside_horizon';
78
+ }
79
+ if (!result.ok) {
80
+ return 'nonexistent_local_time';
81
+ }
82
+ return result.epochMilliseconds <= window.now ? 'past' : null;
83
+ }
84
+ //# sourceMappingURL=occurrences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"occurrences.js","sourceRoot":"","sources":["../src/occurrences.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { type PlannedNotification } from './validation.js';
2
+ export declare const OWNED_NOTIFICATION_DATA_KEY = "baukitNotification";
3
+ export declare const OWNED_NOTIFICATION_MARKER_VERSION = 1;
4
+ export interface OwnedNotificationMarker extends PlannedNotification {
5
+ readonly namespace: string;
6
+ }
7
+ export declare function ownedNotificationIdentifier(namespace: string, logicalId: string): string;
8
+ export declare function encodeOwnedNotificationMarker(marker: OwnedNotificationMarker): string;
9
+ export declare function decodeOwnedNotificationMarker(value: unknown): OwnedNotificationMarker | null;
10
+ export declare function isOwnedBy(namespace: string, identifier: string, marker: OwnedNotificationMarker | null): marker is OwnedNotificationMarker;
11
+ //# sourceMappingURL=ownership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../src/ownership.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,mBAAmB,EACzB,MAAM,iBAAiB,CAAC;AAEzB,eAAO,MAAM,2BAA2B,uBAAuB,CAAC;AAChE,eAAO,MAAM,iCAAiC,IAAI,CAAC;AAKnD,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGxF;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,uBAAuB,GAAG,MAAM,CAQrF;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAyB5F;AAED,wBAAgB,SAAS,CACvB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,uBAAuB,GAAG,IAAI,GACrC,MAAM,IAAI,uBAAuB,CAMnC"}
@@ -0,0 +1,49 @@
1
+ import { assertNamespace, isValidContentDigest, isValidInstant, isValidLogicalId, isValidNamespace, } from './validation.js';
2
+ export const OWNED_NOTIFICATION_DATA_KEY = 'baukitNotification';
3
+ export const OWNED_NOTIFICATION_MARKER_VERSION = 1;
4
+ const IDENTIFIER_PREFIX = 'baukit';
5
+ const IDENTIFIER_SEPARATOR = ':';
6
+ export function ownedNotificationIdentifier(namespace, logicalId) {
7
+ assertNamespace(namespace);
8
+ return [IDENTIFIER_PREFIX, namespace, logicalId].join(IDENTIFIER_SEPARATOR);
9
+ }
10
+ export function encodeOwnedNotificationMarker(marker) {
11
+ return JSON.stringify({
12
+ version: OWNED_NOTIFICATION_MARKER_VERSION,
13
+ namespace: marker.namespace,
14
+ logicalId: marker.logicalId,
15
+ epochMilliseconds: marker.epochMilliseconds,
16
+ contentDigest: marker.contentDigest,
17
+ });
18
+ }
19
+ export function decodeOwnedNotificationMarker(value) {
20
+ if (typeof value !== 'string') {
21
+ return null;
22
+ }
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(value);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ if (typeof parsed !== 'object' || parsed === null) {
31
+ return null;
32
+ }
33
+ const record = parsed;
34
+ const { namespace, logicalId, epochMilliseconds, contentDigest } = record;
35
+ if (record['version'] !== OWNED_NOTIFICATION_MARKER_VERSION ||
36
+ !isValidNamespace(namespace) ||
37
+ !isValidLogicalId(logicalId) ||
38
+ !isValidInstant(epochMilliseconds) ||
39
+ !isValidContentDigest(contentDigest)) {
40
+ return null;
41
+ }
42
+ return { namespace, logicalId, epochMilliseconds, contentDigest };
43
+ }
44
+ export function isOwnedBy(namespace, identifier, marker) {
45
+ return (marker !== null &&
46
+ marker.namespace === namespace &&
47
+ identifier === ownedNotificationIdentifier(namespace, marker.logicalId));
48
+ }
49
+ //# sourceMappingURL=ownership.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownership.js","sourceRoot":"","sources":["../src/ownership.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GAEjB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,2BAA2B,GAAG,oBAAoB,CAAC;AAChE,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,CAAC;AAEnD,MAAM,iBAAiB,GAAG,QAAQ,CAAC;AACnC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAMjC,MAAM,UAAU,2BAA2B,CAAC,SAAiB,EAAE,SAAiB;IAC9E,eAAe,CAAC,SAAS,CAAC,CAAC;IAC3B,OAAO,CAAC,iBAAiB,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,MAA+B;IAC3E,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,OAAO,EAAE,iCAAiC;QAC1C,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;QAC3C,aAAa,EAAE,MAAM,CAAC,aAAa;KACpC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,KAAc;IAC1D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,iBAAiB,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC;IAC1E,IACE,MAAM,CAAC,SAAS,CAAC,KAAK,iCAAiC;QACvD,CAAC,gBAAgB,CAAC,SAAS,CAAC;QAC5B,CAAC,gBAAgB,CAAC,SAAS,CAAC;QAC5B,CAAC,cAAc,CAAC,iBAAiB,CAAC;QAClC,CAAC,oBAAoB,CAAC,aAAa,CAAC,EACpC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,iBAAiB,EAAE,aAAa,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,SAAiB,EACjB,UAAkB,EAClB,MAAsC;IAEtC,OAAO,CACL,MAAM,KAAK,IAAI;QACf,MAAM,CAAC,SAAS,KAAK,SAAS;QAC9B,UAAU,KAAK,2BAA2B,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,CACxE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,57 @@
1
+ import type { FoldPolicy, GapPolicy, LocalTimeTransition } from '@baukit/localization-core';
2
+ import { type NotificationOccurrence, type SkippedOccurrence } from './occurrences.js';
3
+ export interface NotificationPlanVectorCurrent {
4
+ readonly logicalId: string;
5
+ readonly instant: string;
6
+ readonly contentDigest: string;
7
+ }
8
+ export interface NotificationPlanVectorInput {
9
+ readonly timeZone: string;
10
+ readonly gap: GapPolicy;
11
+ readonly fold: FoldPolicy;
12
+ readonly now: string;
13
+ readonly horizonDays: number;
14
+ readonly replaceAll?: boolean;
15
+ readonly occurrences: readonly NotificationOccurrence[];
16
+ readonly current: readonly NotificationPlanVectorCurrent[];
17
+ }
18
+ export interface NotificationPlanVectorResult {
19
+ readonly firstCivilDate: string;
20
+ readonly lastCivilDate: string;
21
+ readonly schedule: readonly {
22
+ readonly logicalId: string;
23
+ readonly instant: string;
24
+ readonly transition: LocalTimeTransition;
25
+ }[];
26
+ readonly keep: readonly string[];
27
+ readonly cancel: readonly string[];
28
+ readonly skipped: readonly SkippedOccurrence[];
29
+ }
30
+ export interface NotificationPlanVectorError {
31
+ readonly error: {
32
+ readonly code: string;
33
+ readonly logicalId?: string;
34
+ };
35
+ }
36
+ export interface NotificationPlanVectorCase {
37
+ readonly name: string;
38
+ readonly input: NotificationPlanVectorInput;
39
+ readonly expected: NotificationPlanVectorResult | NotificationPlanVectorError;
40
+ }
41
+ export interface NotificationPlanVectorFixture {
42
+ readonly version: number;
43
+ readonly cases: readonly NotificationPlanVectorCase[];
44
+ }
45
+ /** One vector: `actual()` must deep-equal `expected` (`undefined` fields count as absent). */
46
+ export interface NotificationPlanVectorCheck {
47
+ readonly label: string;
48
+ readonly expected: unknown;
49
+ readonly actual: () => unknown;
50
+ }
51
+ /**
52
+ * Expands `fixtures/notifications/plan-vectors-v1.json` into checks. Each success case yields
53
+ * three: the expected plan, convergence once the plan is applied, and independence from input
54
+ * order. Each error case yields one check of the thrown `NotificationPlanError` code and ID.
55
+ */
56
+ export declare function notificationPlanVectorChecks(fixture: NotificationPlanVectorFixture): NotificationPlanVectorCheck[];
57
+ //# sourceMappingURL=plan-vectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-vectors.d.ts","sourceRoot":"","sources":["../src/plan-vectors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAG5F,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAI1B,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACxD,QAAQ,CAAC,OAAO,EAAE,SAAS,6BAA6B,EAAE,CAAC;CAC5D;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS;QAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;KAC1C,EAAE,CAAC;IACJ,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAChD;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,2BAA2B,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,4BAA4B,GAAG,2BAA2B,CAAC;CAC/E;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,0BAA0B,EAAE,CAAC;CACvD;AAED,8FAA8F;AAC9F,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC;CAChC;AAQD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,2BAA2B,EAAE,CAI/B"}