@mycroblog/mybot 0.0.0-placeholder.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ /** The key of the account that acted, "" for an event without one. */
2
+ export function senderOf(account) {
3
+ return account?.id ?? "";
4
+ }
5
+ /**
6
+ * Runs the jobs: up to its limit at once, those of one sender one after the other in the order they came (ADR-0094
7
+ * §4). A notification that is queued or running is not queued again, so that the catch-up and the event stream may
8
+ * both bring it. run must not reject.
9
+ */
10
+ export class Dispatcher {
11
+ #run;
12
+ #limit;
13
+ #active = 0;
14
+ #waiting = [];
15
+ #queues = new Map();
16
+ #queued = new Set();
17
+ #drains = new Set();
18
+ #stopping = false;
19
+ constructor(concurrency, run) {
20
+ this.#limit = Math.max(1, concurrency);
21
+ this.#run = run;
22
+ }
23
+ /** Queues job and reports whether it did: not after stop, and not for a notification already queued. */
24
+ submit(job) {
25
+ if (this.#stopping)
26
+ return false;
27
+ if (job.notification !== undefined) {
28
+ if (this.#queued.has(job.notification.id))
29
+ return false;
30
+ this.#queued.add(job.notification.id);
31
+ }
32
+ const queue = this.#queues.get(job.sender);
33
+ if (queue !== undefined) {
34
+ queue.push(job);
35
+ return true;
36
+ }
37
+ this.#queues.set(job.sender, [job]);
38
+ const drain = this.#drain(job.sender).finally(() => this.#drains.delete(drain));
39
+ this.#drains.add(drain);
40
+ return true;
41
+ }
42
+ /** Runs the queue of sender until it is empty, or the dispatcher stops. */
43
+ async #drain(sender) {
44
+ for (;;) {
45
+ const queue = this.#queues.get(sender);
46
+ const job = queue?.[0];
47
+ if (this.#stopping || queue === undefined || job === undefined) {
48
+ // The jobs that never started are left to the next catch-up.
49
+ this.#forgetQueue(sender);
50
+ return;
51
+ }
52
+ await this.#acquire();
53
+ // stop may have been called while the job waited for its turn.
54
+ const stopping = this.#isStopping();
55
+ try {
56
+ if (!stopping)
57
+ await this.#run(job);
58
+ }
59
+ finally {
60
+ this.#release();
61
+ }
62
+ if (job.notification !== undefined)
63
+ this.#queued.delete(job.notification.id);
64
+ if (queue.length <= 1 || stopping) {
65
+ this.#forgetQueue(sender);
66
+ return;
67
+ }
68
+ queue.shift();
69
+ }
70
+ }
71
+ #forgetQueue(sender) {
72
+ for (const job of this.#queues.get(sender) ?? [])
73
+ if (job.notification !== undefined)
74
+ this.#queued.delete(job.notification.id);
75
+ this.#queues.delete(sender);
76
+ }
77
+ async #acquire() {
78
+ if (this.#active < this.#limit) {
79
+ this.#active++;
80
+ return;
81
+ }
82
+ await new Promise((resolve) => this.#waiting.push(resolve));
83
+ }
84
+ #release() {
85
+ const next = this.#waiting.shift();
86
+ if (next === undefined)
87
+ this.#active--;
88
+ else
89
+ next();
90
+ }
91
+ #isStopping() {
92
+ return this.#stopping;
93
+ }
94
+ /** Makes the dispatcher take no more jobs and start none of those queued. */
95
+ stop() {
96
+ this.#stopping = true;
97
+ }
98
+ /** Resolves when every running job has returned. */
99
+ async idle() {
100
+ while (this.#drains.size > 0)
101
+ await Promise.all(this.#drains);
102
+ }
103
+ }
104
+ /**
105
+ * Computes the position of the catch-up (ADR-0094 §3): the newest notification up to which every notification handed
106
+ * to the bot was processed. Handlers end out of order, so a processed notification moves the position only when no
107
+ * older one is still pending; while a catch-up hands out notifications, the position holds still, so that a restart
108
+ * in the middle of it reads them again. Notification IDs are UUIDv7 strings, whose order is the order of time.
109
+ */
110
+ export class Tracker {
111
+ #pending = new Set();
112
+ /** The processed notifications above the position, in order. */
113
+ #done = [];
114
+ #holds = 0;
115
+ /** Marks the notification id pending. */
116
+ add(id) {
117
+ this.#pending.add(id);
118
+ }
119
+ /** Drops a pending notification that was not handed out after all. */
120
+ forget(id) {
121
+ this.#pending.delete(id);
122
+ }
123
+ /** Marks the notification id processed and returns the new position, null when it does not move. */
124
+ complete(id) {
125
+ this.#pending.delete(id);
126
+ const i = lowerBound(this.#done, id);
127
+ if (this.#done[i] !== id)
128
+ this.#done.splice(i, 0, id);
129
+ return this.#advance();
130
+ }
131
+ /** Keeps the position where it is until release. */
132
+ hold() {
133
+ this.#holds++;
134
+ }
135
+ /** Ends a hold and returns the new position, null when it does not move. */
136
+ release() {
137
+ this.#holds--;
138
+ return this.#advance();
139
+ }
140
+ /** Moves the position past the processed notifications older than every pending one. */
141
+ #advance() {
142
+ if (this.#holds > 0 || this.#done.length === 0)
143
+ return null;
144
+ let oldest = null;
145
+ for (const id of this.#pending)
146
+ if (oldest === null || id < oldest)
147
+ oldest = id;
148
+ const n = oldest === null ? this.#done.length : lowerBound(this.#done, oldest);
149
+ if (n === 0)
150
+ return null;
151
+ const position = this.#done[n - 1] ?? null;
152
+ this.#done = this.#done.slice(n);
153
+ return position;
154
+ }
155
+ }
156
+ /** The index of the first element of the sorted xs that is not less than x. */
157
+ function lowerBound(xs, x) {
158
+ let lo = 0;
159
+ let hi = xs.length;
160
+ while (lo < hi) {
161
+ const mid = (lo + hi) >>> 1;
162
+ if ((xs[mid] ?? "") < x)
163
+ lo = mid + 1;
164
+ else
165
+ hi = mid;
166
+ }
167
+ return lo;
168
+ }
@@ -0,0 +1,55 @@
1
+ import type { components } from "@mycroblog/mykit";
2
+ import type { Handler } from "./context.js";
3
+ import type { Schedule } from "./schedule.js";
4
+ type Visibility = components["schemas"]["Visibility"];
5
+ /**
6
+ * What a scheduled job does about the times it missed while the bot was stopped: "skip", the default, leaves them out,
7
+ * and the job runs from its next time after the start; "latest" runs, when the bot starts, the newest of the times
8
+ * after the job's last run in the store, once. The older ones are left out, so that a long stop never posts a pile at
9
+ * once. A job that never ran runs its newest past time.
10
+ */
11
+ export type CatchUp = "skip" | "latest";
12
+ /** The visibilities of the posts of a scheduled job. */
13
+ export type EveryVisibility = Extract<Visibility, "unlisted" | "public" | "local">;
14
+ /** The options of a scheduled job (Bot.every). */
15
+ export type EveryOptions = {
16
+ /**
17
+ * Names the job in the store (its last run) and in the Idempotency-Key of its posts: keep it when the expression
18
+ * changes. Absent or "" means the expression. Two jobs of one bot cannot share a name.
19
+ */
20
+ name?: string;
21
+ /** The IANA time zone of the expression, such as "Asia/Tokyo"; absent or "" means UTC. */
22
+ timeZone?: string;
23
+ /** "skip" (the default) or "latest". */
24
+ catchUp?: CatchUp;
25
+ /**
26
+ * The visibility of the posts of Context.post: "unlisted" (the default), "public", or "local", which reaches the
27
+ * server of the bot and its followers alone (ADR-0017).
28
+ */
29
+ visibility?: EveryVisibility;
30
+ };
31
+ /** A job that Bot.every registered. */
32
+ export type EveryJob = {
33
+ readonly name: string;
34
+ readonly schedule: Schedule;
35
+ readonly handler: Handler;
36
+ readonly catchUp: CatchUp;
37
+ readonly visibility: EveryVisibility;
38
+ };
39
+ /**
40
+ * The Idempotency-Key of the post of the run at (milliseconds since the Unix epoch) of the scheduled job name
41
+ * (ADR-0094 §5): mybot:every:<name>:<the time in RFC 3339, UTC>. Two processes of one bot that run the same time post
42
+ * once. Use it when posting with mykit directly.
43
+ */
44
+ export declare function everyIdempotencyKey(name: string, at: number): string;
45
+ /** The error of Context.post outside a scheduled job. */
46
+ export declare class NotScheduledError extends Error {
47
+ readonly name = "NotScheduledError";
48
+ constructor();
49
+ }
50
+ /** The error of a second Context.post in one run of a scheduled job, whose Idempotency-Key would answer with the first post. */
51
+ export declare class PostedAlreadyError extends Error {
52
+ readonly name = "PostedAlreadyError";
53
+ constructor();
54
+ }
55
+ export {};
package/dist/every.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The Idempotency-Key of the post of the run at (milliseconds since the Unix epoch) of the scheduled job name
3
+ * (ADR-0094 §5): mybot:every:<name>:<the time in RFC 3339, UTC>. Two processes of one bot that run the same time post
4
+ * once. Use it when posting with mykit directly.
5
+ */
6
+ export function everyIdempotencyKey(name, at) {
7
+ return `mybot:every:${name}:${new Date(at).toISOString().replace(/\.\d+Z$/, "Z")}`;
8
+ }
9
+ /** The error of Context.post outside a scheduled job. */
10
+ export class NotScheduledError extends Error {
11
+ name = "NotScheduledError";
12
+ constructor() {
13
+ super("mybot: post is for the handler of a scheduled job");
14
+ }
15
+ }
16
+ /** The error of a second Context.post in one run of a scheduled job, whose Idempotency-Key would answer with the first post. */
17
+ export class PostedAlreadyError extends Error {
18
+ name = "PostedAlreadyError";
19
+ constructor() {
20
+ super("mybot: the run of the scheduled job posted already");
21
+ }
22
+ }
@@ -0,0 +1,17 @@
1
+ import { StateStore, type StoreState } from "./store.js";
2
+ /** The file of the FileStore that a Bot uses when its options name no store. */
3
+ export declare const defaultStatePath = "./mybot-state.json";
4
+ /**
5
+ * A Store in one JSON file, the default Store of a Bot. It reads the file on first use (a missing file is an empty
6
+ * state, created on the first change). Every change rewrites the file: the new content goes to a temporary file in
7
+ * the same directory, which then replaces the file, so that a crash never leaves half of it. The file holds the
8
+ * refresh token: it is created readable by its owner only. The document is that of the Go library's FileStore.
9
+ */
10
+ export declare class FileStore extends StateStore {
11
+ #private;
12
+ /** The file of the store. */
13
+ readonly path: string;
14
+ constructor(path?: string);
15
+ protected load(): Promise<StoreState>;
16
+ protected save(state: StoreState): Promise<void>;
17
+ }
@@ -0,0 +1,95 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // FileStore, the one part of the library that touches the file system. It loads node:fs/promises on first use, with
3
+ // a dynamic import, which Node.js, Bun, and Deno all provide (Deno needs --allow-read and --allow-write for the file),
4
+ // so that the rest of the library runs wherever fetch does.
5
+ import { decodeState, emptyState, encodeState, StateStore } from "./store.js";
6
+ /** The file of the FileStore that a Bot uses when its options name no store. */
7
+ export const defaultStatePath = "./mybot-state.json";
8
+ /** The module name, in a variable so that the type checker, which has no Node.js types here, does not resolve it. */
9
+ const fsModule = "node:fs/promises";
10
+ let fsPromise = null;
11
+ /** node:fs/promises, loaded once. */
12
+ function fs() {
13
+ fsPromise ??= import(fsModule);
14
+ return fsPromise;
15
+ }
16
+ /** Whether error is the error of a missing file. */
17
+ function notFound(error) {
18
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
19
+ }
20
+ /** The directory part of path ("." for a bare name), for either separator. */
21
+ function dirname(path) {
22
+ const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
23
+ if (i < 0)
24
+ return ".";
25
+ return i === 0 ? path.slice(0, 1) : path.slice(0, i);
26
+ }
27
+ /** The last part of path. */
28
+ function basename(path) {
29
+ return path.slice(Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1);
30
+ }
31
+ /**
32
+ * A Store in one JSON file, the default Store of a Bot. It reads the file on first use (a missing file is an empty
33
+ * state, created on the first change). Every change rewrites the file: the new content goes to a temporary file in
34
+ * the same directory, which then replaces the file, so that a crash never leaves half of it. The file holds the
35
+ * refresh token: it is created readable by its owner only. The document is that of the Go library's FileStore.
36
+ */
37
+ export class FileStore extends StateStore {
38
+ /** The file of the store. */
39
+ path;
40
+ #state = null;
41
+ /** The writes in order: each rewrites the whole file, so the last one wins. */
42
+ #writes = Promise.resolve();
43
+ constructor(path = defaultStatePath) {
44
+ super();
45
+ this.path = path;
46
+ }
47
+ load() {
48
+ this.#state ??= this.#read();
49
+ return this.#state;
50
+ }
51
+ async #read() {
52
+ const f = await fs();
53
+ let text;
54
+ try {
55
+ text = await f.readFile(this.path, "utf8");
56
+ }
57
+ catch (error) {
58
+ if (notFound(error))
59
+ return emptyState();
60
+ throw new Error(`mybot: read the state file ${this.path}`, { cause: error });
61
+ }
62
+ try {
63
+ return decodeState(text);
64
+ }
65
+ catch (error) {
66
+ throw new Error(`mybot: read the state file ${this.path}`, { cause: error });
67
+ }
68
+ }
69
+ save(state) {
70
+ const text = encodeState(state);
71
+ const write = this.#writes.then(() => this.#write(text));
72
+ // A failed write fails its caller, not the next write.
73
+ this.#writes = write.catch(() => undefined);
74
+ return write;
75
+ }
76
+ async #write(text) {
77
+ const f = await fs();
78
+ const tmp = `${dirname(this.path)}/.${basename(this.path)}.${crypto.randomUUID()}.tmp`;
79
+ try {
80
+ const handle = await f.open(tmp, "wx", 0o600);
81
+ try {
82
+ await handle.writeFile(text, "utf8");
83
+ await handle.sync();
84
+ }
85
+ finally {
86
+ await handle.close();
87
+ }
88
+ await f.rename(tmp, this.path);
89
+ }
90
+ catch (error) {
91
+ await f.rm(tmp, { force: true }).catch(() => undefined);
92
+ throw new Error(`mybot: write the state file ${this.path}`, { cause: error });
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,42 @@
1
+ import type { components } from "@mycroblog/mykit";
2
+ import { type Handler, type Middleware } from "./context.js";
3
+ type CredentialAccount = components["schemas"]["CredentialAccount"];
4
+ /** Logs what the handler throws, so that one event never stops the bot. */
5
+ export declare function recover(): Middleware;
6
+ /**
7
+ * Skips the handler, the event counting as processed, for a sender whose account has the bot flag (unless the
8
+ * respondToBots option) and for a sender whom the bot replied to as often as the replyLimits option allows (ADR-0094
9
+ * §8). Events of the bot's own account never reach a handler, with or without it.
10
+ */
11
+ export declare function manners(): Middleware;
12
+ /**
13
+ * Makes the helpers of Context (reply, react, favourite, follow) wait for the client-side send budget of the postRate
14
+ * and writeRate options, and for the reset of the server's rate limit when less than a tenth of it remains (ADR-0094
15
+ * §6). A write that would wait longer than the maxSendWait option is dropped and logged.
16
+ */
17
+ export declare function sendBudget(): Middleware;
18
+ /**
19
+ * The chain that every handler runs in unless the noDefaultMiddleware option is set, outermost first: recover,
20
+ * manners, and sendBudget (ADR-0094 §4).
21
+ */
22
+ export declare function defaultMiddleware(): Middleware[];
23
+ /** Wraps handler in middleware, the first outermost. */
24
+ export declare function chain(middleware: readonly Middleware[], handler: Handler): Handler;
25
+ /** A command that onCommand registered. */
26
+ export type Command = {
27
+ /** The lowercase name of the command. */
28
+ readonly name: string;
29
+ /** What the command does, for the answer of help. */
30
+ readonly description: string;
31
+ };
32
+ /** Formats the answer of the help command: a line for each command with its description. */
33
+ export declare function defaultHelp(commands: readonly Command[]): string;
34
+ /**
35
+ * Reads the command of a mention's text: the first word after the mentions of me at its start, lowercase, and the
36
+ * rest of the text. It is null when no word follows.
37
+ */
38
+ export declare function parseCommand(text: string, me: CredentialAccount | undefined): {
39
+ name: string;
40
+ args: string;
41
+ } | null;
42
+ export {};
@@ -0,0 +1,93 @@
1
+ import { internal } from "./context.js";
2
+ /** Logs what the handler throws, so that one event never stops the bot. */
3
+ export function recover() {
4
+ return async (ctx, next) => {
5
+ try {
6
+ await next();
7
+ }
8
+ catch (error) {
9
+ ctx.logger.error("mybot: the handler failed", { error });
10
+ }
11
+ };
12
+ }
13
+ /**
14
+ * Skips the handler, the event counting as processed, for a sender whose account has the bot flag (unless the
15
+ * respondToBots option) and for a sender whom the bot replied to as often as the replyLimits option allows (ADR-0094
16
+ * §8). Events of the bot's own account never reach a handler, with or without it.
17
+ */
18
+ export function manners() {
19
+ return async (ctx, next) => {
20
+ const { runtime, sender } = ctx[internal];
21
+ if (ctx.account?.bot === true && !runtime.respondToBots) {
22
+ ctx.logger.debug("mybot: skipped an event of a bot", { account: ctx.account.acct });
23
+ return;
24
+ }
25
+ if (ctx.account !== undefined && runtime.replyLimits !== null && runtime.replies.full(sender, runtime.now())) {
26
+ ctx.logger.info("mybot: skipped an event of a sender at the reply limit", { account: ctx.account.acct });
27
+ return;
28
+ }
29
+ await next();
30
+ };
31
+ }
32
+ /**
33
+ * Makes the helpers of Context (reply, react, favourite, follow) wait for the client-side send budget of the postRate
34
+ * and writeRate options, and for the reset of the server's rate limit when less than a tenth of it remains (ADR-0094
35
+ * §6). A write that would wait longer than the maxSendWait option is dropped and logged.
36
+ */
37
+ export function sendBudget() {
38
+ return async (ctx, next) => {
39
+ ctx[internal].budgeted = true;
40
+ await next();
41
+ };
42
+ }
43
+ /**
44
+ * The chain that every handler runs in unless the noDefaultMiddleware option is set, outermost first: recover,
45
+ * manners, and sendBudget (ADR-0094 §4).
46
+ */
47
+ export function defaultMiddleware() {
48
+ return [recover(), manners(), sendBudget()];
49
+ }
50
+ /** Wraps handler in middleware, the first outermost. */
51
+ export function chain(middleware, handler) {
52
+ return (ctx) => {
53
+ const step = async (i) => {
54
+ const mw = middleware[i];
55
+ if (mw === undefined) {
56
+ await handler(ctx);
57
+ return;
58
+ }
59
+ await mw(ctx, () => step(i + 1));
60
+ };
61
+ return step(0);
62
+ };
63
+ }
64
+ /** Formats the answer of the help command: a line for each command with its description. */
65
+ export function defaultHelp(commands) {
66
+ return commands.map((c) => (c.description === "" ? c.name : `${c.name}: ${c.description}`)).join("\n");
67
+ }
68
+ /** ASCII punctuation (Unicode category P), but '_', which may follow a mention: `@bot,`. */
69
+ const trailingPunctuation = /[!"#%&'()*,\-./:;?@[\\\]{}]+$/;
70
+ /** Whether word is a mention of me: @username or @username@domain, without case, and with the punctuation that may follow it. */
71
+ function mentionsMe(word, me) {
72
+ if (!word.startsWith("@") || me === undefined)
73
+ return false;
74
+ const handle = word.slice(1).replace(trailingPunctuation, "").toLowerCase();
75
+ return [me.username, me.acct, `${me.username}@${me.domain}`].some((h) => h !== "" && h.toLowerCase() === handle);
76
+ }
77
+ /**
78
+ * Reads the command of a mention's text: the first word after the mentions of me at its start, lowercase, and the
79
+ * rest of the text. It is null when no word follows.
80
+ */
81
+ export function parseCommand(text, me) {
82
+ let s = text.trim();
83
+ for (;;) {
84
+ const m = /^\s*(\S+)([\s\S]*)$/.exec(s);
85
+ if (m === null)
86
+ return null;
87
+ const word = m[1] ?? "";
88
+ const rest = m[2] ?? "";
89
+ if (!mentionsMe(word, me))
90
+ return { name: word.toLowerCase(), args: rest.trim() };
91
+ s = rest;
92
+ }
93
+ }
@@ -0,0 +1,10 @@
1
+ export { AlreadyRunningError, Bot, defaultCatchUpLimit, defaultConcurrency, defaultPollInterval, defaultProcessedRetention, defaultShutdownGrace, NotBotError, ScopeError, type BotOptions, type Mode, } from "./bot.js";
2
+ export { defaultMaxSendWait, defaultPostRate, defaultReplyLimits, defaultWriteRate, type Rate } from "./budget.js";
3
+ export { BudgetExceededError, Context, NoAccountError, NoStatusError, ReplyLimitError, replyIdempotencyKey, replyVisibility, type Handler, type Middleware, } from "./context.js";
4
+ export { everyIdempotencyKey, NotScheduledError, PostedAlreadyError, type CatchUp, type EveryOptions, type EveryVisibility, } from "./every.js";
5
+ export { defaultStatePath, FileStore } from "./filestore.js";
6
+ export { defaultHelp, defaultMiddleware, manners, recover, sendBudget, type Command } from "./handler.js";
7
+ export type { Logger } from "./logger.js";
8
+ export { MemoryStore, type Store } from "./store.js";
9
+ export { parseSchedule, Schedule, ScheduleError } from "./schedule.js";
10
+ export { plainText } from "./text.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @mycroblog/mybot: build bots for mycroblog/my on the API client @mycroblog/mykit (ADR-0094). The author of a bot
3
+ // writes the handlers of its events; the library receives the events, so that none is lost or answered twice, and
4
+ // keeps the bot polite by default. It mirrors the Go library github.com/mycroblog/my/pkg/mybot: the same handlers,
5
+ // options, defaults, idempotency keys, and state file.
6
+ export { AlreadyRunningError, Bot, defaultCatchUpLimit, defaultConcurrency, defaultPollInterval, defaultProcessedRetention, defaultShutdownGrace, NotBotError, ScopeError, } from "./bot.js";
7
+ export { defaultMaxSendWait, defaultPostRate, defaultReplyLimits, defaultWriteRate } from "./budget.js";
8
+ export { BudgetExceededError, Context, NoAccountError, NoStatusError, ReplyLimitError, replyIdempotencyKey, replyVisibility, } from "./context.js";
9
+ export { everyIdempotencyKey, NotScheduledError, PostedAlreadyError, } from "./every.js";
10
+ export { defaultStatePath, FileStore } from "./filestore.js";
11
+ export { defaultHelp, defaultMiddleware, manners, recover, sendBudget } from "./handler.js";
12
+ export { MemoryStore } from "./store.js";
13
+ export { parseSchedule, Schedule, ScheduleError } from "./schedule.js";
14
+ export { plainText } from "./text.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Receives the log of a bot: `console`, the default, or anything with the same four methods. The bot calls each with
3
+ * a message and an object of attributes, such as `logger.warn("mybot: ...", { notification: "...", error })`.
4
+ */
5
+ export type Logger = {
6
+ debug(message: string, ...data: unknown[]): void;
7
+ info(message: string, ...data: unknown[]): void;
8
+ warn(message: string, ...data: unknown[]): void;
9
+ error(message: string, ...data: unknown[]): void;
10
+ };
11
+ /** A Logger that adds attrs to the attributes of every call to logger. */
12
+ export declare function withAttributes(logger: Logger, attrs: Record<string, unknown>): Logger;
package/dist/logger.js ADDED
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /** Whether value is a plain object of attributes. */
3
+ function isAttributes(value) {
4
+ return typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
5
+ }
6
+ /** A Logger that adds attrs to the attributes of every call to logger. */
7
+ export function withAttributes(logger, attrs) {
8
+ const call = (level) => (message, ...data) => {
9
+ const [first, ...more] = data;
10
+ if (isAttributes(first)) {
11
+ logger[level](message, { ...attrs, ...first }, ...more);
12
+ }
13
+ else {
14
+ logger[level](message, attrs, ...data);
15
+ }
16
+ };
17
+ return { debug: call("debug"), info: call("info"), warn: call("warn"), error: call("error") };
18
+ }
@@ -0,0 +1,36 @@
1
+ /** The error of parseSchedule and Bot.every for an expression or a time zone that mybot does not take. */
2
+ export declare class ScheduleError extends Error {
3
+ readonly name = "ScheduleError";
4
+ }
5
+ /**
6
+ * A parsed cron expression of five fields, minute hour day-of-month month day-of-week, in a time zone (ADR-0094 §5).
7
+ * Each field is *, a number, a range a-b, a step * /n or a-b/n, or a list of those separated by commas; the day of the
8
+ * week is 0-7, where 0 and 7 are Sunday. When both the day of the month and the day of the week are other than *, a
9
+ * day matches when either does, as in cron. A field of seconds, the aliases such as @daily, names of months and days,
10
+ * and the other extensions of some crons (?, L, W, #) are refused: mybot takes the same expressions as the Go library,
11
+ * and uses croner to compute the times alone.
12
+ *
13
+ * The times of a Schedule are wall-clock times of its time zone: one that a daylight saving time change skips does not
14
+ * happen, and one that the change repeats happens once, at its first occurrence. Times are milliseconds since the Unix
15
+ * epoch.
16
+ */
17
+ export declare class Schedule {
18
+ #private;
19
+ /** The expression of the schedule. */
20
+ readonly expr: string;
21
+ /** The IANA time zone of the schedule, "UTC" by default. */
22
+ readonly timeZone: string;
23
+ /** Use parseSchedule. */
24
+ private constructor();
25
+ /**
26
+ * Parses the cron expression expr in the IANA time zone timeZone, such as "Asia/Tokyo"; "" or absent is UTC. It
27
+ * throws a ScheduleError for an expression or a time zone it does not take.
28
+ */
29
+ static parse(expr: string, timeZone?: string): Schedule;
30
+ /** The first time of the schedule after at; null when there is none in the next eight years. */
31
+ next(at: number): number | null;
32
+ /** The last time of the schedule at or before at; null when there is none in the eight years before. */
33
+ prev(at: number): number | null;
34
+ }
35
+ /** Parses the cron expression expr in the IANA time zone timeZone ("" or absent is UTC): Schedule.parse. */
36
+ export declare function parseSchedule(expr: string, timeZone?: string): Schedule;