On This Page

Changelog

What changed in each release of the Fluxerly.js SDK, newest first

Unreleased

These changes are in this nightly and not yet in a Stable release

Major Changes

  • Require Effect 4.1.0 or newer. The effect peer range is now ^4.1.0, so every supported Effect release reports combined failures the same way. Since Effect 4.0.6, an expected failure that occurs together with a defect, such as a cleanup fault, is reported as a defect that keeps the failure's value. In the default API, such an SdkDefect lists the original error as a Defect reason instead of a Failure reason, while its cause, message and code still come from the actual fault, such as the cleanup failure. In the native API, the Cause holds the original error as a Die next to the cleanup defect, so catchTag no longer recovers from it. The original error is still the public one, such as MessageOperationError or ConnectionError, never an internal REST or gateway failure

    Migration: Update Effect to 4.1.0 or a later Effect 4 release. Code that read the Failure reason of an SdkDefect to find the original error should also look at its Defect reasons

  • Breaking change for code that builds ShardState objects by hand, such as test doubles of client.shards: The new sessionEpoch field is required. Add sessionEpoch: 0 to such objects. The same applies to code that builds EventContext objects by hand, such as tests that call handlers or middleware directly, which must now set sessionEpoch and receipt, and to code that builds BotEventContext objects by hand to call runBot handlers directly, which must now set context

  • Breaking changes in commands.parseQuoted: A single quote is now an apostrophe unless it starts a word, and its rejections carry the command name. Double quotes and backslash escapes are unchanged, and the rejections for an unclosed quote and a trailing backslash change only as described below. A single quote opens a quoted span only at the start of the text, after whitespace or directly after a span that just closed. Inside a single-quoted span, a single quote ends the span unless the next character is a letter or digit, so the end of the text, whitespace, quotes and punctuation all end it. Every other single quote is literal.

    Inputs that now parse differently, shown as the words each gives (arguments after !say ):

    • don't and it's Ada's: Now don't, then it's and Ada's. Before, the apostrophe opened a span, so don't was rejected as an unclosed single quote and it's Ada's gave the one word its Adas
    • a'b c' and key='a b': Now a'b with c', and key='a with b'. Before, ab c and key=a b
    • a''b, a'b'c and a': Now unchanged as a''b, a'b'c and a'. Before, ab, abc and an unclosed single quote
    • 'it's fine', 'don't' and 'x'y': Now it's fine, don't and x'y. Before, all three were rejected as an unclosed single quote
    • 'the dogs' bowl': Now the dogs and bowl'. Before, rejected as an unclosed single quote
    • 'a'b, 'a b'c, ''a, 'a'1 and '日本'語: Now rejected as an unclosed single quote, because the second quote is followed by a letter or digit and so stays inside the span. Before, ab, a bc, a, a1 and 日本語
    • a' 'b and x'y 'z: Now rejected as an unclosed single quote, because the first apostrophe no longer opens a span and the later quote does. Before, a b and xy z

    Inputs that parse as before include 'Yes', 'No' (Yes, and No), 'a'., 'a'-b, 'a' 'b', 'a''b' (ab), don\'t and every input without a single quote.

    Rejections that commands.parseQuoted returns for an unclosed quote and a trailing backslash now also carry name, the command name read before the mistake, so a reply can say which command it was for. A parse and a rejection can therefore both carry name, and a caller that told them apart with "name" in result must use "reason" in result instead.

    The reason of a commands.parseQuoted rejection for an unclosed single quote now explains the common cause when the open span held a single quote that a letter or digit followed, as in 'a'b. The text then also says that a single quote before a letter or digit may have been meant to close the span, and suggests a space after that quote or double quotes. The reason for a single quote without that cause, for a double quote and for a trailing backslash is unchanged, and the rejection keeps its name. Code that compared reason with the exact text of the first form must match the new text, or the shared start A single quote was opened but never closed.

    To migrate, remove workarounds such as don\'t, which still works but is no longer needed. Wrap values that follow text in double quotes, such as key="a b", and end a single-quoted span before whitespace or punctuation, not before a letter or digit, or use double quotes, which may touch text, such as "a"b. A word that begins with an apostrophe, such as 'twas, still opens a span, so write it as \'twas.

  • Public type names no longer carry Default or Native prefixes, and the per-resource request option types become RequestOptions and AuditedRequestOptions

    Each entry point exports a type that differs between the APIs under one name with its own shape, as Client already did. In @neontechspace/fluxerly, RequestOptions has signal and timeoutMs, and in @neontechspace/fluxerly/effect it has timeoutMs. AuditedRequestOptions adds auditReason in both. Notes that the removed option types carried, such as the per-page deadline of messages.iterateSearch, now sit on the methods that use them. The reply helpers of default command and event contexts take Omit<RequestOptions, "signal">, because they apply the handler's signal. Runtime behavior is unchanged

    Migration:

    Both

    ChannelOperationOptions, GuildOperationOptions, MessageOperationOptions, SendOptions, UserOperationOptions, CountOperationOptions, BotApplicationOperationOptions, MessageSearchOptions, AttachmentRefreshOptions, OAuthOperationOptions, InstanceResolveOptions

    RequestOptions

    Both

    ChannelAuditOperationOptions, GuildAuditOperationOptions, MessageAuditOperationOptions, ModerationOptions, WebhookOperationOptions

    AuditedRequestOptions

    Default

    Default plus any name in the two rows above

    RequestOptions or AuditedRequestOptions

    Default

    Default<Name>, such as DefaultCanManageOptions, DefaultCollectorOptions, DefaultRestRequest, DefaultPrefixCommandContext, DefaultSupervisor, DefaultGuards or DefaultCommands

    <Name>

    Effect

    Native<Name>, such as NativePrefixCommandContext, NativeSupervisor, NativeGuards, NativeCommands or NativeCommandRequirements

    <Name>

    In the default entry point, names such as CanManageOptions, CollectorOptions, PrefixCommandsOptions, SupervisorChildOptions and RestRequest now name the default shape, which includes signal where the old Default type did. The Effect entry point no longer exports the default-API PrefixCommandsOptions and SupervisorChildOptions shapes, and those names now refer to its own

