@gershy/lilac 0.0.11 → 0.0.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.
@@ -0,0 +1,285 @@
1
+ import { rootFact } from '@gershy/disk';
2
+ import proc from '@gershy/nodejs-proc';
3
+ import { PetalTerraform } from "../main.js";
4
+ import retry from '@gershy/util-retry';
5
+ import { skip } from '@gershy/clearing';
6
+ import http from '@gershy/util-http';
7
+ import { regions as awsRegions } from "../util/aws.js";
8
+ export var Soil;
9
+ (function (Soil) {
10
+ class Base {
11
+ registry;
12
+ constructor(args) {
13
+ this.registry = args.registry;
14
+ }
15
+ async getTerraformPetals(ctx) {
16
+ throw Error('not implemented');
17
+ }
18
+ }
19
+ Soil.Base = Base;
20
+ ;
21
+ class LocalStack extends Base {
22
+ static localStackInternalPort = 4566;
23
+ aws;
24
+ localStackDocker;
25
+ procArgs;
26
+ constructor(args) {
27
+ super(args);
28
+ this.aws = args.aws;
29
+ this.localStackDocker = {
30
+ image: 'localstack/localstack:latest',
31
+ port: LocalStack.localStackInternalPort,
32
+ containerName: 'gershyLilacLocalStack'
33
+ }[merge](args.localStackDocker ?? {});
34
+ this.procArgs = { cwd: rootFact, env: process.env };
35
+ }
36
+ getAwsServices() {
37
+ // Note that "overhead" services are essential for initializing localstack:
38
+ // - s3 + ddb used for terraform state locking
39
+ // - sts is used for credential validation
40
+ // - iam is needed for role creation
41
+ const overheadAwsServices = ['s3', 'dynamodb', 'sts', 'iam'];
42
+ return new Set([...overheadAwsServices, ...this.registry.getAwsServices()]);
43
+ }
44
+ async getDockerContainers() {
45
+ const { containerName } = this.localStackDocker;
46
+ const dockerPs = await proc(`docker ps -a --filter "name=${containerName}" --format "{{.Names}},{{.State}}"`, this.procArgs);
47
+ return dockerPs
48
+ .output
49
+ .split('\n')[map](v => v.trim() || skip)[map](v => v[cut](',', 1))[map](([name, state]) => ({ name, state }))
50
+ // Exclude containers which match the `docker ps` filter but don't have the prefix
51
+ [map](v => (v.name === containerName || v.name[hasHead](`${containerName}-`)) ? v : skip);
52
+ }
53
+ run(args) {
54
+ return args.logger.scope('localStack', {}, async (logger) => {
55
+ // Run a localStack container in docker, enabling `terraform apply` on an aws-like target
56
+ const { image, port, containerName } = this.localStackDocker;
57
+ const awsServices = this.getAwsServices();
58
+ await logger.scope('dockerDeploy', { image, containerName, port }, async (logger) => {
59
+ await proc('docker info', this.procArgs).catch(({ output }) => Error('docker unavailable')[fire]({ output }));
60
+ logger.log({ $$: 'dockerActive' });
61
+ const containers = await this.getDockerContainers();
62
+ let state = containers.find(c => c.name === containerName)?.state ?? 'nonexistent';
63
+ // First if a container already exists ensure it's compatible with our given config
64
+ if (['running', 'paused', 'exited'][has](state)) {
65
+ const isExistingContainerReusable = await (async () => {
66
+ const { output: inspectJson } = await proc(`docker inspect ${containerName}`, this.procArgs);
67
+ const [containerInfo] = JSON.parse(inspectJson);
68
+ console.log('DOCKER INSPECT', containerInfo);
69
+ const containerImage = containerInfo.Config.Image;
70
+ const containerEnv = containerInfo.Config.Env[toObj](v => v[cut]('=', 1));
71
+ const containerPort = Number(containerInfo.HostConfig.PortBindings[`${LocalStack.localStackInternalPort}/tcp`]?.[0]?.HostPort ?? 0);
72
+ const services = (containerEnv.SERVICES ?? '').split(',').sort().join(',');
73
+ return true
74
+ && containerImage === image
75
+ && containerPort === port
76
+ && containerEnv.DEFAULT_REGION === this.aws.region
77
+ && services === awsServices[toArr](v => v).sort().join(',');
78
+ })();
79
+ if (isExistingContainerReusable) {
80
+ if (state === 'paused')
81
+ await proc(`docker unpause ${containerName}`, this.procArgs);
82
+ if (state === 'exited')
83
+ await proc(`docker start ${containerName}`, this.procArgs);
84
+ logger.log({ $$: 'containerReused' });
85
+ state = 'running';
86
+ }
87
+ else {
88
+ await this.end({ containers });
89
+ logger.log({ $$: 'previousLocalStackRemoved', containers });
90
+ state = 'nonexistent';
91
+ }
92
+ }
93
+ if (state === 'nonexistent') {
94
+ const runCmd = String[baseline](`
95
+ | docker run
96
+ | --rm
97
+ | -d
98
+ | --privileged${'' /* TODO: consider removing? */}
99
+ | --name ${containerName}
100
+ | -p ${port}:${LocalStack.localStackInternalPort}
101
+ | -v /var/run/docker.sock:/var/run/docker.sock
102
+ | -e SERVICES=${awsServices[toArr](v => v).join(',')}
103
+ | -e DEFAULT_REGION=${this.aws.region}
104
+ | ${image}
105
+ `).split('\n')[map](ln => ln.trim() || skip).join(' ');
106
+ await proc(runCmd, this.procArgs);
107
+ state = 'running';
108
+ }
109
+ if (state !== 'running')
110
+ throw Error('container state unexpected')[mod]({ state });
111
+ });
112
+ const readyEndpoint = {
113
+ $req: null,
114
+ $res: null,
115
+ netProc: { proto: 'http', addr: 'localhost', port },
116
+ path: ['_localstack', 'health'],
117
+ method: 'get'
118
+ };
119
+ const { val: { services } } = await retry({
120
+ // TODO: If the container already exists, it seems its "s3" and "sts" services become unavailable when we try to reinitialize Soil pointing at it??
121
+ attempts: 20,
122
+ delay: n => Math.min(500, 50 * n),
123
+ fn: async () => {
124
+ // Retry all failures and non-200s
125
+ const res = await http(readyEndpoint, {}).catch(err => err[fire]({ retry: true }));
126
+ if (res.code !== 200)
127
+ throw Error('unhealthy')[mod]({ retry: true });
128
+ const { ya = [], no = [] } = res.body.services[group](v => v === 'available' ? 'ya' : 'no')[map](group => group[toArr]((v, k) => k));
129
+ const missingServices = no[map](svc => awsServices.has(svc) ? svc : skip);
130
+ if (missingServices.length)
131
+ throw Error('services unavailable')[mod]({ missingServices })[mod]({ retry: true });
132
+ return { services: ya };
133
+ },
134
+ retryable: err => !!err.retry,
135
+ }).catch(err => err[fire]({ numErrs: err.errs.length, errs: null }));
136
+ logger.log({ $$: 'localStackActive', services });
137
+ return {
138
+ aws: { services: [...awsServices], region: this.aws.region, },
139
+ netProc: { proto: 'http', addr: 'localhost', port },
140
+ url: `http://localhost:${port}`
141
+ };
142
+ });
143
+ }
144
+ async end(args) {
145
+ const containers = args?.containers ?? await this.getDockerContainers();
146
+ await proc(`docker rm -f ${containers.map(c => c.name).join(' ')}`, this.procArgs)
147
+ .catch(err => {
148
+ console.log('ERROR ENDING LOCALSTACK DOCKER CONTAINER:\n', err.output);
149
+ return;
150
+ });
151
+ return containers;
152
+ }
153
+ async getTerraformPetals(ctx) {
154
+ const { aws } = this;
155
+ const awsServices = [...this.getAwsServices()];
156
+ const netProc = { proto: 'http', addr: 'localhost', port: this.localStackDocker.port };
157
+ const localStackUrl = `${netProc.proto}://${netProc.addr}:${netProc.port}`;
158
+ return {
159
+ boot: () => [
160
+ new PetalTerraform.Terraform({
161
+ $requiredProviders: {
162
+ aws: {
163
+ source: 'hashicorp/aws',
164
+ version: `~> 5.0`
165
+ }
166
+ }
167
+ }),
168
+ new PetalTerraform.Provider('aws', {
169
+ region: aws.region,
170
+ skipCredentialsValidation: true,
171
+ skipRequestingAccountId: true,
172
+ s3UsePathStyle: true, // Otherwise requests can go to "bucket.s3.amazonaws.com", outside localStack
173
+ // Note our localStack setup always includes s3 and ddb (required for tf state storage)
174
+ $endpoints: awsServices[toObj](svc => [svc, localStackUrl])
175
+ })
176
+ ],
177
+ main: function* (args) {
178
+ yield new PetalTerraform.Terraform({
179
+ $requiredProviders: {
180
+ aws: {
181
+ source: 'hashicorp/aws',
182
+ version: `~> 5.0` // Consider parameterizing??
183
+ }
184
+ },
185
+ '$backend.s3': {
186
+ region: aws.region,
187
+ encrypt: true,
188
+ bucket: args.s3Name,
189
+ key: `tf`,
190
+ dynamodbTable: args.ddbName,
191
+ usePathStyle: true,
192
+ // Point the S3 backend at LocalStack when testing
193
+ endpoints: awsServices[toObj](svc => [svc, localStackUrl]),
194
+ }
195
+ });
196
+ for (const { term } of awsRegions)
197
+ yield new PetalTerraform.Provider('aws', {
198
+ region: term,
199
+ skipCredentialsValidation: true,
200
+ skipRequestingAccountId: true,
201
+ // Omit the alias for the default provider!
202
+ ...(term !== aws.region && { alias: term.split('-').join('_') }),
203
+ // Point providers at LocalStack when testing
204
+ s3UsePathStyle: true,
205
+ $endpoints: awsServices[toObj](svc => [svc, localStackUrl])
206
+ });
207
+ }
208
+ };
209
+ }
210
+ }
211
+ Soil.LocalStack = LocalStack;
212
+ ;
213
+ class AwsCloud extends Base {
214
+ aws;
215
+ constructor(args) {
216
+ super(args);
217
+ this.aws = args.aws;
218
+ }
219
+ async getTerraformPetals(ctx) {
220
+ const { aws } = this;
221
+ return {
222
+ boot: function* () {
223
+ const tfAwsCredsFile = new PetalTerraform.File('creds.ini', String[baseline](`
224
+ | [default]
225
+ | aws_region = ${aws.region}
226
+ | aws_access_key_id = ${aws.accessKey.id}
227
+ | aws_secret_access_key = ${aws.accessKey['!secret']}
228
+ `));
229
+ yield tfAwsCredsFile;
230
+ yield new PetalTerraform.Terraform({
231
+ $requiredProviders: {
232
+ aws: {
233
+ source: 'hashicorp/aws',
234
+ version: `~> 5.0`
235
+ }
236
+ }
237
+ });
238
+ yield new PetalTerraform.Provider('aws', {
239
+ sharedCredentialsFiles: [tfAwsCredsFile.refStr()],
240
+ profile: 'default', // References a section within the credentials file
241
+ region: aws.region,
242
+ });
243
+ },
244
+ main: function* (args) {
245
+ const credFileProfile = 'default';
246
+ const tfAwsCredsFile = new PetalTerraform.File('creds.ini', String[baseline](`
247
+ | [${credFileProfile}]
248
+ | aws_region = ${aws.region}
249
+ | aws_access_key_id = ${aws.accessKey.id}
250
+ | aws_secret_access_key = ${aws.accessKey['!secret']}
251
+ `));
252
+ yield tfAwsCredsFile;
253
+ yield new PetalTerraform.Terraform({
254
+ $requiredProviders: {
255
+ aws: {
256
+ source: 'hashicorp/aws',
257
+ version: `~> 5.0` // Consider parameterizing??
258
+ }
259
+ },
260
+ '$backend.s3': {
261
+ sharedCredentialsFiles: [tfAwsCredsFile.refStr()],
262
+ profile: credFileProfile,
263
+ region: aws.region,
264
+ encrypt: true,
265
+ bucket: args.s3Name,
266
+ key: `tf`,
267
+ dynamodbTable: args.ddbName
268
+ }
269
+ });
270
+ for (const { term } of awsRegions)
271
+ yield new PetalTerraform.Provider('aws', {
272
+ sharedCredentialsFiles: [tfAwsCredsFile.refStr()],
273
+ profile: credFileProfile,
274
+ region: term,
275
+ // Omit the alias for the default provider!
276
+ ...(term !== aws.region && { alias: term.split('-').join('_') }),
277
+ });
278
+ }
279
+ };
280
+ }
281
+ }
282
+ Soil.AwsCloud = AwsCloud;
283
+ ;
284
+ })(Soil || (Soil = {}));
285
+ ;
@@ -1,5 +1,19 @@
1
1
  export declare const capitalKeys: (v: any) => any;
