@empyria/restate 0.4.18 → 0.4.21
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 +3 -3
- package/lib/Admin.js +53 -29
- package/package.json +4 -4
- package/test/Admin.test.js +5 -3
package/README.md
CHANGED
|
@@ -125,9 +125,9 @@ Cron expressions are `croner` patterns (five fields, or six with seconds for tes
|
|
|
125
125
|
### Serving and registering an endpoint
|
|
126
126
|
|
|
127
127
|
`setupRestate({ restateAdminURL, serviceURL: 'https://agent-service.internal:9080', services })` takes one complete URL, the
|
|
128
|
-
address Restate reaches the endpoint at. It is registered as given and the endpoint listens on its port
|
|
129
|
-
|
|
130
|
-
unless `healthPort` is set.
|
|
128
|
+
address Restate reaches the endpoint at. It is registered as given and the endpoint listens on its port (80 or 443 when
|
|
129
|
+
the URL has none or states the scheme's default), so the scheme is required (there is no separate host or port option);
|
|
130
|
+
the health endpoint listens on the next port unless `healthPort` is set.
|
|
131
131
|
|
|
132
132
|
It serves, registers the deployment, and deletes it again on SIGINT/SIGTERM. Invocations in flight are
|
|
133
133
|
pinned to their deployment, so deleting it strands them: a restarted process cannot finish them and they can only be
|
package/lib/Admin.js
CHANGED
|
@@ -578,6 +578,34 @@ export function defineObject(def) {
|
|
|
578
578
|
return restate.object(def)
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
+
/**
|
|
582
|
+
* The health check `setupRestate` uses unless given a `healthCheckFn`: `GET /health`, `/health/liveness` and
|
|
583
|
+
* `/health/readiness` answer 200 once the deployment is registered and 503 before, anything else 404.
|
|
584
|
+
* @param {import('node:http').IncomingMessage} req
|
|
585
|
+
* @param {import('node:http').ServerResponse} res
|
|
586
|
+
* @param {{isHealthy: boolean, deploymentId?: string, services?: string[]}} state
|
|
587
|
+
*/
|
|
588
|
+
export function defaultHealthCheck(req, res, { isHealthy, deploymentId, services }) {
|
|
589
|
+
if (
|
|
590
|
+
(req.url === '/health' ||
|
|
591
|
+
req.url === '/health/liveness' ||
|
|
592
|
+
req.url === '/health/readiness') &&
|
|
593
|
+
req.method === 'GET'
|
|
594
|
+
) {
|
|
595
|
+
res.writeHead(isHealthy ? 200 : 503, { 'Content-Type': 'application/json' })
|
|
596
|
+
res.end(
|
|
597
|
+
JSON.stringify({
|
|
598
|
+
status: isHealthy ? 'ok' : 'unavailable',
|
|
599
|
+
deployment: deploymentId,
|
|
600
|
+
services,
|
|
601
|
+
}),
|
|
602
|
+
)
|
|
603
|
+
} else {
|
|
604
|
+
res.writeHead(404)
|
|
605
|
+
res.end()
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
581
609
|
/**
|
|
582
610
|
* Starts the HTTP/2 Restate endpoint, a health-check HTTP server, registers
|
|
583
611
|
* the deployment with the admin API, and wires SIGTERM/SIGINT for graceful
|
|
@@ -592,13 +620,17 @@ export function defineObject(def) {
|
|
|
592
620
|
* that was removed or renamed cannot resume.
|
|
593
621
|
* @param {Object} params
|
|
594
622
|
* @param {string} params.restateAdminURL
|
|
595
|
-
* @param {string} params.serviceURL Complete URL Restate uses to reach this endpoint
|
|
596
|
-
*
|
|
597
|
-
* the
|
|
623
|
+
* @param {string} params.serviceURL Complete URL Restate uses to reach this endpoint (e.g.
|
|
624
|
+
* `https://agent-service.internal:9080`). It is registered as given, and the endpoint listens on its port: 80 or 443
|
|
625
|
+
* when the URL has none or states the scheme's default (listening there needs the privilege to bind it).
|
|
598
626
|
* @param {number} [params.healthPort] - Defaults to `port + 1`.
|
|
599
627
|
* @param {Parameters<typeof restate.createEndpointHandler>[0]['services']} [params.services]
|
|
600
628
|
* @param {() => void} [params.shutdownCallbackFn]
|
|
601
629
|
* @param {(deploymentId: string) => void} [params.registrationCallbackFn]
|
|
630
|
+
* @param {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, state: {isHealthy: boolean, deploymentId?: string, services?: string[]}) => void | Promise<void>} [params.healthCheckFn]
|
|
631
|
+
* Takes over every request to the health port: routing, status code, headers and body are the function's to set and
|
|
632
|
+
* it must end the response. `state.isHealthy` is true once the deployment is registered. Without it, `/health`,
|
|
633
|
+
* `/health/liveness` and `/health/readiness` answer 200 or 503 with a small JSON body, healthy once registered.
|
|
602
634
|
* @param {boolean} [params.keepDeploymentOnShutdown=false] Leave the deployment registered on SIGINT/SIGTERM.
|
|
603
635
|
* @param {boolean} [params.forceRegistration=false] Register with `force: true`, updating an existing deployment at
|
|
604
636
|
* the same address in place.
|
|
@@ -611,6 +643,7 @@ export async function setupRestate({
|
|
|
611
643
|
services = [],
|
|
612
644
|
shutdownCallbackFn,
|
|
613
645
|
registrationCallbackFn,
|
|
646
|
+
healthCheckFn,
|
|
614
647
|
keepDeploymentOnShutdown = false,
|
|
615
648
|
forceRegistration = false,
|
|
616
649
|
}) {
|
|
@@ -624,25 +657,20 @@ export async function setupRestate({
|
|
|
624
657
|
// --- Health server ---
|
|
625
658
|
let isHealthy = false
|
|
626
659
|
|
|
660
|
+
const checkFn = healthCheckFn ?? defaultHealthCheck
|
|
627
661
|
const healthServer = createHttpServer((req, res) => {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
req.method === 'GET'
|
|
633
|
-
) {
|
|
634
|
-
const status = isHealthy ? 200 : 503
|
|
635
|
-
const body = JSON.stringify({
|
|
636
|
-
status: isHealthy ? 'ok' : 'unavailable',
|
|
637
|
-
deployment: server.deploymentId,
|
|
638
|
-
services: server.services,
|
|
639
|
-
})
|
|
640
|
-
res.writeHead(status, { 'Content-Type': 'application/json' })
|
|
641
|
-
res.end(body)
|
|
642
|
-
} else {
|
|
643
|
-
res.writeHead(404)
|
|
644
|
-
res.end()
|
|
662
|
+
const state = {
|
|
663
|
+
isHealthy,
|
|
664
|
+
deploymentId: server.deploymentId,
|
|
665
|
+
services: server.services,
|
|
645
666
|
}
|
|
667
|
+
Promise.resolve()
|
|
668
|
+
.then(() => checkFn(req, res, state))
|
|
669
|
+
.catch((err) => {
|
|
670
|
+
console.error('healthCheckFn failed', err)
|
|
671
|
+
if (!res.headersSent) res.writeHead(500)
|
|
672
|
+
res.end()
|
|
673
|
+
})
|
|
646
674
|
})
|
|
647
675
|
|
|
648
676
|
healthServer.listen(healthPort, () => console.log(`Health endpoint on :${healthPort}/health`))
|
|
@@ -764,10 +792,9 @@ export async function setupRestate({
|
|
|
764
792
|
|
|
765
793
|
/**
|
|
766
794
|
* Validates the URL `setupRestate` registers with Restate and returns it with the port the endpoint listens on.
|
|
767
|
-
* @param {string} serviceURL Complete http(s) URL
|
|
795
|
+
* @param {string} serviceURL Complete http(s) URL; a port left out (or equal to the scheme's default) means 80/443
|
|
768
796
|
* @returns {{serviceURL: string, port: number}} `serviceURL` unchanged
|
|
769
|
-
* @throws {Error} If it is missing
|
|
770
|
-
* be a guess)
|
|
797
|
+
* @throws {Error} If it is missing or not an absolute http(s) URL
|
|
771
798
|
*/
|
|
772
799
|
export function parseServiceURL(serviceURL) {
|
|
773
800
|
if (!serviceURL) {
|
|
@@ -786,12 +813,9 @@ export function parseServiceURL(serviceURL) {
|
|
|
786
813
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
787
814
|
throw new Error(`serviceURL must use http or https, got '${serviceURL}'`)
|
|
788
815
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
)
|
|
793
|
-
}
|
|
794
|
-
return { serviceURL, port: Number(url.port) }
|
|
816
|
+
// `URL.port` is '' for the scheme's default port, written or not; that is the port Restate dials, so listen on it.
|
|
817
|
+
const port = url.port ? Number(url.port) : url.protocol === 'https:' ? 443 : 80
|
|
818
|
+
return { serviceURL, port }
|
|
795
819
|
}
|
|
796
820
|
|
|
797
821
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empyria/restate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.21",
|
|
4
4
|
"description": "Restate.dev helpers for the Empyria nanoservice framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Imre Fazekas <imre.fazekas@icloud.com>",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"croner": "10.0.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@ai-sdk/openai-compatible": "3.0.
|
|
40
|
-
"@ai-sdk/provider": "4.0.
|
|
39
|
+
"@ai-sdk/openai-compatible": "3.0.65",
|
|
40
|
+
"@ai-sdk/provider": "4.0.24",
|
|
41
41
|
"@restatedev/restate-server": "1.7.13",
|
|
42
|
-
"ai": "7.0.
|
|
42
|
+
"ai": "7.0.130",
|
|
43
43
|
"oxfmt": "0.72.0",
|
|
44
44
|
"oxlint": "1.87.0"
|
|
45
45
|
},
|
package/test/Admin.test.js
CHANGED
|
@@ -566,9 +566,11 @@ describe('parseServiceURL', () => {
|
|
|
566
566
|
expect(() => parseServiceURL('ftp://svc:21')).toThrow(/http or https/)
|
|
567
567
|
})
|
|
568
568
|
|
|
569
|
-
test(
|
|
570
|
-
expect(
|
|
571
|
-
expect(
|
|
569
|
+
test("uses the scheme's default port when the URL has none or states it", () => {
|
|
570
|
+
expect(parseServiceURL('https://svc.example.com').port).toBe(443)
|
|
571
|
+
expect(parseServiceURL('https://svc.example.com:443').port).toBe(443)
|
|
572
|
+
expect(parseServiceURL('http://svc').port).toBe(80)
|
|
573
|
+
expect(parseServiceURL('http://svc:80').port).toBe(80)
|
|
572
574
|
})
|
|
573
575
|
})
|
|
574
576
|
|