Minor Changes

  • Behavior change: A channel write no longer clears the whole channel cache. A write to a cached channel removes only that channel, plus its cached threads when the write deletes the channel or converts its type, and its cached forum posts that apply a tag the write removes, as channels.deleteForumTag and a channels.edit with availableTags can, because Fluxer drops removed tags from posts without a thread update. Removing tags also stops pending reads from storing that forum's posts, while other pending reads by ID and reads of other communities still store what they return. A write to a category, and a write that names a community, such as channels.create, channels.reorder or guilds.leave, removes only that community's cached channels. A write to a channel the cache does not hold still clears every cached channel. Code that relied on the full clear can call client.cache.clear("channels") after the write

    Behavior change: A guildChannelUpdate event for a forum or media channel now removes the channel's cached posts that apply a tag missing from the update's availableTags, because Fluxer removes a tag through that update alone and drops it from posts on read. Fluxer gives each new tag a new ID, so posts whose tags all remain stay cached. Every such update also stops pending reads from storing that forum's posts, because a read of the forum can store its new tags before the update arrives. Other pending reads by ID and reads of other communities still store what they return, so an update in one community does not keep an unrelated read from filling the cache, while a pending read of the updated channel's own community stores nothing, as after any channel update

    Behavior change: A guildChannelUpdate event for a text or announcement channel now removes that channel's cached threads whose type the channel's type cannot hold, because Fluxer retypes a converted channel's threads and sends no thread update for archived ones. This works whether the channel was cached before the update or not, and after a read already stored its new type. Every such update also stops pending reads from storing that channel's threads, while other pending reads by ID and reads of other communities still store what they return

    Behavior change: A shard's first session now releases its communities' cached snapshots, as every later new session already did, and stops pending reads from storing them. Community, member, role, emoji, sticker and channel snapshots read through REST before connecting no longer outlast the first READY, because the events between that read and the session were never received. The release happens as READY arrives, before the communities Fluxer then sends again through guildCreate, which refill the community, role, emoji, sticker and channel caches. This also fixes later new sessions, which until now could release those communities after they arrived, leaving the caches empty until each community was read again. Read again after connecting to keep other snapshots, such as members. A saved session resumed through sharding.sessions is not a new session and keeps them

    The new roles.getAll(guildId) method returns every cached role of a community, including everyone and ordered like roles.fetchAll, or undefined unless the cache knows the list is complete. A guildCreate event makes it complete, as does an uncontested roles.fetchAll read that finishes while the community's shard is connected, because only role events keep the list current. A full read that finishes while the community's shard is disconnected therefore does not make the list complete, while a list that was already complete stays complete through a recovery that resumes the session. Role create and update events keep it complete. Removing any of its roles, a new session for its shard and every successful role write by the bot, including create, edit and reorder, or one whose outcome is unknown, make it unknown until the next guildCreate or full read. The roles cache limit counts the roles of every community together, so a bot in many communities needs a larger maxEntries to keep complete lists

    The new members.getSelf(guildId) method returns the bot's own cached member of a community, or undefined when it is not cached or the bot's account ID is not yet known from READY or a members.fetchSelf result. With cache.members enabled, each guildCreate now stores the bot's own member. The guildCreate events sent while a session starts list member accounts by ID only, so the SDK completes the bot's own member from READY's account and leaves out the others. A guildCreate sent after startup, such as for a community the bot joins or one that becomes available again, lists complete members, and the SDK stores all of them, which are the bot's own and those in voice channels

    The new ShardState.sessionEpoch field identifies a shard's current gateway session. It is 0 until the shard first starts a new session in this client, so a process that resumes a saved session after a restart reports 0 until that shard next starts a new session. It changes, without repeating, whenever the shard starts a new session, including after a move to a larger plan. A Resume keeps it. A changed value means that shard's community events may have been missed, and Fluxer then sends each community again through guildCreate. It is the shard's current value, so an event still waiting in a handler's queue can already see a newer one

    The new EventContext.sessionEpoch field gives the receiving shard's sessionEpoch when the client accepted the event, so an event received before a new session keeps its earlier value even when its handler runs after the new session started. Compare context.sessionEpoch per community, not per shard, because one shard serves many communities. Events delivered through the testing entry points carry the epoch of the test gateway's session in the same way

    The new context field of BotEventContext gives each runBot event handler, in both APIs and in bots run by createTestBot, the same EventContext that a client.on handler receives, so a runBot bot can compare context.sessionEpoch per community as well

  • The commands namespace has two new helpers in both APIs, and the quoted parser and its rejections changed. The new commands.suggest(name, candidates) function returns the candidate closest to a misspelled name, using the rule behind the suggestion of CommandUnknownName: Case is ignored, one edit is allowed for names of up to four characters and two for longer ones, an adjacent swap counts as one edit, and the earliest of equally close candidates wins. It returns undefined when none is close and for an empty name. It agrees with the router for its default case-insensitive matching when given the registered names of one level, while the router also considers aliases and reports the command's own name for a near alias. The new commands.parseArgument(token, descriptor, options?) function converts one word with any built-in argument descriptor except custom, using the router's own grammar. It returns the descriptor's value type, undefined for a rejected word, and the descriptor's default or undefined for a missing one. The new CommandArgumentParseOptions type carries guildId, which member descriptors need. Invalid input to either helper, such as a bad descriptor, a non-string name or candidates that are not an iterable of strings, throws ConfigurationError in both APIs, like other registration misuse

    PrefixCommandParseRejection and the CommandParserRejected outcome of onUnmatched have an optional name, the command name the parser read before it declined, as typed. The commands.parseQuoted parser sets it for an unclosed quote and a trailing backslash, a custom parser may set it, and the router passes it through, so a reply can say which command a syntax mistake was for. A name that is not command-name syntax is reported as a parser failure. Existing parsers and callbacks keep working, because the member is optional.

  • Add defineBot to the default and Effect entry points. It returns a bot's runBot options without the token unchanged, and keeps the types of command arguments and, in the Effect API, of handler errors and services. A module can then export the options for both runBot and createTestBot

    The starter bot in examples/starter now exports its options as bot with defineBot, answers !ping through the commands option instead of matching the message text, and starts only when Node.js runs the file. It now also answers !ping followed by more text or in other letter cases. The fluxerly init command writes these shipped files instead of its own copies, so the generated bot is the one the documentation shows, and its generated test imports bot. The repository examples, the quick start, the testing guide and the README use the same shape

  • Behavior change: The default maxPages of an iteration now grows with maxItems, so a caller who sets only maxItems gets up to that many items. It is the pages maxItems needs at the page size plus 100 spare pages for short pages and skipped duplicates, instead of a fixed 100. For example, messages.iterateHistory with maxItems: 10000 and the default page size of 50 used to fail with PaginationError reason pageLimit after 5,000 items and now reads all 10,000. This applies to iterateHistory, members.iterate, members.iterateSearch, guilds.iterate, auditLogs.iterate, iterateReactionUsers, iteratePins, threads.iterateMembers and messages.iterateSearch, whose pages stay capped at Fluxer's 400-page ceiling. The default stays finite, so a server that keeps returning short pages still ends with pageLimit. An explicit maxPages is used as given, including a value above or below the new default, except that messages.iterateSearch never reads more than 400 pages. Code that relied on the old default to stop a long scan early can set maxPages: 100

    Behavior change: The directMessages.open and directMessages.send methods no longer clear every cached private channel when they start. A successful opening replaces only the cached snapshot of the channel Fluxer returns, because opening a conversation changes no other channel. The overlap rules change in detail. A read that started before the opening is still not stored. A read of another private channel that overlaps the opening can now stay cached, because the opening no longer clears the cache when it finishes. A conflict involving the returned channel evicts that channel, and a stale successful opening evicts only its own channel. A failed opening that was started still clears the direct-message cache, because its outcome may be unknown and no channel is known to evict. Invalid input, such as open("bad"), fails before the cache is touched and leaves it intact. Code that relied on the full clear can call client.cache.clear("directMessages") after the write

    The PaginationError hint for reason pageLimit now tells callers to set maxPages explicitly or lower maxItems, and to narrow the filters of a message search, which reads at most 400 pages. It no longer suggests a larger pageSize, because the default maxPages shrinks as pageSize grows

  • Three additions to the shared errors object and the text helper, plus two fixes.

    The new errors.outcome(error) function returns whether a failed request may have reached Fluxer, as "notDispatched", "rejected" or "unknown". It reads the outcome of any SDK error that has one and, for an Effect Cause, the least certain outcome of its failures. It returns undefined when the SDK cannot establish an outcome, such as for CancelledError, SdkDefect, non-SDK values and a Cause with a defect, an interruption or a failure without an outcome, so the caller applies its own policy. Only "notDispatched" shows that a write was not sent, so treat every other value, including undefined, as a write that may have applied and reconcile before sending it again. A Cause covers only the failures it kept, and Effect's concurrent forEach and all drop the reasons of siblings they interrupt, so for several concurrent writes check each item's own result, for example with { mode: "result" }. Wrapping an SDK call in Effect.timeout yields only a TimeoutError, so use the operation's timeoutMs option to keep the outcome.

    The new errors.isNotFound(error, resource?) function is true for an SDK error with HTTP status 404, including a 404 that messages.send reports with reason rejected. Pass a NotFoundResource such as "member" or "channel" to also require Fluxer's matching UNKNOWN_* code. A 404 says only that the resource is missing, not why: Fluxer answers UNKNOWN_CHANNEL for a missing channel and also for message and reaction requests in a thread from a bot that is no longer in the community, while other requests from such a bot get a 403 missingPermissions. A 404 without a recognized code is true without a resource and false with one. Any other resource name throws HelperError with operation errors.isNotFound and the new reason resource. The NotFoundResource type is exported from both entry points.

    The new text.truncate(content, { maxLength, suffix? }) function shortens text to at most maxLength UTF-16 units and ends it with suffix (default "…") when anything was cut. It never splits a surrogate pair and does not trim or look for word boundaries. Invalid text or options throw HelperError with operation text.truncate. The TextTruncateOptions type is exported from both entry points.

    ClientClosedError gains outcome, an addition. A REST call ended by a closing client now reports how far it got: notDispatched when the client was already closed or the request was still queued, rejected when closure came while waiting out a 429 before a retry, and unknown when the request was in flight. The outcome is also recorded in details.outcome, and an unknown outcome adds a hint to check whether Fluxer applied the change before repeating it with a new client. Closures that are not a REST request, such as gateway commands, collectors, waits, counts, member chunks, downloads and connecting, keep outcome as undefined. The preparation steps of a message send, namely opening a direct message and uploading attachments, count as not sent for the message, so a closure during them reports notDispatched for the message request even if the earlier step took effect. A closure that cuts off a read records details.read and gets no hint to check for an applied change. The POST reads messages.search, members.search and attachments.refreshUrls count as reads in a closure and in their other failures, so their network and timeout failures are now retryable by errors.isRetryable. An OAuth request that closure cuts off in flight still fails with OAuthError reason network and outcome unknown, and a closure during OAuth instance discovery now reports notDispatched. A message cleanup closed while a batch waits out a rate limit now reports that batch's rejected outcome instead of unknown, and a closure between batches already reported notDispatched. The constructor takes an optional { outcome, read } object, so existing new ClientClosedError() calls are unchanged.

    Partitioned inviteCreate handlers now order events per community. They previously ignored the invite's nested guild.id and channel.id, so every invite shared one key and ran one at a time. With partition: "guild", invites of different communities run side by side while one community's invites stay ordered, and a group invite without a community is keyed by its channel.

    The embed input documentation now states Fluxer's limit of 6,000 characters of combined embed text per message. The SDK does not check it locally, and exceeding it fails remotely with reason rejected and an invalidFormBody API error.

  • Add messages.fetchHasReaction to both APIs. It answers whether one user currently has a given reaction on a message, with one request and no cache, so bots with reaction roles, verification or voting can recheck a reaction after being offline. A deleted or unavailable message fails instead of answering false

  • Fields that Fluxer documents on gateway events and the SDK used to drop now reach handlers, in both APIs

    A messageCreate event carries mentionedHere, channelType and nicknames, and the messageFields option accepts them. They are set only on a created message, so messageUpdate events, REST reads and the message cache never carry them, and a cached message never mixes them with a later update. Fluxer sends the here mention only as true. The channel type is Fluxer's number, kept as received, with 1 for a direct message and 3 for a group direct message. The nicknames of a group direct message are a frozen object keyed by user ID, like DirectMessageChannel.nicknames. A value of the wrong type rejects the created message, like mentionedEveryone

    A messageDelete event carries member, a new frozen DeletedMessageMember that is a GuildMember without username and isBot, because Fluxer removes the member's user. It is left off when Fluxer sends none, when the deletion names no community or author, and when it cannot be read, which never costs the event and logs nothing because no cache is involved. The member cache does not store it

    Voice states from voiceStateUpdate, voiceStateSnapshot and the call events carry isE2eeCapable and isPeerToPeer, which read false when omitted, and version, which is absent when Fluxer sends none. Behavior change: A voice state whose e2ee_capable or p2p is not a boolean, or whose version is not a non-negative integer, is now rejected like the other malformed voice state fields

    A guildCreate event carries memberCount, onlineCount and joinedAt as Fluxer reported them when it delivered the event. They are optional because the SDK keeps accepting a body without them, and a value of the wrong type rejects the event. The guild cache does not store them, so guilds.fetchCounts remains the way to read current totals

    The testing entry points' WireMessage accepts optional channel_type, nicks and mention_here, so fixtures.message({ channel_type: 3 }) emits a created message in full

  • GuildFeatureToggles.VisitsEnabled lets guilds.edit keep community visits on. Fluxer now enables VISITS_ENABLED for new communities and removes every toggleable feature that a featureToggles list leaves out, so an edit without it turns visits off and ends current visits. Before this, the SDK rejected VISITS_ENABLED locally, so every featureToggles edit turned visits off

  • ResolvedInstance has a new discoveryEnabled flag, in both APIs, that reports whether the instance has community directory discovery switched on. Fluxer documents a missing flag as enabled, so it reads true when the instance omits it. While it is false, the discovery namespace's search, apply, edit and withdraw fail with reason rejected, HTTP 400 and details.providerCode DISCOVERY_DISABLED, while fetchStatus and fetchCategories still answer, and fetchStatus then reports eligible as false. The value is read once when the client resolves the instance. Behavior change: A discovery document whose features.discovery_enabled is present but not a boolean is now rejected as a protocol failure, like a malformed presigned_attachment_uploads

  • Behavior change: messages.keepTyping now returns the task's own result when a later typing refresh fails. Before, the default API returned Err with the typing failure even when the task succeeded, which lost the task's value, and the Effect API combined the refresh failure with the task's Cause. Now a refresh failure stops the refreshes without interrupting the task, and it logs one Warn record with the new code rest.typingRefreshStopped, whose fields.channelId names the channel and whose error holds the typing failure. A task that succeeds returns its value, and a task that fails returns only its own failure. A refresh stopped by client closure is not logged. The first typing request still has to succeed before the task starts, and its failure still fails the call. A defect in the refresh work still surfaces as SdkDefect in the default API and in the Cause in the Effect API

    Code that relied on the Err or the combined Cause to learn about a failed refresh should read the rest.typingRefreshStopped record from the logging sink instead.

    Before, client closure during the task also returned ClientClosedError (Err in the default API, Fail in the Effect API), and now the task's own result is returned. Code that detected shutdown that way should watch the client's state or observeState instead

  • Both Client interfaces have a new read-only lastReceipt property: The receipt of the most recently accepted event, as EventContext.receipt numbers events, or 0 before the first. It is read live and counts every accepted event, whether or not a handler receives it. Work that is not an event, such as a timer or a command's follow-up, can record it when it starts and treat any event with a larger receipt as newer, for example to drop a result that a later edit made stale. Under a supervisor each child process numbers its own events, so values from different processes cannot be compared

  • The member that Fluxer attaches to community events now reaches handlers. The member field of messageCreate and messageUpdate messages, of messageReactionAdd and messageReactionRemove reactions, and of typingStart notices carries the author's or actor's GuildMember, read from the community's current state when Fluxer sends the event. A message's member is completed with the author's username and bot flag. The field is absent on REST reads, direct messages, webhook messages, users who left the community, messageDelete events (Fluxer sends no username there) and batch or clear events. The messageFields option now accepts member, and the message cache keeps messages without it, so read it in the handler

    A member that Fluxer sends in a form the SDK cannot read, including a reaction or typing member without the user's name, is left off the event and the event still arrives. This differs from voice states, which reject the whole voice state

    Behavior change: With cache.members enabled, these events also store their member in the member cache, whichever messageFields the client selects, so members.get answers for an author without a REST read. A member that cannot be read removes the account's cached member and logs gateway.memberSkipped, because the cached copy may now be stale. An event rejected as malformed that carried a member removes the account's cached member too, or the community's cached members when the event does not identify the account, or every cached member when it does not identify the community either. The exception is that a reaction or typing member without the user's name is completed from the member already cached. The cache still requests no extra gateway dispatches, and a voice state's member is still never stored because it can lag behind role edits

    The testing entry points' WireMessage accepts an optional member without its user and guild ID, so fixtures.message({ member }) emits a decorated message in tests

  • client.on("messageCreate", ...) handlers now run up to eight messages at a time by default in both the default and Effect APIs, matching runBot and the command router. Before, a createClient bot ran them one at a time, so one slow message handler stalled every later message, while a runBot bot did not. Handlers of other events still run one at a time unless partition is set, which also allows eight. Messages can now finish out of order, so a handler that must see them in order passes { concurrency: 1 } for strict order or { partition: "channel" } to keep each channel in order

  • Both entry points gain helpers for Fluxer's permission overwrite rules, one call for a member's permissions in every channel, error helpers for retry waits, and an arrival number on every event

    The new permissions.fetchChannels(target, options?) method returns one member's permission bits in every channel of a community. Pass the bot's own ID from client.users.getSelf() as userId to see what the bot can do in each channel. It always reads fresh: The guild, the member, the roles and the channel list, once each and under one deadline. It then calculates each channel locally, the same way as permissions.calculate and permissions.fetch. The result is a frozen array of frozen ChannelPermissions entries ({ channel, permissions }), in the order Fluxer lists the channels

    Fluxer lists only the channels the bot can view, and no threads, so call permissions.fetch with a thread's ID for one. The options are the plain RequestOptions, without preferCache, because no cache holds a complete channel list. Failures use GuildOperationError with the new operation permissions.fetchChannels, and errors from the reads keep their own type. A listed channel that arrives without its permission overwrites fails as it does in permissions.fetch, with reason response, outcome "unknown", details.read and details.channelId

    The new permissionBits.refusedGrants(current, next, held) helper returns, as a bigint, the bits Fluxer would refuse in an overwrite change. Unless the bot has Administrator, it can allow, or stop denying, only permissions it has in the channel, and Fluxer answers missingPermissions for the rest. Pass the overwrite's current allow and deny as current and the new ones as next, with undefined for no overwrite, so a removal passes undefined as next. Pass the bot's permission bits in that channel as held. The helper returns 0n when held includes Administrator, and invalid input throws HelperError with operation permissionBits.refusedGrants

    The documentation of channels.create, edit, reorder, setPermissionOverwrite and removePermissionOverwrite now states this rule for each method. Their missing-permission hints name the channel that Fluxer checks and point to the helper

    The new permissionBits.sameOverwrites(first, second) helper compares two overwrite lists as Fluxer does: The same target IDs and, for each, the same type, allow and deny, in any order. Fluxer copies a category's overwrite change to every child whose overwrites equal the category's, with no way to opt out, so compare the children with the category before changing it. A list that is not valid, or that names a target ID twice, throws HelperError with operation permissionBits.sameOverwrites, because Fluxer rejects duplicate IDs on write

    The new errors.retryAfterMs(error) function returns how many milliseconds an SDK error says to wait before a retry, read from the retryAfterMs of any operation error or RateLimitError. For a ShardConnectionError it returns the wait of the failure that stopped its shard, and for an Effect Cause the longest wait among its failures. It returns undefined for an error without a usable wait and for any other value, and never throws

    The errors.isRetryable(error) function now also accepts an Effect Cause, as errors.outcome does. It is true only when the Cause has at least one reason and every reason is a retryable failure, so a defect, an interruption or an empty Cause gives false

    EventContext has a new receipt field that tells which event arrived first. The client numbers each event when it accepts it, starting at 1 and counting across all of its shards in the order events arrive. Every handler of an event sees the same number, and events decoded from one frame, such as guildCreate and its voiceStateSnapshot, get consecutive numbers in dispatch order. Handlers of one community can run out of order because of concurrency, partitions or partitionReady, so compare receipts to tell which event came first. Under a supervisor each child process numbers its own events

    A validation error with Fluxer's code INVALID_URL_FORMAT now has an SDK explanation in its details. Fluxer sends this code for a URL field it does not accept, such as text that is not an http or https URL, a URL with a user name or password, or a host without a top-level domain

    AuditLogActions.MemberRoleUpdate now documents the shape Fluxer records: A change with the key roles whose oldValue and newValue are the member's full role ID lists, so the added roles are those in newValue that oldValue lacks. A MemberUpdate entry that changes roles carries the same change

    TestDisconnectOptions has a new endSession option, default false, in both testing entry points. With endSession: true, the test gateway forgets the shard's session before disconnect closes the connection, so the client's Resume gets Invalid Session and the client identifies a new session. As after a real session loss, the shard's sessionEpoch increases, the next event's context shows the higher value, and the cache release and new-session handling run. Use it with a recoverable close code, because a fatal code still ends the client. A value that is not a boolean throws ConfigurationError, and the native disconnect dies with it

    Without endSession, a disconnect resumes the same session after every recoverable close code, including 4007 (invalid sequence), because the SDK starts a new session after that code only when it closes a Resume attempt. TestDisconnectOptions.code now says this

  • Partitioned event handlers can now wait for a partition's own setup without holding a handler slot, and a full partitioned queue no longer lets a longer backlog take room from a shorter one. The partitionReady option of client.on and of a runBot event handler (EventHandlerOptions, BotEventOptions) takes the partition key, and in the default API a signal that aborts when the subscription stops, and returns a promise. In the Effect API it takes the key and returns an Effect that runs with the services available at registration. Before the first waiting event of a key starts, the SDK calls the hook, and the key's events stay in the queue until it settles while other keys keep starting. A hook that returns a plain value marks the key ready at once. Readiness is kept only while the key has waiting or running events, so the next burst after the key went idle calls the hook again, and hooks for different keys run at the same time, so the hook should be a cheap, idempotent check. A hook that throws, rejects or fails reports every waiting event of that key as a failure of its handler, and a later event calls the hook again. Held events count toward maxPendingMessages, maxPendingBytes and a draining shutdown. The option needs partition and otherwise fails with ConfigurationError whose field is "partitionReady", a new member of the ConfigurationError.field union

    Dropped events are also counted per event name. ClientCounters has the new eventsDroppedByEvent member, which counts overflow, closed and collector drops and malformed dispatches whose type names exactly one event, and the eventsDropped reason totals are unchanged. Drops that have no known event name appear only in the reason totals, so the per-event sum can be lower. The metric fluxerly_events_dropped_total gains an event attribute where the event name is known, next to reason

    Behaviour changes to know about:

    • A partitioned handler with overflow: "dropOldest", which is the default for client.on and runBot, now drops from the longest backlog instead of the oldest waiting event overall. The backlog of the arriving event's key counts the arriving event. When another key has a longer backlog, that key loses its oldest waiting event, with ties going to the key whose oldest waiting event is oldest. Otherwise the arriving event's own key loses its oldest waiting event, or the arriving event itself when that key has none waiting, and this repeats until the arriving event fits. When the arriving event would be discarded anyway, it is the only event discarded. Under maxPendingBytes the choice follows the number of waiting events, not their sizes. The Warn record, counters and policy field are the same, but a handler that relied on the globally oldest event being dropped, or on every partition losing events in arrival order, sees different events dropped. Partitioned commands delivery follows the same rule. Handlers without a partition and the policies dropNewest and stop are unchanged
    • The partition function now runs when an event is received, instead of when the handler first considers it, so a slow partition function runs on the gateway callback. A throwing function still reports that event's handler as failed
  • Command routers can now wait for a partition's own setup before its commands run. The partitionReady option works for router.attach and for the commands option of runBot and createTestBot (BotCommandsOptions), in both APIs, with the same rules as the partitionReady option of client.on. Before the first waiting message of a key starts, the SDK calls the hook, and the key's messages stay in the queue without using a command slot until it settles, while other keys keep running. The option needs partition and otherwise fails with ConfigurationError whose field is "partitionReady". A hook that fails reports each waiting message of that key to the onError option of the commands, and those messages run no command

    Behavior change for command routers that have an onError: A failing partitionReady hook, a throwing partition function and an overflow report of the router's subscription now reach that onError, in arrival order with the command failures. Before, a throwing partition function and an overflow report went to the client-level onError or the log, so a bot that replied from commands.onError stayed silent. A router without its own onError is unchanged

    In the Effect API the hook returns an Effect, and its required services are added to the requirements of runBot and createTestBot, which gain a trailing type parameter for them, and BotCommandsOptions gains a fifth one

  • Permission and role helpers now cover cache-first reads, role rank, timeouts, partial overwrite changes and readable permission names, in both entry points. The permissions.fetch method takes preferCache: true (PermissionFetchOptions) to use enabled caches for the guild, member, roles, channel and a thread's parent and fetch only what is missing, while the default stays a fresh read. The hierarchy.highestRole(member, roles) and hierarchy.canManageRole({ guild, actor, role, roles }) helpers (RoleManagementInput) check rank from held snapshots, and members.timeoutState(member, { now }) reports "none", "active" or "unknown" from communicationDisabledUntil. The permissionBits.patchOverwrite(current, { allow, deny, inherit }) helper (OverwritePatch) changes named permissions of an overwrite and keeps every other bit, and permissionBits.label(name) returns Fluxer's English name for a permission

    Behaviour changes to know about:

    • Missing-permission hints and the requirePermissions guard denial now use Fluxer's labels instead of identifiers or split identifiers, such as "Manage community" instead of "Manage Guild" and "ManageGuild", "Use voice activity" instead of "Use Vad" and "Time out members" instead of "Moderate Members". Code or tests that match this text must change. The details.requiredPermissions field still holds Permissions keys
    • The requirePermissions guard now decides through permissions.fetch with preferCache, so it shares one deadline for its reads. The command is denied only when the channel's permissions cannot be confirmed, meaning Fluxer sent a channel without its permission overwrite list, and the reply is "This command cannot confirm this channel's permissions", replacing "This command cannot check permissions in this kind of channel". Code or tests that match this text must change. Every other failure still fails the command
    • A fetched community channel that is not a thread and comes without a permission overwrite list fails permissions.fetch with reason response on both paths, because Fluxer sends the list as an empty list when there are no overwrites. The failure has outcome "unknown" as other unusable responses of a read do, records details.read, details.channelId and details.responseField, and carries a hint. A cached channel without the list is fetched again. With preferCache, a closing client now fails a cache lookup with ClientClosedError outcome "notDispatched", as fresh reads do
    • The channels.removePermissionOverwrite comment now says that Fluxer requires ManageRoles, and a bot without Administrator can remove only an overwrite whose denied permissions it has all of
  • Handlers of an update or delete event now receive the cached value from before the change, as previous on the EventContext that client.on handlers, runBot handlers and event middleware receive in both APIs, so edit and deletion logs can show what changed. The messageUpdate, messageDelete and messageDeleteBulk events read the message cache, guildMemberUpdate and guildMemberRemove the member cache, guildRoleUpdate and guildRoleDelete the role cache, guildChannelUpdate, guildChannelDelete, threadUpdate and threadDelete the channel cache, and guildUpdate and guildDelete the community cache. The snapshot is the frozen value the client's own cache held just before it applied the event, never fetched. A messageDeleteBulk snapshot is a frozen list of the messages the cache held, once each in the order of the event's ids, because Fluxer sends only IDs for a bulk deletion. It is undefined when that cache category is not enabled, the cache never saw the entity, or the entry expired or was removed, and for every other event. A write through an SDK resource method updates the cache too, so the event after it can hold the written value, or undefined where the write removed the entry, instead of the value from before the write. A queued event keeps its snapshot in memory until its handler runs or the event is dropped, and maxPendingBytes does not count it. The test clients deliver it the same way when their cache option is enabled

    The new PreviousSnapshots interface lists the snapshot type per event, and the new PreviousSnapshot<K, M> type gives the type of previous for one event. EventContext now takes optional type parameters for the event and message type, and EventInvocationOf.context, BotEventContext.context and the client.on handler's context are typed for their event. A helper that takes a plain EventContext still accepts any handler's context, including one from a client with messageFields, and a context built by hand, such as in a unit test, can leave previous out

  • The ratelimit.wait and ratelimit.deadline log records and the rateLimit observation now repeat what a 429 response sent about the limiter that answered it. Log records carry these values in fields, and RateLimitObservation has them as optional members: scope, bucket, limit, remaining and resetAfterSeconds from the X-RateLimit-Scope, X-RateLimit-Bucket, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset-After headers, retryAfterHeaderSeconds from a numeric Retry-After header, and retryAfterSeconds and bodyGlobal from the body's retry_after and global members. Each value is present only when the response sent it in the expected form. A ratelimit.deadline record for a request refused before sending, behind a known pause, has none. Waits, retries and the existing global value are unchanged

  • Roles can have icons. GuildRole now has icon, the image's hash, next to unicodeEmoji, and assets.roleIcon builds the image's URL. RoleCreate and RoleEdit take icon, an image data URI, and unicodeEmoji, where null removes the icon and setting one replaces the other. RoleCreate also takes iconSourceRoleId to copy another role's icon. The input fails locally when it sets an image together with an emoji or a role to copy from, and the everyone role accepts neither field

  • Behavior change: text.truncate, text.split and the pages of router.help now cut at a grapheme boundary, as Intl.Segmenter defines it, so a visible character stays whole. Before, they kept the two UTF-16 units of a code point together but could cut inside a character made of several code points, such as a flag, a skin-tone emoji, a joined emoji, a base letter with combining marks, or the pair "\r\n". For example, text.truncate("aaaaaaaaa🇩🇪", { maxLength: 11, suffix: "" }) returned "aaaaaaaaa🇩", half a flag, and now returns "aaaaaaaaa". A piece or truncated result can now be shorter than maxLength and never longer, and the newline and whitespace preferences of text.split only choose positions between characters

    text.truncate keeps whole characters only. When the first character does not fit before the suffix, it returns the suffix alone, so text.truncate("🇩🇪x", { maxLength: 4 }) now returns "…" instead of half a flag, and a letter with more combining marks than fit is dropped entirely. text.split still cuts a single character that is longer than the limit between code points, because it must advance, and it still throws HelperError when the limit cannot hold one code point. Joining the pieces of text.split still reconstructs the input exactly, except that each lone surrogate becomes U+FFFD

    No code change is needed unless a caller relied on a cut inside a character, or on a result of exactly maxLength - suffix.length units of text

  • The text.truncate and text.split helpers now replace each lone surrogate with U+FFFD (�) instead of throwing HelperError. Ordinary code can create one, for example slice can cut an emoji in half, and Fluxer refuses a message that contains one, so a bot's text helper no longer fails on it. The replacement has the same length, so maxLength still holds, and text.split pieces join to content.toWellFormed(). A code path that relied on the error can check content.isWellFormed() first. A suffix that is not well-formed still throws, and so does text that is not a string

