Skip to content

Commit

Permalink
feat: add @cordisjs/timer
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 4, 2024
1 parent bdd36a0 commit 94b114a
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 11 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"shx": "^0.3.4",
"tsx": "patch:tsx@npm%3A4.7.0#./.yarn/patches/tsx-npm-4.7.0-86d7b66640.patch",
"typescript": "^5.3.2",
"yakumo": "^1.0.0-beta.5",
"yakumo": "^1.0.0-beta.6",
"yakumo-esbuild": "^1.0.0-beta.2",
"yakumo-tsc": "^1.0.0-beta.2"
"yakumo-tsc": "^1.0.0-beta.3"
}
}
6 changes: 3 additions & 3 deletions packages/cordis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "cordis",
"description": "CLI for cordis loader",
"type": "module",
"version": "3.7.1",
"version": "3.8.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"bin": "lib/bin/cordis.js",
Expand Down Expand Up @@ -38,8 +38,8 @@
"cli"
],
"dependencies": {
"@cordisjs/core": "^3.7.1",
"@cordisjs/loader": "0.3.3",
"@cordisjs/core": "^3.8.0",
"@cordisjs/loader": "0.4.0",
"@cordisjs/logger": "^0.1.4",
"cac": "^6.7.14",
"cosmokit": "^1.5.2",
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cordisjs/core",
"description": "AOP Framework for Modern JavaScript Applications",
"version": "3.7.1",
"version": "3.8.0",
"sideEffects": false,
"type": "module",
"main": "lib/index.cjs",
Expand All @@ -13,6 +13,7 @@
"import": "./lib/index.mjs",
"types": "./lib/index.d.ts"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
Expand Down
4 changes: 2 additions & 2 deletions packages/hmr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@
}
},
"peerDependencies": {
"cordis": "^3.7.1"
"cordis": "^3.8.0"
},
"devDependencies": {
"@types/babel__code-frame": "^7.0.6",
"cordis": "^3.7.1",
"cordis": "^3.8.0",
"esbuild": "^0.18.20"
},
"dependencies": {
Expand Down
7 changes: 4 additions & 3 deletions packages/loader/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cordisjs/loader",
"description": "Loader for cordis",
"version": "0.3.3",
"version": "0.4.0",
"type": "module",
"module": "lib/index.js",
"typings": "lib/index.d.ts",
Expand All @@ -11,6 +11,7 @@
"browser": "./lib/shared.js",
"types": "./lib/index.d.ts"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
Expand All @@ -36,11 +37,11 @@
"service"
],
"devDependencies": {
"@cordisjs/core": "^3.7.1",
"@cordisjs/core": "^3.8.0",
"@cordisjs/logger": "^0.1.4"
},
"peerDependencies": {
"@cordisjs/core": "^3.7.1"
"@cordisjs/core": "^3.8.0"
},
"dependencies": {
"cosmokit": "^1.5.2",
Expand Down
44 changes: 44 additions & 0 deletions packages/timer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@cordisjs/timer",
"description": "Timer service for cordis",
"version": "0.2.3",
"type": "module",
"main": "lib/index.cjs",
"module": "lib/index.mjs",
"types": "lib/index.d.ts",
"exports": {
".": {
"require": "./lib/index.cjs",
"import": "./lib/index.mjs",
"types": "./lib/index.d.ts"
},
"./package.json": "./package.json"
},
"files": [
"lib",
"src"
],
"author": "Shigma <[email protected]>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/cordisjs/std.git",
"directory": "packages/timer"
},
"bugs": {
"url": "https://github.com/cordisjs/std/issues"
},
"homepage": "https://github.com/cordisjs/std",
"keywords": [
"cordis",
"timer",
"service",
"plugin"
],
"devDependencies": {
"cordis": "^3.8.0"
},
"peerDependencies": {
"cordis": "^3.8.0"
}
}
1 change: 1 addition & 0 deletions packages/timer/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @cordisjs/timer
102 changes: 102 additions & 0 deletions packages/timer/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Context, Service } from 'cordis'
import { remove } from 'cosmokit'

