Repository navigation
Add core TypeScript submodules: cron, retry, rate-limit, and crypto - #5903
bradleyshep wants to merge 40 commits into
Conversation
clockwork-tien
left a comment
There was a problem hiding this comment.
There are places where we are using snake_case instead of camelCase, worth checking thoroughly to ensure consistency. I have also left additional comments inline.
clockwork-tien
left a comment
There was a problem hiding this comment.
I noticed a lot of the public surface is only public because the settings are passed in on every call instead of being encapsulated by the library. I would be interested to hear if there is reasoning behind this.
The example writes its own wrapper in consumer code to call the rate limiter which should be handled internally by the library. Here buildRateLimitKey is exported only so a caller can build the key, and scope is passed in twice. With the current design the caller has to build the argument, so the builder has to be exported.
function consumeAction(
tx: Tx,
scope: string,
actorKey: string,
limit: number,
windowSeconds: number,
cost = 1,
) {
return rateLimit.consumeRateLimit(tx.as.rateLimit, {
key: rateLimit.buildRateLimitKey(scope, actorKey),
scope,
limit,
windowSeconds,
cost,
});
}
const result = consumeAction(tx, TAP_SCOPE, key, tapLimit, TAP_WINDOW_SECONDS);Possible improvements, one way is a client(config) per package:
const tapLimiter = rateLimit.client({ scope: TAP_SCOPE, windowSeconds: TAP_WINDOW_SECONDS });
const result = tapLimiter.consume(tx.as.rateLimit, { key, limit: tapLimit });A few more points:
- Worth exporting the error codes, seems there are quite a few of them, e.g.
rate_limit.not_authorized, since renaming one breaks callers silently - Worth having a setup function per package with consistent naming and signature,
client(config)when the module loads andinstall(ctx)ininit(resend.installrather thanresend.installResend), instead of
spacetimeCron(sdk) + createCron(jobs, opts) // needs both
createRetrySubmodule(deps, handlers, auth?)
installRateLimit(ctx)
installRateLimitState(ctx, opts?)
installResend(ctx)
installRetry(ctx) Configure fixed policies once, expose stable errors, keep installers idempotent, and support admin revocation. Remove internal sweep exports and align init naming. Verify packed consumers with the default TypeScript template.
Drop the install options, the standalone install and sweep exports, and helpers exported only for tests. runSweep and resetBuckets share one maxRows validation.
client({ handlers }) imports the SDK directly and returns the tables,
install, submit, views, and reducers. Contexts are typed against the
retry tables instead of unknown, and thrown codes come from an exported
errors object.
retryHandler no longer defines a property on the caller's type builder,
so one builder can back several handlers. The custom auth policy is
replaced by submit(), which host reducers call after their own checks.
History is written once per attempt with its final status, pruned by
removing the oldest row through the ranAt index, and served newest first
without sorting. The unused taskName and status indexes are removed,
admin rows store addedAt as a timestamp, and the task view returns the
newest tasks.
verifyStripeSignature, verifySvixSignature, and verifyGithubSignature
return { ok: true } or { ok: false, reason } with a code from the
exported errors object, so callers can tell a missing signature, a bad
or stale timestamp, an invalid Svix secret, and a mismatch apart.
Also remove the unused SHA256_INTERNAL_BLOCK_SIZE constant, make
timingSafeEqual compile under noUncheckedIndexedAccess and enable that
check, and drop allowImportingTsExtensions now that tests import
without extensions.
Limiters are configured once per scope and expose their policy and a peek read. Remove per-call limit overrides, the admin consume procedure, and the raw consume and key exports. Export isAdmin and requireAdmin for host operations. The example uses one limiter per upgrade tier, reads status through peek, and uses reactor.* codes for its own errors. Regenerate example bindings.
cron imports the SDK directly instead of receiving it as a parameter,
so tables and types come from the real SDK and the dynamic SDK casts
are gone. client({ jobs, ...options }) replaces spacetimeCron(sdk) and
createCron(jobs, opts) and returns tables, schedule, unschedule,
reconcileReducer, and publicViews. cronTable() stays the job
declaration.
cronReducer and cronProcedure infer the handler context from the host
schema, and registration fails to compile when the schema lacks
cron.tables. cron.tables is typed per job, so host code can read fire
tables without casts. Thrown codes come from an exported errors object.
Also set the unpublished package version to 0.1.0, describe Failed runs
accurately, drop decorative separators from the modules, and remove
allowImportingTsExtensions.
client({ tasks }) declares each task's argument type before schema().
retry.retryReducer(spacetimedb, handlers) registers the scheduled
reducer afterward, so every handler receives the host's reducer context
and its task's arguments with their real types, and the host schema must
include retry.tables. retryHandler is removed along with the fixture's
context cast.
Libraries that create tables, schemas, and enum builders with this package could not emit .d.ts files because the inferred types named modules outside the package exports (TS2742) or an unexported class with private members (TS4094). The types are exported by name only; runtime exports are unchanged.
cron, crypto, rate-limit, and retry build ESM JavaScript and .d.ts files into dist with tsc (tsconfig.build.json). pnpm pack and publish apply publishConfig, which points main, types, and exports at dist and keeps the subpath names; files lists dist, LICENSE, and README.md. prepack builds before copying the license. Inside the workspace the manifests still point at src, so examples, fixture modules, and tests do not need a build first. Relative imports in src carry .js extensions so the emitted modules resolve in Node as well as in bundlers. rate-limit drops allowImportingTsExtensions like the other packages.
|
Changes since review
|
Scheduled procedures run in a pool of instances, so later occurrences can execute while the recovery probe blocks and before the host is killed. The fireCount bound counted those runs as replays. Assert on runs scheduled before the restart and completed after the stop, and use a two-second schedule so the pre-kill checks finish before the next occurrence.
An index scan in a transaction yields rows inserted by that transaction first, so pruning after the insert deleted the new attempt instead of the oldest one once history reached 1,000 rows.
# Description of Changes Adds a Resend submodule for sending transactional email and tracking delivery state in SpacetimeDB. - Synchronous procedures for outbound Resend requests and email operations. - Webhook signature verification, payload validation, idempotent ingestion, and replay. - Private delivery state and administrative configuration. - Package documentation, unit tests, and a dispatch example with generated client bindings. Builds on `bradley/submodules-core`. The package uses crypto; the example also uses rate-limit and shared example support. ## Example screenshots Existing example screenshots from #5823: ### Resend  # API and ABI breaking changes No existing SpacetimeDB API or ABI is changed. This adds new TypeScript package APIs that need review before merge. # Rollback safety impact n/a. This adds opt-in TypeScript packages and examples; it does not change existing server storage formats. # Expected complexity level and risk **2/5** An isolated, opt-in Resend integration that does not change existing server behavior. For applications that use it, review delivery idempotency, webhook verification, and access to provider credentials. # Testing Verified locally on `bradley/submodules-email`: - [x] Frozen-lockfile install for the root, SDK, and present submodule workspaces. - [x] Type checks for the added packages. - [x] Existing test scripts for the added packages and examples. - [x] Build scripts for the added packages and examples. - [x] Regenerate client bindings and verify no tracked changes. - [x] Lint and formatting checks for the added packages and examples. - [x] Verify workspace dependencies are present in this branch. - [ ] Review the public email API, authorization, and webhook handling. - [ ] Run the Resend smoke test with credentials and a local SpacetimeDB instance. Commands used for this group: ```sh pnpm -r -F "./spacetime-resend-ts/**" run typecheck pnpm -r -F "./spacetime-resend-ts/**" run test pnpm -r -F "./spacetime-resend-ts/**" run build pnpm -r -F "./spacetime-resend-ts/**" run lint ``` The TypeScript SDK was built first. Live deployment and provider tests were not run during split validation. Existing workspace peer-dependency warnings remain.
cloutiertyler
left a comment
There was a problem hiding this comment.
Except for the one comment, this LGTM
Description of Changes
Adds reusable scheduling, retry, rate-limit, and cryptographic helpers for SpacetimeDB TypeScript modules.
This is the prerequisite branch for the remaining submodule groups. It is based on master commit
3653d2ed4.Example screenshots
Existing example screenshots from #5823:
Cron
Rate Limit
API and ABI breaking changes
No existing SpacetimeDB API or ABI is changed. This adds new TypeScript package APIs that need review before merge.
Rollback safety impact
n/a. This adds opt-in TypeScript packages and examples; it does not change existing server storage formats.
Expected complexity level and risk
3/5
The packages are opt-in and do not change existing server behavior. Complexity is in scheduling, retry recovery, and cron's direct use of the internal
spacetime:sys@2.0host ABI. Review failure recovery and repeated execution for applications that use these packages.Testing
Verified locally on
bradley/submodules-core:Commands used for this group:
The TypeScript SDK was built first. Live deployment and provider tests were not run during split validation. Existing workspace peer-dependency warnings remain.