@clivly/core 0.10.2 → 0.10.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/dist/integration-state.cjs +46 -2
- package/dist/integration-state.d.cts +37 -1
- package/dist/integration-state.d.mts +37 -1
- package/dist/integration-state.mjs +46 -2
- package/package.json +13 -13
|
@@ -31,6 +31,32 @@ const STEP_PREREQUISITES = {
|
|
|
31
31
|
function stepIndex(id) {
|
|
32
32
|
return INTEGRATION_STEPS.indexOf(id);
|
|
33
33
|
}
|
|
34
|
+
const UNREACHABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
35
|
+
502,
|
|
36
|
+
503,
|
|
37
|
+
504,
|
|
38
|
+
520,
|
|
39
|
+
521,
|
|
40
|
+
522,
|
|
41
|
+
523,
|
|
42
|
+
524,
|
|
43
|
+
525,
|
|
44
|
+
526,
|
|
45
|
+
527,
|
|
46
|
+
530
|
|
47
|
+
]);
|
|
48
|
+
/**
|
|
49
|
+
* Did the last trigger attempt fail to reach the app at all?
|
|
50
|
+
*
|
|
51
|
+
* Deliberately narrow. A signature rejection (401) or a handler error (500)
|
|
52
|
+
* both prove the trigger works — the request arrived. Only a total failure to
|
|
53
|
+
* get an answer un-ticks the rung, so this cannot demote setup progress for
|
|
54
|
+
* faults that live somewhere else.
|
|
55
|
+
*/
|
|
56
|
+
function triggerUnreachable(facts) {
|
|
57
|
+
const status = facts.lastRun?.status;
|
|
58
|
+
return facts.lastRun?.ok === false && status !== void 0 && UNREACHABLE_STATUSES.has(status);
|
|
59
|
+
}
|
|
34
60
|
function formatAge(ms) {
|
|
35
61
|
if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))}s ago`;
|
|
36
62
|
if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
|
|
@@ -103,6 +129,19 @@ function deriveLiveness(facts) {
|
|
|
103
129
|
} } : {}
|
|
104
130
|
};
|
|
105
131
|
if (facts.presence && facts.presence.transport === "websocket") return websocketLiveness(facts, facts.presence, base);
|
|
132
|
+
const probe = facts.reachabilityProbe;
|
|
133
|
+
if (probe?.reachable && facts.now - probe.checkedAt < staleMs) {
|
|
134
|
+
const probeAgeMs = facts.now - probe.checkedAt;
|
|
135
|
+
const issues = livenessIssues(facts);
|
|
136
|
+
return {
|
|
137
|
+
...base,
|
|
138
|
+
ageMs: probeAgeMs,
|
|
139
|
+
ageLabel: formatAge(probeAgeMs),
|
|
140
|
+
health: issues.length > 0 ? "live_with_issues" : "live",
|
|
141
|
+
issues,
|
|
142
|
+
reachable: true
|
|
143
|
+
};
|
|
144
|
+
}
|
|
106
145
|
if (ageMs === null || ageMs >= staleMs) return {
|
|
107
146
|
...base,
|
|
108
147
|
health: "offline",
|
|
@@ -131,7 +170,7 @@ function isSatisfied(id, facts) {
|
|
|
131
170
|
case "application": return facts.everConnected || age !== null;
|
|
132
171
|
case "schema": return facts.discoveredTableCount > 0;
|
|
133
172
|
case "mapping": return facts.mappedEntityCount > 0;
|
|
134
|
-
case "trigger": return facts.triggerConfigured;
|
|
173
|
+
case "trigger": return facts.triggerConfigured && !triggerUnreachable(facts);
|
|
135
174
|
case "sync": return facts.lastRun?.ok === true;
|
|
136
175
|
default: return facts.widgetInstalled;
|
|
137
176
|
}
|
|
@@ -141,11 +180,16 @@ function evidenceFor(id, facts) {
|
|
|
141
180
|
if (id === "application" && age !== null) return `heartbeat ${formatAge(age)}`;
|
|
142
181
|
if (id === "schema" && facts.discoveredTableCount > 0) return `found ${facts.discoveredTableCount} tables`;
|
|
143
182
|
if (id === "mapping" && facts.mappedEntityCount > 0) return `${facts.mappedEntityCount} entity mapped`;
|
|
144
|
-
if (id === "trigger" && facts.triggerConfigured) return "trigger URL and secret set";
|
|
183
|
+
if (id === "trigger" && facts.triggerConfigured) return triggerUnreachable(facts) ? "set, but the last attempt could not reach your app" : "trigger URL and secret set";
|
|
145
184
|
if (id === "sync" && facts.lastRun) return `last run ${formatAge(facts.now - facts.lastRun.at)}`;
|
|
146
185
|
return null;
|
|
147
186
|
}
|
|
148
187
|
function problemFor(id, facts) {
|
|
188
|
+
if (id === "trigger" && triggerUnreachable(facts)) return {
|
|
189
|
+
code: "trigger_unreachable",
|
|
190
|
+
message: "The trigger URL is set, but Clivly could not reach your app there.",
|
|
191
|
+
fix: "Check the app is running and serving that URL. A dev tunnel URL changes every session — `clivly dev` keeps it registered for you."
|
|
192
|
+
};
|
|
149
193
|
if (id === "sync" && facts.lastRun && !facts.lastRun.ok) return {
|
|
150
194
|
code: "sync_failed",
|
|
151
195
|
message: facts.lastRun.message ?? "The last sync run failed.",
|
|
@@ -18,6 +18,14 @@ interface RunFact {
|
|
|
18
18
|
at: number;
|
|
19
19
|
message?: string;
|
|
20
20
|
ok: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The HTTP status the host answered with, when there was one.
|
|
23
|
+
*
|
|
24
|
+
* Absent for a run that failed before any response existed, and for callers
|
|
25
|
+
* that do not track it — so every read must treat `undefined` as "unknown",
|
|
26
|
+
* never as "fine".
|
|
27
|
+
*/
|
|
28
|
+
status?: number;
|
|
21
29
|
}
|
|
22
30
|
/**
|
|
23
31
|
* Where the reporting app is running, as the app itself understands it.
|
|
@@ -55,6 +63,19 @@ interface IntegrationFacts {
|
|
|
55
63
|
* transport keeps the cadence-derived behaviour every SDK in the field uses.
|
|
56
64
|
*/
|
|
57
65
|
presence?: DeploymentPresence | null;
|
|
66
|
+
/**
|
|
67
|
+
* The result of Clivly asking the app directly whether it is up.
|
|
68
|
+
*
|
|
69
|
+
* Every other signal here is *pushed* by the app. On a serverless host there
|
|
70
|
+
* is no process between requests to push anything: the SDK's heartbeat timer
|
|
71
|
+
* fires once at boot and is then frozen, so cadence alone reports a perfectly
|
|
72
|
+
* healthy deployment as offline forever. A probe inverts that — Clivly asks,
|
|
73
|
+
* and a host that answers is up by definition.
|
|
74
|
+
*
|
|
75
|
+
* Absent keeps the pure cadence behaviour, so nothing changes for apps that
|
|
76
|
+
* do hold a long-lived process.
|
|
77
|
+
*/
|
|
78
|
+
reachabilityProbe?: ReachabilityProbe | null;
|
|
58
79
|
/**
|
|
59
80
|
* Faults the running app diagnosed about itself on its last heartbeat — an
|
|
60
81
|
* unreadable database, a secret missing from the process environment. These
|
|
@@ -84,6 +105,21 @@ declare function formatAge(ms: number): string;
|
|
|
84
105
|
/** The cadence `createClivlySDK` uses when the host app configures none. */
|
|
85
106
|
declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
|
|
86
107
|
type ConnectionHealth = "live" | "live_with_issues" | "stale" | "offline";
|
|
108
|
+
/**
|
|
109
|
+
* Clivly's own answer to "is this app up", obtained by asking it rather than
|
|
110
|
+
* waiting to be told.
|
|
111
|
+
*
|
|
112
|
+
* `reachable` is deliberately about the *connection*, not the response body: an
|
|
113
|
+
* app that answers `401` has proved it is running just as conclusively as one
|
|
114
|
+
* that answers `200`. Only silence — a timeout, a refused connection, DNS that
|
|
115
|
+
* does not resolve — means offline.
|
|
116
|
+
*/
|
|
117
|
+
interface ReachabilityProbe {
|
|
118
|
+
/** When Clivly performed the check. Probe results are cached, so this ages. */
|
|
119
|
+
checkedAt: number;
|
|
120
|
+
/** Did anything answer at the other end? */
|
|
121
|
+
reachable: boolean;
|
|
122
|
+
}
|
|
87
123
|
/** Which algorithm decides whether this deployment is up. */
|
|
88
124
|
type PresenceTransport = "websocket" | "http";
|
|
89
125
|
type PresenceConnectionState = "connected" | "reconnecting" | "disconnected";
|
|
@@ -172,4 +208,4 @@ declare const INSTALL_COMMAND = "npx clivly init";
|
|
|
172
208
|
declare function nextAction(steps: IntegrationStep[], status: IntegrationStatus, liveness: ConnectionLiveness): NextAction | null;
|
|
173
209
|
declare function computeIntegrationState(facts: IntegrationFacts): IntegrationState;
|
|
174
210
|
//#endregion
|
|
175
|
-
export { ConnectionHealth, ConnectionLiveness, DEFAULT_HEARTBEAT_INTERVAL_MS, DeploymentPresence, HostEnvironment, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, PresenceConnectionState, PresenceTransport, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
|
211
|
+
export { ConnectionHealth, ConnectionLiveness, DEFAULT_HEARTBEAT_INTERVAL_MS, DeploymentPresence, HostEnvironment, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, PresenceConnectionState, PresenceTransport, ReachabilityProbe, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
|
@@ -18,6 +18,14 @@ interface RunFact {
|
|
|
18
18
|
at: number;
|
|
19
19
|
message?: string;
|
|
20
20
|
ok: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The HTTP status the host answered with, when there was one.
|
|
23
|
+
*
|
|
24
|
+
* Absent for a run that failed before any response existed, and for callers
|
|
25
|
+
* that do not track it — so every read must treat `undefined` as "unknown",
|
|
26
|
+
* never as "fine".
|
|
27
|
+
*/
|
|
28
|
+
status?: number;
|
|
21
29
|
}
|
|
22
30
|
/**
|
|
23
31
|
* Where the reporting app is running, as the app itself understands it.
|
|
@@ -55,6 +63,19 @@ interface IntegrationFacts {
|
|
|
55
63
|
* transport keeps the cadence-derived behaviour every SDK in the field uses.
|
|
56
64
|
*/
|
|
57
65
|
presence?: DeploymentPresence | null;
|
|
66
|
+
/**
|
|
67
|
+
* The result of Clivly asking the app directly whether it is up.
|
|
68
|
+
*
|
|
69
|
+
* Every other signal here is *pushed* by the app. On a serverless host there
|
|
70
|
+
* is no process between requests to push anything: the SDK's heartbeat timer
|
|
71
|
+
* fires once at boot and is then frozen, so cadence alone reports a perfectly
|
|
72
|
+
* healthy deployment as offline forever. A probe inverts that — Clivly asks,
|
|
73
|
+
* and a host that answers is up by definition.
|
|
74
|
+
*
|
|
75
|
+
* Absent keeps the pure cadence behaviour, so nothing changes for apps that
|
|
76
|
+
* do hold a long-lived process.
|
|
77
|
+
*/
|
|
78
|
+
reachabilityProbe?: ReachabilityProbe | null;
|
|
58
79
|
/**
|
|
59
80
|
* Faults the running app diagnosed about itself on its last heartbeat — an
|
|
60
81
|
* unreadable database, a secret missing from the process environment. These
|
|
@@ -84,6 +105,21 @@ declare function formatAge(ms: number): string;
|
|
|
84
105
|
/** The cadence `createClivlySDK` uses when the host app configures none. */
|
|
85
106
|
declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
|
|
86
107
|
type ConnectionHealth = "live" | "live_with_issues" | "stale" | "offline";
|
|
108
|
+
/**
|
|
109
|
+
* Clivly's own answer to "is this app up", obtained by asking it rather than
|
|
110
|
+
* waiting to be told.
|
|
111
|
+
*
|
|
112
|
+
* `reachable` is deliberately about the *connection*, not the response body: an
|
|
113
|
+
* app that answers `401` has proved it is running just as conclusively as one
|
|
114
|
+
* that answers `200`. Only silence — a timeout, a refused connection, DNS that
|
|
115
|
+
* does not resolve — means offline.
|
|
116
|
+
*/
|
|
117
|
+
interface ReachabilityProbe {
|
|
118
|
+
/** When Clivly performed the check. Probe results are cached, so this ages. */
|
|
119
|
+
checkedAt: number;
|
|
120
|
+
/** Did anything answer at the other end? */
|
|
121
|
+
reachable: boolean;
|
|
122
|
+
}
|
|
87
123
|
/** Which algorithm decides whether this deployment is up. */
|
|
88
124
|
type PresenceTransport = "websocket" | "http";
|
|
89
125
|
type PresenceConnectionState = "connected" | "reconnecting" | "disconnected";
|
|
@@ -172,4 +208,4 @@ declare const INSTALL_COMMAND = "npx clivly init";
|
|
|
172
208
|
declare function nextAction(steps: IntegrationStep[], status: IntegrationStatus, liveness: ConnectionLiveness): NextAction | null;
|
|
173
209
|
declare function computeIntegrationState(facts: IntegrationFacts): IntegrationState;
|
|
174
210
|
//#endregion
|
|
175
|
-
export { ConnectionHealth, ConnectionLiveness, DEFAULT_HEARTBEAT_INTERVAL_MS, DeploymentPresence, HostEnvironment, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, PresenceConnectionState, PresenceTransport, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
|
211
|
+
export { ConnectionHealth, ConnectionLiveness, DEFAULT_HEARTBEAT_INTERVAL_MS, DeploymentPresence, HostEnvironment, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, PresenceConnectionState, PresenceTransport, ReachabilityProbe, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
|
@@ -30,6 +30,32 @@ const STEP_PREREQUISITES = {
|
|
|
30
30
|
function stepIndex(id) {
|
|
31
31
|
return INTEGRATION_STEPS.indexOf(id);
|
|
32
32
|
}
|
|
33
|
+
const UNREACHABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
34
|
+
502,
|
|
35
|
+
503,
|
|
36
|
+
504,
|
|
37
|
+
520,
|
|
38
|
+
521,
|
|
39
|
+
522,
|
|
40
|
+
523,
|
|
41
|
+
524,
|
|
42
|
+
525,
|
|
43
|
+
526,
|
|
44
|
+
527,
|
|
45
|
+
530
|
|
46
|
+
]);
|
|
47
|
+
/**
|
|
48
|
+
* Did the last trigger attempt fail to reach the app at all?
|
|
49
|
+
*
|
|
50
|
+
* Deliberately narrow. A signature rejection (401) or a handler error (500)
|
|
51
|
+
* both prove the trigger works — the request arrived. Only a total failure to
|
|
52
|
+
* get an answer un-ticks the rung, so this cannot demote setup progress for
|
|
53
|
+
* faults that live somewhere else.
|
|
54
|
+
*/
|
|
55
|
+
function triggerUnreachable(facts) {
|
|
56
|
+
const status = facts.lastRun?.status;
|
|
57
|
+
return facts.lastRun?.ok === false && status !== void 0 && UNREACHABLE_STATUSES.has(status);
|
|
58
|
+
}
|
|
33
59
|
function formatAge(ms) {
|
|
34
60
|
if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))}s ago`;
|
|
35
61
|
if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
|
|
@@ -102,6 +128,19 @@ function deriveLiveness(facts) {
|
|
|
102
128
|
} } : {}
|
|
103
129
|
};
|
|
104
130
|
if (facts.presence && facts.presence.transport === "websocket") return websocketLiveness(facts, facts.presence, base);
|
|
131
|
+
const probe = facts.reachabilityProbe;
|
|
132
|
+
if (probe?.reachable && facts.now - probe.checkedAt < staleMs) {
|
|
133
|
+
const probeAgeMs = facts.now - probe.checkedAt;
|
|
134
|
+
const issues = livenessIssues(facts);
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
ageMs: probeAgeMs,
|
|
138
|
+
ageLabel: formatAge(probeAgeMs),
|
|
139
|
+
health: issues.length > 0 ? "live_with_issues" : "live",
|
|
140
|
+
issues,
|
|
141
|
+
reachable: true
|
|
142
|
+
};
|
|
143
|
+
}
|
|
105
144
|
if (ageMs === null || ageMs >= staleMs) return {
|
|
106
145
|
...base,
|
|
107
146
|
health: "offline",
|
|
@@ -130,7 +169,7 @@ function isSatisfied(id, facts) {
|
|
|
130
169
|
case "application": return facts.everConnected || age !== null;
|
|
131
170
|
case "schema": return facts.discoveredTableCount > 0;
|
|
132
171
|
case "mapping": return facts.mappedEntityCount > 0;
|
|
133
|
-
case "trigger": return facts.triggerConfigured;
|
|
172
|
+
case "trigger": return facts.triggerConfigured && !triggerUnreachable(facts);
|
|
134
173
|
case "sync": return facts.lastRun?.ok === true;
|
|
135
174
|
default: return facts.widgetInstalled;
|
|
136
175
|
}
|
|
@@ -140,11 +179,16 @@ function evidenceFor(id, facts) {
|
|
|
140
179
|
if (id === "application" && age !== null) return `heartbeat ${formatAge(age)}`;
|
|
141
180
|
if (id === "schema" && facts.discoveredTableCount > 0) return `found ${facts.discoveredTableCount} tables`;
|
|
142
181
|
if (id === "mapping" && facts.mappedEntityCount > 0) return `${facts.mappedEntityCount} entity mapped`;
|
|
143
|
-
if (id === "trigger" && facts.triggerConfigured) return "trigger URL and secret set";
|
|
182
|
+
if (id === "trigger" && facts.triggerConfigured) return triggerUnreachable(facts) ? "set, but the last attempt could not reach your app" : "trigger URL and secret set";
|
|
144
183
|
if (id === "sync" && facts.lastRun) return `last run ${formatAge(facts.now - facts.lastRun.at)}`;
|
|
145
184
|
return null;
|
|
146
185
|
}
|
|
147
186
|
function problemFor(id, facts) {
|
|
187
|
+
if (id === "trigger" && triggerUnreachable(facts)) return {
|
|
188
|
+
code: "trigger_unreachable",
|
|
189
|
+
message: "The trigger URL is set, but Clivly could not reach your app there.",
|
|
190
|
+
fix: "Check the app is running and serving that URL. A dev tunnel URL changes every session — `clivly dev` keeps it registered for you."
|
|
191
|
+
};
|
|
148
192
|
if (id === "sync" && facts.lastRun && !facts.lastRun.ok) return {
|
|
149
193
|
code: "sync_failed",
|
|
150
194
|
message: facts.lastRun.message ?? "The last sync run failed.",
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/core",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"private": false,
|
|
5
|
-
"type": "module",
|
|
6
5
|
"description": "ORM/auth-agnostic CRM adapter contracts and shared domain types for Clivly.",
|
|
7
6
|
"license": "MIT",
|
|
8
7
|
"sideEffects": false,
|
|
8
|
+
"type": "module",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"import": {
|
|
@@ -161,12 +161,15 @@
|
|
|
161
161
|
"files": [
|
|
162
162
|
"dist"
|
|
163
163
|
],
|
|
164
|
-
"publishConfig": {
|
|
165
|
-
"access": "public"
|
|
166
|
-
},
|
|
167
164
|
"dependencies": {
|
|
168
165
|
"zod": "^4.1.13"
|
|
169
166
|
},
|
|
167
|
+
"devDependencies": {
|
|
168
|
+
"drizzle-orm": "^0.45.1",
|
|
169
|
+
"tsdown": "^0.22.14",
|
|
170
|
+
"typescript": "^6",
|
|
171
|
+
"vitest": "3.2.7"
|
|
172
|
+
},
|
|
170
173
|
"peerDependencies": {
|
|
171
174
|
"drizzle-orm": ">=0.30 || >=1.0.0-rc.0"
|
|
172
175
|
},
|
|
@@ -175,21 +178,18 @@
|
|
|
175
178
|
"optional": true
|
|
176
179
|
}
|
|
177
180
|
},
|
|
178
|
-
"devDependencies": {
|
|
179
|
-
"drizzle-orm": "^0.45.1",
|
|
180
|
-
"tsdown": "^0.22.14",
|
|
181
|
-
"typescript": "^6",
|
|
182
|
-
"vitest": "3.2.7"
|
|
183
|
-
},
|
|
184
181
|
"engines": {
|
|
185
182
|
"node": ">=22"
|
|
186
183
|
},
|
|
184
|
+
"publishConfig": {
|
|
185
|
+
"access": "public"
|
|
186
|
+
},
|
|
187
187
|
"scripts": {
|
|
188
188
|
"build": "tsdown",
|
|
189
189
|
"check-types": "tsc --noEmit",
|
|
190
190
|
"test": "vitest run"
|
|
191
191
|
},
|
|
192
192
|
"main": "./dist/index.cjs",
|
|
193
|
-
"
|
|
194
|
-
"
|
|
193
|
+
"module": "./dist/index.mjs",
|
|
194
|
+
"types": "./dist/index.d.cts"
|
|
195
195
|
}
|