@phis/contracts 0.7.3 → 0.7.4

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.
package/README.md CHANGED
@@ -61,7 +61,14 @@ erased at compile time apart from a handful of constants.
61
61
  What it covers:
62
62
 
63
63
  - **Manifest** -- `PhisAddonManifestV1`: identity, version, required Core capabilities, the routes an
64
- Add-on claims, the services it provides.
64
+ Add-on claims, the services it provides, the jobs it runs and the events it listens to or emits.
65
+ - **Jobs and events** -- a job carries a `PhisAddonJobSchedule` (`every` one of
66
+ `PHIS_ADDON_JOB_INTERVALS`, five minutes at the shortest, with `at` and `weekday` for the daily and
67
+ weekly steps) that Core keeps: each moment of a schedule is run by exactly one instance. An event an
68
+ Add-on emits is a `PhisAddonEmittedEventId`, its own id in front (`@scope/name:what.happened`),
69
+ declared under `emits` and sent through the `events` capability (`PhisEventsCapabilityV1`); a listener
70
+ receives a `PhisAddonEmittedEventSubject` with emitter, time, hop count, dedupe key and facts. Core's
71
+ own events keep their bare names. `PHIS_ADDON_EVENT_MAX_HOPS` bounds a chain of handlers emitting.
65
72
  - **Runtime** -- `PhisAddonRuntimeV1`, `PhisAddonHandler`, `PhisAddonRequestContext`. The artifact
66
73
  exports it under the name `phisAddon` (`PhisAddonRuntimeModuleV1`).
67
74
  - **Schema** -- `PhisAddonSchemaDescriptor` and its column, index and constraint descriptors. Tables are
package/dist/addon.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * back a constant, it is in the wrong package.
13
13
  */
14
14
  import type { PhisAddonCapabilities, PhisAddonEventHandler, PhisAddonJobHandler, PhisSiteAccess } from "./capabilities.js";
15
- import type { PhisAddonEventId, PhisCoreCapabilityId, PhisServiceKind } from "./core.js";
15
+ import type { PhisAddonEmittedEventId, PhisAddonEventId, PhisCoreCapabilityId, PhisServiceKind } from "./core.js";
16
16
  import type { PhisAddonQueryCatalog } from "./queries.js";
17
17
  import type { PhisAddonSchemaDescriptor } from "./schema.js";
18
18
  export declare const PHIS_ADDON_MANIFEST_VERSION: 1;
@@ -101,17 +101,61 @@ export type PhisAddonServiceProviderDescriptor = {
101
101
  factoryExport: string;
102
102
  requiredCoreCapabilities: PhisCoreCapabilityId[];
103
103
  };
