From 6de3cfc03d0f8c7bc14fed4364d6a94b0c49da5b Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Mon, 22 Jan 2024 16:33:47 +0900 Subject: [PATCH] feat: worker and delay. (#88) * change year. * update deps. * feat: worker-task * fix test code. * fix test code. * worker type. * remove unstable permission. * add jsdoc. * transfer typedarray. * revoke url each time. * dispose method. * feat: delay * wording. * feat: primitive type convert * change file name. * add jsdoc. * fix primitive. * feat primitive convert and remove env. * fix log deprecated. --- LICENSE.md | 2 +- deps.test.ts | 6 +-- deps.ts | 6 +-- mod.test.ts | 5 +- mod.ts | 3 +- mod.universal.ts | 4 +- src/env.deno.ts | 35 ------------- src/log.deno.ts | 6 +-- src/primitive.ts | 116 +++++++++++++++++++++++++++++++++++++++++ src/time.ts | 11 ++++ src/worker.ts | 84 +++++++++++++++++++++++++++++ test/env.deno.test.ts | 19 ------- test/primitive.test.ts | 34 ++++++++++++ test/text.test.ts | 2 +- test/time.test.ts | 9 +++- test/worker.test.ts | 23 ++++++++ 16 files changed, 295 insertions(+), 70 deletions(-) delete mode 100644 src/env.deno.ts create mode 100644 src/primitive.ts create mode 100644 src/worker.ts delete mode 100644 test/env.deno.test.ts create mode 100644 test/primitive.test.ts create mode 100644 test/worker.test.ts diff --git a/LICENSE.md b/LICENSE.md index 1991302..e9a6825 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Kazuki Ota +Copyright (c) 2024 Kazuki Ota Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/deps.test.ts b/deps.test.ts index 6922058..ed61948 100644 --- a/deps.test.ts +++ b/deps.test.ts @@ -1,3 +1,3 @@ -export {assertEquals} from "https://deno.land/std@0.210.0/assert/mod.ts"; -export {dirname, fromFileUrl} from "https://deno.land/std@0.210.0/path/mod.ts"; -export {exists} from "https://deno.land/std@0.210.0/fs/mod.ts"; \ No newline at end of file +export {assertEquals} from "https://deno.land/std@0.212.0/assert/mod.ts"; +export {dirname, fromFileUrl} from "https://deno.land/std@0.212.0/path/mod.ts"; +export {exists} from "https://deno.land/std@0.212.0/fs/mod.ts"; \ No newline at end of file diff --git a/deps.ts b/deps.ts index debf70c..a7a3195 100644 --- a/deps.ts +++ b/deps.ts @@ -1,3 +1,3 @@ -export {dirname, fromFileUrl} from "https://deno.land/std@0.210.0/path/mod.ts"; -export {Logger, handlers} from "https://deno.land/std@0.210.0/log/mod.ts"; -export {format} from "https://deno.land/std@0.210.0/datetime/mod.ts"; \ No newline at end of file +export {dirname, fromFileUrl} from "https://deno.land/std@0.212.0/path/mod.ts"; +export {Logger, ConsoleHandler, FileHandler} from "https://deno.land/std@0.212.0/log/mod.ts"; +export {format} from "https://deno.land/std@0.212.0/datetime/mod.ts"; \ No newline at end of file diff --git a/mod.test.ts b/mod.test.ts index 44d2d4b..f760494 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -3,7 +3,6 @@ import "./test/byte.test.ts"; import "./test/crypto.test.ts"; import "./test/deep.test.ts"; import "./test/deflate.test.ts"; -import "./test/env.deno.test.ts"; import "./test/fetch.test.ts"; import "./test/import.test.ts"; import "./test/json.deno.test.ts"; @@ -11,7 +10,9 @@ import "./test/log.deno.test.ts"; import "./test/minipack.test.ts"; import "./test/path.deno.test.ts"; import "./test/platform.deno.test.ts"; +import "./test/primitive.test.ts"; import "./test/process.deno.test.ts"; import "./test/stream.test.ts"; import "./test/text.test.ts"; -import "./test/time.test.ts"; \ No newline at end of file +import "./test/time.test.ts"; +import "./test/worker.test.ts"; \ No newline at end of file diff --git a/mod.ts b/mod.ts index b1fd6ed..3b074ec 100644 --- a/mod.ts +++ b/mod.ts @@ -6,11 +6,12 @@ export * from "./src/deflate.ts"; export * from "./src/fetch.ts"; export * from "./src/import.ts"; export * from "./src/minipack.ts"; +export * from "./src/primitive.ts"; export * from "./src/stream.ts"; export * from "./src/text.ts"; export * from "./src/time.ts"; +export * from "./src/worker.ts"; -export * from "./src/env.deno.ts"; export * from "./src/json.deno.ts"; export * from "./src/log.deno.ts"; export * from "./src/path.deno.ts"; diff --git a/mod.universal.ts b/mod.universal.ts index 51d84c2..0cdb7b7 100644 --- a/mod.universal.ts +++ b/mod.universal.ts @@ -6,6 +6,8 @@ export * from "./src/deflate.ts"; export * from "./src/fetch.ts"; export * from "./src/import.ts"; export * from "./src/minipack.ts"; +export * from "./src/primitive.ts"; export * from "./src/stream.ts"; export * from "./src/text.ts"; -export * from "./src/time.ts"; \ No newline at end of file +export * from "./src/time.ts"; +export * from "./src/worker.ts"; \ No newline at end of file diff --git a/src/env.deno.ts b/src/env.deno.ts deleted file mode 100644 index 8330da0..0000000 --- a/src/env.deno.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** -* Map of env value type and string specify them. -*/ -export interface EnvType{ - "string": string; - "number": number; - "boolean": boolean; -} - -/** -* Convert environment variable to specified type and get. -* @example -* ```ts -* const port = envGet("SERVER_PORT", "number", true); -* ``` -*/ -export function envGet(key:string, type:T, required:U):U extends true ? EnvType[T] : EnvType[T] | undefined{ - const env = Deno.env.get(key); - - if(env === undefined){ - if(required){ - throw new Error(); - } - else{ - return env; - } - } - - switch(type){ - case "string": return env; - case "number": return Number(env); - case "boolean": return (env === "true"); - default: throw new Error(); - } -} \ No newline at end of file diff --git a/src/log.deno.ts b/src/log.deno.ts index 0e0d577..94a0abf 100644 --- a/src/log.deno.ts +++ b/src/log.deno.ts @@ -1,4 +1,4 @@ -import {Logger, handlers, format} from "../deps.ts"; +import {Logger, ConsoleHandler, FileHandler, format} from "../deps.ts"; import {mainPath} from "./path.deno.ts"; function logRecord(date:Date, level:string, message:string){ @@ -19,7 +19,7 @@ export function logEntry(name?:string):Logger{ const log = new Logger("operation", level, { handlers: [ - new handlers.ConsoleHandler(level, { + new ConsoleHandler(level, { formatter({datetime, levelName, msg}){ return logRecord(datetime, levelName, msg); } @@ -29,7 +29,7 @@ export function logEntry(name?:string):Logger{ if(name){ log.handlers.push(...[ - new handlers.FileHandler(level, { + new FileHandler(level, { filename: `${mainPath()}/${name}.log`, formatter({datetime, levelName, msg}){ return logRecord(datetime, levelName, msg); diff --git a/src/primitive.ts b/src/primitive.ts new file mode 100644 index 0000000..48950c1 --- /dev/null +++ b/src/primitive.ts @@ -0,0 +1,116 @@ +/** +* Infer data type from literal type. +*/ +export type WidenLiteral = T extends string ? string : T extends number ? number : T extends boolean ? boolean : T; + +/** +* Something that might be string. +*/ +export type MaybeString = string | null | undefined; + +/** +* Whether to allow `undefined`. +*/ +export type TypeStrict = U extends true ? T : T | undefined; + +/** +* Map of primitive types and string that specify them. +*/ +export interface PrimitiveMap{ + "string": string; + "number": number; + "boolean": boolean; +} + +function undef(strict?:boolean){ + if(strict){ + throw new Error(); + } + + return undefined; +} + +/** +* Convert from dirty text to specified type. +* Enabling `strict` flag will throw exception if parsing is not possible. +* @example +* ```ts +* const value = primitiveParse("123", "number", true); +* ``` +*/ +export function primitiveParse(text:MaybeString, type:T, strict?:U):TypeStrict{ + switch(type){ + case "string": { + const v = String(text); + + if(text === undefined || text === null){ + return >undef(strict); + } + + return >v; + } + + case "number": { + const v = Number(text); + + if(text === undefined || text === null || isNaN(v)){ + return >undef(strict); + } + + return >v; + } + + case "boolean": { + switch(text){ + case "true": return >true; + case "false": return >false; + default: return >undef(strict); + } + } + + default: throw new Error(); + } +} + +/** +* Convert from dirty text to specified type. +* If cannot be parsed, use default (`def`) value. +* Convert to same type as default value. +* @example +* ```ts +* const value = primitiveParseX("123", 0); +* ``` +*/ +export function primitiveParseX(text:MaybeString, def:T):WidenLiteral{ + switch(typeof def){ + case "string": { + const v = String(text); + + if(text === undefined || text === null){ + return >def; + } + + return >v; + } + + case "number": { + const v = Number(text); + + if(text === undefined || text === null || isNaN(v)){ + return >def; + } + + return >v; + } + + case "boolean": { + switch(text){ + case "true": return >true; + case "false": return >false; + default: return >def; + } + } + + default: throw new Error(); + } +} \ No newline at end of file diff --git a/src/time.ts b/src/time.ts index 883f158..b59663c 100644 --- a/src/time.ts +++ b/src/time.ts @@ -35,4 +35,15 @@ export function utParse(ds:string):number{ const [y, m, d, h, mi, s] = ds.split(/[/ :TZ_.-]/i).map(v => Number(v)); return utEncode(new Date(y, (m ?? 1) - 1, d ?? 1, h ?? 0, mi ?? 0, s ?? 0)); +} + +/** +* Wait for specified time. +* @example +* ```ts +* await delay(1000); +* ``` +*/ +export async function delay(ms:number):Promise{ + await new Promise(done => setTimeout(done, ms)); } \ No newline at end of file diff --git a/src/worker.ts b/src/worker.ts new file mode 100644 index 0000000..b322ab0 --- /dev/null +++ b/src/worker.ts @@ -0,0 +1,84 @@ +/** +* TypedArray also automatically unwrap to `ArrayBuffer`. +*/ +export type TaskTransfer = Transferable | ArrayBufferView; + +/** +* Content of processing run by worker thread. +*/ +export type TaskAction = (message:T) => TaskMessage | Promise>; + +/** +* Run registered `TaskAction` in worker thread. +*/ +export type TaskContext = (message:T, transfers?:TaskTransfer[]) => Promise; + +/** +* Communication content between main thread and worker thread. +*/ +export interface TaskMessage{ + message: T; + transfers?: TaskTransfer[]; +} + +/** +* Register `TaskAction` and return reusable task execution context. +* `Worker` instance is created and destroyed each time they run `TaskContext`. +* `import` can only use "syntax", not "declaration". +* @example +* ```ts +* const task = createTask(async(data)=>{ +* const {delay} = await import("https://deno.land/std/async/mod.ts"); +* await delay(1000); +* return { +* message: data * 2 +* }; +* }); +* const result1 = await task(1); +* const result2 = await task(2); +* ``` +*/ +export function createTask(task:TaskAction):TaskContext{ + const script = task.toString(); + const regist = /*js*/` + globalThis.onmessage = async({data})=>{ + const {message, transfers} = await(${script})(data); + globalThis.postMessage(message, { + transfer: transfers?.map(v => "buffer" in v ? v.buffer : v) + }); + }; + `; + + return (message, transfers)=>{ + return new Promise((res, rej)=>{ + const url = URL.createObjectURL(new Blob([regist])); + const worker = new Worker(url, { + type: "module" + }); + + function dispose(){ + worker.terminate(); + URL.revokeObjectURL(url); + } + + worker.onmessage = ({data})=>{ + res(data); + dispose(); + }; + + worker.onerror = (e)=>{ + rej(e); + dispose(); + }; + + worker.onmessageerror = (e)=>{ + rej(e); + dispose(); + }; + + worker.postMessage(message, { + transfer: transfers?.map(v => "buffer" in v ? v.buffer : v) + }); + }); + }; +} \ No newline at end of file diff --git a/test/env.deno.test.ts b/test/env.deno.test.ts deleted file mode 100644 index 3f44e25..0000000 --- a/test/env.deno.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {assertEquals} from "../deps.test.ts"; -import {envGet} from "../src/env.deno.ts"; - -Deno.env.set("TEST_ENV_1", "abc"); -Deno.env.set("TEST_ENV_2", "123"); -Deno.env.set("TEST_ENV_3", "true"); - -Deno.test({ - name: "Env: Get", - fn(){ - const env1 = envGet("TEST_ENV_1", "string", true); - const env2 = envGet("TEST_ENV_2", "number", true); - const env3 = envGet("TEST_ENV_3", "boolean", true); - - assertEquals(env1, "abc"); - assertEquals(env2, 123); - assertEquals(env3, true); - } -}); \ No newline at end of file diff --git a/test/primitive.test.ts b/test/primitive.test.ts new file mode 100644 index 0000000..b2727e0 --- /dev/null +++ b/test/primitive.test.ts @@ -0,0 +1,34 @@ +import {assertEquals} from "../deps.test.ts"; +import {primitiveParse, primitiveParseX} from "../src/primitive.ts"; + +Deno.test({ + name: "Primitive: Parse", + fn(){ + const result1 = primitiveParse("foo", "string", true); + const result2 = primitiveParse(null, "string"); + const result3 = primitiveParse("12345", "number", true); + const result4 = primitiveParse("foo", "number"); + const result5 = primitiveParse("true", "boolean", true); + const result6 = primitiveParse("foo", "boolean"); + + assertEquals(result1, "foo"); + assertEquals(result2, undefined); + assertEquals(result3, 12345); + assertEquals(result4, undefined); + assertEquals(result5, true); + assertEquals(result6, undefined); + } +}); + +Deno.test({ + name: "Primitive: ParseX", + fn(){ + const result1 = primitiveParseX("foo", ""); + const result2 = primitiveParseX("123", 0); + const result3 = primitiveParseX("true", false); + + assertEquals(result1, "foo"); + assertEquals(result2, 123); + assertEquals(result3, true); + } +}); \ No newline at end of file diff --git a/test/text.test.ts b/test/text.test.ts index 21d7fd7..c70270e 100644 --- a/test/text.test.ts +++ b/test/text.test.ts @@ -11,7 +11,7 @@ const sampleBin = new Uint8Array([ ]); Deno.test({ - name: "Text: UTF8 Encode and Decode", + name: "Text: UTF-8 Encode and Decode", fn(){ const encode = u8Encode(sampleText); const decode = u8Decode(encode); diff --git a/test/time.test.ts b/test/time.test.ts index 83ecccc..4a9e172 100644 --- a/test/time.test.ts +++ b/test/time.test.ts @@ -1,5 +1,5 @@ import {assertEquals} from "../deps.test.ts"; -import {utEncode, utDecode, utParse} from "../src/time.ts"; +import {utEncode, utDecode, utParse, delay} from "../src/time.ts"; const sample = new Date(2000, 0, 1, 0, 0, 0, 0); @@ -20,4 +20,11 @@ Deno.test({ assertEquals(result, 946684800); } +}); + +Deno.test({ + name: "Time: Delay", + async fn(){ + await delay(100); + } }); \ No newline at end of file diff --git a/test/worker.test.ts b/test/worker.test.ts new file mode 100644 index 0000000..7fbd9ef --- /dev/null +++ b/test/worker.test.ts @@ -0,0 +1,23 @@ +import {assertEquals} from "../deps.test.ts"; +import {createTask} from "../src/worker.ts"; + +const sample1 = new Uint8Array([1, 2, 3, 4]); +const sample2 = new Uint8Array([2, 4, 6, 8]); + +Deno.test({ + name: "Worker: Create Task.", + async fn(){ + const task = createTask((v)=>{ + const result = v.map(n => n * 2); + + return { + message: result, + transfers: [result] + }; + }); + + const result = await task(sample1, [sample1]); + + assertEquals(result, sample2); + } +}); \ No newline at end of file