2
- export declare const regions: {
3
- term: string;
4
- mini: string;
5
- }[];
2
+ export declare const regions: readonly [{
3
+ term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
4
+ mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
5
+ }, {
6
+ term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
7
+ mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
8
+ }, {
9
+ term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
10
+ mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
11
+ }, {
12
+ term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
13
+ mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
14
+ }, {
15
+ term: "ca-central-1" | "us-east-1" | "us-east-2" | "us-west-1" | "us-west-2";
16
+ mini: `${"ca" | "us"}${"c" | "u"}${"c" | "u" | "e" | "w"}${"1" | "2"}`;
17
+ }];
18
+ export type RegionTerm = (typeof regions)[number]['term'];
19
+ export type RegionMini = (typeof regions)[number]['mini'];
@@ -50,5 +50,8 @@ export const regions = [
50
50
  ][map](region => {
51
51
  const [c, z, num] = region.split('-');
52
52
  const [dir0, dir1 = dir0] = z.match(/central|north|south|east|west/g);
53
- return { term: region, mini: [c, dir0[0], dir1[0], num].join('') };
53
+ return {
54
+ term: region,
55
+ mini: [c, dir0[0], dir1[0], num].join('')
56
+ };
54
57
  });
@@ -1,8 +1,10 @@
1
- import { rootFact } from '@gershy/disk';
2
- import { ProcOpts } from '@gershy/util-nodejs-proc';
3
- type Fact = typeof rootFact;
4
- declare const _default: (fact: Fact, cmd: string, opts?: ProcOpts) => Promise<{
5
- logDb: import("@gershy/disk").Fact;
1
+ import { Fact } from '@gershy/disk';
2
+ import { ProcOpts } from '@gershy/nodejs-proc';
3
+ export type ProcTerraformArgs = ProcOpts & {};
4
+ declare const _default: (fact: Fact, cmd: string, opts?: ProcTerraformArgs) => Promise<{
5
+ logDb: Fact;
6
6
  output: string;
7
- }>;
7
+ }> & {
8
+ proc: import("child_process").ChildProcessWithoutNullStreams;
9
+ };
8
10
  export default _default;