declare module 'cordis' {
interface Context {
timer: TimerService
setTimeout(callback: () => void, delay: number): () => void
setInterval(callback: () => void, delay: number): () => void
sleep(delay: number): Promise<void>
throttle<F extends (...args: any[]) => void>(callback: F, delay: number, noTrailing?: boolean): WithDispose<F>
debounce<F extends (...args: any[]) => void>(callback: F, delay: number): WithDispose<F>
}
}

type WithDispose<T> = T & { dispose: () => void }

export class TimerService extends Service {
constructor(ctx: Context) {
super(ctx, 'timer', true)
ctx.mixin('timer', ['setTimeout', 'setInterval', 'sleep', 'throttle', 'debounce'])
}

setTimeout(callback: () => void, delay: number) {
const dispose = this[Context.current].effect(() => {
const timer = setTimeout(() => {
dispose()
callback()
}, delay)
return () => clearTimeout(timer)
})
return dispose
}

setInterval(callback: () => void, delay: number) {
return this[Context.current].effect(() => {
const timer = setInterval(callback, delay)
return () => clearInterval(timer)
})
}

sleep(delay: number) {
const caller = this[Context.current]
return new Promise<void>((resolve, reject) => {
const dispose1 = this.setTimeout(() => {
dispose1()
dispose2()
resolve()
}, delay)
const dispose2 = caller.on('dispose', () => {
dispose1()
dispose2()
reject(new Error('Context has been disposed'))
})
})
}

private createWrapper(callback: (args: any[], check: () => boolean) => any, isDisposed = false) {
const caller = this[Context.current]
caller.scope.assertActive()

let timer: number | NodeJS.Timeout | undefined
const dispose = () => {
isDisposed = true
remove(caller.scope.disposables, dispose)
clearTimeout(timer)
}

const wrapper: any = (...args: any[]) => {
clearTimeout(timer)
timer = callback(args, () => !isDisposed && caller.scope.isActive)
}
wrapper.dispose = dispose
caller.scope.disposables.push(dispose)
return wrapper
}

throttle<F extends (...args: any[]) => void>(callback: F, delay: number, noTrailing?: boolean): WithDispose<F> {
let lastCall = -Infinity
const execute = (...args: any[]) => {
lastCall = Date.now()
callback(...args)
}
return this.createWrapper((args, isActive) => {
const now = Date.now()
const remaining = delay - (now - lastCall)
if (remaining <= 0) {
execute(...args)
} else if (isActive()) {
return setTimeout(execute, remaining, ...args)
}
}, noTrailing)
}

debounce<F extends (...args: any[]) => void>(callback: F, delay: number): WithDispose<F> {
return this.createWrapper((args, isActive) => {
if (!isActive()) return
return setTimeout(callback, delay, ...args)
})
}
}

export default TimerService
146 changes: 146 additions & 0 deletions packages/timer/tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { describe, mock, test } from 'node:test'
import { FakeTimerInstallOpts, install, InstalledClock } from '@sinonjs/fake-timers'
import { Context } from 'cordis'
import assert from 'node:assert'
import Timer from '../src'

declare module 'cordis' {
interface Context {
clock: InstalledClock
}
}

function withContext(callback: (ctx: Context) => Promise<void>, config?: FakeTimerInstallOpts) {
return () => new Promise<void>((resolve, reject) => {
const ctx = new Context()
ctx.clock = install(config)
ctx.plugin(Timer)
ctx.plugin(() => {
callback(ctx).then(resolve, reject).finally(() => ctx.clock.uninstall())
})
})
}

describe('ctx.setTimeout()', () => {
test('basic support', withContext(async (ctx) => {
const callback = mock.fn()
ctx.setTimeout(callback, 1000)
assert.strictEqual(callback.mock.calls.length, 0)
await ctx.clock.tickAsync(1000)
assert.strictEqual(callback.mock.calls.length, 1)
await ctx.clock.tickAsync(1000)
assert.strictEqual(callback.mock.calls.length, 1)
}))

test('dispose', withContext(async (ctx) => {
const callback = mock.fn()
const dispose = ctx.setTimeout(callback, 1000)
assert.strictEqual(callback.mock.calls.length, 0)
dispose()
await ctx.clock.tickAsync(2000)
assert.strictEqual(callback.mock.calls.length, 0)
}))
})

