diff --git a/packages/time/package.json b/packages/time/package.json index 081903b..44f5812 100644 --- a/packages/time/package.json +++ b/packages/time/package.json @@ -55,5 +55,12 @@ "files": [ "dist", "src" - ] + ], + "dependencies": { + "@js-temporal/polyfill": "^0.4.4", + "@tanstack/store": "^0.4.1" + }, + "devDependencies": { + "csstype": "^3.1.3" + } } diff --git a/packages/time/src/core/time.ts b/packages/time/src/core/time.ts new file mode 100644 index 0000000..dc419a3 --- /dev/null +++ b/packages/time/src/core/time.ts @@ -0,0 +1,60 @@ +import { Temporal } from '@js-temporal/polyfill' +import { Store } from '@tanstack/store' +import { getDefaultTimeZone } from '../utils/dateDefaults' + +export interface TimeCoreOptions { + /** + * The time zone to use for the current time. + * @default Intl.DateTimeFormat().resolvedOptions().timeZone + */ + timeZone?: Temporal.TimeZoneLike +} + +interface TimeState { + /** + * The current time. + * @default Temporal.Now.zonedDateTimeISO() + * @readonly + * @type Temporal.ZonedDateTime + */ + currentTime: Temporal.ZonedDateTime +} + +export abstract class TimeCore { + protected store: Store + protected interval: NodeJS.Timeout | null = null + protected timeZone: Temporal.TimeZoneLike + + constructor(options: TimeCoreOptions = {}) { + const defaultTimeZone = getDefaultTimeZone() + this.timeZone = options.timeZone || defaultTimeZone + this.store = new Store({ + currentTime: Temporal.Now.zonedDateTimeISO(this.timeZone), + }) + this.updateCurrentTime() + } + + protected updateCurrentTime() { + this.store.setState((prev) => ({ + ...prev, + currentTime: Temporal.Now.zonedDateTimeISO(this.timeZone), + })) + } + + startUpdatingTime(intervalMs: number = 1000) { + if (!this.interval) { + this.interval = setInterval(() => this.updateCurrentTime(), intervalMs) + } + } + + stopUpdatingTime() { + if (this.interval) { + clearInterval(this.interval) + this.interval = null + } + } + + getCurrentTime(): Temporal.ZonedDateTime { + return this.store.state.currentTime + } +} diff --git a/packages/time/src/tests/isValidDate.test.ts b/packages/time/src/tests/isValidDate.test.ts index 57a3556..01afdc6 100644 --- a/packages/time/src/tests/isValidDate.test.ts +++ b/packages/time/src/tests/isValidDate.test.ts @@ -4,7 +4,7 @@ import {isValidDate} from '../utils/isValidDate'; describe('isValidDate', () => { test('should return true for a valid date', () => { expect(isValidDate(new Date())).toBe(true); - }); + }) test('should return false for an invalid date', () => { expect(isValidDate(new Date("invalid"))).toBe(false); @@ -13,4 +13,4 @@ describe('isValidDate', () => { test("should return false for null", () => { expect(isValidDate(null)).toBe(false); }); -}); \ No newline at end of file +}); diff --git a/packages/time/src/tests/time.test.ts b/packages/time/src/tests/time.test.ts new file mode 100644 index 0000000..29135d7 --- /dev/null +++ b/packages/time/src/tests/time.test.ts @@ -0,0 +1,64 @@ +import { Temporal } from '@js-temporal/polyfill' +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { TimeCore } from '../core/time' + + +export class TestTimeCore extends TimeCore { + getCurrentTime(): Temporal.ZonedDateTime { + return super.getCurrentTime() + } + + startUpdatingTime(intervalMs: number = 1000) { + super.startUpdatingTime(intervalMs) + } + + stopUpdatingTime() { + super.stopUpdatingTime() + } +} + +describe('TimeCore', () => { +beforeEach(() => { + vi.useFakeTimers() + const mockNow = Temporal.PlainDateTime.from({ year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0 }) + vi.setSystemTime(mockNow.toZonedDateTime('UTC').epochMilliseconds) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + test('should initialize with the current time in the default time zone', () => { + const timeCore = new TestTimeCore() + const currentTime = Temporal.Now.zonedDateTimeISO() + expect(timeCore.getCurrentTime().toString()).toBe(currentTime.toString()) + }) + + test('should initialize with the current time in the specified time zone', () => { + const timeZone = 'America/New_York' + const timeCore = new TestTimeCore({ timeZone }) + const currentTime = Temporal.Now.zonedDateTimeISO(timeZone) + expect(timeCore.getCurrentTime().toString()).toBe(currentTime.toString()) + }) + + test('should start updating the current time', () => { + const timeCore = new TestTimeCore() + timeCore.startUpdatingTime() + vi.advanceTimersByTime(1000) + const currentTime = Temporal.Now.zonedDateTimeISO() + expect(timeCore.getCurrentTime().epochMilliseconds).toBe(currentTime.epochMilliseconds) + }) + + test('should stop updating the current time', () => { + const timeCore = new TestTimeCore() + timeCore.startUpdatingTime() + + vi.advanceTimersByTime(1000) + timeCore.stopUpdatingTime() + const stoppedTime = timeCore.getCurrentTime() + + vi.advanceTimersByTime(1000) + const timeAfterStop = timeCore.getCurrentTime() + expect(timeAfterStop.epochMilliseconds).toBe(stoppedTime.epochMilliseconds) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37c8fc7..b27a380 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,7 +183,18 @@ importers: specifier: ^2.10.1 version: 2.10.1(@testing-library/jest-dom@6.4.2)(solid-js@1.7.8)(vite@5.2.6) - packages/time: {} + packages/time: + dependencies: + '@js-temporal/polyfill': + specifier: ^0.4.4 + version: 0.4.4 + '@tanstack/store': + specifier: ^0.4.1 + version: 0.4.1 + devDependencies: + csstype: + specifier: ^3.1.3 + version: 3.1.3 packages/vue-time: dependencies: @@ -541,7 +552,7 @@ packages: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.24.2 - '@babel/generator': 7.23.6 + '@babel/generator': 7.24.1 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) '@babel/helpers': 7.24.1 @@ -2619,6 +2630,14 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@js-temporal/polyfill@0.4.4: + resolution: {integrity: sha512-2X6bvghJ/JAoZO52lbgyAPFj8uCflhTo2g7nkFzEQdXd/D8rEeD4HtmTEpmtGCva260fcd66YNXBOYdnmHqSOg==} + engines: {node: '>=12'} + dependencies: + jsbi: 4.3.0 + tslib: 2.6.2 + dev: false + /@leichtgewicht/ip-codec@2.0.5: resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} dev: true @@ -3288,6 +3307,10 @@ packages: - vite dev: true + /@tanstack/store@0.4.1: + resolution: {integrity: sha512-NvW3MomYSTzQK61AWdtWNIhWgszXFZDRgCNlvSDw/DBaoLqJIlZ0/gKLsditA8un/BGU1NR06+j0a/UNLgXA+Q==} + dev: false + /@testing-library/dom@9.3.4: resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} engines: {node: '>=14'} @@ -5113,7 +5136,7 @@ packages: dom-serializer: 2.0.0 domhandler: 5.0.3 htmlparser2: 8.0.2 - postcss: 8.4.35 + postcss: 8.4.38 postcss-media-query-parser: 0.2.3 dev: true @@ -5151,12 +5174,12 @@ packages: webpack: optional: true dependencies: - icss-utils: 5.1.0(postcss@8.4.35) - postcss: 8.4.35 - postcss-modules-extract-imports: 3.0.0(postcss@8.4.35) - postcss-modules-local-by-default: 4.0.4(postcss@8.4.35) - postcss-modules-scope: 3.1.1(postcss@8.4.35) - postcss-modules-values: 4.0.0(postcss@8.4.35) + icss-utils: 5.1.0(postcss@8.4.38) + postcss: 8.4.38 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.38) + postcss-modules-local-by-default: 4.0.4(postcss@8.4.38) + postcss-modules-scope: 3.1.1(postcss@8.4.38) + postcss-modules-values: 4.0.0(postcss@8.4.38) postcss-value-parser: 4.2.0 semver: 7.6.0 webpack: 5.90.3(esbuild@0.20.2) @@ -7006,13 +7029,13 @@ packages: safer-buffer: 2.1.2 dev: true - /icss-utils@5.1.0(postcss@8.4.35): + /icss-utils@5.1.0(postcss@8.4.38): resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.35 + postcss: 8.4.38 dev: true /identity-function@1.0.0: @@ -7495,7 +7518,7 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.3 '@babel/parser': 7.24.1 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -7641,6 +7664,10 @@ packages: argparse: 2.0.1 dev: true + /jsbi@4.3.0: + resolution: {integrity: sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==} + dev: false + /jsdom@24.0.0: resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==} engines: {node: '>=18'} @@ -7891,8 +7918,6 @@ packages: peerDependenciesMeta: webpack: optional: true - webpack-sources: - optional: true dependencies: webpack: 5.90.3(esbuild@0.20.2) webpack-sources: 3.2.3 @@ -9113,45 +9138,45 @@ packages: resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} dev: true - /postcss-modules-extract-imports@3.0.0(postcss@8.4.35): + /postcss-modules-extract-imports@3.0.0(postcss@8.4.38): resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.35 + postcss: 8.4.38 dev: true - /postcss-modules-local-by-default@4.0.4(postcss@8.4.35): + /postcss-modules-local-by-default@4.0.4(postcss@8.4.38): resolution: {integrity: sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.35) - postcss: 8.4.35 + icss-utils: 5.1.0(postcss@8.4.38) + postcss: 8.4.38 postcss-selector-parser: 6.0.16 postcss-value-parser: 4.2.0 dev: true - /postcss-modules-scope@3.1.1(postcss@8.4.35): + /postcss-modules-scope@3.1.1(postcss@8.4.38): resolution: {integrity: sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - postcss: 8.4.35 + postcss: 8.4.38 postcss-selector-parser: 6.0.16 dev: true - /postcss-modules-values@4.0.0(postcss@8.4.35): + /postcss-modules-values@4.0.0(postcss@8.4.38): resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: postcss: ^8.1.0 dependencies: - icss-utils: 5.1.0(postcss@8.4.35) - postcss: 8.4.35 + icss-utils: 5.1.0(postcss@8.4.38) + postcss: 8.4.38 dev: true /postcss-selector-parser@6.0.16: @@ -9458,7 +9483,7 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.24.1 dev: true /regex-parser@2.3.0: @@ -9545,7 +9570,7 @@ packages: adjust-sourcemap-loader: 4.0.0 convert-source-map: 1.9.0 loader-utils: 2.0.4 - postcss: 8.4.35 + postcss: 8.4.38 source-map: 0.6.1 dev: true @@ -10741,7 +10766,6 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: true /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}