Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/modules/nodeCompatibility/headers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default `class Headers {
export default `if (typeof globalThis.Headers === 'undefined') {
class Headers {
constructor(init) {
this.map = {};

Expand Down Expand Up @@ -51,6 +52,7 @@ export default `class Headers {
});
}
}

globalThis.Headers = Headers;
}
export default globalThis.Headers;
`
4 changes: 3 additions & 1 deletion src/modules/nodeCompatibility/request.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export default `
export default {}
const RequestClass = typeof Request !== 'undefined' ? Request : class Request {};
globalThis.Request = RequestClass;
export default RequestClass;
`
4 changes: 3 additions & 1 deletion src/modules/nodeCompatibility/response.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export default `
export default {}
const ResponseClass = typeof Response !== 'undefined' ? Response : class Response {};
globalThis.Response = ResponseClass;
export default ResponseClass;
`
4 changes: 3 additions & 1 deletion src/modules/timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export setTimeout
export clearTimeout
export setInterval
export clearInterval
export setImmediate
export clearImmediate

export default {setTimeout, clearTimeout, setInterval, clearInterval }
export default {setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate }
`
41 changes: 40 additions & 1 deletion src/modules/timers_promises.js
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
export default ''
export default `
export const setTimeout = (delay, value, options) => new Promise((resolve, reject) => {
const id = globalThis.setTimeout(() => resolve(value), delay);
if (options && options.signal) {
const abortHandler = () => {
globalThis.clearTimeout(id);
reject(options.signal.reason ?? new Error('AbortError'));
};
if (options.signal.aborted) {
abortHandler();
return;
}
options.signal.addEventListener('abort', abortHandler, { once: true });
}
});

export const setImmediate = (value, options) => new Promise((resolve, reject) => {
const id = globalThis.setTimeout(() => resolve(value), 0);
if (options && options.signal) {
const abortHandler = () => {
globalThis.clearTimeout(id);
reject(options.signal.reason ?? new Error('AbortError'));
};
if (options.signal.aborted) {
abortHandler();
return;
}
options.signal.addEventListener('abort', abortHandler, { once: true });
}
});

export async function* setInterval(delay, value) {
while (true) {
await new Promise((resolve) => globalThis.setTimeout(resolve, delay));
yield value;
}
}

export default { setTimeout, setImmediate, setInterval }
`
81 changes: 66 additions & 15 deletions src/sandbox/provide/provideTimingFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ export const provideTimingFunctions = (
maxIntervalCount: number
},
) => {
const scope = new Scope()
const scope = new Scope()

const timeouts = new Map<number, ReturnType<typeof setTimeout>>()
let timeoutCounter = 0
const timeouts = new Map<number, ReturnType<typeof setTimeout>>()
let timeoutCounter = 0

const immediates = new Map<number, ReturnType<typeof setTimeout>>()
let immediateCounter = 0

const intervals = new Map<number, ReturnType<typeof setTimeout>>()
let intervalCounter = 0
Expand Down Expand Up @@ -56,7 +59,49 @@ export const provideTimingFunctions = (
})

scope.manage(_clearTimeout)
ctx.setProp(ctx.global, 'clearTimeout', _clearTimeout)
ctx.setProp(ctx.global, 'clearTimeout', _clearTimeout)

const _setImmediate = ctx.newFunction('setImmediate', vmFnHandle => {
const currentCounter = immediateCounter++
if (timeouts.size + 1 > max.maxTimeoutCount) {
throw new Error(
`Client tries to use setImmediate, which exceeds the limit of max ${max.maxTimeoutCount} concurrent running timeout functions`,
)
}

const vmFnHandleCopy = vmFnHandle.dup()
scope.manage(vmFnHandleCopy)

const timeoutID = setTimeout(() => {
const t = immediates.get(currentCounter)
if (t) {
clearTimeout(t)
immediates.delete(currentCounter)
}
ctx.callFunction(vmFnHandleCopy, ctx.undefined)
}, 0)

immediates.set(currentCounter, timeoutID)

return ctx.newNumber(currentCounter)
})

scope.manage(_setImmediate)
ctx.setProp(ctx.global, 'setImmediate', _setImmediate)

const _clearImmediate = ctx.newFunction('clearImmediate', idHandle => {
const id: number = ctx.dump(idHandle)
idHandle.dispose()

const t = immediates.get(id)
if (t) {
clearTimeout(t)
immediates.delete(id)
}
})

scope.manage(_clearImmediate)
ctx.setProp(ctx.global, 'clearImmediate', _clearImmediate)

const _setInterval = ctx.newFunction('setInterval', (vmFnHandle, intervalHandle) => {
const currentCounter = intervalCounter++
Expand Down Expand Up @@ -95,17 +140,23 @@ export const provideTimingFunctions = (
scope.manage(_clearInterval)
ctx.setProp(ctx.global, 'clearInterval', _clearInterval)

const dispose = () => {
for (const [_key, value] of timeouts) {
clearTimeout(value)
}
timeouts.clear()
timeoutCounter = 0

for (const [_key, value] of intervals) {
clearInterval(value)
}
intervals.clear()
const dispose = () => {
for (const [_key, value] of timeouts) {
clearTimeout(value)
}
timeouts.clear()
timeoutCounter = 0

for (const [_key, value] of immediates) {
clearTimeout(value)
}
immediates.clear()
immediateCounter = 0

for (const [_key, value] of intervals) {
clearInterval(value)
}
intervals.clear()
intervalCounter = 0

scope.dispose()
Expand Down
18 changes: 16 additions & 2 deletions src/test/async/core-timers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('async - core - timers', () => {
expect((result as OkResponse).data).toBe('interval reached')
})

it('clearInterval works correctly', async () => {
it('clearInterval works correctly', async () => {
const code = `
export default await new Promise((resolve) => {
let count = 0
Expand All @@ -87,5 +87,19 @@ describe('async - core - timers', () => {
// but it should be around 3 if intervals are 100ms and we clear after 500ms.
expect((result as OkResponse).data).toBeGreaterThanOrEqual(3)
expect((result as OkResponse).data).toBeLessThanOrEqual(5)
})
})

it('setImmediate works correctly', async () => {
const code = `
export default await new Promise((resolve) => {
setImmediate(() => {
resolve('immediate reached')
})
})
`

const result = await runCode(code)
expect(result.ok).toBeTrue()
expect((result as OkResponse).data).toBe('immediate reached')
})
})
39 changes: 39 additions & 0 deletions src/test/async/timers-promises.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { beforeAll, describe, expect, it } from 'bun:test'
import { loadAsyncQuickJs } from '../../loadAsyncQuickJs.js'
import type { OkResponse } from '../../types/OkResponse.js'

describe('async - node:timers/promises', () => {
let runtime: Awaited<ReturnType<typeof loadAsyncQuickJs>>

beforeAll(async () => {
runtime = await loadAsyncQuickJs()
})

const runCode = async (code: string) => {
return await runtime.runSandboxed(async ({ evalCode }) => {
return await evalCode(code)
})
}

it('setTimeout resolves', async () => {
const code = `
import { setTimeout } from 'node:timers/promises'
export default await setTimeout(100, 'done')
`

const result = await runCode(code)
expect(result.ok).toBeTrue()
expect((result as OkResponse).data).toBe('done')
})

it('setImmediate resolves', async () => {
const code = `
import { setImmediate } from 'node:timers/promises'
export default await setImmediate('immediate')
`

const result = await runCode(code)
expect(result.ok).toBeTrue()
expect((result as OkResponse).data).toBe('immediate')
})
})
18 changes: 16 additions & 2 deletions src/test/sync/core-timers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('sync - core - timers', () => {
expect((result as OkResponse).data).toBe('interval reached')
})

it('clearInterval works correctly', async () => {
it('clearInterval works correctly', async () => {
const code = `
export default await new Promise((resolve) => {
let count = 0
Expand All @@ -87,5 +87,19 @@ describe('sync - core - timers', () => {
// but it should be around 3 if intervals are 100ms and we clear after 500ms.
expect((result as OkResponse).data).toBeGreaterThanOrEqual(3)
expect((result as OkResponse).data).toBeLessThanOrEqual(5)
})
})

it('setImmediate works correctly', async () => {
const code = `
export default await new Promise((resolve) => {
setImmediate(() => {
resolve('immediate reached')
})
})
`

const result = await runCode(code)
expect(result.ok).toBeTrue()
expect((result as OkResponse).data).toBe('immediate reached')
})
})
39 changes: 39 additions & 0 deletions src/test/sync/timers-promises.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { beforeAll, describe, expect, it } from 'bun:test'
import { loadQuickJs } from '../../loadQuickJs.js'
import type { OkResponse } from '../../types/OkResponse.js'

describe('sync - node:timers/promises', () => {
let runtime: Awaited<ReturnType<typeof loadQuickJs>>

beforeAll(async () => {
runtime = await loadQuickJs()
})

const runCode = async (code: string) => {
return await runtime.runSandboxed(async ({ evalCode }) => {
return await evalCode(code)
})
}

it('setTimeout resolves', async () => {
const code = `
import { setTimeout } from 'node:timers/promises'
export default await setTimeout(100, 'done')
`

const result = await runCode(code)
expect(result.ok).toBeTrue()
expect((result as OkResponse).data).toBe('done')
})

it('setImmediate resolves', async () => {
const code = `
import { setImmediate } from 'node:timers/promises'
export default await setImmediate('immediate')
`

const result = await runCode(code)
expect(result.ok).toBeTrue()
expect((result as OkResponse).data).toBe('immediate')
})
})
3 changes: 2 additions & 1 deletion website/docs/module-resolution/node-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ This library tries to provide basic support for most common used Node.js modules
| `repl` | ❌ | Provides a Read-Eval-Print Loop (REPL) interface |
| `stream` | ❌ | Provides an API for implementing stream-based I/O |
| `string_decoder` | ✅ | Provides utilities for decoding buffer objects into strings |
| `timers` | ❌ | Provides timer functions similar to those in JavaScript |
| `timers` | ✅ | Provides timer functions similar to those in JavaScript |
| `timers/promises`| ✅ | Promise-based timer functions |
| `tls` | ❌ | Provides an implementation of TLS and SSL protocols |
| `trace_events` | ❌ | Provides a mechanism to centralize tracing information |
| `tty` | ❌ | Provides utilities for working with TTYs (terminals) |
Expand Down
Loading