@empyria/restate 0.4.12 → 0.4.13

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.
@@ -5,7 +5,7 @@
5
5
 
6
6
  version: 2
7
7
  updates:
8
- - package-ecosystem: "" # See documentation for possible values
9
- directory: "/" # Location of package manifests
10
- schedule:
11
- interval: "daily"
8
+ - package-ecosystem: '' # See documentation for possible values
9
+ directory: '/' # Location of package manifests
10
+ schedule:
11
+ interval: 'daily'
package/AGENTS.md CHANGED
@@ -74,6 +74,11 @@ Attempted to assign to readonly property`). Use `mock.module('@restatedev/restat
74
74
  any mocking) if you need to restore it — reading it back off the live `clients` binding later
75
75
  may observe an already-mocked value. See `test/Admin.test.js`.
76
76
 
77
+ `setupRestate`'s shutdown decision is `shouldDeleteOnShutdown` (unit tested); `keepDeploymentOnShutdown` and
78
+ `forceRegistration` exist because invocations are pinned to their deployment: Restate 1.7 keeps the deployment ID when the
79
+ same address registers again, but only `force: true` updates its handler list. Check both against a live server when
80
+ touching them.
81
+
77
82
  `setupRestate` (in `lib/Admin.js`) is not unit tested: it binds real HTTP/2 + health-check
78
83
  ports, installs process-wide `SIGTERM`/`SIGINT` handlers, and its returned `forceClose` calls
79
84
  `process.exit()` — none of which are safe to exercise in-process in a test run.