@@ -1,5 +1,5 @@
1
- import proc from '@gershy/util-nodejs-proc';
2
- export default (fact, cmd, opts) => {
1
+ import proc from '@gershy/nodejs-proc';
2
+ export default (fact, cmd, opts = {}) => {
3
3
  const numTailingTfLogLines = 20;
4
4
  const writeLog = async (result) => {
5
5
  const [yr, mo, dy, hr, mn, sc, ms] = new Date().toISOString().match(/([0-9]{4})[-]([0-9]{2})[-]([0-9]{2})[T]([0-9]{2})[:]([0-9]{2})[:]([0-9]{2})[.]([0-9]+)[Z]/).slice(1);
@@ -8,12 +8,13 @@ export default (fact, cmd, opts) => {
8
8
  await logDb.setData(result);
9
9
  return logDb;
10
10
  };
11
- return proc(cmd, {
11
+ const prm = proc(cmd, {
12
12
  timeoutMs: 0,
13
13
  ...opts,
14
14
  cwd: fact,
15
15
  env: { TF_DATA_DIR: '' }
16
- }).then(async (result) => {
16
+ });
17
+ return Object.assign(prm.then(async (result) => {
17
18
  const logDb = await writeLog(result.output);
18
19
  return { logDb, output: result.output.split('\n').slice(-numTailingTfLogLines).join('\n') };
19
20
  }, async (err) => {
@@ -22,5 +23,5 @@ export default (fact, cmd, opts) => {
22
23
  logDb,
23
24
  ...(err.output ? { output: err.output.split('\n').slice(-numTailingTfLogLines).join('\n') } : { cause: err })
24
25
  });
25
- });
26
+ }), { proc: prm.proc });
26
27
  };
@@ -0,0 +1 @@
1
+ export type SuperIterable<T> = Iterable<T> | Promise<Iterable<T>> | AsyncIterable<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
+ // TODO: Flower definers (Lilac consumers) will want to use these - make sure they're well-exposed
1
2
  export const embed = (v) => '${' + v + '}';
2
3
  export const json = (v) => [
3
4
  '| <<EOF',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gershy/lilac",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Luscious infrastructure Living as Code - an opinionated approach to IAC",
5
5
  "keywords": [
6
6
  "lilac",
@@ -22,16 +22,20 @@
22
22
  "@gershy/clearing": "^0.0.29"
23
23
  },
24
24
  "devDependencies": {
25
+ "@aws-sdk/client-api-gateway": "^3.1009.0",
26
+ "@gershy/util-hash": "^0.0.3",
25
27
  "@types/node": "^24.10.1",
28
+ "jszip": "^3.10.1",
26
29
  "tsx": "^4.21.0",
27
30
  "typescript": "^5.9.3"
28
31
  },
29
32
  "dependencies": {
30
33
  "@gershy/disk": "^0.0.10",
31
- "@gershy/logger": "^0.0.3",
34
+ "@gershy/logger": "^0.0.4",
35
+ "@gershy/nodejs-proc": "^0.0.4",
32
36
  "@gershy/util-http": "^0.0.4",
33
- "@gershy/util-nodejs-proc": "^0.0.10",
34
- "@gershy/util-phrasing": "^0.0.4"
37
+ "@gershy/util-phrasing": "^0.0.4",
38
+ "@gershy/util-try-with-healing": "^0.0.2"
35
39
  },
36
40
  "type": "module",
37
41
  "files": [
@@ -1,13 +0,0 @@
1
- export type MockAwsArgs = {
2
- port: number;
3
- image: `localstack/localstack${':' | '@'}${string}`;
4
- };
5
- declare const _default: (args: MockAwsArgs) => Promise<{
6
- netProc: {
7
- proto: string;
8
- addr: string;
9
- port: number;
10
- };
11
- end: () => Promise<void>;
12
- }>;
13
- export default _default;
@@ -1,39 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const util_nodejs_proc_1 = __importDefault(require("@gershy/util-nodejs-proc"));
7
- const disk_1 = require("@gershy/disk");
8
- const util_http_1 = __importDefault(require("@gershy/util-http"));
9
- const LOCALSTACK_IMAGE = 'localstack/localstack:latest';
10
- const LOCALSTACK_PORT = 4566;
11
- exports.default = async (args) => {
12
- // Ensure docker is running
13
- await (async () => {
14
- const output = await (0, util_nodejs_proc_1.default)('docker ps', { cwd: disk_1.rootFact });
15
- if (!output[has]('TODO'))
16
- throw Error('docker unavailable');
17
- })();
18
- // Deploy localstack to docker
19
- const { containerName } = await (async () => {
20
- const containerName = `mockAws${Date.now()}`;
21
- await (0, util_nodejs_proc_1.default)(`docker run --rm -d --name ${containerName} -p ${args.port}:${args.port} ${args.image}`, { cwd: disk_1.rootFact });
22
- const readyEndpoint = {
23
- $req: null,
24
- $res: null,
25
- netProc: { proto: 'http', addr: 'localhost', port: args.port },
26
- path: ['health', 'ready'],
27
- method: 'get'
28
- };
29
- const res = await (0, util_http_1.default)(readyEndpoint, { query: {}, body: {} });
30
- console.log('localstack http health', { res });
31
- return { containerName };
32
- })();
33
- return {
34
- netProc: { proto: 'http', addr: 'localhost', port: args.port },
35
- end: async () => {
36
- await (0, util_nodejs_proc_1.default)(`docker rm -f ${containerName}`, { cwd: disk_1.rootFact });
37
- }
38
- };
39
- };
@@ -1,7 +0,0 @@
1
- export type TryWithHealingArgs<T> = {
2
- fn: () => Promise<T>;
3
- canHeal: (err: any) => boolean;
4
- heal: () => Promise<any>;
5
- };
6
- declare const _default: <T>(args: TryWithHealingArgs<T>) => Promise<T>;
7
- export default _default;
@@ -1,11 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = async (args) => {
4
- const { fn, heal, canHeal } = args;
5
- return fn().catch(async (err) => {
6
- if (!canHeal(err))
7
- throw err;
8
- await heal();
9
- return fn();
10
- });
11
- };
@@ -1,13 +0,0 @@
1
- export type MockAwsArgs = {
2
- port: number;
3
- image: `localstack/localstack${':' | '@'}${string}`;
4
- };
5
- declare const _default: (args: MockAwsArgs) => Promise<{
6
- netProc: {
7
- proto: string;
8
- addr: string;
9
- port: number;
10
- };
11
- end: () => Promise<void>;
12
- }>;
13
- export default _default;
@@ -1,34 +0,0 @@
1
- import proc from '@gershy/util-nodejs-proc';
2
- import { rootFact } from '@gershy/disk';
3
- import http from '@gershy/util-http';
4
- const LOCALSTACK_IMAGE = 'localstack/localstack:latest';
5
- const LOCALSTACK_PORT = 4566;
6
- export default async (args) => {
7
- // Ensure docker is running
8
- await (async () => {
9
- const output = await proc('docker ps', { cwd: rootFact });
10
- if (!output[has]('TODO'))
11
- throw Error('docker unavailable');
12
- })();
13
- // Deploy localstack to docker
14
- const { containerName } = await (async () => {
15
- const containerName = `mockAws${Date.now()}`;
16
- await proc(`docker run --rm -d --name ${containerName} -p ${args.port}:${args.port} ${args.image}`, { cwd: rootFact });
17
- const readyEndpoint = {
18
- $req: null,
19
- $res: null,
20
- netProc: { proto: 'http', addr: 'localhost', port: args.port },
21
- path: ['health', 'ready'],
22
- method: 'get'
23
- };
24
- const res = await http(readyEndpoint, { query: {}, body: {} });
25
- console.log('localstack http health', { res });
26
- return { containerName };
27
- })();
28
- return {
29
- netProc: { proto: 'http', addr: 'localhost', port: args.port },
30
- end: async () => {
31
- await proc(`docker rm -f ${containerName}`, { cwd: rootFact });
32
- }
33
- };
34
- };
@@ -1,7 +0,0 @@
1
- export type TryWithHealingArgs<T> = {
2
- fn: () => Promise<T>;
3
- canHeal: (err: any) => boolean;
4
- heal: () => Promise<any>;
5
- };
6
- declare const _default: <T>(args: TryWithHealingArgs<T>) => Promise<T>;
7
- export default _default;
@@ -1,9 +0,0 @@
1
- export default async (args) => {
2
- const { fn, heal, canHeal } = args;
3
- return fn().catch(async (err) => {
4
- if (!canHeal(err))
5
- throw err;
6
- await heal();
7
- return fn();
8
- });
9
- };