@rudderjs/support 0.0.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Suleiman Shahbari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,336 @@
1
+ # @rudderjs/support
2
+
3
+ Shared utility primitives for RudderJS: collections, environment access, config lookup, debug helpers, and general-purpose functions.
4
+
5
+ All exports are also available from `@rudderjs/core` — you rarely need to install this package directly.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @rudderjs/support
11
+ ```
12
+
13
+ ---
14
+
15
+ ## `config()`
16
+
17
+ Read values from the application's `ConfigRepository` using dot-notation keys. The store is populated from your `config/` files at bootstrap time.
18
+
19
+ ```ts
20
+ import { config } from '@rudderjs/core'
21
+
22
+ config('app.name') // → 'RudderJS'
23
+ config('app.debug') // → false
24
+ config('cache.ttl', 60) // → number (with fallback)
25
+ ```
26
+
27
+ Keys follow the `file.key` pattern — `app.name` reads `configs.app.name` from `config/index.ts`.
28
+
29
+ ### `ConfigRepository` class
30
+
31
+ ```ts
32
+ import { ConfigRepository } from '@rudderjs/support'
33
+
34
+ const repo = new ConfigRepository({ db: { host: 'localhost', port: 5432 } })
35
+
36
+ repo.get('db.host') // 'localhost'
37
+ repo.get('db.port', 3306) // 5432 (falsy-safe — 0, false, '' are returned as-is)
38
+ repo.get('db.missing', 'n/a') // 'n/a'
39
+ repo.has('db.host') // true
40
+ repo.set('db.name', 'myapp') // creates nested key
41
+ repo.all() // entire data object
42
+ ```
43
+
44
+ `set()` silently ignores keys containing `__proto__`, `constructor`, or `prototype`.
45
+
46
+ ---
47
+
48
+ ## `dd()` / `dump()`
49
+
50
+ Debug helpers inspired by Laravel.
51
+
52
+ ```ts
53
+ import { dd, dump } from '@rudderjs/core'
54
+
55
+ // dump() — pretty-prints and continues
56
+ dump({ user, session })
57
+ dump(req.body, req.headers) // multiple args supported
58
+
59
+ // dd() — pretty-prints then terminates the process
60
+ dd(req.body)
61
+ ```
62
+
63
+ Both format arguments with `JSON.stringify` at 2-space indent. `dd()` calls `process.exit(1)` — development use only.
64
+
65
+ ---
66
+
67
+ ## `env()`
68
+
69
+ Read a string environment variable.
70
+
71
+ ```ts
72
+ import { env } from '@rudderjs/support'
73
+
74
+ env('APP_NAME', 'RudderJS') // → 'RudderJS'
75
+ env('APP_ENV') // throws if missing and no fallback
76
+ ```
77
+
78
+ ---
79
+
80
+ ## `Env`
81
+
82
+ Type-safe access to `process.env`.
83
+
84
+ ```ts
85
+ import { Env } from '@rudderjs/support'
86
+
87
+ Env.get('APP_NAME', 'RudderJS') // string (throws if missing and no fallback)
88
+ Env.getNumber('PORT', 3000) // number
89
+ Env.getBool('APP_DEBUG', false) // boolean — case-insensitive 'true' | '1' → true
90
+ Env.has('REDIS_URL') // boolean
91
+ ```
92
+
93
+ | Method | Return | Description |
94
+ |---|---|---|
95
+ | `get(key, fallback?)` | `string` | Returns the value or fallback. Throws if both are absent. |
96
+ | `getNumber(key, fallback?)` | `number` | Coerces to number. Throws if absent or NaN. |
97
+ | `getBool(key, fallback?)` | `boolean` | Case-insensitive `'true'` / `'1'` → `true`; everything else → `false`. |
98
+ | `has(key)` | `boolean` | `true` if the variable is set in `process.env`. |
99
+
100
+ ---
101
+
102
+ ## `defineEnv()`
103
+
104
+ Validate environment variables at startup using a Zod schema. Throws with a clear error listing all missing/invalid keys before the application boots.
105
+
106
+ ```ts
107
+ import { defineEnv } from '@rudderjs/support'
108
+ import { z } from 'zod'
109
+
110
+ export const env = defineEnv(z.object({
111
+ DATABASE_URL: z.string().url(),
112
+ PORT: z.coerce.number().default(3000),
113
+ APP_DEBUG: z.enum(['true', 'false']).transform(v => v === 'true').default('false'),
114
+ }))
115
+
116
+ env.PORT // number
117
+ env.APP_DEBUG // boolean
118
+ ```
119
+
120
+ ---
121
+
122
+ ## `Collection<T>`
123
+
124
+ Fluent, typed wrapper around arrays — inspired by Laravel Collections.
125
+
126
+ ```ts
127
+ import { Collection } from '@rudderjs/support'
128
+
129
+ const users = Collection.of([
130
+ { id: 1, name: 'Alice', role: 'admin' },
131
+ { id: 2, name: 'Bob', role: 'user' },
132
+ { id: 3, name: 'Carol', role: 'admin' },
133
+ ])
134
+
135
+ users.filter(u => u.role === 'admin').pluck('name').toArray() // ['Alice', 'Carol']
136
+ users.groupBy('role') // { admin: [...], user: [...] }
137
+ users.chunk(2).toArray() // [[...], [...]]
138
+ users.partition(u => u.role === 'admin') // [Collection<admin>, Collection<user>]
139
+ ```
140
+
141
+ **Core**
142
+
143
+ | Method | Description |
144
+ |---|---|
145
+ | `all()` | Underlying array. |
146
+ | `count()` | Number of items. |
147
+ | `first(fn?)` | First item (or first matching if `fn` given). |
148
+ | `last(fn?)` | Last item (or last matching if `fn` given). |
149
+ | `isEmpty()` | `true` when empty. |
150
+ | `isNotEmpty()` | `true` when not empty. |
151
+ | `each(fn)` | Iterate; returns `this`. |
152
+ | `toArray()` | Shallow copy. |
153
+ | `toJSON()` | Returns `T[]` — `JSON.stringify` works correctly. |
154
+
155
+ **Transform**
156
+
157
+ | Method | Description |
158
+ |---|---|
159
+ | `map<U>(fn)` | Transform each item; returns `Collection<U>`. |
160
+ | `flatMap<U>(fn)` | Map then flatten one level. |
161
+ | `filter(fn)` | Keep matching items. |
162
+ | `reject(fn)` | Remove matching items (inverse of `filter`). |
163
+ | `pluck(key)` | Extract a single field from each item. |
164
+ | `mapSpread<U>(fn)` | Spread each item as args to `fn` (useful for tuples). |
165
+
166
+ **Search**
167
+
168
+ | Method | Description |
169
+ |---|---|
170
+ | `find(fn)` | First matching item or `undefined`. |
171
+ | `contains(fn\|value)` | `true` if predicate matches or value is present. |
172
+ | `sole(fn?)` | Single matching item — throws if 0 or >1 found. |
173
+
174
+ **Grouping**
175
+
176
+ | Method | Description |
177
+ |---|---|
178
+ | `groupBy(key\|fn)` | Groups into `Record<string, T[]>`. |
179
+ | `keyBy(key\|fn)` | Index by key — returns `Record<string, T>`. Last write wins. |
180
+ | `mapWithKeys(fn)` | Transform to `Record<string, V>` via `fn` returning `[key, value]`. |
181
+
182
+ **Splitting**
183
+
184
+ | Method | Description |
185
+ |---|---|
186
+ | `chunk(size)` | Split into `Collection<T[]>` of fixed size. |
187
+ | `splitIn(n)` | Split into exactly `n` roughly-equal groups. |
188
+ | `partition(fn)` | Split into `[passing, failing]` tuple of collections. |
189
+ | `sliding(size, step?)` | Overlapping windows of `size`. |
190
+
191
+ **Combination**
192
+
193
+ | Method | Description |
194
+ |---|---|
195
+ | `zip(other)` | Pair items with another array/collection (shortest wins). |
196
+ | `crossJoin(other)` | Cartesian product with another array/collection. |
197
+ | `combine(values)` | Use this collection as keys, `values` as values → `Record<string, V>`. |
198
+
199
+ **Conditional / Pipe**
200
+
201
+ | Method | Description |
202
+ |---|---|
203
+ | `when(cond, fn, otherwise?)` | Apply `fn` if condition is truthy. |
204
+ | `unless(cond, fn, otherwise?)` | Apply `fn` if condition is falsy. |
205
+ | `pipe(fn)` | Pass collection through `fn`, return result (break the chain). |
206
+ | `tap(fn)` | Side-effect — calls `fn(this)` then returns `this`. |
207
+
208
+ ---
209
+
210
+ ---
211
+
212
+ ## `Str`
213
+
214
+ 30+ static string helpers — case conversion, truncation, search, extraction, masking, pluralisation, and generation.
215
+
216
+ ```ts
217
+ import { Str } from '@rudderjs/support'
218
+
219
+ Str.camel('hello_world') // 'helloWorld'
220
+ Str.snake('helloWorld') // 'hello_world'
221
+ Str.kebab('helloWorld') // 'hello-world'
222
+ Str.studly('hello_world') // 'HelloWorld'
223
+ Str.headline('user_profile') // 'User Profile'
224
+ Str.slug('Hello World!') // 'hello-world'
225
+
226
+ Str.limit('The quick brown fox', 10) // 'The quick...'
227
+ Str.excerpt('The quick brown fox', 'quick') // 'The quick brown fox'
228
+ Str.mask('4111111111111111', '*', 0, 12) // '************1111'
229
+
230
+ Str.before('user@example.com', '@') // 'user'
231
+ Str.after('user@example.com', '@') // 'example.com'
232
+ Str.between('<tag>content</tag>', '<tag>', '</tag>') // 'content'
233
+
234
+ Str.plural('post') // 'posts'
235
+ Str.plural('post', 1) // 'post'
236
+ Str.singular('posts') // 'post'
237
+
238
+ Str.uuid() // 'f47ac10b-...'
239
+ Str.random(16) // 'aB3xK9mZ...'
240
+ Str.password(32) // cryptographically random password
241
+ ```
242
+
243
+ | Category | Methods |
244
+ |---|---|
245
+ | Case | `camel`, `snake`, `kebab`, `studly`, `title`, `headline` |
246
+ | Truncation | `limit`, `words`, `excerpt` |
247
+ | Search | `contains`, `containsAll`, `startsWith`, `endsWith` |
248
+ | Extraction | `before`, `beforeLast`, `after`, `afterLast`, `between` |
249
+ | Replacement | `replaceFirst`, `replaceLast` |
250
+ | Padding | `padLeft`, `padRight`, `padBoth` |
251
+ | Whitespace | `squish`, `trim` |
252
+ | Masking | `mask` |
253
+ | Normalisation | `ascii`, `slug` |
254
+ | Identification | `uuid`, `isUuid`, `isUlid` |
255
+ | Generation | `random`, `password` |
256
+ | Pluralisation | `plural`, `singular` |
257
+
258
+ ---
259
+
260
+ ## `Num`
261
+
262
+ Static numeric helpers — formatting, abbreviation, ordinals, and more.
263
+
264
+ ```ts
265
+ import { Num } from '@rudderjs/support'
266
+
267
+ Num.format(1234567.89, 2) // '1,234,567.89'
268
+ Num.currency(9.99) // '$9.99'
269
+ Num.currency(9.99, 'EUR', 'de-DE') // '9,99 €'
270
+ Num.percentage(73.5, 1) // '73.5%'
271
+ Num.fileSize(1536) // '1.50 KB'
272
+ Num.abbreviate(1_500_000) // '1.5M'
273
+ Num.ordinal(22) // '22nd'
274
+ Num.clamp(150, 0, 100) // 100
275
+ Num.trim(1.5000) // '1.5'
276
+ Num.spell(42) // 'forty-two'
277
+ Num.spell(1_001) // 'one thousand one'
278
+ ```
279
+
280
+ | Method | Description |
281
+ |---|---|
282
+ | `format(n, decimals?, locale?)` | Locale-aware number with separators. |
283
+ | `currency(n, currency?, locale?)` | Currency string via `Intl.NumberFormat`. |
284
+ | `percentage(n, decimals?, locale?)` | `n` as a percentage (`50` → `'50%'`). |
285
+ | `fileSize(bytes, precision?)` | Human-readable file size (`1536` → `'1.50 KB'`). |
286
+ | `abbreviate(n, precision?)` | Short form (`1_500_000` → `'1.5M'`). |
287
+ | `ordinal(n)` | Ordinal suffix (`1` → `'1st'`, `22` → `'22nd'`). |
288
+ | `clamp(n, min, max)` | Clamp to range. |
289
+ | `trim(n, decimals?)` | Remove trailing zeros (`1.500` → `'1.5'`). |
290
+ | `spell(n)` | Integer to English words (`42` → `'forty-two'`). |
291
+
292
+ ---
293
+
294
+ ## Helper Functions
295
+
296
+ ```ts
297
+ import { sleep, ucfirst, pick, omit, tap, deepClone, isObject, toSnakeCase, toCamelCase } from '@rudderjs/support'
298
+
299
+ await sleep(500)
300
+
301
+ ucfirst('hello world') // 'Hello world'
302
+ toSnakeCase('fooBarBaz') // 'foo_bar_baz'
303
+ toCamelCase('foo_bar_baz') // 'fooBarBaz'
304
+
305
+ pick({ id: 1, name: 'A', secret: 'x' }, ['id', 'name']) // { id: 1, name: 'A' }
306
+ omit({ id: 1, secret: 'x' }, ['secret']) // { id: 1 }
307
+
308
+ tap(new Map(), m => m.set('key', 1)) // returns the Map
309
+ deepClone({ nested: { value: 1 } }) // deep copy via JSON round-trip
310
+
311
+ isObject({}) // true
312
+ isObject(new Date()) // false — only plain objects pass
313
+ isObject([]) // false
314
+ isObject(null) // false
315
+ ```
316
+
317
+ | Function | Description |
318
+ |---|---|
319
+ | `sleep(ms)` | Resolves after `ms` milliseconds. |
320
+ | `ucfirst(str)` | Capitalises the first character. |
321
+ | `toSnakeCase(str)` | `camelCase` / `PascalCase` → `snake_case`. |
322
+ | `toCamelCase(str)` | `snake_case` → `camelCase`. |
323
+ | `pick(obj, keys)` | New object with only the specified keys. |
324
+ | `omit(obj, keys)` | New object with the specified keys removed. |
325
+ | `tap(value, fn)` | Calls `fn(value)` and returns `value`. |
326
+ | `deepClone(value)` | Deep clone via JSON round-trip. |
327
+ | `isObject(value)` | `true` for plain objects only — `false` for `Date`, `Map`, arrays, `null`. |
328
+
329
+ ---
330
+
331
+ ## Notes
332
+
333
+ - All exports are re-exported from `@rudderjs/core` — you rarely need to import `@rudderjs/support` directly.
334
+ - `defineEnv()` validates eagerly at module evaluation time — failures surface at boot.
335
+ - `dd()` calls `process.exit(1)` — development use only.
336
+ - `resolveOptionalPeer()` resolves optional peer packages from the app root — used internally by adapters.
@@ -0,0 +1,86 @@
1
+ export declare class Collection<T> {
2
+ private items;
3
+ constructor(items?: T[]);
4
+ static of<T>(items: T[]): Collection<T>;
5
+ all(): T[];
6
+ count(): number;
7
+ first(fn?: (item: T) => boolean): T | undefined;
8
+ last(fn?: (item: T) => boolean): T | undefined;
9
+ isEmpty(): boolean;
10
+ isNotEmpty(): boolean;
11
+ each(fn: (item: T, index: number) => void): this;
12
+ map<U>(fn: (item: T, index: number) => U): Collection<U>;
13
+ flatMap<U>(fn: (item: T, index: number) => U[]): Collection<U>;
14
+ filter(fn: (item: T) => boolean): Collection<T>;
15
+ reject(fn: (item: T) => boolean): Collection<T>;
16
+ pluck<K extends keyof T>(key: K): Collection<T[K]>;
17
+ find(fn: (item: T) => boolean): T | undefined;
18
+ contains(fn: ((item: T) => boolean) | T): boolean;
19
+ /**
20
+ * Return the single matching item. Throws if 0 or more than 1 item matches.
21
+ */
22
+ sole(fn?: (item: T) => boolean): T;
23
+ groupBy<K extends keyof T>(key: K | ((item: T) => string)): Record<string, T[]>;
24
+ /** Index items by a key or resolver — last write wins on collision. */
25
+ keyBy<K extends keyof T>(key: K | ((item: T) => string)): Record<string, T>;
26
+ /** Transform into a key→value record. `fn` returns `[key, value]` for each item. */
27
+ mapWithKeys<V>(fn: (item: T, index: number) => [string, V]): Record<string, V>;
28
+ /**
29
+ * Split into chunks of `size`.
30
+ * @example collect([1,2,3,4,5]).chunk(2) → [[1,2],[3,4],[5]]
31
+ */
32
+ chunk(size: number): Collection<T[]>;
33
+ /**
34
+ * Split into exactly `n` roughly-equal groups.
35
+ * @example collect([1,2,3,4,5]).splitIn(2) → [[1,2,3],[4,5]]
36
+ */
37
+ splitIn(n: number): Collection<T[]>;
38
+ /**
39
+ * Split into [passing, failing] based on `fn`.
40
+ * @example collect([1,2,3,4]).partition(n => n % 2 === 0) → [[2,4], [1,3]]
41
+ */
42
+ partition(fn: (item: T) => boolean): [Collection<T>, Collection<T>];
43
+ /**
44
+ * Sliding window — yields overlapping sub-arrays of `size`.
45
+ * @example collect([1,2,3,4]).sliding(2) → [[1,2],[2,3],[3,4]]
46
+ */
47
+ sliding(size: number, step?: number): Collection<T[]>;
48
+ /**
49
+ * Zip this collection with one or more arrays/collections (shortest wins).
50
+ * @example collect([1,2]).zip(['a','b']) → [[1,'a'],[2,'b']]
51
+ */
52
+ zip<U>(other: U[] | Collection<U>): Collection<[T, U]>;
53
+ /**
54
+ * Cross-join with another array/collection — returns the cartesian product.
55
+ * @example collect([1,2]).crossJoin(['a','b']) → [[1,'a'],[1,'b'],[2,'a'],[2,'b']]
56
+ */
57
+ crossJoin<U>(other: U[] | Collection<U>): Collection<[T, U]>;
58
+ /**
59
+ * Use this collection as keys and `values` as values — produces a plain object.
60
+ * @example collect(['name','age']).combine(['Alice', 30]) → { name: 'Alice', age: 30 }
61
+ */
62
+ combine<V>(values: V[] | Collection<V>): Record<string, V>;
63
+ /**
64
+ * Map where each item is spread as individual arguments to `fn`.
65
+ * Useful for tuple collections.
66
+ * @example collect([[1,'a'],[2,'b']]).mapSpread((n, s) => `${n}-${s}`) → ['1-a','2-b']
67
+ */
68
+ mapSpread<U>(fn: (...args: unknown[]) => U): Collection<U>;
69
+ /** Apply `fn` to this collection if `condition` is truthy — chainable. */
70
+ when(condition: boolean | ((c: Collection<T>) => boolean), fn: (c: Collection<T>) => Collection<T>, otherwise?: (c: Collection<T>) => Collection<T>): Collection<T>;
71
+ /** Apply `fn` to this collection if `condition` is falsy — chainable. */
72
+ unless(condition: boolean | ((c: Collection<T>) => boolean), fn: (c: Collection<T>) => Collection<T>, otherwise?: (c: Collection<T>) => Collection<T>): Collection<T>;
73
+ /**
74
+ * Pass this collection through a callback and return the result.
75
+ * Useful for breaking out of method chains.
76
+ */
77
+ pipe<U>(fn: (collection: Collection<T>) => U): U;
78
+ /**
79
+ * Tap into the chain for side-effects — returns `this`.
80
+ * @example collect([1,2,3]).tap(c => console.log(c.count())).map(...)
81
+ */
82
+ tap(fn: (collection: Collection<T>) => void): this;
83
+ toArray(): T[];
84
+ toJSON(): T[];
85
+ }
86
+ //# sourceMappingURL=collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,228 @@
1
+ // ─── Collection ────────────────────────────────────────────
2
+ export class Collection {
3
+ items;
4
+ constructor(items = []) {
5
+ this.items = [...items];
6
+ }
7
+ static of(items) {
8
+ return new Collection(items);
9
+ }
10
+ // ── Core ─────────────────────────────────────────────────
11
+ all() {
12
+ return this.items;
13
+ }
14
+ count() {
15
+ return this.items.length;
16
+ }
17
+ first(fn) {
18
+ return fn ? this.items.find(fn) : this.items[0];
19
+ }
20
+ last(fn) {
21
+ if (fn) {
22
+ const filtered = this.items.filter(fn);
23
+ return filtered[filtered.length - 1];
24
+ }
25
+ return this.items[this.items.length - 1];
26
+ }
27
+ isEmpty() {
28
+ return this.items.length === 0;
29
+ }
30
+ isNotEmpty() {
31
+ return this.items.length > 0;
32
+ }
33
+ // ── Iteration ────────────────────────────────────────────
34
+ each(fn) {
35
+ this.items.forEach(fn);
36
+ return this;
37
+ }
38
+ // ── Transform ────────────────────────────────────────────
39
+ map(fn) {
40
+ return new Collection(this.items.map(fn));
41
+ }
42
+ flatMap(fn) {
43
+ return new Collection(this.items.flatMap(fn));
44
+ }
45
+ filter(fn) {
46
+ return new Collection(this.items.filter(fn));
47
+ }
48
+ reject(fn) {
49
+ return new Collection(this.items.filter(item => !fn(item)));
50
+ }
51
+ pluck(key) {
52
+ return new Collection(this.items.map(item => item[key]));
53
+ }
54
+ // ── Search ───────────────────────────────────────────────
55
+ find(fn) {
56
+ return this.items.find(fn);
57
+ }
58
+ contains(fn) {
59
+ if (typeof fn === 'function')
60
+ return this.items.some(fn);
61
+ return this.items.includes(fn);
62
+ }
63
+ /**
64
+ * Return the single matching item. Throws if 0 or more than 1 item matches.
65
+ */
66
+ sole(fn) {
67
+ const filtered = fn ? this.items.filter(fn) : this.items;
68
+ if (filtered.length === 0)
69
+ throw new Error('[Collection] sole() found no matching items.');
70
+ if (filtered.length > 1)
71
+ throw new Error(`[Collection] sole() found ${filtered.length} items — expected exactly 1.`);
72
+ return filtered[0];
73
+ }
74
+ // ── Grouping ─────────────────────────────────────────────
75
+ groupBy(key) {
76
+ return this.items.reduce((acc, item) => {
77
+ const group = typeof key === 'function' ? key(item) : String(item[key]);
78
+ acc[group] = [...(acc[group] ?? []), item];
79
+ return acc;
80
+ }, {});
81
+ }
82
+ /** Index items by a key or resolver — last write wins on collision. */
83
+ keyBy(key) {
84
+ const result = {};
85
+ for (const item of this.items) {
86
+ const k = typeof key === 'function' ? key(item) : String(item[key]);
87
+ result[k] = item;
88
+ }
89
+ return result;
90
+ }
91
+ /** Transform into a key→value record. `fn` returns `[key, value]` for each item. */
92
+ mapWithKeys(fn) {
93
+ const result = {};
94
+ for (let i = 0; i < this.items.length; i++) {
95
+ const [k, v] = fn(this.items[i], i);
96
+ result[k] = v;
97
+ }
98
+ return result;
99
+ }
100
+ // ── Splitting ────────────────────────────────────────────
101
+ /**
102
+ * Split into chunks of `size`.
103
+ * @example collect([1,2,3,4,5]).chunk(2) → [[1,2],[3,4],[5]]
104
+ */
105
+ chunk(size) {
106
+ if (size < 1)
107
+ throw new Error('[Collection] chunk() size must be >= 1.');
108
+ const chunks = [];
109
+ for (let i = 0; i < this.items.length; i += size) {
110
+ chunks.push(this.items.slice(i, i + size));
111
+ }
112
+ return new Collection(chunks);
113
+ }
114
+ /**
115
+ * Split into exactly `n` roughly-equal groups.
116
+ * @example collect([1,2,3,4,5]).splitIn(2) → [[1,2,3],[4,5]]
117
+ */
118
+ splitIn(n) {
119
+ return this.chunk(Math.ceil(this.items.length / n));
120
+ }
121
+ /**
122
+ * Split into [passing, failing] based on `fn`.
123
+ * @example collect([1,2,3,4]).partition(n => n % 2 === 0) → [[2,4], [1,3]]
124
+ */
125
+ partition(fn) {
126
+ const pass = [];
127
+ const fail = [];
128
+ for (const item of this.items) {
129
+ ;
130
+ (fn(item) ? pass : fail).push(item);
131
+ }
132
+ return [new Collection(pass), new Collection(fail)];
133
+ }
134
+ /**
135
+ * Sliding window — yields overlapping sub-arrays of `size`.
136
+ * @example collect([1,2,3,4]).sliding(2) → [[1,2],[2,3],[3,4]]
137
+ */
138
+ sliding(size, step = 1) {
139
+ const result = [];
140
+ for (let i = 0; i <= this.items.length - size; i += step) {
141
+ result.push(this.items.slice(i, i + size));
142
+ }
143
+ return new Collection(result);
144
+ }
145
+ // ── Combination ──────────────────────────────────────────
146
+ /**
147
+ * Zip this collection with one or more arrays/collections (shortest wins).
148
+ * @example collect([1,2]).zip(['a','b']) → [[1,'a'],[2,'b']]
149
+ */
150
+ zip(other) {
151
+ const arr = other instanceof Collection ? other.all() : other;
152
+ const len = Math.min(this.items.length, arr.length);
153
+ const result = [];
154
+ for (let i = 0; i < len; i++)
155
+ result.push([this.items[i], arr[i]]);
156
+ return new Collection(result);
157
+ }
158
+ /**
159
+ * Cross-join with another array/collection — returns the cartesian product.
160
+ * @example collect([1,2]).crossJoin(['a','b']) → [[1,'a'],[1,'b'],[2,'a'],[2,'b']]
161
+ */
162
+ crossJoin(other) {
163
+ const arr = other instanceof Collection ? other.all() : other;
164
+ const result = [];
165
+ for (const a of this.items) {
166
+ for (const b of arr)
167
+ result.push([a, b]);
168
+ }
169
+ return new Collection(result);
170
+ }
171
+ /**
172
+ * Use this collection as keys and `values` as values — produces a plain object.
173
+ * @example collect(['name','age']).combine(['Alice', 30]) → { name: 'Alice', age: 30 }
174
+ */
175
+ combine(values) {
176
+ const vals = values instanceof Collection ? values.all() : values;
177
+ const result = {};
178
+ for (let i = 0; i < this.items.length; i++) {
179
+ result[String(this.items[i])] = vals[i];
180
+ }
181
+ return result;
182
+ }
183
+ /**
184
+ * Map where each item is spread as individual arguments to `fn`.
185
+ * Useful for tuple collections.
186
+ * @example collect([[1,'a'],[2,'b']]).mapSpread((n, s) => `${n}-${s}`) → ['1-a','2-b']
187
+ */
188
+ mapSpread(fn) {
189
+ return new Collection(this.items.map(item => fn(...(Array.isArray(item) ? item : [item]))));
190
+ }
191
+ // ── Conditional / Pipe ───────────────────────────────────
192
+ /** Apply `fn` to this collection if `condition` is truthy — chainable. */
193
+ when(condition, fn, otherwise) {
194
+ const cond = typeof condition === 'function' ? condition(this) : condition;
195
+ if (cond)
196
+ return fn(this);
197
+ if (otherwise)
198
+ return otherwise(this);
199
+ return this;
200
+ }
201
+ /** Apply `fn` to this collection if `condition` is falsy — chainable. */
202
+ unless(condition, fn, otherwise) {
203
+ return this.when(typeof condition === 'function' ? (c) => !condition(c) : !condition, fn, otherwise);
204
+ }
205
+ /**
206
+ * Pass this collection through a callback and return the result.
207
+ * Useful for breaking out of method chains.
208
+ */
209
+ pipe(fn) {
210
+ return fn(this);
211
+ }
212
+ /**
213
+ * Tap into the chain for side-effects — returns `this`.
214
+ * @example collect([1,2,3]).tap(c => console.log(c.count())).map(...)
215
+ */
216
+ tap(fn) {
217
+ fn(this);
218
+ return this;
219
+ }
220
+ // ── Serialisation ────────────────────────────────────────
221
+ toArray() {
222
+ return [...this.items];
223
+ }
224
+ toJSON() {
225
+ return this.items;
226
+ }
227
+ }
228
+ //# sourceMappingURL=collection.js.map