-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
317 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# @cordisjs/timer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
})) | ||
}) |
Oops, something went wrong.