@tim-code/my-util 0.3.1 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tim-code/my-util",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "Tim Sprowl",
package/src/find.js CHANGED
@@ -153,6 +153,7 @@ export function findClosestGTE(array, desired, { key, cutoff = Infinity } = {})
153
153
 
154
154
  /**
155
155
  * Find the closest element in an array. If there is a tie, then returns the first matching element by order in the array.
156
+ * If some values are undefined or null, they will be ignored. If no element is found, returns undefined.
156
157
  * If using for strings, need to specify different values for "cutoff" and "comparator".
157
158
  * "~" and "" are good cutoff string values for gt/gte and lt/lte respectively.
158
159
  * @template T, V
package/src/fs.js CHANGED
@@ -1,47 +1,40 @@
1
1
  import { readFile, stat, writeFile } from "node:fs/promises"
2
2
  import { tmpdir } from "node:os"
3
3
  import { promisify } from "node:util"
4
- import { gunzip as _gunzip } from "node:zlib"
4
+ import { gunzip as _gunzip, gzip as _gzip } from "node:zlib"
5
5
 
6
6
  const gunzip = promisify(_gunzip)
7
+ const gzip = promisify(_gzip)
7
8
 
8
9
  // the integration tests for this file are the s3-fs integration tests in lambda-integrations
9
10
  // changes to this file should be tested against lambda-integrations' integration tests
10
11
 
11
12
  /**
12
- * Get JSON from a path.
13
+ * Get JSON from a path. If path ends with .gz, will automatically decompress data.
13
14
  * @param {string} path
14
15
  * @returns {Object|Array}
15
16
  */