Patch Changes

  • The shipped agent guide in agents/AGENTS.md now maps common bot tasks, such as sending messages, moderating members, checking permissions and testing, to the declaration file and members that cover them, instead of pointing agents at the re-export file dist/index.d.ts. It sends agents who need the website to its task index and method-by-task list first, and explains how to tell whether those pages match the installed version. The section fluxerly agents writes names the installed package's manifest as the place to compare versions and mentions the task map

  • API comments now state several limits precisely. By default a client runs four REST or upload requests per local shard, up to 64, and four attachment downloads, and the rest.concurrency and rest.mediaConcurrency options change these numbers. Effect API users, direct messages, webhooks and application calls use the client's rest.defaultTimeoutMs, not a fixed 30 seconds. The users.fetchSelf and directMessages.open comments explain when a result is not cached, and the users.fetch and directMessages.fetch comments state the 256-ID exception

  • A native on, messages.collect or messages.collectReactions registered in an already closed scope closes at once again. With Effect 4.0.6 or newer, the subscription or collector stayed open and client shutdown waited for it forever

  • Contain accidentally returned Promise and thenable rejections in the default API's Effect logger adapter and in structured logging callbacks, without changing SDK operation outcomes or adding asynchronous delivery or flushing guarantees

  • The messages.deleteOwnMessages comments now describe Fluxer's wider deletion. Called with a channel ID, it also deletes the bot's messages in every thread under that channel, active or archived, and for a forum or media channel it deletes the bot's messages in its posts. Called with a thread ID, it still clears only that thread

  • The missing-permission hint for messages.addReaction now says that only a new reaction with a custom emoji from another community needs Use external emoji, matching Fluxer, which no longer checks that permission for the community's own emoji

  • Member avatar and banner inputs and group direct message icons now accept an image data URI with parameters such as ;charset=utf-8, as community icons already did. Before, the SDK rejected them locally although Fluxer accepts them

  • The .env file that fluxerly init creates for the bot token is now readable only by its owner on Linux and macOS, with mode 0600, so other local accounts cannot read the token. An existing .env keeps its content and permissions

  • Request text with a lone surrogate, half of a character such as an emoji left by slice, now fails locally with reason input and a message that explains the fix. Before, Fluxer answered it with an HTTP 503 error page and stored nothing, which looked like an outage. This covers every request the SDK encodes, such as message content, embeds and channel topics. The text.truncate and text.split helpers and toWellFormed() clean such text, and client.rest.request still sends its body as given

  • The members.move comments now explain a refusal Fluxer added. Moving a member into an occupied voice channel whose participants all use peer-to-peer voice leaves the member in its current channel and fails with reason rejected, HTTP 400 and details.providerCode USER_NOT_IN_VOICE, even though the member is in voice. A retry fails the same way while the destination stays in peer-to-peer mode

  • The Effect API supervisor.child.run helper now succeeds when the parent stops the child before its gateway session is ready, as the default API helper already did. It previously failed with ClientClosedError after that normal stop

  • Releases now come in two kinds, Nightly and Stable, and the README's install advice says so. Every Stable ships the same code as a published nightly.

  • A nightly is an automatic build of the latest source, published under the nightly npm tag twice a day when the SDK changed, with a version such as 1000.0.0-nightly.20261019.57, which carries the UTC build date and the run number. Run numbers only grow but can skip values, while the core of a nightly version can drop when a pending major or minor note is removed. A nightly can include breaking changes, so install it with --save-exact, or --exact with Bun.

  • The rest.rejected Warn record logged for a refused permission overwrite write now carries a precise fields.hint. Before, a 403 MISSING_PERMISSIONS on setting or removing a channel's permission overwrite logged the general missing-permission hint. Now the hint is the one that channels.setPermissionOverwrite and channels.removePermissionOverwrite already put on their errors: It names Manage roles, where the channel's permission overrides title it "Manage permissions", the rule that the bot can allow, or stop denying, only permissions it has in the channel (for a removal, that it needs every permission the overwrite denies), and the permissionBits.refusedGrants helper. No code change is needed

  • Fixes for the guild cache, command replies, retry classification and message cleanup. A successful discovery.apply, discovery.edit or guilds.editVanityUrl call no longer stores its reply as a cached guild, so guilds.get and guild enumeration no longer return an application or vanity object after one of these writes. These writes now only remove the cached guild

    The MessageCleanupError from a preview whose filter threw now keeps the thrown value as cause, as documented

    With mentionPrefix, help pages and the usage line of a rejection reply now put one space between the bot mention and the command name, such as <@99> say <text...> instead of <@99>say <text...>. A cooldown reply now gives a wait over 60 seconds in minutes, hours or days, with at most two units rounded up, such as "Try again in 1 hour 1 minute" or "Try again in 1 day 1 hour", instead of a count of seconds

    The errors.isRetryable function now returns false for a ConnectionError with reason network and an HTTP 4xx status, such as a discovery 404, matching the SDK's own retries. A network failure without a status or with a 5xx status stays retryable

    The default API client.observeState now throws ConfigurationError for a listener that is not a function, instead of logging an observer failure on every state change

    The CommandArgumentMetadata comments no longer give user, which registration rejects, as a type example, and say that a custom argument can also use rest

  • A session saved by sharding.sessions is now resumed at most once. Before, a restart that resumed a saved session and then crashed left the old snapshot in the store, so a restart within 60 seconds of the original shutdown could resume it again and receive the same dispatches a second time, including any dispatch that caused the crash. Before resuming, startup now saves a copy of the loaded snapshot with an empty sessionId. A later load of that copy starts a new session and logs lifecycle.sessionSnapshotIgnored at Warn, until the next shutdown saves a resumable session again. Custom stores receive this extra save call at startup. If it fails, the shard logs lifecycle.sessionSaveFailed and starts a new session instead of resuming

    Durations in log messages no longer read "1 min 60 s" or "60.0 s" near a minute boundary, or "1000 ms" just below a second. They now read "2 min 0 s", "1 min 0 s" and "1.0 s"

    A command reply for a rejected duration argument now shows its bounds in the argument's own syntax, such as "from 1s to 1h30m", instead of in milliseconds such as "from 1000 ms to 5400000 ms"

  • An explicit client.shutdown() of a connected client now logs a failed cleanup step, such as a socket close that throws or an event handler or collector callback whose cancellation cleanup fails, at Error in the lifecycle category and counts it in diagnostics().counters.cleanupFailures. Its final record previously said the shutdown completed and the counter stayed at 0

  • When client.shutdown() cancels a running message or reaction collector callback whose cleanup fails, that failure now reaches the shutdown outcome, as an event handler's cleanup failure already did. It previously appeared only in the collector's own result

  • The package's documentation links, the agent guide and the init command's guide link now point to the website at https://fluxerly.neontechspace.com.

  • The comments on GuildBan.moderatorId and AuditLogEntry.userId now state that the ID "0" is the Fluxer system user. Fluxer records a member removal, ban or ban removal made by its staff under that ID, and a staff ban stores it as the moderator. An invite pause that Fluxer places, lifts or lets expire is also recorded under "0" as a GuildUpdate entry. Both IDs already decoded, so no behavior changes

  • With the default childOutput of prefix, a supervisor now reads each child record and prints it in the format of the stream it goes to. SDK records print as readable labeled lines on a pretty stream, and as JSON Lines with a fields.child entry on a json stream. For this, default-API children always log json, as do Effect API children whose logging.format is json. Setting FLUXERLY_LOG_FORMAT or FLUXERLY_LOG_COLOR in the child environment still takes precedence

    Before, starting a bot with a terminal on standard output and standard error redirected, such as node supervisor.js 2> errors.log, gave children readable colored lines. Their Warn and Error records then reached the parent's JSON standard error as supervisor.childOutput text, without their level, code and fields, and without the documented fields.child entry. Those records now keep their shape and gain fields.child, while standard output still shows readable lines

  • The supervisor no longer fails with a SupervisorError of reason spawn when it stops a child while the supervisor keeps running, such as for a reshard or an unconfirmed Identify permission, and a message to that child fails because the child is exiting on its own at that moment. The supervisor waits for that exit, or force-terminates the child at the shutdownTimeoutMs deadline, and then handles the exit as usual, in both APIs

  • The supervisor waitForClose method now returns success after a normal shutdown even when a child's stop request fails because the child is exiting on its own at that moment. It previously returned a SupervisorError of reason spawn. The shutdown completes once the child exits, or at the shutdownTimeoutMs deadline for a child that is still running, in both APIs

  • The test kit now normalizes message content as Fluxer does. Fluxer removes form feed (U+000C) and U+202E from sent message text and trims surrounding whitespace before it stores and delivers the message, so a user who types !ping reaches the bot as !ping. Before, say delivered the content unchanged, so a test could ignore input that a live bot answers, or the other way round. Now say delivers the normalized content, and the automatic reply to an unregistered message send or edit echoes the normalized content too. A test that expected a bot to ignore content with surrounding whitespace through say now sees the bot answer it, as it would on Fluxer. Delivering fixtures.message through emit still passes content unchanged