104
+ /**
105
+ * The steps a job may run at. Every step divides the hour or the day, so the moments a schedule names
106
+ * fall on the clock -- `5m` is :00, :05, :10 -- and every instance of Core computes the same moment
107
+ * from the same clock without asking another.
108
+ *
109
+ * Nothing shorter than five minutes: a job that has to look more often than that is polling for an
110
+ * event, and an event is what it should be given. Jobs run per Site, so a short step multiplies by
111
+ * the Sites of a stage, and the services a job reads from have quotas of their own.
112
+ */
113
+ export declare const PHIS_ADDON_JOB_INTERVALS: readonly ["5m", "15m", "30m", "hourly", "daily", "weekly"];
114
+ export type PhisAddonJobInterval = (typeof PHIS_ADDON_JOB_INTERVALS)[number];
115
+ /**
116
+ * When a job wants to run. Core keeps it: the schedule is a promise, not a note for the operator.
117
+ *
118
+ * A schedule is a series of moments, and each moment is claimed by exactly one instance of Core --
119
+ * the claim is a row whose key is the moment, so the database, not a leader, decides who runs. A
120
+ * moment nobody claimed while every instance was down is run once at the next start, the latest one
121
+ * only; a job does not receive a week of missed runs after a week of downtime. A job still running
122
+ * when its next moment comes has that moment recorded as skipped rather than run beside itself, which
123
+ * is why a handler must finish well inside its own step.
124
+ */
125
+ export type PhisAddonJobSchedule = {
126
+ every: PhisAddonJobInterval;
127
+ /**
128
+ * The time of day, `HH:MM` in UTC, for `daily` and `weekly`; absent means `00:00`. Not for the shorter
129
+ * steps, which fall on the clock by themselves.
130
+ */
131
+ at?: string;
132
+ /** The ISO weekday for `weekly`, 1 (Monday) to 7 (Sunday); absent means Monday. */
133
+ weekday?: number;
134
+ };
104
135
  export type PhisAddonJobDescriptor = {
105
136
  id: string;
106
137
  handler: string;
107
138
  /**
108
- * How often this job expects to run, for the operator to read -- never a schedule Core keeps.
139
+ * When Core runs this job. Absent means it runs only when an operator starts it with `phis addon
140
+ * job`, which also runs a scheduled job out of turn.
109
141
  *
110
- * phi-server serves from several instances, so an in-process timer would fire once per instance for
111
- * one intended run. The operator schedules `phis addon job run`, which is one runner by construction,
112
- * and this field tells them what the Add-on had in mind.
142
+ * Optional like every field added after this manifest version was in the wild.
113
143
  */
114
- suggestedInterval?: "hourly" | "daily" | "weekly";
144
+ schedule?: PhisAddonJobSchedule;
145
+ };
146
+ /**
147
+ * One event this Add-on announces under its own name.
148
+ *
149
+ * Declared like a job or a route: the id, its emitter's own id in front, is part of the manifest and
150
+ * therefore of the digest, so what an installed Add-on can say to the others is readable at install.
151
+ * Emitting an undeclared event is refused. `audience` says who may listen: every Add-on of the Site
152
+ * that declares a handler for it, which is the default, because the operator installed each of them;
153
+ * or `self`, for an Add-on that uses the outbox to hand work to its own handler after the fact.
154
+ */
155
+ export type PhisAddonEmittedEventDescriptor = {
156
+ id: PhisAddonEmittedEventId;
157
+ description?: string;
158
+ audience?: "stage" | "self";
115
159
  };
116
160
  /**
117
161
  * One setting an operator may configure for this Add-on.
@@ -259,6 +303,13 @@ export type PhisAddonManifestV1 = {
259
303
  * Add-on that declares them and links nothing is told nothing.
260
304
  */
261
305
  eventHandlers?: PhisAddonEventHandlerDescriptor[];
306
+ /**
307
+ * The events this Add-on emits. Absent means none, and an emit of anything is refused.
308
+ *
309
+ * Optional like `eventHandlers`. Declaring an event is not delivering it: a row is written only for
310
+ * an Add-on of the Site that declares a handler for the id, and none at all where nobody listens.
311
+ */
312
+ emits?: PhisAddonEmittedEventDescriptor[];
262
313
  /**
263
314
  * What an operator may configure. Absent means nothing is.
264
315
  *
package/dist/addon.js CHANGED
@@ -13,3 +13,13 @@
13
13
  */
14
14
  export const PHIS_ADDON_MANIFEST_VERSION = 1;
15
15
  export const PHIS_ADDON_ABI_VERSION = 1;
16
+ /**
17
+ * The steps a job may run at. Every step divides the hour or the day, so the moments a schedule names
18
+ * fall on the clock -- `5m` is :00, :05, :10 -- and every instance of Core computes the same moment
19
+ * from the same clock without asking another.
20
+ *
21
+ * Nothing shorter than five minutes: a job that has to look more often than that is polling for an
22
+ * event, and an event is what it should be given. Jobs run per Site, so a short step multiplies by
23
+ * the Sites of a stage, and the services a job reads from have quotas of their own.
24
+ */
25
+ export const PHIS_ADDON_JOB_INTERVALS = ["5m", "15m", "30m", "hourly", "daily", "weekly"];
@@ -12,7 +12,7 @@
12
12
  * at the point where it made the mistake.
13
13
  */
14
14
  import type { PhisAssetsCapabilityV1 } from "./assets.js";
15
- import type { PhisAddonThreadEventId } from "./core.js";
15
+ import type { PhisAddonEmittedEventId, PhisAddonThreadEventId } from "./core.js";
16
16
  import type { PhisPaymentChangedEventFacts, PhisPaymentNotificationsCapabilityV1, PhisPaymentsCapabilityV1 } from "./payments.js";
17
17
  import type { PhisDataCapabilityV1 } from "./queries.js";
18
18
  import type { PhisResourceLinksCapabilityV1 } from "./resource-links.js";