package/README.md CHANGED
@@ -124,7 +124,15 @@ Cron expressions are `croner` patterns (five fields, or six with seconds for tes
124
124
 
125
125
  ### An endpoint that does not register itself
126
126
 
127
- `setupRestate` serves, registers the deployment, and deletes it again on SIGINT/SIGTERM. When something else owns
127
+ `setupRestate` serves, registers the deployment, and deletes it again on SIGINT/SIGTERM. Invocations in flight are
128
+ pinned to their deployment, so deleting it strands them: a restarted process cannot finish them and they can only be
129
+ killed. For a process restarted at the same address, pass `keepDeploymentOnShutdown: true` and `forceRegistration: true`:
130
+ Restate keeps the deployment ID when the same address registers again (checked against Restate 1.7), running
131
+ invocations resume on the new process, and `force` updates the services and handlers in place (a plain
132
+ re-registration keeps the old handler list). Only compatible changes are safe this way: an invocation of a removed or
133
+ renamed handler cannot resume.
134
+
135
+ When something else owns
128
136
  registration (a supervisor that registers a process's address once and restarts the process), use
129
137
  `serveEndpoint({port, services})` instead: it only serves. Invocations already running on a deployment are pinned to its
130
138
  address, so the deployment must survive the process stopping and come back at the same address.
package/lib/Admin.js CHANGED
@@ -581,7 +581,15 @@ export function defineObject(def) {
581
581
  /**
582
582
  * Starts the HTTP/2 Restate endpoint, a health-check HTTP server, registers
583
583
  * the deployment with the admin API, and wires SIGTERM/SIGINT for graceful
584
- * shutdown (deregistering the deployment before exiting).
584
+ * shutdown (deregistering the deployment before exiting, unless `keepDeploymentOnShutdown`).
585
+ *
586
+ * Invocations in flight are pinned to their deployment. Deleting it on shutdown means a restarted process cannot
587
+ * finish them, and they cannot even be cancelled (only killed). For a process that is restarted at the same address
588
+ * (a dev loop, a service that is redeployed in place), pass `keepDeploymentOnShutdown: true` and
589
+ * `forceRegistration: true`: Restate keeps the deployment ID when the same address registers again, so running
590
+ * invocations resume on the new process, and `force` updates the deployment's services and handlers in place (a plain
591
+ * re-registration keeps the old handler list). Only compatible changes are safe that way: an invocation of a handler
592
+ * that was removed or renamed cannot resume.
585
593
  * @param {Object} params
586
594
  * @param {string} params.restateAdminURL
587
595
  * @param {string} params.host
@@ -590,6 +598,9 @@ export function defineObject(def) {
590
598
  * @param {Parameters<typeof restate.createEndpointHandler>[0]['services']} [params.services]
591
599
  * @param {() => void} [params.shutdownCallbackFn]
592
600
  * @param {(deploymentId: string) => void} [params.registrationCallbackFn]
601
+ * @param {boolean} [params.keepDeploymentOnShutdown=false] Leave the deployment registered on SIGINT/SIGTERM.
602
+ * @param {boolean} [params.forceRegistration=false] Register with `force: true`, updating an existing deployment at
603
+ * the same address in place.
593
604
  * @returns {Promise<RestateServer>}
594
605
  */
595
606
  export async function setupRestate({
@@ -600,6 +611,8 @@ export async function setupRestate({
600
611
  services = [],
601
612
  shutdownCallbackFn,
602
613
  registrationCallbackFn,
614
+ keepDeploymentOnShutdown = false,
615
+ forceRegistration = false,
603
616
  }) {
604
617
  const handler = restate.createEndpointHandler({ services })
605
618
 
@@ -647,8 +660,12 @@ export async function setupRestate({
647
660
  isHealthy = false
648
661
 
649
662
  try {
650
- if (deploymentId && deploymentId !== 'NA') {
663
+ if (shouldDeleteOnShutdown({ deploymentId, keepDeploymentOnShutdown })) {
651
664
  await deleteDeployment(restateAdminURL, deploymentId)
665
+ } else if (keepDeploymentOnShutdown && deploymentId) {
666
+ console.log(
667
+ `Keeping deployment ${deploymentId}: invocations in flight resume when it comes back`,
668
+ )
652
669
  }
653
670
  } catch (err) {
654
671
  console.error(err)
@@ -688,7 +705,11 @@ export async function setupRestate({
688
705
 
689
706
  for (let attempt = 1; attempt <= attempts; attempt++) {
690
707
  try {
691
- const result = await registerDeployment({ restateAdminURL, serviceURL })
708
+ const result = await registerDeployment({
709
+ restateAdminURL,
710
+ serviceURL,
711
+ force: forceRegistration,
712
+ })
692
713
 
693
714
  try {
694
715
  registrationCallbackFn?.(result.id)
@@ -751,15 +772,27 @@ export async function setupRestate({
751
772
  }
752
773
 
753
774
  /**
754
- * POSTs this process's HTTP endpoint to the Restate admin API so its services are discoverable.
755
- * @param {{restateAdminURL: string, serviceURL: string}} params
775
+ * Whether `setupRestate`'s shutdown removes the deployment: only a real one (registered, not `'NA'`), and not when the
776
+ * caller keeps it for the next start.
777
+ * @param {{deploymentId: string|undefined, keepDeploymentOnShutdown?: boolean}} params
778
+ * @returns {boolean}
779
+ */
780
+ export function shouldDeleteOnShutdown({ deploymentId, keepDeploymentOnShutdown = false }) {
781
+ return Boolean(deploymentId) && deploymentId !== 'NA' && !keepDeploymentOnShutdown
782
+ }
783
+
784
+ /**
785
+ * POSTs this process's HTTP endpoint to the Restate admin API so its services are discoverable. Registering an
786
+ * address that is already registered returns the same deployment; with `force` its services and handlers are
787
+ * updated in place (without it, Restate keeps the ones it knew).
788
+ * @param {{restateAdminURL: string, serviceURL: string, force?: boolean}} params
756
789
  * @returns {Promise<RegisterDeploymentResponse>}
757
790
  */
758
- export async function registerDeployment({ restateAdminURL, serviceURL }) {
791
+ export async function registerDeployment({ restateAdminURL, serviceURL, force = false }) {
759
792
  const uri = serviceURL
760
793
 
761
794
  /** @type {RegisterDeploymentRequest} */
762
- const body = { uri, force: null }
795
+ const body = { uri, force: force ? true : null }
763
796
 
764
797
  console.log(`Registering: ${uri} at: ${restateAdminURL}/deployments`)
765
798
 
@@ -872,7 +905,8 @@ export function createRestateAdmin({ restateAdminURL, restateURL }) {
872
905
  pollInvocation: ({ invocationId, intervalMs }) =>
873
906
  pollInvocation({ restateURL, invocationId, intervalMs }),
874
907
 
875
- getInvocationOutput: ({ invocationId }) => getInvocationOutput({ restateURL, invocationId }),
908
+ getInvocationOutput: ({ invocationId }) =>
909
+ getInvocationOutput({ restateURL, invocationId }),
876
910
 
877
911
  sendMessage: ({ name, message, key, payload, idempotencyKey }) =>
878
912
  sendMessage({ restateURL, name, message, key, payload, idempotencyKey }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empyria/restate",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "description": "Restate.dev helpers for the Empyria nanoservice framework",
5
5
  "license": "MIT",
6
6
  "author": "Imre Fazekas <imre.fazekas@icloud.com>",
@@ -20,6 +20,7 @@ import {
20
20
  sendMessageWithDiscovery,
21
21
  sendMessageAsyncWithDiscovery,
22
22
  registerDeployment,
23
+ shouldDeleteOnShutdown,
23
24
  sendMessageAsynSafe,
24
25
  defineWorkflow,
25
26
  defineService,
@@ -521,6 +522,42 @@ describe('registerDeployment', () => {
521
522
  registerDeployment({ restateAdminURL: 'http://admin', serviceURL: 'http://svc' }),
522
523
  ).rejects.toThrow(/bad uri/)
523
524
  })
525
+
526
+ test('sends force: null by default and force: true when asked, to the deployments endpoint', async () => {
527
+ const sent = []
528
+ globalThis.fetch = mock(async (url, init) => {
529
+ sent.push({ url, body: JSON.parse(init.body) })
530
+ return jsonResponse({ id: 'dep-1', services: [] })
531
+ })
532
+ await registerDeployment({ restateAdminURL: 'http://admin', serviceURL: 'http://svc:9080' })
533
+ await registerDeployment({
534
+ restateAdminURL: 'http://admin',
535
+ serviceURL: 'http://svc:9080',
536
+ force: true,
537
+ })
538
+ expect(sent).toEqual([
539
+ { url: 'http://admin/deployments', body: { uri: 'http://svc:9080', force: null } },
540
+ { url: 'http://admin/deployments', body: { uri: 'http://svc:9080', force: true } },
541
+ ])
542
+ })
543
+ })
544
+
545
+ describe('shouldDeleteOnShutdown', () => {
546
+ test('deletes a registered deployment by default', () => {
547
+ expect(shouldDeleteOnShutdown({ deploymentId: 'dep-1' })).toBe(true)
548
+ })
549
+
550
+ test('keeps it when asked, so invocations in flight can resume after a restart', () => {
551
+ expect(
552
+ shouldDeleteOnShutdown({ deploymentId: 'dep-1', keepDeploymentOnShutdown: true }),
553
+ ).toBe(false)
554
+ })
555
+
556
+ test('never deletes without a real deployment', () => {
557
+ for (const deploymentId of [undefined, '', 'NA']) {
558
+ expect(shouldDeleteOnShutdown({ deploymentId })).toBe(false)
559
+ }
560
+ })
524
561
  })
525
562
 
526
563
  // `clients` is a live ESM namespace binding (read-only — Object.assign/property