first commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# `services/apn-service`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Apple Push Notification delivery service. Validates an `ApnConfig` at
|
||||
construction-time, builds a pluggable `ApnTransport` (defaulting to
|
||||
the deterministic `MemoryApnTransport`), translates the public
|
||||
`PushNotificationPayload` shape into the transport's library-agnostic
|
||||
notification format, and maps the transport's response into a
|
||||
`PushNotificationResult` envelope. Per-device delivery failures NEVER
|
||||
throw — they land in the result's `errors[]` array so callers can
|
||||
branch on them without try/catch noise.
|
||||
|
||||
## Features
|
||||
|
||||
- Three stable failure modes: `APN_CONFIG_MISSING` (no auth
|
||||
material), `APN_CONFIG_INVALID` (partial token auth),
|
||||
`APN_NOT_INITIALIZED` (send called after `shutdown()`), plus
|
||||
transport-level codes (`APN_TRANSPORT_INIT_FAILED`,
|
||||
`APN_TRANSPORT_SHUTDOWN_FAILED`).
|
||||
- Auto-initialise on first send — `sendNotification(...)` calls
|
||||
`initialize()` if the transport isn't live yet.
|
||||
- Bulk send batches in a single transport call so the underlying
|
||||
HTTP/2 connection is reused.
|
||||
- `isInitialized()` returns the live state so route handlers can
|
||||
short-circuit to `503` when push is disabled.
|
||||
- Default `pushType` resolution mirrors APN's contract: `"alert"` when
|
||||
the payload carries title or body, `"background"` when
|
||||
`contentAvailable` is set, otherwise `"alert"`. Callers may
|
||||
override.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
ApnService,
|
||||
createMemoryApnTransport,
|
||||
} from "@suaveplan/api-plugin-notifications";
|
||||
|
||||
const service = new ApnService({
|
||||
config: {
|
||||
token: { key: "/keys/AuthKey_ABC.p8", keyId: "ABC", teamId: "TEAM" },
|
||||
production: false,
|
||||
defaultTopic: "com.example.app",
|
||||
},
|
||||
transportFactory: createMemoryApnTransport,
|
||||
});
|
||||
await service.initialize();
|
||||
const result = await service.sendNotification({
|
||||
deviceToken: "deadbeef",
|
||||
alert: { title: "Hi", body: "Welcome" },
|
||||
badge: 1,
|
||||
});
|
||||
console.log(result.sent, result.failed);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a custom transport (e.g. a real `node-apn` adapter) and let the
|
||||
service own the validation + payload translation:
|
||||
|
||||
```ts
|
||||
import { ApnService, type ApnTransport } from "@suaveplan/api-plugin-notifications";
|
||||
|
||||
class NodeApnTransport implements ApnTransport {
|
||||
async initialize() { /* … */ }
|
||||
async send(n, tokens) { /* … */ }
|
||||
async shutdown() { /* … */ }
|
||||
}
|
||||
|
||||
const service = new ApnService({
|
||||
config: parsedApnConfig,
|
||||
transport: new NodeApnTransport(),
|
||||
});
|
||||
const result = await service.sendBulkNotifications(
|
||||
["tok1", "tok2", "tok3"],
|
||||
{ alert: { title: "Bulk", body: "Send" } }
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ApnService` — class with `initialize` / `sendNotification` /
|
||||
`sendBulkNotifications` / `shutdown` / `isInitialized`.
|
||||
- `ApnServiceOptions` — `{ config, transportFactory?, transport? }`.
|
||||
- `PushNotificationPayload` — single-message payload.
|
||||
- `PushNotificationFailure` — per-device failure (`deviceToken`,
|
||||
`error`, optional `status` / `reason`).
|
||||
- `PushNotificationResult` — `{ sent, failed, errors? }`.
|
||||
- `createApnService(options)` — thin factory matching the upstream
|
||||
surface.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The service never imports `@parse/node-apn` directly — that library
|
||||
is deliberately absent from the catalog (network-touching
|
||||
adapters are injected by the host). Tests use the
|
||||
`MemoryApnTransport` with optional failure plans so every error
|
||||
branch is exercised without touching the network. Production hosts
|
||||
write a thin `NodeApnTransport` adapter against their existing
|
||||
`apn.Provider` and pass it in via `options.transport`.
|
||||
@@ -0,0 +1,62 @@
|
||||
# `services/registry-augmentation`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Module-augmentation file that declares three new service slots on
|
||||
`@suaveplan/api-plugin/service-registry`'s `ServiceRegistry`
|
||||
interface: `notificationRepository`, `deviceTokenRepository`, and
|
||||
`apnService`. Without this augmentation the typed `ctx.getService(...)`
|
||||
helper would return `unknown` for these keys; with it, downstream
|
||||
plugins resolve the slots with no casts.
|
||||
|
||||
## Features
|
||||
|
||||
- Cumulative module augmentation — the file only declares the slots
|
||||
this plugin owns. Host slots (`app`, `db`, `logger`, `eventBus`,
|
||||
`clock`) are declared by `@suaveplan/api`'s own augmentation; the
|
||||
TypeScript compiler merges every declaration in the program into a
|
||||
single `ServiceRegistry` shape.
|
||||
- Side-effect import — the package barrel re-imports this module via
|
||||
`import "./services/registry-augmentation.js"` so any consumer
|
||||
that pulls a single public symbol from `@suaveplan/api-plugin-notifications`
|
||||
gets the type-only augmentation automatically.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import "@suaveplan/api-plugin-notifications";
|
||||
|
||||
const repo = ctx.getService("notificationRepository");
|
||||
// repo is INotificationRepository | undefined — no cast.
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
When using a downstream plugin that depends on the notifications
|
||||
plugin's typed registry, the registry import flows transitively
|
||||
through the side-effect import:
|
||||
|
||||
```ts
|
||||
import { createMyDownstreamPlugin } from "@my-org/api-plugin-downstream";
|
||||
import "@suaveplan/api-plugin-notifications";
|
||||
|
||||
// Downstream plugin's own augmentation merges with this one — both
|
||||
// `myService` and `notificationRepository` resolve typed.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
There is no runtime export — the file only declares
|
||||
`declare module "@suaveplan/api-plugin/service-registry"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The file deliberately re-declares the host slots `app` and `db` so
|
||||
intellisense never claims they are absent in a context that imports
|
||||
only this plugin. Module augmentation is cumulative, so the
|
||||
re-declaration merges with `@suaveplan/api`'s own augmentation rather
|
||||
than overriding it.
|
||||
Reference in New Issue
Block a user