describe('ctx.setInterval()', () => {
test('basic support', withContext(async (ctx) => {
const callback = mock.fn()
const dispose = ctx.setInterval(callback, 1000)
assert.strictEqual(callback.mock.calls.length, 0)
await ctx.clock.tickAsync(1000)
assert.strictEqual(callback.mock.calls.length, 1)
await ctx.clock.tickAsync(1000)
assert.strictEqual(callback.mock.calls.length, 2)
dispose()
await ctx.clock.tickAsync(2000)
assert.strictEqual(callback.mock.calls.length, 2)
}))
})

describe('ctx.sleep()', () => {
test('basic support', withContext(async (ctx) => {
const resolve = mock.fn()
const reject = mock.fn()
ctx.sleep(1000).then(resolve, reject)
await ctx.clock.tickAsync(500)
assert.strictEqual(resolve.mock.calls.length, 0)
assert.strictEqual(reject.mock.calls.length, 0)
await ctx.clock.tickAsync(500)
assert.strictEqual(resolve.mock.calls.length, 1)
assert.strictEqual(reject.mock.calls.length, 0)
ctx.scope.dispose()
await ctx.clock.tickAsync(2000)
assert.strictEqual(resolve.mock.calls.length, 1)
assert.strictEqual(reject.mock.calls.length, 0)
}))
})

describe('ctx.throttle()', () => {
test('basic support', withContext(async (ctx) => {
const callback = mock.fn()
const throttled = ctx.throttle(callback, 1000)
throttled()
assert.strictEqual(callback.mock.calls.length, 1)
await ctx.clock.tickAsync(600)
throttled()
assert.strictEqual(callback.mock.calls.length, 1)
await ctx.clock.tickAsync(600)
throttled()
assert.strictEqual(callback.mock.calls.length, 2)
await ctx.clock.tickAsync(2000)
assert.strictEqual(callback.mock.calls.length, 3)
}))

test('trailing mode', withContext(async (ctx) => {
const callback = mock.fn()
const throttled = ctx.throttle(callback, 1000)
throttled()
assert.strictEqual(callback.mock.calls.length, 1)
await ctx.clock.tickAsync(500)
throttled()
assert.strictEqual(callback.mock.calls.length, 1)
await ctx.clock.tickAsync(500)
assert.strictEqual(callback.mock.calls.length, 2)
await ctx.clock.tickAsync(2000)
assert.strictEqual(callback.mock.calls.length, 2)
}))

test('disposed', withContext(async (ctx) => {
const callback = mock.fn()
const throttled = ctx.throttle(callback, 1000)
throttled.dispose()
throttled()
assert.strictEqual(callback.mock.calls.length, 1)
await ctx.clock.tickAsync(500)
throttled()
await ctx.clock.tickAsync(2000)
assert.strictEqual(callback.mock.calls.length, 1)
}))
})

describe('ctx.debounce()', () => {
test('basic support', withContext(async (ctx) => {
const callback = mock.fn()
const debounced = ctx.debounce(callback, 1000)
debounced()
assert.strictEqual(callback.mock.calls.length, 0)
await ctx.clock.tickAsync(400)
debounced()
assert.strictEqual(callback.mock.calls.length, 0)
await ctx.clock.tickAsync(400)
debounced()
assert.strictEqual(callback.mock.calls.length, 0)
await ctx.clock.tickAsync(1000)
assert.strictEqual(callback.mock.calls.length, 1)
}))

test('disposed', withContext(async (ctx) => {
const callback = mock.fn()
const debounced = ctx.debounce(callback, 1000)
debounced.dispose()
debounced()
assert.strictEqual(callback.mock.calls.length, 0)
await ctx.clock.tickAsync(2000)
assert.strictEqual(callback.mock.calls.length, 0)
}))
})
Loading

0 comments on commit 94b114a

Please sign in to comment.