@@ -109,6 +109,52 @@ export type PhisAddonCapabilities = {
109
109
  paymentNotifications?: PhisPaymentNotificationsCapabilityV1;
110
110
  /** Writing to a person about an occasion this Add-on declared; Core owns the frame and the sending. */
111
111
  notifications?: PhisNotificationsCapabilityV1;
112
+ /** Announcing an event this Add-on declared under its own name; Core owns the outbox and the delivery. */
113
+ events?: PhisEventsCapabilityV1;
114
+ };
115
+ /**
116
+ * The events capability: `@phis/server/events:v1`.
117
+ *
118
+ * An Add-on says that something happened; Core writes one outbox row per Add-on of this Site that
119
+ * declares a handler for the event, and delivers them the way it delivers its own events -- after the
120
+ * fact, in order, retried until they arrive. Emitting is only a write to the outbox, so it is cheap and
121
+ * never waits for a listener.
122
+ *
123
+ * `facts` are values about what happened, as JSON, bounded in size, and never references into another
124
+ * Add-on's tables: a listener runs with authority over its own tables only, and a fact it cannot read
125
+ * is a fact it should have been handed. `dedupeKey` makes a repeated emit safe -- a second call with
126
+ * the same key writes nothing for a listener already owed the first.
127
+ *
128
+ * Refused: an event the manifest does not declare under `emits`, a call from an unverified hook, and
129
+ * an emit from a handler already at `PHIS_ADDON_EVENT_MAX_HOPS`. Answers how many listeners were
130
+ * written for; zero means nobody on this Site listens, which is not a failure.
131
+ */
132
+ export type PhisEventsCapabilityV1 = {
133
+ emit(input: PhisAddonEmitEventInput): Promise<{
134
+ recipients: number;
135
+ }>;
136
+ };
137
+ export type PhisAddonEmitEventInput = {
138
+ event: PhisAddonEmittedEventId;
139
+ facts?: Readonly<Record<string, unknown>>;
140
+ /** Unique within the Site and the event; a second emit with the same key writes nothing new. */
141
+ dedupeKey?: string;
142
+ };
143
+ /**
144
+ * What a listener is handed about an event another Add-on -- or the same one -- emitted.
145
+ *
146
+ * The emitter's id says who, `occurredAt` when (the moment the outbox row was written), `hop` how many
147
+ * handlers the chain has passed through so far, and `facts` what the emitter said. `dedupeKey` is the
148
+ * emitter's, where it gave one; the delivery's own key, stable across retries, is `deliveryId` on the
149
+ * context, as for every event.
150
+ */
151
+ export type PhisAddonEmittedEventSubject = {
152
+ kind: "emitted";
153
+ emitter: string;
154
+ occurredAt: string;
155
+ hop: number;
156
+ dedupeKey: string | null;
157
+ facts: Readonly<Record<string, unknown>>;
112
158
  };
113
159
  /**
114
160
  * The roles capability: `@phis/server/roles:v1`.
@@ -399,8 +445,9 @@ export type PhisSettingsCapabilityV1 = {
399
445
  *
400
446
  * No request, no headers, and above all no actor: a job is the Add-on acting on its own data, not a
401
447
  * person acting through it. That is why the row-level ladder does not apply inside one, and why a job
402
- * is reachable only from `phis addon job run` and never from a route -- an Add-on that could start its
403
- * own job from a request would have built itself a way around the ladder.
448
+ * is started only by Core's scheduler at the moments its `schedule` names, or by an operator with
449
+ * `phis addon job`, and never from a route -- an Add-on that could start its own job from a request
450
+ * would have built itself a way around the ladder.
404
451
  */
