effect 2.0.1 → 2.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.
Files changed (44) hide show
  1. package/Cron/package.json +6 -0
  2. package/dist/cjs/Cron.js +457 -0
  3. package/dist/cjs/Cron.js.map +1 -0
  4. package/dist/cjs/Fiber.js.map +1 -1
  5. package/dist/cjs/ReadonlyArray.js +13 -7
  6. package/dist/cjs/ReadonlyArray.js.map +1 -1
  7. package/dist/cjs/Schedule.js +12 -1
  8. package/dist/cjs/Schedule.js.map +1 -1
  9. package/dist/cjs/index.js +4 -2
  10. package/dist/cjs/index.js.map +1 -1
  11. package/dist/cjs/internal/schedule.js +29 -2
  12. package/dist/cjs/internal/schedule.js.map +1 -1
  13. package/dist/cjs/internal/version.js +1 -1
  14. package/dist/dts/Cron.d.ts +170 -0
  15. package/dist/dts/Cron.d.ts.map +1 -0
  16. package/dist/dts/Fiber.d.ts +6 -0
  17. package/dist/dts/Fiber.d.ts.map +1 -1
  18. package/dist/dts/ReadonlyArray.d.ts +26 -26
  19. package/dist/dts/ReadonlyArray.d.ts.map +1 -1
  20. package/dist/dts/Schedule.d.ts +11 -0
  21. package/dist/dts/Schedule.d.ts.map +1 -1
  22. package/dist/dts/index.d.ts +4 -0
  23. package/dist/dts/index.d.ts.map +1 -1
  24. package/dist/dts/internal/version.d.ts +1 -1
  25. package/dist/esm/Cron.js +418 -0
  26. package/dist/esm/Cron.js.map +1 -0
  27. package/dist/esm/Fiber.js.map +1 -1
  28. package/dist/esm/ReadonlyArray.js +14 -8
  29. package/dist/esm/ReadonlyArray.js.map +1 -1
  30. package/dist/esm/Schedule.js +11 -0
  31. package/dist/esm/Schedule.js.map +1 -1
  32. package/dist/esm/index.js +4 -0
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/internal/schedule.js +26 -0
  35. package/dist/esm/internal/schedule.js.map +1 -1
  36. package/dist/esm/internal/version.js +1 -1
  37. package/package.json +9 -1
  38. package/src/Cron.ts +525 -0
  39. package/src/Fiber.ts +7 -0
  40. package/src/ReadonlyArray.ts +74 -53
  41. package/src/Schedule.ts +12 -0
  42. package/src/index.ts +5 -0
  43. package/src/internal/schedule.ts +42 -0
  44. package/src/internal/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Functional programming in TypeScript",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -98,6 +98,11 @@
98
98
  "import": "./dist/esm/Context.js",
99
99
  "default": "./dist/cjs/Context.js"
100
100
  },
