You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
External npm components may require asynchronous module initialization before they are safe to render or before non-React APIs on the same module can be used. mendix-ironcalc-spreadui currently owns this lifecycle in project-local JavaScript:
a module-level cached initialization Promise implements one-flight behavior;
import("@ironcalc/workbook") dynamically loads the module;
a module-level ironCalcModule stores the initialized namespace;
loaders/importers await the cached Promise and translate failures to callbacks.
Glendix already owns configured npm bindings, but Promise orchestration should use the Gleam ecosystem rather than becoming another handwritten JavaScript FFI.
Ecosystem-first FFI assessment (2026-09-07)
Current JavaScript responsibility
Candidate Gleam package/API
Assessment
Promise creation/chaining and one-flight result sharing
Use directly. Glendix already depends on gleam_javascript; do not duplicate Promise operations. Coordinate with #12.
Consume an initializer from a React component with Suspense
redraw.use_promise
Optional React-specific consumer API. It is not the default for the existing Lustre TEA flow, which should dispatch completion through an effect.
Store explicit initialization state
pure Gleam Uninitialized / Initializing(Promise) / Ready / Failed state owned by the runtime/model
Preferred over hidden JavaScript globals where lifecycle ownership can be explicit.
Dynamic import
plinth/browser/window.import_
Package-backed but returns Dynamic and changes the current static-import/Rollup contract. It may be useful for an explicitly dynamic mode, but is not the default solution here.
Decode dynamic module data/errors
gleam/dynamic/decode
Suitable for data/error decoding, but it does not by itself provide a typed call to an arbitrary configured function export.
Statically import and call a config-selected npm export
generated Glendix binding or generated direct @external declaration
Residual boundary. Package APIs cannot know a package/export name supplied later in gleam.toml. Keep this generated and minimal; do not handwrite per-widget .mjs.
Revised proposal
Design the feature as a package-backed lifecycle around the smallest possible generated binding boundary:
Extend binding configuration to identify an initialization export and whether initialization is one-flight/retryable.
Preserve static imports for deterministic Rollup/WASM behavior.
Have the generated binding expose only the typed initializer/module handle needed by Gleam; investigate generated direct @external(javascript, package, export) declarations before adding runtime reflection.
Implement Promise composition, result mapping, and lifecycle state in Gleam using gleam/javascript/promise.
Use Lustre effects for TEA completion; expose redraw.use_promise only as an optional React/Suspense integration.
Existing component-only bindings must continue to generate static imports and require no initialization config.
Concurrent consumers must share one in-flight initialization attempt.
Specify retry semantics: either cache failures until explicitly reset, or clear the failed attempt for a later retry; do not leave this implicit.
Preserve module lookup, export lookup, initialization rejection, and WASM-load failures as distinct typed errors where possible.
Avoid plinth/browser/window.import_ in the default path because it changes bundle determinism and returns an untyped module namespace.
Non-goals
Reimplementing JavaScript Promise combinators in Glendix.
Using runtime reflection when generated static declarations can preserve types and bundler behavior.
Hard-coding IronCalc package names or initialization semantics.
Treating React Suspense as mandatory for Lustre consumers.
Acceptance criteria
An implementation design records the exact residual generated FFI, and why no package can replace the config-selected static import/export boundary.
Promise orchestration uses gleam/javascript/promise; no new handwritten Promise adapter is added.
Tests use a synthetic configured module and cover success, failure, concurrent one-flight calls, the selected retry policy, and reuse by both rendering and non-React API consumers.
Existing component-only bindings continue to work without initialization configuration.
Generated build output still uses deterministic static imports and resolves WebAssembly assets correctly.
Dynamic-import mode, if offered, is explicit and tested separately from the default static mode.
Documentation explains the package-backed and retained/generated parts of the boundary.
Verification
Local implementation loop: ./scripts/verify.sh inner glendix
Context
External npm components may require asynchronous module initialization before they are safe to render or before non-React APIs on the same module can be used.
mendix-ironcalc-spreaduicurrently owns this lifecycle in project-local JavaScript:initializationPromise implements one-flight behavior;import("@ironcalc/workbook")dynamically loads the module;module.init()performs async WebAssembly initialization;ironCalcModulestores the initialized namespace;Glendix already owns configured npm bindings, but Promise orchestration should use the Gleam ecosystem rather than becoming another handwritten JavaScript FFI.
Ecosystem-first FFI assessment (2026-09-07)
gleam/javascript/promise(resolve,await,map,tap,rescue)gleam_javascript; do not duplicate Promise operations. Coordinate with #12.redraw.use_promiseUninitialized / Initializing(Promise) / Ready / Failedstate owned by the runtime/modelplinth/browser/window.import_Dynamicand changes the current static-import/Rollup contract. It may be useful for an explicitly dynamic mode, but is not the default solution here.gleam/dynamic/decode@externaldeclarationgleam.toml. Keep this generated and minimal; do not handwrite per-widget.mjs.Revised proposal
Design the feature as a package-backed lifecycle around the smallest possible generated binding boundary:
@external(javascript, package, export)declarations before adding runtime reflection.gleam/javascript/promise.redraw.use_promiseonly as an optional React/Suspense integration.Compatibility and public API impact
plinth/browser/window.import_in the default path because it changes bundle determinism and returns an untyped module namespace.Non-goals
Acceptance criteria
gleam/javascript/promise; no new handwritten Promise adapter is added.Verification
./scripts/verify.sh inner glendix./scripts/verify.sh shared glendix./scripts/verify.sh final