@raubjo/architect 0.5.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.
Files changed (68) hide show
  1. package/README.md +860 -0
  2. package/package.json +121 -0
  3. package/src/cache/cache.ts +46 -0
  4. package/src/cache/contract.ts +9 -0
  5. package/src/cache/manager.ts +110 -0
  6. package/src/cache/provider.ts +11 -0
  7. package/src/config/contract.ts +63 -0
  8. package/src/config/discovery.ts +99 -0
  9. package/src/config/env.global.d.ts +6 -0
  10. package/src/config/env.ts +68 -0
  11. package/src/config/index.ts +5 -0
  12. package/src/config/provider.ts +17 -0
  13. package/src/config/repository.ts +164 -0
  14. package/src/container/adapters/builtin.ts +323 -0
  15. package/src/container/contract.ts +43 -0
  16. package/src/container/runtime.ts +29 -0
  17. package/src/events/bus.ts +174 -0
  18. package/src/events/concerns/dispatchable.ts +10 -0
  19. package/src/events/provider.ts +9 -0
  20. package/src/events/types.ts +9 -0
  21. package/src/foundation/application.ts +136 -0
  22. package/src/foundation/current-application.ts +20 -0
  23. package/src/index.ts +58 -0
  24. package/src/log/contract.ts +21 -0
  25. package/src/log/drivers/console.ts +54 -0
  26. package/src/log/drivers/null.ts +23 -0
  27. package/src/log/drivers/stack.ts +46 -0
  28. package/src/log/manager.ts +76 -0
  29. package/src/log/provider.ts +11 -0
  30. package/src/react.ts +2 -0
  31. package/src/renderers/adapters/react.tsx +25 -0
  32. package/src/renderers/adapters/solid.tsx +26 -0
  33. package/src/renderers/adapters/svelte.ts +73 -0
  34. package/src/renderers/adapters/vue.ts +22 -0
  35. package/src/renderers/contract.ts +12 -0
  36. package/src/runtimes/react.tsx +81 -0
  37. package/src/runtimes/solid.tsx +47 -0
  38. package/src/runtimes/svelte.ts +17 -0
  39. package/src/runtimes/vue.ts +34 -0
  40. package/src/solid.ts +2 -0
  41. package/src/store/adapters/contract.ts +11 -0
  42. package/src/store/adapters/indexed-db.ts +187 -0
  43. package/src/store/adapters/local-storage.ts +48 -0
  44. package/src/store/adapters/memory.ts +35 -0
  45. package/src/store/manager.ts +68 -0
  46. package/src/store/provider.ts +10 -0
  47. package/src/store/store.ts +1 -0
  48. package/src/support/arr.ts +372 -0
  49. package/src/support/collection.ts +889 -0
  50. package/src/support/facades/cache.ts +6 -0
  51. package/src/support/facades/config.ts +6 -0
  52. package/src/support/facades/event.ts +6 -0
  53. package/src/support/facades/facade.ts +146 -0
  54. package/src/support/facades/index.ts +5 -0
  55. package/src/support/facades/log.ts +6 -0
  56. package/src/support/facades/store.ts +6 -0
  57. package/src/support/fluent.ts +56 -0
  58. package/src/support/globals.ts +8 -0
  59. package/src/support/lazy-collection.ts +341 -0
  60. package/src/support/manager.ts +53 -0
  61. package/src/support/num.ts +50 -0
  62. package/src/support/pipeline.ts +29 -0
  63. package/src/support/service-provider.ts +19 -0
  64. package/src/support/str.ts +682 -0
  65. package/src/svelte.ts +2 -0
  66. package/src/types/peer-deps.d.ts +10 -0
  67. package/src/vue.ts +2 -0
  68. package/tsconfig.json +15 -0