101
+ "./Cron": {
102
+ "types": "./dist/dts/Cron.d.ts",
103
+ "import": "./dist/esm/Cron.js",
104
+ "default": "./dist/cjs/Cron.js"
105
+ },
101
106
  "./Data": {
102
107
  "types": "./dist/dts/Data.d.ts",
103
108
  "import": "./dist/esm/Data.js",
@@ -789,6 +794,9 @@
789
794
  "Context": [
790
795
  "./dist/dts/Context.d.ts"
791
796
  ],
797
+ "Cron": [
798
+ "./dist/dts/Cron.d.ts"
799
+ ],
792
800
  "Data": [
793
801
  "./dist/dts/Data.d.ts"
794
802
  ],
package/src/Cron.ts ADDED
@@ -0,0 +1,525 @@
1
+ /**
2
+ * @since 2.0.0
3
+ */
4
+ import * as Either from "./Either.js"
5
+ import * as Equal from "./Equal.js"
6
+ import * as equivalence from "./Equivalence.js"
7
+ import { dual, pipe } from "./Function.js"
8
+ import * as Hash from "./Hash.js"
9
+ import { format, type Inspectable, NodeInspectSymbol } from "./Inspectable.js"
10
+ import * as N from "./Number.js"
11
+ import { type Pipeable, pipeArguments } from "./Pipeable.js"
12
+ import { hasProperty } from "./Predicate.js"
13
+ import * as ReadonlyArray from "./ReadonlyArray.js"
14
+ import * as String from "./String.js"
15
+ import type { Mutable } from "./Types.js"
16
+
17
+ /**
18
+ * @since 2.0.0
19
+ * @category symbols
20
+ */
21
+ export const TypeId: unique symbol = Symbol.for("effect/Cron")
22
+
23
+ /**
24
+ * @since 2.0.0
25
+ * @category symbol
26
+ */
27
+ export type TypeId = typeof TypeId
28
+
29
+ /**
30
+ * @since 2.0.0
31
+ * @category models
32
+ */
33
+ export interface Cron extends Pipeable, Equal.Equal, Inspectable {
34
+ readonly [TypeId]: TypeId
35
+ readonly minutes: ReadonlySet<number>
36
+ readonly hours: ReadonlySet<number>
37
+ readonly days: ReadonlySet<number>
38
+ readonly months: ReadonlySet<number>
39
+ readonly weekdays: ReadonlySet<number>
40
+ }
41
+
42
+ const CronProto: Omit<Cron, "minutes" | "hours" | "days" | "months" | "weekdays"> = {
43
+ [TypeId]: TypeId,
44
+ [Equal.symbol](this: Cron, that: unknown) {
45
+ return isCron(that) && equals(this, that)
46
+ },
47
+ [Hash.symbol](this: Cron): number {
48
+ return pipe(
49
+ Hash.array(ReadonlyArray.fromIterable(this.minutes)),
50
+ Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.hours))),
51
+ Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.days))),
52
+ Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.months))),
53
+ Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.weekdays)))
54
+ )
55
+ },
56
+ toString(this: Cron) {
57
+ return format(this.toJSON())
58
+ },
59
+ toJSON(this: Cron) {
60
+ return {
61
+ _id: "Cron",
62
+ minutes: ReadonlyArray.fromIterable(this.minutes),
63
+ hours: ReadonlyArray.fromIterable(this.hours),
64
+ days: ReadonlyArray.fromIterable(this.days),
65
+ months: ReadonlyArray.fromIterable(this.months),
66
+ weekdays: ReadonlyArray.fromIterable(this.weekdays)
67
+ }
68
+ },
69
+ [NodeInspectSymbol](this: Cron) {
70
+ return this.toJSON()
71
+ },
72
+ pipe() {
73
+ return pipeArguments(this, arguments)
74
+ }
75
+ } as const
76
+
77
+ /**
78
+ * Checks if a given value is a `Cron` instance.
79
+ *
80
+ * @param u - The value to check.
81
+ *
82
+ * @since 2.0.0
83
+ * @category guards
84
+ */
85
+ export const isCron = (u: unknown): u is Cron => hasProperty(u, TypeId)
86
+
87
+ /**
88
+ * Creates a `Cron` instance from.
89
+ *
90
+ * @param constraints - The cron constraints.
91
+ *
92
+ * @since 2.0.0
93
+ * @category constructors
94
+ */
95
+ export const make = ({
96
+ days,
97
+ hours,
98
+ minutes,
99
+ months,
100
+ weekdays
101
+ }: {
102
+ readonly minutes: Iterable<number>
103
+ readonly hours: Iterable<number>
104
+ readonly days: Iterable<number>
105
+ readonly months: Iterable<number>
106
+ readonly weekdays: Iterable<number>
107
+ }): Cron => {
108
+ const o: Mutable<Cron> = Object.create(CronProto)
109
+ o.minutes = new Set(ReadonlyArray.sort(minutes, N.Order))
110
+ o.hours = new Set(ReadonlyArray.sort(hours, N.Order))
111
+ o.days = new Set(ReadonlyArray.sort(days, N.Order))
112
+ o.months = new Set(ReadonlyArray.sort(months, N.Order))
113
+ o.weekdays = new Set(ReadonlyArray.sort(weekdays, N.Order))
114
+ return o
115
+ }
116
+
117
+ /**
118
+ * @since 2.0.0
119
+ * @category symbol
120
+ */
121
+ export const ParseErrorTypeId: unique symbol = Symbol.for("effect/Cron/errors/ParseError")
122
+
123
+ /**
124
+ * @since 2.0.0
125
+ * @category symbols
126
+ */
127
+ export type ParseErrorTypeId = typeof ParseErrorTypeId
128
+
129
+ /**
130
+ * Represents a checked exception which occurs when decoding fails.
131
+ *
132
+ * @since 2.0.0
133
+ * @category models
134
+ */
135
+ export interface ParseError {
136
+ readonly _tag: "ParseError"
137
+ readonly [ParseErrorTypeId]: ParseErrorTypeId
138
+ readonly message: string
139
+ readonly input?: string
140
+ }
141
+
142
+ const ParseErrorProto: Omit<ParseError, "input" | "message"> = {
143
+ _tag: "ParseError",
144
+ [ParseErrorTypeId]: ParseErrorTypeId
145
+ }
146
+
147
+ const ParseError = (message: string, input?: string): ParseError => {
148
+ const o: Mutable<ParseError> = Object.create(ParseErrorProto)
149
+ o.message = message
150
+ if (input !== undefined) {
151
+ o.input = input
152
+ }
153
+ return o
154
+ }
155
+
156
+ /**
157
+ * Returns `true` if the specified value is an `ParseError`, `false` otherwise.
158
+ *
159
+ * @param u - The value to check.
160
+ *
161
+ * @since 2.0.0
162
+ * @category guards
163
+ */
164
+ export const isParseError = (u: unknown): u is ParseError => hasProperty(u, ParseErrorTypeId)
165
+
166
+ /**
167
+ * Parses a cron expression into a `Cron` instance.
168
+ *
169
+ * @param cron - The cron expression to parse.
170
+ *
171
+ * @example
172
+ * import * as Cron from "effect/Cron"
173
+ * import * as Either from "effect/Either"
174
+ *
175
+ * // At 04:00 on every day-of-month from 8 through 14.
176
+ * assert.deepStrictEqual(Cron.parse("0 4 8-14 * *"), Either.right(Cron.make({
177
+ * minutes: [0],
178
+ * hours: [4],
179
+ * days: [8, 9, 10, 11, 12, 13, 14],
180
+ * months: [],
181
+ * weekdays: []
182
+ * })))
183
+ *
184
+ * @since 2.0.0
185
+ * @category constructors
186
+ */
187
+ export const parse = (cron: string): Either.Either<ParseError, Cron> => {
188
+ const segments = cron.split(" ").filter(String.isNonEmpty)
189
+ if (segments.length !== 5) {
190
+ return Either.left(ParseError(`Invalid number of segments in cron expression`, cron))
191
+ }
192
+
193
+ const [minutes, hours, days, months, weekdays] = segments
194
+ return Either.all({
195
+ minutes: parseSegment(minutes, minuteOptions),
196
+ hours: parseSegment(hours, hourOptions),
197
+ days: parseSegment(days, dayOptions),
198
+ months: parseSegment(months, monthOptions),
199
+ weekdays: parseSegment(weekdays, weekdayOptions)
200
+ }).pipe(Either.map((segments) => make(segments)))
201
+ }
202
+
203
+ /**
204
+ * Checks if a given `Date` falls within an active `Cron` time window.
205
+ *
206
+ * @param cron - The `Cron` instance.
207
+ * @param date - The `Date` to check against.
208
+ *
209
+ * @example
210
+ * import * as Cron from "effect/Cron"
211
+ * import * as Either from "effect/Either"
212
+ *
213
+ * const cron = Either.getOrThrow(Cron.parse("0 4 8-14 * *"))
214
+ * assert.deepStrictEqual(Cron.match(cron, new Date("2021-01-08 04:00:00")), true)
215
+ * assert.deepStrictEqual(Cron.match(cron, new Date("2021-01-08 05:00:00")), false)
216
+ *
217
+ * @since 2.0.0
218
+ */
219
+ export const match = (cron: Cron, date: Date): boolean => {
220
+ const { days, hours, minutes, months, weekdays } = cron
221
+
222
+ const minute = date.getMinutes()
223
+ if (minutes.size !== 0 && !minutes.has(minute)) {
224
+ return false
225
+ }
226
+
227
+ const hour = date.getHours()
228
+ if (hours.size !== 0 && !hours.has(hour)) {
229
+ return false
230
+ }
231
+
232
+ const month = date.getMonth() + 1
233
+ if (months.size !== 0 && !months.has(month)) {
234
+ return false
235
+ }
236
+
237
+ if (days.size === 0 && weekdays.size === 0) {
238
+ return true
239
+ }
240
+
241
+ const day = date.getDate()
242
+ if (weekdays.size === 0) {
243
+ return days.has(day)
244
+ }
245
+
246
+ const weekday = date.getDay()
247
+ if (days.size === 0) {
248
+ return weekdays.has(weekday)
249
+ }
250
+
251
+ return days.has(day) || weekdays.has(weekday)
252
+ }
253
+
254
+ /**
255
+ * Returns the next run `Date` for the given `Cron` instance.
256
+ *
257
+ * Uses the current time as a starting point if no value is provided for `now`.
258
+ *
259
+ * @example
260
+ * import * as Cron from "effect/Cron"
261
+ * import * as Either from "effect/Either"
262
+ *
263
+ * const after = new Date("2021-01-01 00:00:00")
264
+ * const cron = Either.getOrThrow(Cron.parse("0 4 8-14 * *"))
265
+ * assert.deepStrictEqual(Cron.next(cron, after), new Date("2021-01-08 04:00:00"))
266
+ *
267
+ * @param cron - The `Cron` instance.
268
+ * @param now - The `Date` to start searching from.
269
+ *
270
+ * @since 2.0.0
271
+ */
272
+ export const next = (cron: Cron, now?: Date): Date => {
273
+ const { days, hours, minutes, months, weekdays } = cron
274
+
275
+ const restrictMinutes = minutes.size !== 0
276
+ const restrictHours = hours.size !== 0
277
+ const restrictDays = days.size !== 0
278
+ const restrictMonths = months.size !== 0
279
+ const restrictWeekdays = weekdays.size !== 0
280
+
281
+ const current = now ? new Date(now.getTime()) : new Date()
282
+ // Increment by one minute to ensure we don't match the current date.
283
+ current.setMinutes(current.getMinutes() + 1)
284
+ current.setSeconds(0)
285
+ current.setMilliseconds(0)
286
+
287
+ // Only search 8 years into the future.
288
+ const limit = new Date(current).setFullYear(current.getFullYear() + 8)
289
+ while (current.getTime() <= limit) {
290
+ if (restrictMonths && !months.has(current.getMonth() + 1)) {
291
+ current.setMonth(current.getMonth() + 1)
292
+ current.setDate(1)
293
+ current.setHours(0)
294
+ current.setMinutes(0)
295
+ continue
296
+ }
297
+
298
+ if (restrictDays && restrictWeekdays) {
299
+ if (!days.has(current.getDate()) && !weekdays.has(current.getDay())) {
300
+ current.setDate(current.getDate() + 1)
301
+ current.setHours(0)
302
+ current.setMinutes(0)
303
+ continue
304
+ }
305
+ } else if (restrictDays) {
306
+ if (!days.has(current.getDate())) {
307
+ current.setDate(current.getDate() + 1)
308
+ current.setHours(0)
309
+ current.setMinutes(0)
310
+ continue
311
+ }
312
+ } else if (restrictWeekdays) {
313
+ if (!weekdays.has(current.getDay())) {
314
+ current.setDate(current.getDate() + 1)
315
+ current.setHours(0)
316
+ current.setMinutes(0)
317
+ continue
318
+ }
319
+ }
320
+
321
+ if (restrictHours && !hours.has(current.getHours())) {
322
+ current.setHours(current.getHours() + 1)
323
+ current.setMinutes(0)
324
+ continue
325
+ }
326
+
327
+ if (restrictMinutes && !minutes.has(current.getMinutes())) {
328
+ current.setMinutes(current.getMinutes() + 1)
329
+ continue
330
+ }
331
+
332
+ return current
333
+ }
334
+
335
+ throw new Error("Unable to find next cron date")
336
+ }
337
+
338
+ /**
339
+ * Returns an `IterableIterator` which yields the sequence of `Date`s that match the `Cron` instance.
340
+ *
341
+ * @param cron - The `Cron` instance.
342
+ * @param now - The `Date` to start searching from.
343
+ *
344
+ * @since 2.0.0
345
+ */
346
+ export const sequence = function*(cron: Cron, now?: Date): IterableIterator<Date> {
347
+ while (true) {
348
+ yield now = next(cron, now)
349
+ }
350
+ }
351
+
352
+ /**
353
+ * @category instances
354
+ * @since 2.0.0
355
+ */
356
+ export const Equivalence: equivalence.Equivalence<Cron> = equivalence.make((self, that) =>
357
+ restrictionsEquals(self.minutes, that.minutes) &&
358
+ restrictionsEquals(self.hours, that.hours) &&
359
+ restrictionsEquals(self.days, that.days) &&
360
+ restrictionsEquals(self.months, that.months) &&
361
+ restrictionsEquals(self.weekdays, that.weekdays)
362
+ )
363
+
364
+ const restrictionsArrayEquals = equivalence.array(equivalence.number)
365
+ const restrictionsEquals = (self: ReadonlySet<number>, that: ReadonlySet<number>): boolean =>
366
+ restrictionsArrayEquals(ReadonlyArray.fromIterable(self), ReadonlyArray.fromIterable(that))
367
+
368
+ /**
369
+ * Checks if two `Cron`s are equal.
370
+ *
371
+ * @since 2.0.0
372
+ * @category predicates
373
+ */
374
+ export const equals: {
375
+ (that: Cron): (self: Cron) => boolean
376
+ (self: Cron, that: Cron): boolean
377
+ } = dual(2, (self: Cron, that: Cron): boolean => Equivalence(self, that))
378
+
379
+ interface SegmentOptions {
380
+ segment: string
381
+ min: number
382
+ max: number
383
+ aliases?: Record<string, number> | undefined
384
+ }
385
+
386
+ const minuteOptions: SegmentOptions = {
387
+ segment: "minute",
388
+ min: 0,
389
+ max: 59
390
+ }
391
+
392
+ const hourOptions: SegmentOptions = {
393
+ segment: "hour",
394
+ min: 0,
395
+ max: 23
396
+ }
397
+
398
+ const dayOptions: SegmentOptions = {
399
+ segment: "day",
400
+ min: 1,
401
+ max: 31
402
+ }
403
+
404
+ const monthOptions: SegmentOptions = {
405
+ segment: "month",
406
+ min: 1,
407
+ max: 12,
408
+ aliases: {
409
+ jan: 1,
410
+ feb: 2,
411
+ mar: 3,
412
+ apr: 4,
413
+ may: 5,
414
+ jun: 6,
415
+ jul: 7,
416
+ aug: 8,
417
+ sep: 9,
418
+ oct: 10,
419
+ nov: 11,
420
+ dec: 12
421
+ }
422
+ }
423
+
424
+ const weekdayOptions: SegmentOptions = {
425
+ segment: "weekday",
426
+ min: 0,
427
+ max: 6,
428
+ aliases: {
429
+ sun: 0,
430
+ mon: 1,
431
+ tue: 2,
432
+ wed: 3,
433
+ thu: 4,
434
+ fri: 5,
435
+ sat: 6
436
+ }
437
+ }
438
+
439
+ const parseSegment = (
440
+ input: string,
441
+ options: SegmentOptions
442
+ ): Either.Either<ParseError, ReadonlySet<number>> => {
443
+ const capacity = options.max - options.min + 1
444
+ const values = new Set<number>()
445
+ const fields = input.split(",")
446
+
447
+ for (const field of fields) {
448
+ const [raw, step] = splitStep(field)
449
+ if (raw === "*" && step === undefined) {
450
+ return Either.right(new Set())
451
+ }
452
+
453
+ if (step !== undefined) {
454
+ if (!Number.isInteger(step)) {
455
+ return Either.left(ParseError(`Expected step value to be a positive integer`, input))
456
+ }
457
+ if (step < 1) {
458
+ return Either.left(ParseError(`Expected step value to be greater than 0`, input))
459
+ }
460
+ if (step > options.max) {
461
+ return Either.left(ParseError(`Expected step value to be less than ${options.max}`, input))
462
+ }
463
+ }
464
+
465
+ if (raw === "*") {
466
+ for (let i = options.min; i <= options.max; i += step ?? 1) {
467
+ values.add(i)
468
+ }
469
+ } else {
470
+ const [left, right] = splitRange(raw, options.aliases)
471
+ if (!Number.isInteger(left)) {
472
+ return Either.left(ParseError(`Expected a positive integer`, input))
473
+ }
474
+ if (left < options.min || left > options.max) {
475
+ return Either.left(ParseError(`Expected a value between ${options.min} and ${options.max}`, input))
476
+ }
477
+
478
+ if (right === undefined) {
479
+ values.add(left)
480
+ } else {
481
+ if (!Number.isInteger(right)) {
482
+ return Either.left(ParseError(`Expected a positive integer`, input))
483
+ }
484
+ if (right < options.min || right > options.max) {
485
+ return Either.left(ParseError(`Expected a value between ${options.min} and ${options.max}`, input))
486
+ }
487
+ if (left > right) {
488
+ return Either.left(ParseError(`Invalid value range`, input))
489
+ }
490
+
491
+ for (let i = left; i <= right; i += step ?? 1) {
492
+ values.add(i)
493
+ }
494
+ }
495
+ }
496
+
497
+ if (values.size >= capacity) {
498
+ return Either.right(new Set())
499
+ }
500
+ }
501
+
502
+ return Either.right(values)
503
+ }
504
+
505
+ const splitStep = (input: string): [string, number | undefined] => {
506
+ const seperator = input.indexOf("/")
507
+ if (seperator !== -1) {
508
+ return [input.slice(0, seperator), Number(input.slice(seperator + 1))]
509
+ }
510
+
511
+ return [input, undefined]
512
+ }
513
+
514
+ const splitRange = (input: string, aliases?: Record<string, number>): [number, number | undefined] => {
515
+ const seperator = input.indexOf("-")
516
+ if (seperator !== -1) {
517
+ return [aliasOrValue(input.slice(0, seperator), aliases), aliasOrValue(input.slice(seperator + 1), aliases)]
518
+ }
519
+
520
+ return [aliasOrValue(input, aliases), undefined]
521
+ }
522
+
523
+ function aliasOrValue(field: string, aliases?: Record<string, number>): number {
524
+ return aliases?.[field.toLocaleLowerCase()] ?? Number(field)
525
+ }
package/src/Fiber.ts CHANGED
@@ -148,6 +148,13 @@ export interface RuntimeFiber<out E, out A> extends Fiber<E, A>, Fiber.RuntimeVa
148
148
  * already done.
149
149
  */
150
150
  unsafePoll(): Exit.Exit<E, A> | null
151
+
152
+ /**
153
+ * In the background, interrupts the fiber as if interrupted from the
154
+ * specified fiber. If the fiber has already exited, the returned effect will
155
+ * resume immediately. Otherwise, the effect will resume when the fiber exits.
156
+ */
157
+ unsafeInterruptAsFork(fiberId: FiberId.FiberId): void
151
158
  }
152
159
 
153
160
  /**