16
- export async function getJSON(path) {
17
- const buffer = await readFile(path)
17
+ export async function readJSON(path) {
18
+ let buffer = await readFile(path)
19
+ if (path.endsWith(".gz")) {
20
+ buffer = await gunzip(buffer)
21
+ }
18
22
  return JSON.parse(buffer.toString())
19
23
  }
20
24
 
21
25
  /**
22
- * Write JSON to a path.
26
+ * Write JSON to a path. If path ends with .gz, will automatically compress data.
23
27
  * @param {string} path
24
28
  * @param {Object|Array} object
25
29
  * @param {Object} $1
26
30
  * @param {number=} $1.indent Indent used to format JSON object. Default 2. If 0, does not indent object.
27
31
  */
28
32
  export async function writeJSON(path, object, { indent = 2 } = {}) {
29
- const string = JSON.stringify(object, undefined, indent)
30
- await writeFile(path, string)
31
- }
32
-
33
- /**
34
- * Get gzipped JSON from a path.
35
- * @param {string} path
36
- * @returns {Object|Array}
37
- */
38
- export async function getCompressedJSON(path) {
39
- if (!path.endsWith(".gz")) {
40
- throw new Error("path does not suggest a compressed file")
33
+ let data = JSON.stringify(object, undefined, indent)
34
+ if (path.endsWith(".gz")) {
35
+ data = await gzip(data)
41
36
  }
42
- const buffer = await readFile(path)
43
- const uncompressed = await gunzip(buffer)
44
- return JSON.parse(uncompressed.toString())
37
+ await writeFile(path, data)
45
38
  }
46
39
 
47
40
  /**
package/src/fs.test.js CHANGED
@@ -4,6 +4,7 @@ const readFileMock = jest.fn()
4
4
  const statMock = jest.fn()
5
5
  const tmpdirMock = jest.fn()
6
6
  const gunzipMock = jest.fn()
7
+ const gzipMock = jest.fn()
7
8
  const writeFileMock = jest.fn()
8
9
  jest.unstable_mockModule("node:fs/promises", () => ({
9
10
  readFile: readFileMock,
@@ -15,6 +16,7 @@ jest.unstable_mockModule("node:os", () => ({
15
16
  }))
16
17
  jest.unstable_mockModule("node:zlib", () => ({
17
18
  gunzip: gunzipMock,
19
+ gzip: gzipMock,
18
20
  }))
19
21
  jest.unstable_mockModule("node:util", () => ({
20
22
  promisify: (mock) => mock,
@@ -22,16 +24,24 @@ jest.unstable_mockModule("node:util", () => ({
22
24
 
23
25
  // Now import the module under test
24
26
  const mod = await import("./fs.js")
25
- const { getJSON, writeJSON, getCompressedJSON, pathExists, makeTempDirectory } = mod
27
+ const { readJSON, writeJSON, pathExists, makeTempDirectory } = mod
26
28
 
27
- describe("getJSON", () => {
29
+ describe("readJSON", () => {
28
30
  beforeEach(() => jest.clearAllMocks())
29
31
  it("parses JSON from file", async () => {
30
32
  readFileMock.mockResolvedValue(Buffer.from('{"a":1}'))
31
- const result = await getJSON("foo.json")
33
+ const result = await readJSON("foo.json")
32
34
  expect(result).toEqual({ a: 1 })
33
35
  expect(readFileMock).toHaveBeenCalledWith("foo.json")
34
36
  })
37
+ it("decompresses and parses JSON from .gz file", async () => {
38
+ readFileMock.mockResolvedValue(Buffer.from("gzipped"))
39
+ gunzipMock.mockResolvedValue(Buffer.from('{"b":2}'))
40
+ const result = await readJSON("bar.gz")
41
+ expect(result).toEqual({ b: 2 })
42
+ expect(readFileMock).toHaveBeenCalledWith("bar.gz")
43
+ expect(gunzipMock).toHaveBeenCalledWith(Buffer.from("gzipped"))
44
+ })
35
45
  })
36
46
 
37
47
  describe("writeJSON", () => {
@@ -55,20 +65,18 @@ describe("writeJSON", () => {
55
65
  await writeJSON("foo.json", { x: 2 }, { indent: 0 })
56
66
  expect(writeFileMock).toHaveBeenCalledWith("foo.json", '{"x":2}')
57
67
  })
58
- })
59
-
60
- describe("getCompressedJSON", () => {
61
- beforeEach(() => jest.clearAllMocks())
62
- it("reads, decompresses, and parses JSON", async () => {
63
- readFileMock.mockResolvedValue(Buffer.from("gzipped"))
64
- gunzipMock.mockResolvedValue(Buffer.from('{"b":2}'))
65
- const result = await getCompressedJSON("bar.gz")
66
- expect(result).toEqual({ b: 2 })
67
- expect(readFileMock).toHaveBeenCalledWith("bar.gz")
68
- expect(gunzipMock).toHaveBeenCalled()
69
- })
70
- it("throws for bad filename", async () => {
71
- await expect(getCompressedJSON("bar")).rejects.toThrow(/a compressed file/u)
68
+ it("compresses and writes JSON to .gz file", async () => {
69
+ gzipMock.mockResolvedValue(Buffer.from("compressed"))
70
+ await writeJSON("foo.gz", { y: 3 })
71
+ // Should gzip the JSON string and write the compressed buffer
72
+ expect(gzipMock).toHaveBeenCalledWith(JSON.stringify({ y: 3 }, undefined, 2))
73
+ expect(writeFileMock).toHaveBeenCalledWith("foo.gz", Buffer.from("compressed"))
74
+ })
75
+ it("compresses and writes JSON with custom indent to .gz file", async () => {
76
+ gzipMock.mockResolvedValue(Buffer.from("compressed"))
77
+ await writeJSON("foo.gz", { y: 3 }, { indent: 0 })
78
+ expect(gzipMock).toHaveBeenCalledWith(JSON.stringify({ y: 3 }, undefined, 0))
79
+ expect(writeFileMock).toHaveBeenCalledWith("foo.gz", Buffer.from("compressed"))
72
80
  })
73
81
  })
74
82
 
package/src/promise.js CHANGED
@@ -70,10 +70,15 @@ export async function sleep(ms) {
70
70
  * @param {Function=} $1.limiter A function awaited after a group of parallel calls is processed.
71
71
  * It is called with the number of parallel calls processed. Could be as simple as `() => sleep(10000)` if you wanted to wait 10 seconds between.
72
72
  * @param {boolean=} $1.flatten Flattens values before returning; useful if promises return arrays
73
+ * @param {boolean=} $1.abort If true, will return early if there are errors.
74
+ * If false (default), will process all elements in the array (like Promise.allSettled()).
73
75
  * @param {Function} callback
74
76
  * @returns {Object} {results, values, returned, errors}
75
77
  */
76
- export async function allSettled({ array, limit, limiter, flatten = false }, callback) {
78
+ export async function allSettled(
79
+ { array, limit, limiter, flatten = false, abort = false },
80
+ callback
81
+ ) {
77
82
  const results = []
78
83
  let returned = []
79
84
  let values = []
@@ -92,6 +97,9 @@ export async function allSettled({ array, limit, limiter, flatten = false }, cal
92
97
  errors.push(reason)
93
98
  }
94
99
  }
100
+ if (abort && errors.length) {
101
+ break
102
+ }
95
103
  await limiter?.(elements.length)
96
104
  }
97
105
  if (flatten) {
@@ -139,6 +147,7 @@ export function alert(result) {
139
147
  return result
140
148
  }
141
149
 
150
+ // unused but included for reference
142
151
  /**
143
152
  * Parallelize executions of a function using `Promise.all()`.
144
153
  * This is useful because usually you want to set a limit to the number of parallel requests possible at once.
@@ -2,7 +2,15 @@
2
2
  /* eslint-disable prefer-promise-reject-errors */
3
3
  import { jest } from "@jest/globals"
4
4
 
5
- import { alert, allSettled, poll, PollError, sleep, throwFirstReject, intervalLimiter } from "./promise.js"
5
+ import {
6
+ alert,
7
+ allSettled,
8
+ intervalLimiter,
9
+ poll,
10
+ PollError,
11
+ sleep,
12
+ throwFirstReject,
13
+ } from "./promise.js"
6
14
 
7
15
  describe("poll", () => {
8
16
  it("resolves immediately if callback returns a non-undefined/null/false value", async () => {
@@ -182,6 +190,22 @@ describe("allSettled", () => {
182
190
  expect(limiter).toHaveBeenCalledTimes(3)
183
191
  expect(limiterCalls).toEqual([2, 2, 1])
184
192
  })
193
+
194
+ it("returns early if abort=true and any error occurs", async () => {
195
+ // Should process only up to the first chunk with a rejection, then stop
196
+ const arr = [1, 2, 3, 4, 5, 6]
197
+ const cb = jest
198
+ .fn()
199
+ .mockImplementation((x) => (x === 2 || x === 4 ? Promise.reject(`fail${x}`) : x))
200
+ // limit=2 so chunks: [1,2], [3,4], [5,6]
201
+ const result = await allSettled({ array: arr, limit: 2, abort: true }, cb)
202
+ // The first chunk: [1,2] => 1 fulfilled, 1 rejected
203
+ // Should stop after first chunk with error
204
+ expect(result.values.length).toBe(2)
205
+ expect(result.errors).toEqual(["fail2"])
206
+ expect(cb).toHaveBeenCalledTimes(2)
207
+ // Should not process [3,4] or [5,6]
208
+ })
185
209
  })
186
210
 
187
211
  describe("intervalLimiter", () => {