@@ -0,0 +1,889 @@
1
+ export class Collection<T> {
2
+ protected items: T[]
3
+
4
+ constructor(items: T[] = []) {
5
+ this.items = [...items]
6
+ }
7
+
8
+ // ── Static constructors ────────────────────────────────────────────────────
9
+
10
+ static make<T>(items?: T[] | null): Collection<T> {
11
+ return new Collection(items ?? [])
12
+ }
13
+
14
+ static fromJson(json: string): Collection<unknown> {
15
+ const parsed = JSON.parse(json)
16
+ return new Collection(Array.isArray(parsed) ? parsed : [parsed])
17
+ }
18
+
19
+ static range(start: number, end: number): Collection<number> {
20
+ const result: number[] = []
21
+ for (let i = start; i <= end; i++) result.push(i)
22
+ return new Collection(result)
23
+ }
24
+
25
+ static times<U>(count: number, callback: (n: number) => U): Collection<U> {
26
+ const result: U[] = []
27
+ for (let i = 1; i <= count; i++) result.push(callback(i))
28
+ return new Collection(result)
29
+ }
30
+
31
+ static wrap<U>(value: U | U[] | Collection<U> | null | undefined): Collection<U> {
32
+ if (value == null) return new Collection<U>()
33
+ if (value instanceof Collection) return new Collection(value.all())
34
+ if (Array.isArray(value)) return new Collection(value)
35
+ return new Collection([value as U])
36
+ }
37
+
38
+ static unwrap<U>(value: U | U[] | Collection<U>): U[] | U {
39
+ if (value instanceof Collection) return value.all()
40
+ if (Array.isArray(value)) return value
41
+ return value
42
+ }
43
+
44
+ // ── Core ──────────────────────────────────────────────────────────────────
45
+
46
+ all(): T[] {
47
+ return [...this.items]
48
+ }
49
+
50
+ toArray(): T[] {
51
+ return this.all()
52
+ }
53
+
54
+ count(): number {
55
+ return this.items.length
56
+ }
57
+
58
+ isEmpty(): boolean {
59
+ return this.items.length === 0
60
+ }
61
+
62
+ isNotEmpty(): boolean {
63
+ return this.items.length > 0
64
+ }
65
+
66
+ first(callback?: (item: T, index: number) => boolean): T | null {
67
+ if (!callback) return this.items[0] ?? null
68
+ return this.items.find(callback) ?? null
69
+ }
70
+
71
+ last(callback?: (item: T, index: number) => boolean): T | null {
72
+ if (!callback) return this.items[this.items.length - 1] ?? null
73
+ return [...this.items].reverse().find((item, i) => callback(item, this.items.length - 1 - i)) ?? null
74
+ }
75
+
76
+ // ── Aliases ───────────────────────────────────────────────────────────────
77
+
78
+ average(key?: keyof T | ((item: T) => number)): number {
79
+ return this.avg(key)
80
+ }
81
+
82
+ avg(key?: keyof T | ((item: T) => number)): number {
83
+ if (this.items.length === 0) return 0
84
+ return this.sum(key) / this.items.length
85
+ }
86
+
87
+ some(callback: (item: T, index: number) => boolean): boolean {
88
+ return this.items.some(callback)
89
+ }
90
+
91
+ // ── Navigation ────────────────────────────────────────────────────────────
92
+
93
+ after(item: T | ((item: T, index: number) => boolean), strict = false): T | null {
94
+ const idx = this._findIndex(item, strict)
95
+ if (idx === -1 || idx >= this.items.length - 1) return null
96
+ return this.items[idx + 1] ?? null
97
+ }
98
+
99
+ before(item: T | ((item: T, index: number) => boolean), strict = false): T | null {
100
+ const idx = this._findIndex(item, strict)
101
+ if (idx <= 0) return null
102
+ return this.items[idx - 1] ?? null
103
+ }
104
+
105
+ protected _findIndex(item: T | ((item: T, index: number) => boolean), strict = false): number {
106
+ if (typeof item === "function") {
107
+ return this.items.findIndex(item as (item: T, index: number) => boolean)
108
+ }
109
+ if (strict) {
110
+ return this.items.indexOf(item)
111
+ }
112
+ // biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality
113
+ return this.items.findIndex((i) => i == item)
114
+ }
115
+
116
+ // ── Counting / stats ──────────────────────────────────────────────────────
117
+
118
+ countBy(callback?: (item: T) => string | number): Record<string, number> {
119
+ const result: Record<string, number> = {}
120
+ for (const item of this.items) {
121
+ const key = String(callback ? callback(item) : item)
122
+ result[key] = (result[key] ?? 0) + 1
123
+ }
124
+ return result
125
+ }
126
+
127
+ sum(key?: keyof T | ((item: T) => number)): number {
128
+ return this.items.reduce((s, i) => s + (this.extractValue(i, key) as number), 0)
129
+ }
130
+
131
+ min(key?: keyof T | ((item: T) => number)): T | null {
132
+ if (this.items.length === 0) return null
133
+ return this.items.reduce((min, i) =>
134
+ (this.extractValue(i, key) as number) < (this.extractValue(min, key) as number) ? i : min,
135
+ )
136
+ }
137
+
138
+ max(key?: keyof T | ((item: T) => number)): T | null {
139
+ if (this.items.length === 0) return null
140
+ return this.items.reduce((max, i) =>
141
+ (this.extractValue(i, key) as number) > (this.extractValue(max, key) as number) ? i : max,
142
+ )
143
+ }
144
+
145
+ median(key?: keyof T | ((item: T) => number)): number | null {
146
+ if (this.items.length === 0) return null
147
+ const vals = this.items.map((i) => this.extractValue(i, key) as number).sort((a, b) => a - b)
148
+ const mid = Math.floor(vals.length / 2)
149
+ return vals.length % 2 === 0 ? (vals[mid - 1] + vals[mid]) / 2 : vals[mid]
150
+ }
151
+
152
+ mode(key?: keyof T | ((item: T) => number)): T[] {
153
+ if (this.items.length === 0) return []
154
+ const freq = new Map<unknown, number>()
155
+ for (const item of this.items) {
156
+ const v = this.extractValue(item, key)
157
+ freq.set(v, (freq.get(v) ?? 0) + 1)
158
+ }
159
+ const maxFreq = Math.max(...freq.values())
160
+ const modalVals = new Set([...freq.entries()].filter(([, c]) => c === maxFreq).map(([v]) => v))
161
+ return this.items.filter((item) => modalVals.has(this.extractValue(item, key)))
162
+ }
163
+
164
+ percentage(callback: (item: T) => boolean, precision = 2): number {
165
+ if (this.items.length === 0) return 0
166
+ const pct = (this.items.filter(callback).length / this.items.length) * 100
167
+ return Math.round(pct * 10 ** precision) / 10 ** precision
168
+ }
169
+
170
+ // ── Searching ─────────────────────────────────────────────────────────────
171
+
172
+ firstOrFail(callback?: (item: T, index: number) => boolean): T {
173
+ const found = this.first(callback)
174
+ if (found === null) throw new Error("Item not found.")
175
+ return found
176
+ }
177
+
178
+ firstWhere(key: keyof T, operatorOrValue: unknown, value?: unknown): T | null {
179
+ const [op, val] = value === undefined ? ["==", operatorOrValue] : [operatorOrValue as string, value]
180
+ return (
181
+ this.items.find((item) => {
182
+ return this._compareOp(item[key], op as string, val)
183
+ }) ?? null
184
+ )
185
+ }
186
+
187
+ search(item: T | ((item: T, index: number) => boolean), strict = false): number | false {
188
+ if (typeof item === "function") {
189
+ const idx = this.items.findIndex(item as (item: T, index: number) => boolean)
190
+ return idx === -1 ? false : idx
191
+ }
192
+ if (strict) {
193
+ const idx = this.items.indexOf(item)
194
+ return idx === -1 ? false : idx
195
+ }
196
+ // biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality
197
+ const idx = this.items.findIndex((i) => i == item)
198
+ return idx === -1 ? false : idx
199
+ }
200
+
201
+ sole(callback?: (item: T, index: number) => boolean): T {
202
+ const matches = callback ? this.items.filter(callback) : this.items
203
+ if (matches.length === 0) throw new Error("No items match the given criteria.")
204
+ if (matches.length > 1) throw new Error("Multiple items match the given criteria.")
205
+ return matches[0]
206
+ }
207
+
208
+ value(key: keyof T): T[keyof T] | null {
209
+ const first = this.first()
210
+ if (first === null || first === undefined) return null
211
+ return first[key]
212
+ }
213
+
214
+ // ── Transforming ──────────────────────────────────────────────────────────
215
+
216
+ map<U>(callback: (item: T, index: number) => U): Collection<U> {
217
+ return new Collection(this.items.map(callback))
218
+ }
219
+
220
+ filter(callback?: (item: T, index: number) => boolean): Collection<T> {
221
+ return new Collection(
222
+ callback ? this.items.filter(callback) : this.items.filter(Boolean as unknown as (item: T) => boolean),
223
+ )
224
+ }
225
+
226
+ reject(callback: (item: T, index: number) => boolean): Collection<T> {
227
+ return new Collection(this.items.filter((item, i) => !callback(item, i)))
228
+ }
229
+
230
+ reduce<U>(callback: (carry: U, item: T, index: number) => U, initial: U): U {
231
+ return this.items.reduce(callback, initial)
232
+ }
233
+
234
+ each(callback: (item: T, index: number) => void): this {
235
+ this.items.forEach(callback)
236
+ return this
237
+ }
238
+
239
+ collect(): Collection<T> {
240
+ return new Collection(this.items)
241
+ }
242
+
243
+ flatMap<U>(callback: (item: T, index: number) => U[]): Collection<U> {
244
+ return new Collection(this.items.flatMap(callback))
245
+ }
246
+
247
+ mapSpread<U>(callback: (...args: unknown[]) => U): Collection<U> {
248
+ return new Collection(this.items.map((item) => callback(...(item as unknown as unknown[]))))
249
+ }
250
+
251
+ mapToGroups<K extends string, V>(callback: (item: T, index: number) => [K, V]): Record<string, Collection<V>> {
252
+ const result: Record<string, V[]> = {}
253
+ this.items.forEach((item, i) => {
254
+ const [k, v] = callback(item, i)
255
+ if (!result[k]) result[k] = []
256
+ result[k].push(v)
257
+ })
258
+ return Object.fromEntries(Object.entries(result).map(([k, v]) => [k, new Collection(v as V[])]))
259
+ }
260
+
261
+ mapWithKeys<K extends string, V>(callback: (item: T, index: number) => [K, V]): Record<K, V> {
262
+ const result = {} as Record<K, V>
263
+ this.items.forEach((item, i) => {
264
+ const [k, v] = callback(item, i)
265
+ result[k] = v
266
+ })
267
+ return result
268
+ }
269
+
270
+ transform(callback: (item: T, index: number) => T): this {
271
+ this.items = this.items.map(callback)
272
+ return this
273
+ }
274
+
275
+ undot(): Collection<Record<string, unknown>> {
276
+ return new Collection(
277
+ (this.items as unknown as Record<string, unknown>[]).map((item) => {
278
+ const result: Record<string, unknown> = {}
279
+ for (const [dotKey, val] of Object.entries(item)) {
280
+ const parts = dotKey.split(".")
281
+ let cur = result
282
+ for (let i = 0; i < parts.length - 1; i++) {
283
+ if (!cur[parts[i]] || typeof cur[parts[i]] !== "object") {
284
+ cur[parts[i]] = {}
285
+ }
286
+ cur = cur[parts[i]] as Record<string, unknown>
287
+ }
288
+ cur[parts[parts.length - 1]] = val
289
+ }
290
+ return result
291
+ }),
292
+ )
293
+ }
294
+
295
+ dot(): Collection<Record<string, unknown>> {
296
+ const flattenObj = (obj: Record<string, unknown>, prefix = ""): Record<string, unknown> => {
297
+ const result: Record<string, unknown> = {}
298
+ for (const [k, v] of Object.entries(obj)) {
299
+ const newKey = prefix ? `${prefix}.${k}` : k
300
+ if (v && typeof v === "object" && !Array.isArray(v)) {
301
+ Object.assign(result, flattenObj(v as Record<string, unknown>, newKey))
302
+ } else {
303
+ result[newKey] = v
304
+ }
305
+ }
306
+ return result
307
+ }
308
+ return new Collection((this.items as unknown as Record<string, unknown>[]).map((item) => flattenObj(item)))
309
+ }
310
+
311
+ // ── Set operations ────────────────────────────────────────────────────────
312
+
313
+ diff(items: T[] | Collection<T>): Collection<T> {
314
+ const other = items instanceof Collection ? items.all() : items
315
+ return new Collection(this.items.filter((i) => !other.includes(i)))
316
+ }
317
+
318
+ intersect(items: T[] | Collection<T>): Collection<T> {
319
+ const other = items instanceof Collection ? items.all() : items
320
+ return new Collection(this.items.filter((i) => other.includes(i)))
321
+ }
322
+
323
+ union(items: T[] | Collection<T>): Collection<T> {
324
+ const other = items instanceof Collection ? items.all() : items
325
+ const result = [...this.items]
326
+ for (let i = 0; i < other.length; i++) {
327
+ if (i >= result.length) {
328
+ result.push(other[i])
329
+ }
330
+ }
331
+ return new Collection(result)
332
+ }
333
+
334
+ // ── Key/object operations ─────────────────────────────────────────────────
335
+
336
+ pluck<K extends keyof T>(key: K): Collection<T[K]> {
337
+ return new Collection(this.items.map((item) => item[key]))
338
+ }
339
+
340
+ contains(itemOrCallback: T | ((item: T) => boolean)): boolean {
341
+ if (typeof itemOrCallback === "function") {
342
+ return this.items.some(itemOrCallback as (item: T) => boolean)
343
+ }
344
+ return this.items.includes(itemOrCallback)
345
+ }
346
+
347
+ unique(key?: keyof T | ((item: T) => unknown)): Collection<T> {
348
+ const seen = new Set<unknown>()
349
+ return new Collection(
350
+ this.items.filter((item) => {
351
+ const val = this.extractValue(item, key)
352
+ if (seen.has(val)) return false
353
+ seen.add(val)
354
+ return true
355
+ }),
356
+ )
357
+ }
358
+
359
+ except(keys: string[]): Collection<Partial<T>> {
360
+ return new Collection(
361
+ this.items.map((item) => {
362
+ const result = { ...(item as object) } as Partial<T>
363
+ for (const k of keys) {
364
+ delete (result as Record<string, unknown>)[k]
365
+ }
366
+ return result
367
+ }),
368
+ )
369
+ }
370
+
371
+ only(keys: Array<keyof T>): Collection<Partial<T>> {
372
+ return new Collection(
373
+ this.items.map((item) => {
374
+ const result: Partial<T> = {}
375
+ for (const k of keys) {
376
+ result[k] = item[k]
377
+ }
378
+ return result
379
+ }),
380
+ )
381
+ }
382
+
383
+ select(keys: Array<keyof T>): Collection<Partial<T>> {
384
+ return this.only(keys)
385
+ }
386
+
387
+ forget(index: number): this {
388
+ this.items.splice(index, 1)
389
+ return this
390
+ }
391
+
392
+ pull(index: number): T | null {
393
+ if (index < 0 || index >= this.items.length) return null
394
+ return this.items.splice(index, 1)[0] ?? null
395
+ }
396
+
397
+ put(index: number, value: T): this {
398
+ this.items[index] = value
399
+ return this
400
+ }
401
+
402
+ keyBy(key: keyof T | ((item: T, index: number) => string)): Record<string, T> {
403
+ const result: Record<string, T> = {}
404
+ this.items.forEach((item, i) => {
405
+ const k = typeof key === "function" ? key(item, i) : String(item[key])
406
+ result[k] = item
407
+ })
408
+ return result
409
+ }
410
+
411
+ // ── Stack/queue ───────────────────────────────────────────────────────────
412
+
413
+ pop(count?: number): T | Collection<T> | null {
414
+ if (count !== undefined) {
415
+ const removed = this.items.splice(this.items.length - count, count)
416
+ return new Collection(removed)
417
+ }
418
+ return this.items.pop() ?? null
419
+ }
420
+
421
+ shift(count?: number): T | Collection<T> | null {
422
+ if (count !== undefined) {
423
+ const removed = this.items.splice(0, count)
424
+ return new Collection(removed)
425
+ }
426
+ return this.items.shift() ?? null
427
+ }
428
+
429
+ splice(offset: number, length?: number, replacement: T[] = []): Collection<T> {
430
+ const removed =
431
+ length !== undefined
432
+ ? this.items.splice(offset, length, ...replacement)
433
+ : this.items.splice(offset, this.items.length, ...replacement)
434
+ return new Collection(removed)
435
+ }
436
+
437
+ push(...items: T[]): this {
438
+ this.items.push(...items)
439
+ return this
440
+ }
441
+
442
+ prepend(...items: T[]): this {
443
+ this.items.unshift(...items)
444
+ return this
445
+ }
446
+
447
+ // ── Pagination / windowing ────────────────────────────────────────────────
448
+
449
+ forPage(page: number, perPage: number): Collection<T> {
450
+ return new Collection(this.items.slice((page - 1) * perPage, page * perPage))
451
+ }
452
+
453
+ nth(n: number, offset = 0): Collection<T> {
454
+ const result: T[] = []
455
+ for (let i = offset; i < this.items.length; i += n) {
456
+ result.push(this.items[i])
457
+ }
458
+ return new Collection(result)
459
+ }
460
+
461
+ pad(size: number, value: T): Collection<T> {
462
+ const abs = Math.abs(size)
463
+ if (abs <= this.items.length) return new Collection(this.items)
464
+ const padding = Array(abs - this.items.length).fill(value) as T[]
465
+ return size < 0 ? new Collection([...padding, ...this.items]) : new Collection([...this.items, ...padding])
466
+ }
467
+
468
+ slice(offset: number, length?: number): Collection<T> {
469
+ const start = offset
470
+ const end = length !== undefined ? offset + length : undefined
471
+ return new Collection(this.items.slice(start, end))
472
+ }
473
+
474
+ sliding(size: number, step = 1): Collection<Collection<T>> {
475
+ const result: Collection<T>[] = []
476
+ for (let i = 0; i + size <= this.items.length; i += step) {
477
+ result.push(new Collection(this.items.slice(i, i + size)))
478
+ }
479
+ return new Collection(result)
480
+ }
481
+
482
+ split(groups: number): Collection<Collection<T>> {
483
+ if (groups <= 0) return new Collection()
484
+ const size = Math.ceil(this.items.length / groups)
485
+ const result: Collection<T>[] = []
486
+ for (let i = 0; i < this.items.length; i += size) {
487
+ result.push(new Collection(this.items.slice(i, i + size)))
488
+ }
489
+ while (result.length < groups) result.push(new Collection())
490
+ return new Collection(result)
491
+ }
492
+
493
+ splitIn(groups: number): Collection<Collection<T>> {
494
+ if (groups <= 0) return new Collection()
495
+ const size = Math.ceil(this.items.length / groups)
496
+ const result: Collection<T>[] = []
497
+ for (let i = 0; i < groups; i++) {
498
+ result.push(new Collection(this.items.slice(i * size, (i + 1) * size)))
499
+ }
500
+ return new Collection(result)
501
+ }
502
+
503
+ // ── Conditional / tap ─────────────────────────────────────────────────────
504
+
505
+ pipe<U>(callback: (collection: Collection<T>) => U): U {
506
+ return callback(this)
507
+ }
508
+
509
+ pipeInto<U>(ctor: new (c: Collection<T>) => U): U {
510
+ return new ctor(this)
511
+ }
512
+
513
+ pipeThrough(pipes: Array<(c: Collection<T>) => Collection<T>>): Collection<T> {
514
+ return pipes.reduce((c, fn) => fn(c), this as Collection<T>)
515
+ }
516
+
517
+ tap(callback: (collection: this) => void): this {
518
+ callback(this)
519
+ return this
520
+ }
521
+
522
+ when(condition: boolean, callback: (c: this) => unknown, fallback?: (c: this) => unknown): this {
523
+ if (condition) callback(this)
524
+ else if (fallback) fallback(this)
525
+ return this
526
+ }
527
+
528
+ unless(condition: boolean, callback: (c: this) => unknown, fallback?: (c: this) => unknown): this {
529
+ return this.when(!condition, callback, fallback)
530
+ }
531
+
532
+ whenEmpty(callback: (c: this) => unknown, fallback?: (c: this) => unknown): this {
533
+ return this.when(this.isEmpty(), callback, fallback)
534
+ }
535
+
536
+ whenNotEmpty(callback: (c: this) => unknown, fallback?: (c: this) => unknown): this {
537
+ return this.when(this.isNotEmpty(), callback, fallback)
538
+ }
539
+
540
+ unlessEmpty(callback: (c: this) => unknown, fallback?: (c: this) => unknown): this {
541
+ return this.whenNotEmpty(callback, fallback)
542
+ }
543
+
544
+ unlessNotEmpty(callback: (c: this) => unknown, fallback?: (c: this) => unknown): this {
545
+ return this.whenEmpty(callback, fallback)
546
+ }
547
+
548
+ // ── Chunking / grouping ───────────────────────────────────────────────────
549
+
550
+ sortBy(key: keyof T | ((item: T) => unknown), direction: "asc" | "desc" = "asc"): Collection<T> {
551
+ return new Collection(
552
+ [...this.items].sort((a, b) => {
553
+ const av = this.extractValue(a, key) as string | number
554
+ const bv = this.extractValue(b, key) as string | number
555
+ if (av < bv) return direction === "asc" ? -1 : 1
556
+ if (av > bv) return direction === "asc" ? 1 : -1
557
+ return 0
558
+ }),
559
+ )
560
+ }
561
+
562
+ groupBy<K extends string | number>(key: keyof T | ((item: T) => K)): Record<string, Collection<T>> {
563
+ const groups: Record<string, T[]> = {}
564
+ for (const item of this.items) {
565
+ const k = String(this.extractValue(item, key))
566
+ if (!groups[k]) groups[k] = []
567
+ groups[k].push(item)
568
+ }
569
+ return Object.fromEntries(Object.entries(groups).map(([k, v]) => [k, new Collection(v)]))
570
+ }
571
+
572
+ chunk(size: number): Collection<Collection<T>> {
573
+ const chunks: Collection<T>[] = []
574
+ for (let i = 0; i < this.items.length; i += size) {
575
+ chunks.push(new Collection(this.items.slice(i, i + size)))
576
+ }
577
+ return new Collection(chunks)
578
+ }
579
+
580
+ chunkWhile(callback: (current: T, index: number, chunk: Collection<T>) => boolean): Collection<Collection<T>> {
581
+ if (this.items.length === 0) return new Collection()
582
+ const result: Collection<T>[] = []
583
+ let currentChunk: T[] = [this.items[0]]
584
+ for (let i = 1; i < this.items.length; i++) {
585
+ if (callback(this.items[i], i, new Collection(currentChunk))) {
586
+ currentChunk.push(this.items[i])
587
+ } else {
588
+ result.push(new Collection(currentChunk))
589
+ currentChunk = [this.items[i]]
590
+ }
591
+ }
592
+ result.push(new Collection(currentChunk))
593
+ return new Collection(result)
594
+ }
595
+
596
+ crossJoin<U>(...arrays: U[][]): Collection<unknown[]> {
597
+ let result: unknown[][] = [[]]
598
+ const allArrays: unknown[][] = [this.items as unknown[], ...arrays.map((a) => a as unknown[])]
599
+ for (const arr of allArrays) {
600
+ const newResult: unknown[][] = []
601
+ for (const existing of result) {
602
+ for (const item of arr) {
603
+ newResult.push([...existing, item])
604
+ }
605
+ }
606
+ result = newResult
607
+ }
608
+ return new Collection(result)
609
+ }
610
+
611
+ eachSpread(callback: (...args: unknown[]) => unknown): this {
612
+ for (const item of this.items) {
613
+ callback(...(item as unknown as unknown[]))
614
+ }
615
+ return this
616
+ }
617
+
618
+ duplicates(key?: keyof T | ((item: T) => unknown)): Collection<T> {
619
+ const seen = new Set<unknown>()
620
+ const result: T[] = []
621
+ for (const item of this.items) {
622
+ const v = this.extractValue(item, key)
623
+ if (seen.has(v)) {
624
+ result.push(item)
625
+ } else {
626
+ seen.add(v)
627
+ }
628
+ }
629
+ return new Collection(result)
630
+ }
631
+
632
+ multiply(times: number): Collection<T> {
633
+ let result: T[] = []
634
+ for (let i = 0; i < times; i++) result = [...result, ...this.items]
635
+ return new Collection(result)
636
+ }
637
+
638
+ zip<U>(...arrays: U[][]): Collection<unknown[]> {
639
+ const maxLen = Math.max(this.items.length, ...arrays.map((a) => a.length))
640
+ const result: unknown[][] = []
641
+ for (let i = 0; i < maxLen; i++) {
642
+ result.push([this.items[i] as unknown, ...arrays.map((a) => a[i] as unknown)])
643
+ }
644
+ return new Collection(result)
645
+ }
646
+
647
+ // ── Sorting ───────────────────────────────────────────────────────────────
648
+
649
+ sort(callback?: (a: T, b: T) => number): Collection<T> {
650
+ return new Collection([...this.items].sort(callback))
651
+ }
652
+
653
+ sortByDesc(key: keyof T | ((item: T) => unknown)): Collection<T> {
654
+ return this.sortBy(key, "desc")
655
+ }
656
+
657
+ sortDesc(): Collection<T> {
658
+ return new Collection(
659
+ [...this.items].sort((a, b) => {
660
+ if (a > b) return -1
661
+ if (a < b) return 1
662
+ return 0
663
+ }),
664
+ )
665
+ }
666
+
667
+ sortKeys(): Collection<T> {
668
+ return this.values()
669
+ }
670
+
671
+ reverse(): Collection<T> {
672
+ return new Collection([...this.items].reverse())
673
+ }
674
+
675
+ // ── Stringification ───────────────────────────────────────────────────────
676
+
677
+ implode(keyOrGlue: keyof T | string, glue?: string): string {
678
+ if (glue !== undefined) {
679
+ return this.items.map((i) => i[keyOrGlue as keyof T]).join(glue)
680
+ }
681
+ return this.items.join(keyOrGlue as string)
682
+ }
683
+
684
+ join(glue: string, finalGlue?: string): string {
685
+ if (!finalGlue || this.items.length <= 1) return this.items.join(glue)
686
+ const all = this.items.map(String)
687
+ const last = all.pop() as string
688
+ return all.join(glue) + finalGlue + last
689
+ }
690
+
691
+ toJson(pretty?: boolean): string {
692
+ return JSON.stringify(this.items, null, pretty ? 2 : undefined)
693
+ }
694
+
695
+ toPrettyJson(): string {
696
+ return JSON.stringify(this.items, null, 2)
697
+ }
698
+
699
+ // ── Filtering ─────────────────────────────────────────────────────────────
700
+
701
+ doesntContain(itemOrCallback: T | ((item: T, index: number) => boolean)): boolean {
702
+ return !this.contains(itemOrCallback as T | ((item: T) => boolean))
703
+ }
704
+
705
+ every(callback: (item: T, index: number) => boolean): boolean {
706
+ return this.items.every(callback)
707
+ }
708
+
709
+ skipUntil(valueOrCallback: T | ((item: T, index: number) => boolean)): Collection<T> {
710
+ let found = false
711
+ return new Collection(
712
+ this.items.filter((item, i) => {
713
+ if (!found) {
714
+ const match =
715
+ typeof valueOrCallback === "function"
716
+ ? (valueOrCallback as (item: T, index: number) => boolean)(item, i)
717
+ : item === valueOrCallback
718
+ if (match) found = true
719
+ return found
720
+ }
721
+ return true
722
+ }),
723
+ )
724
+ }
725
+
726
+ skipWhile(callback: (item: T, index: number) => boolean): Collection<T> {
727
+ let skipping = true
728
+ return new Collection(
729
+ this.items.filter((item, i) => {
730
+ if (skipping && callback(item, i)) return false
731
+ skipping = false
732
+ return true
733
+ }),
734
+ )
735
+ }
736
+
737
+ takeUntil(valueOrCallback: T | ((item: T, index: number) => boolean)): Collection<T> {
738
+ const result: T[] = []
739
+ for (let i = 0; i < this.items.length; i++) {
740
+ const item = this.items[i]
741
+ const match =
742
+ typeof valueOrCallback === "function"
743
+ ? (valueOrCallback as (item: T, index: number) => boolean)(item, i)
744
+ : item === valueOrCallback
745
+ if (match) break
746
+ result.push(item)
747
+ }
748
+ return new Collection(result)
749
+ }
750
+
751
+ takeWhile(callback: (item: T, index: number) => boolean): Collection<T> {
752
+ const result: T[] = []
753
+ for (let i = 0; i < this.items.length; i++) {
754
+ if (!callback(this.items[i], i)) break
755
+ result.push(this.items[i])
756
+ }
757
+ return new Collection(result)
758
+ }
759
+
760
+ flatten(depth = Infinity): Collection<unknown> {
761
+ return new Collection(this.items.flat(depth) as unknown[])
762
+ }
763
+
764
+ take(count: number): Collection<T> {
765
+ return new Collection(count >= 0 ? this.items.slice(0, count) : this.items.slice(count))
766
+ }
767
+
768
+ skip(count: number): Collection<T> {
769
+ return new Collection(this.items.slice(count))
770
+ }
771
+
772
+ concat(other: T[] | Collection<T>): Collection<T> {
773
+ const otherItems = other instanceof Collection ? other.all() : other
774
+ return new Collection([...this.items, ...otherItems])
775
+ }
776
+
777
+ values(): Collection<T> {
778
+ return new Collection([...this.items])
779
+ }
780
+
781
+ // ── where-family ──────────────────────────────────────────────────────────
782
+
783
+ where(key: keyof T, operatorOrValue: unknown, value?: unknown): Collection<T> {
784
+ const [op, val] = value === undefined ? ["==", operatorOrValue] : [operatorOrValue as string, value]
785
+ return new Collection(this.items.filter((item) => this._compareOp(item[key], op as string, val)))
786
+ }
787
+
788
+ whereStrict(key: keyof T, value: unknown): Collection<T> {
789
+ return new Collection(this.items.filter((item) => item[key] === value))
790
+ }
791
+
792
+ whereBetween(key: keyof T, range: [unknown, unknown]): Collection<T> {
793
+ const [min, max] = range
794
+ return new Collection(
795
+ this.items.filter((item) => {
796
+ const v = item[key] as never
797
+ return v >= (min as never) && v <= (max as never)
798
+ }),
799
+ )
800
+ }
801
+
802
+ whereNotBetween(key: keyof T, range: [unknown, unknown]): Collection<T> {
803
+ const [min, max] = range
804
+ return this.reject((item) => {
805
+ const v = item[key] as never
806
+ return v >= (min as never) && v <= (max as never)
807
+ })
808
+ }
809
+
810
+ whereIn(key: keyof T, values: unknown[]): Collection<T> {
811
+ // biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality
812
+ return new Collection(this.items.filter((item) => values.some((v) => item[key] == v)))
813
+ }
814
+
815
+ whereInStrict(key: keyof T, values: unknown[]): Collection<T> {
816
+ return new Collection(this.items.filter((item) => values.includes(item[key] as unknown)))
817
+ }
818
+
819
+ whereNotIn(key: keyof T, values: unknown[]): Collection<T> {
820
+ // biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality
821
+ return new Collection(this.items.filter((item) => !values.some((v) => item[key] == v)))
822
+ }
823
+
824
+ whereNotInStrict(key: keyof T, values: unknown[]): Collection<T> {
825
+ return new Collection(this.items.filter((item) => !values.includes(item[key] as unknown)))
826
+ }
827
+
828
+ whereNull(key: keyof T): Collection<T> {
829
+ return new Collection(this.items.filter((item) => item[key] == null))
830
+ }
831
+
832
+ whereNotNull(key: keyof T): Collection<T> {
833
+ return new Collection(this.items.filter((item) => item[key] != null))
834
+ }
835
+
836
+ whereInstanceOf<U>(ctor: new (...args: unknown[]) => U): Collection<U> {
837
+ return new Collection(this.items.filter((item) => item instanceof ctor) as unknown as U[])
838
+ }
839
+
840
+ // ── Merge/replace ─────────────────────────────────────────────────────────
841
+
842
+ merge(items: T[] | Collection<T>): Collection<T> {
843
+ const other = items instanceof Collection ? items.all() : items
844
+ return new Collection([...this.items, ...other])
845
+ }
846
+
847
+ replace(items: T[] | Collection<T>): Collection<T> {
848
+ const other = items instanceof Collection ? items.all() : items
849
+ const result = [...this.items]
850
+ for (let i = 0; i < other.length; i++) {
851
+ result[i] = other[i]
852
+ }
853
+ return new Collection(result)
854
+ }
855
+
856
+ // ── protected helpers ───────────────────────────────────────────────────────
857
+
858
+ protected extractValue(item: T, key?: keyof T | ((item: T) => unknown)): unknown {
859
+ if (!key) return item
860
+ return typeof key === "function" ? key(item) : item[key]
861
+ }
862
+
863
+ protected _compareOp(itemVal: unknown, op: string, val: unknown): boolean {
864
+ switch (op) {
865
+ case "===":
866
+ return itemVal === val
867
+ case "!==":
868
+ return itemVal !== val
869
+ case "==":
870
+ case "=":
871
+ // biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality
872
+ return itemVal == val
873
+ case "!=":
874
+ case "<>":
875
+ // biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality
876
+ return itemVal != val
877
+ case ">":
878
+ return (itemVal as never) > (val as never)
879
+ case "<":
880
+ return (itemVal as never) < (val as never)
881
+ case ">=":
882
+ return (itemVal as never) >= (val as never)
883
+ case "<=":
884
+ return (itemVal as never) <= (val as never)
885
+ default:
886
+ return false
887
+ }
888
+ }
889
+ }