405
452
  export type PhisAddonJobContext = {
406
453
  addonId: string;
@@ -434,7 +481,10 @@ export type PhisAddonEventFacts = {
434
481
  threadId: number;
435
482
  messageId: number | null;
436
483
  };
437
- } | PhisPaymentChangedEventFacts;
484
+ } | PhisPaymentChangedEventFacts | {
485
+ event: PhisAddonEmittedEventId;
486
+ subject: PhisAddonEmittedEventSubject;
487
+ };
438
488
  export type PhisAddonEventContext = PhisAddonEventFacts & {
439
489
  addonId: string;
440
490
  /** This delivery, stable across retries, so a handler can recognise one it already settled. */
package/dist/core.d.ts CHANGED
@@ -50,6 +50,8 @@ export declare const PHIS_CORE_CAPABILITIES: {
50
50
  readonly paymentNotifications: "@phis/server/payment-notifications:v1";
51
51
  /** Writing to a person about an occasion the Add-on declared, with Core owning frame and transport. */
52
52
  readonly notifications: "@phis/server/notifications:v1";
53
+ /** Announcing an event this Add-on declared under its own name, for the Add-ons of this Site that listen. */
54
+ readonly events: "@phis/server/events:v1";
53
55
  };
54
56
  export type PhisCoreCapabilityId = (typeof PHIS_CORE_CAPABILITIES)[keyof typeof PHIS_CORE_CAPABILITIES];
55
57
  /**
@@ -75,8 +77,27 @@ export declare const PHIS_ADDON_EVENTS: {
75
77
  readonly paymentChanged: "payment.changed";
76
78
  };
77
79
  export type PhisAddonThreadEventId = Exclude<(typeof PHIS_ADDON_EVENTS)[keyof typeof PHIS_ADDON_EVENTS], "payment.changed">;
78
- /** Delivered v1 event vocabulary, discriminated by its owned subject. */
79
- export type PhisAddonEventId = PhisAddonThreadEventId | PhisPaymentChangedEventFacts["event"];
80
+ /** The events Core itself emits: the thread events and the Payment event. */
81
+ export type PhisCoreEventId = PhisAddonThreadEventId | PhisPaymentChangedEventFacts["event"];
82
+ /**
83
+ * An event an Add-on emits, named under the Add-on that emits it: `@scope/name:what.happened`.
84
+ *
85
+ * The emitter's own id is the first half, so two Add-ons cannot name the same event and a reader of
86
+ * a manifest sees who announces it without looking anything up. The second half is the Add-on's own
87
+ * word for it, `^[a-z][a-z0-9.-]*$`, the shape Core's own event names have. Core's names stay bare:
88
+ * a bare name is Core's, a prefixed one is an Add-on's, and the two can never be confused.
89
+ */
90
+ export type PhisAddonEmittedEventId = `@${string}/${string}:${string}`;
91
+ /** Delivered v1 event vocabulary: Core's own events, and what an installed Add-on emits. */
92
+ export type PhisAddonEventId = PhisCoreEventId | PhisAddonEmittedEventId;
93
+ /**
94
+ * How many Add-ons an event may pass through.
95
+ *
96
+ * An Add-on that emits inside an event handler writes an event with the incoming hop plus one. A
97
+ * handler that answers every event with an event, met by another doing the same, would otherwise
98
+ * write forever; at this depth the emit is refused and logged instead of delivered.
99
+ */
100
+ export declare const PHIS_ADDON_EVENT_MAX_HOPS = 4;
80
101
  /**
81
102
  * The Core-owned service kinds an Add-on may implement.
82
103
  *
package/dist/core.js CHANGED
@@ -49,6 +49,8 @@ export const PHIS_CORE_CAPABILITIES = {
49
49
  paymentNotifications: "@phis/server/payment-notifications:v1",
50
50
  /** Writing to a person about an occasion the Add-on declared, with Core owning frame and transport. */
51
51
  notifications: "@phis/server/notifications:v1",
52
+ /** Announcing an event this Add-on declared under its own name, for the Add-ons of this Site that listen. */
53
+ events: "@phis/server/events:v1",
52
54
  };
53
55
  /**
54
56
  * What Core tells an Add-on about after the fact.
@@ -72,6 +74,14 @@ export const PHIS_ADDON_EVENTS = {
72
74
  threadLinkRemoved: "thread.link.removed",
73
75
  paymentChanged: "payment.changed",
74
76
  };
77
+ /**
78
+ * How many Add-ons an event may pass through.
79
+ *
80
+ * An Add-on that emits inside an event handler writes an event with the incoming hop plus one. A
81
+ * handler that answers every event with an event, met by another doing the same, would otherwise
82
+ * write forever; at this depth the emit is refused and logged instead of delivered.
83
+ */
84
+ export const PHIS_ADDON_EVENT_MAX_HOPS = 4;
75
85
  /**
76
86
  * The Core-owned service kinds an Add-on may implement.
77
87
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phis/contracts",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "publishConfig": {