Skip to content

Support async rules in eager enforce(value) chains with awaitable chains and type updates#1261

Open
ealush wants to merge 1 commit intolatestfrom
codex/implement-async-rule-support-in-enforce-library
Open

Support async rules in eager enforce(value) chains with awaitable chains and type updates#1261
ealush wants to merge 1 commit intolatestfrom
codex/implement-async-rule-support-in-enforce-library

Conversation

@ealush
Copy link
Copy Markdown
Owner

@ealush ealush commented Mar 15, 2026

Motivation

  • Enable using Promise-returning custom rules with the eager enforce(value) API while preserving existing fail-fast sync behavior.
  • Allow awaiting enforce chains (e.g. await enforce(x).rule()) so mixed sync/async validation flows are ergonomic.
  • Propagate async-ness through types so TS signatures reflect when a chain becomes awaitable.

Description

  • Implemented internal pendingPromise tracking in eager.ts and exposed reserved then/catch handlers so enforce(value) becomes awaitable when an async rule is used.
  • Updated ruleCallGenerator.ts to detect Promise results (via isPromise), queue subsequent rule execution behind pendingPromise, and unify sync/async result processing via processRuleResult.
  • Extended type system: added AsyncMode generics and ThenableEnforce in eager/eagerTypes.ts, and updated n4sTypes.ts so custom matchers may return Promise and the eager return type reflects async transitions.
  • Added new and updated tests to cover async behavior and typing changes: extendAsync.test.ts, asyncEnforce.types.test.ts, rules/__tests__/asyncEnforce.test.ts, and adjustments to extend.test.ts; also added plans/async_enforcements_summary.md documenting the approach.

Testing

  • Ran the vitest suite including the new tests under packages/n4s/src/__tests__ and packages/n4s/src/rules/__tests__. All tests passed.
  • New tests specifically verify awaiting chains (asyncEnforce.types.test.ts), async custom rules execution and rejection (extendAsync.test.ts and rules/__tests__/asyncEnforce.test.ts), and updated behavior in extend.test.ts.
  • No automated test failures were observed after the changes.

Codex Task

Summary by CodeRabbit

Release Notes

  • New Features

    • Added asynchronous matcher support to enforce chains
    • Custom async rules can now be registered via enforce.extend
    • Enforce chains are now awaitable for async validation workflows
  • Tests

    • Added comprehensive test coverage for async enforcement scenarios
  • Documentation

    • Added technical documentation for async enforcement architecture

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vest Ready Ready Preview, Comment Mar 15, 2026 2:22pm
vest-next Ready Ready Preview, Comment Mar 15, 2026 2:22pm

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Support async rules in eager enforce chains with promise queuing and type updates

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add async rule support to eager enforce(value) chains with Promise handling
• Implement internal pendingPromise tracking to queue rules after async execution
• Extend type system with AsyncMode generics to reflect async transitions
• Preserve fail-fast sync behavior while enabling awaitable chains
• Add comprehensive tests for async custom rules and type safety
Diagram
flowchart LR
  A["enforce(value)"] --> B["Sync Rule Execution"]
  B --> C{Rule Returns Promise?}
  C -->|No| D["Continue Sync Chain"]
  C -->|Yes| E["Create pendingPromise"]
  E --> F["Queue Subsequent Rules"]
  F --> G["Return Awaitable Chain"]
  D --> H["Return Proxy"]
  G --> I["Implement then/catch"]
Loading

Grey Divider

File Changes

1. packages/n4s/src/eager.ts ✨ Enhancement +35/-2

Add pendingPromise tracking and reserved property handlers

packages/n4s/src/eager.ts


2. packages/n4s/src/eager/eagerTypes.ts ✨ Enhancement +45/-21

Add AsyncMode generic parameter for type propagation

packages/n4s/src/eager/eagerTypes.ts


3. packages/n4s/src/eager/ruleCallGenerator.ts ✨ Enhancement +68/-14

Implement promise detection and rule queuing logic

packages/n4s/src/eager/ruleCallGenerator.ts


View more (6)
4. packages/n4s/src/n4sTypes.ts ✨ Enhancement +14/-3

Allow Promise returns in custom matchers with async tracking

packages/n4s/src/n4sTypes.ts


5. packages/n4s/src/__tests__/asyncEnforce.types.test.ts 🧪 Tests +31/-0

Test async enforce type safety and awaitable chains

packages/n4s/src/tests/asyncEnforce.types.test.ts


6. packages/n4s/src/__tests__/extendAsync.test.ts 🧪 Tests +18/-0

Test async custom rule registration and execution

packages/n4s/src/tests/extendAsync.test.ts


7. packages/n4s/src/__tests__/extend.test.ts 🧪 Tests +2/-4

Update test to verify promise-returning rules work

packages/n4s/src/tests/extend.test.ts


8. packages/n4s/src/rules/__tests__/asyncEnforce.test.ts 🧪 Tests +49/-0

Test async enforcement with fail-fast and queuing behavior

packages/n4s/src/rules/tests/asyncEnforce.test.ts


9. plans/async_enforcements_summary.md 📝 Documentation +25/-0

Document async enforcement architecture and guarantees

plans/async_enforcements_summary.md


Grey Divider

Qodo Logo

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

This pull request adds asynchronous enforcement support to the N4S eager enforce API. A new AsyncMode generic parameter is introduced across type definitions to track async state, and runtime implementation adds pendingPromise chaining to defer and queue rule execution when promises are encountered. Tests and documentation demonstrate async matcher registration via enforce.extend and type augmentation.

Changes

Cohort / File(s) Summary
Type System Updates
packages/n4s/src/eager/eagerTypes.ts, packages/n4s/src/n4sTypes.ts
Introduces AsyncMode generic parameter across type definitions (Msg, TRules, TSchemaRules, TArraySchemaRules, TCompoundRules, TOptionalRule, EnforceEagerReturn). Adds ThenableEnforce type for promise-like behavior and IsPromiseLike/NextAsyncMode helper types to detect and propagate async state through rule chains. Updates EnforceCustomMatcher to allow Promise returns.
Runtime Implementation
packages/n4s/src/eager.ts, packages/n4s/src/eager/ruleCallGenerator.ts
Implements pendingPromise state management and deferred execution pipeline. Adds ensurePendingPromise helper, getReservedProperty mapper for reserved keys (MESSAGE, THEN, CATCH), and processRuleResult to validate rule results. Rules returning promises are queued to pendingPromise chain while subsequent rules execute in order; marks proxy as awaitable with then/catch methods.
Test Coverage
packages/n4s/src/__tests__/asyncEnforce.types.test.ts, packages/n4s/src/__tests__/extendAsync.test.ts, packages/n4s/src/__tests__/extend.test.ts, packages/n4s/src/rules/__tests__/asyncEnforce.test.ts
Adds comprehensive async enforcement tests covering type augmentation, async matcher registration via enforce.extend, resolved/rejected promise handling, and fail-fast behavior for sync rules before async execution. Validates chaining behavior and error propagation.
Documentation
plans/async_enforcements_summary.md
Describes architectural approach: pending promise queue for deferred execution, fail-fast guarantee before async rules, mixed sync/async chain support, and type system updates enabling promise returns.

Sequence Diagram

sequenceDiagram
    participant User
    participant Enforce as Enforce<br/>(eager)
    participant RuleCall as Rule Execution<br/>Pipeline
    participant Promise as Pending Promise<br/>Queue
    participant CustomRule as Custom Rule<br/>(Async)

    User->>Enforce: enforce(value).rule1().rule2Async()
    
    Enforce->>RuleCall: Execute rule1 (sync)
    RuleCall->>RuleCall: processRuleResult
    RuleCall->>Enforce: pass, return proxy
    
    Enforce->>RuleCall: Execute rule2Async
    RuleCall->>CustomRule: Invoke async rule
    CustomRule-->>RuleCall: Return Promise
    
    RuleCall->>Promise: setPendingPromise
    Promise->>Promise: Queue rule execution
    RuleCall->>Enforce: Return proxy (awaitable)
    
    User->>Enforce: await result
    Promise->>CustomRule: Execute queued rule
    CustomRule-->>Promise: Resolve/Reject
    Promise-->>User: Result void or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Wiggles nose with glee
Rules once raced, now gently queue,
Sync and async—promises true!
Chain by chain, await with care,
Enforcement flows beyond compare! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding async rule support to enforce chains with awaitable syntax and type updates. It is specific, concise, and directly relates to the main objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/implement-async-rule-support-in-enforce-library
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 15, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Context lost during messages 🐞 Bug ✓ Correctness
Description
transformResult() now runs outside ctx.run(), so rule-returned message functions that rely on
enforce.context() will see no context and can produce incorrect/missing messages. This affects
both sync rules and queued async rules because the context implementation resets immediately after
the ctx.run callback returns.
Code

packages/n4s/src/eager/ruleCallGenerator.ts[R19-50]

+function processRuleResult(
+  result: any,
+  ruleName: string,
+  value: any,
+  customMessage: string | undefined,
+  args: any[],
+): void {
+  const transformedResult = transformResult(result, ruleName, value, ...args);
+
+  invariant(
+    transformedResult.pass,
+    enforceMessage(ruleName, transformedResult, value, customMessage),
+  );
+}
+
export function createRuleCall(config: RuleCallConfig) {
-  const { target, rule, ruleName, value, customMessage, clearMessage } = config;
+  const {
+    target,
+    rule,
+    ruleName,
+    value,
+    customMessage,
+    clearMessage,
+    getPendingPromise,
+    setPendingPromise,
+  } = config;

  return function ruleCall(...args: any[]): any {
-    const transformedResult = ctx.run({ value }, () =>
-      transformResult(
+    const runRule = () =>
+      ctx.run({ value }, () =>
        (rule as (...args: any[]) => any)(value, ...args),
-        ruleName,
-        value,
-        ...args,
-      ),
-    );
+      );
Evidence
ctx.run is synchronous and restores the prior context immediately after the callback returns, so
any message evaluation happening after ctx.run(...) cannot read context. processRuleResult calls
transformResult, which executes result.message via dynamicValue; enforce.context() reads
ctx.use(), which returns the default (no active context) outside a ctx.run.

packages/n4s/src/eager/ruleCallGenerator.ts[19-50]
packages/context/src/context.ts[37-46]
packages/n4s/src/ruleResult.ts[27-44]
packages/n4s/src/n4s.ts[90-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`transformResult()` is executed outside `ctx.run()`, so message functions (`result.message`) evaluated via `dynamicValue` cannot read `enforce.context()` (the context is reset immediately after `ctx.run` returns).

### Issue Context
`packages/context/src/context.ts` shows `ctx.run` restores the parent context right after the callback returns, so any result/message processing must happen inside the callback if it needs context.

### Fix Focus Areas
- packages/n4s/src/eager/ruleCallGenerator.ts[19-95]

Suggested approach:
- Move `processRuleResult(...)` invocation inside `ctx.run({ value }, ...)`.
- For async rules, when the promise resolves, wrap the `processRuleResult(resolvedResult, ...)` call in a fresh `ctx.run({ value }, ...)` so message functions can still access context at evaluation time.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Async rules break lazy API 🐞 Bug ✓ Correctness
Description
Promise-returning custom rules registered via enforce.extend(...) are also registered into the
lazy RuleInstance API, but lazy execution wraps results with RuleRunReturn.create, which treats
a Promise as an invalid object and returns pass: false. This makes
enforce.myAsyncRule().run(value) silently fail even when the async rule would pass in eager mode.
Code

packages/n4s/src/n4sTypes.ts[R44-53]

+type IsPromiseLike<T> = T extends PromiseLike<any> ? true : false;
+type NextAsyncMode<
+  CurrentAsync extends boolean,
+  RuleReturn,
+> = CurrentAsync extends true ? true : IsPromiseLike<RuleReturn>;
+
export type EnforceCustomMatcher<F extends CB> = (
  ...args: CustomMatcherArgs<F extends keyof n4s.EnforceMatchers ? F : never>
-) => boolean | RuleRunReturn<any>;
+) => boolean | RuleRunReturn<any> | Promise<boolean | RuleRunReturn<any>>;
Evidence
This PR explicitly allows custom matchers to return Promises at the type level, and the eager
runtime now accepts Promise results. However, extendEnforce registers the same custom rule into
the lazy API and wraps its result with RuleRunReturn.create(res, value); when res is a Promise,
RuleRunReturn.create falls through to fromObject, where pass.pass is not a boolean and the
rule is treated as failing.

packages/n4s/src/n4sTypes.ts[50-53]
packages/n4s/src/extendLogic.ts[48-64]
packages/n4s/src/utils/RuleRunReturn.ts[44-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Async (Promise-returning) custom rules are now allowed/encouraged for eager enforcement, but `enforce.extend` also wires those rules into the lazy `RuleInstance` API where Promises are not supported, leading to silent incorrect failures.

### Issue Context
`extendEnforce` registers rules into both eager and lazy APIs. Lazy `RuleRunReturn.create` cannot interpret a Promise result.

### Fix Focus Areas
- packages/n4s/src/extendLogic.ts[48-64]
- packages/n4s/src/utils/RuleRunReturn.ts[44-65]
- packages/n4s/src/n4sTypes.ts[50-53]

Suggested approach options (pick one):
1) **Guard/throw in lazy wrapper**: In `ruleWrapper`, if `res` is promise-like, throw an explicit error (e.g., &quot;Async custom rules are not supported in lazy RuleInstance API; use eager enforce(value) and await the chain&quot;).
2) **Do not register lazy rules** for promise-returning matchers (hard to detect reliably without calling the function; you can at least block reserved `async` declarations in user typing or provide a separate `extendAsync` that only extends eager).
3) Implement an **AsyncRuleInstance** (larger change) so lazy `.run/.test` can return promises.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. AsyncMode loses thenables 🐞 Bug ✓ Correctness
Description
NextAsyncMode uses a distributive conditional, so matchers typed as a union like `boolean |
Promise<boolean> widen AsyncMode to boolean and the awaitable ThenableEnforce` surface
disappears from the resulting eager chain type. This causes false-negative typing where a chain that
may be async is not typed as awaitable.
Code

packages/n4s/src/n4sTypes.ts[R44-70]

+type IsPromiseLike<T> = T extends PromiseLike<any> ? true : false;
+type NextAsyncMode<
+  CurrentAsync extends boolean,
+  RuleReturn,
+> = CurrentAsync extends true ? true : IsPromiseLike<RuleReturn>;
+
export type EnforceCustomMatcher<F extends CB> = (
  ...args: CustomMatcherArgs<F extends keyof n4s.EnforceMatchers ? F : never>
-) => boolean | RuleRunReturn<any>;
+) => boolean | RuleRunReturn<any> | Promise<boolean | RuleRunReturn<any>>;

/**
 * Maps custom rules to eager API signatures (drops the value parameter).
 * Only includes rules where T matches the first parameter type.
 */
-export type TCustomRules<T, A, S> = {
+export type TCustomRules<T, A, S, AsyncMode extends boolean> = {
  [K in keyof n4s.EnforceMatchers as T extends FirstParam<
    n4s.EnforceMatchers[K]
  >
    ? K
    : never]: (
    ...args: CustomMatcherArgs<K>
-  ) => import('./eager').EnforceEagerReturn<T, A, S>;
+  ) => import('./eager').EnforceEagerReturn<
+    T,
+    A,
+    S,
+    NextAsyncMode<AsyncMode, ReturnType<Extract<n4s.EnforceMatchers[K], CB>>>
+  >;
Evidence
Because IsPromiseLike<T> is distributive, any union that includes both promise and non-promise
returns becomes boolean instead of a literal true. When AsyncMode becomes boolean,
EnforceEagerReturn’s conditional (AsyncMode extends true ? ThenableEnforce : unknown) collapses
to unknown, losing .then/.catch typing even though runtime supports it.

packages/n4s/src/n4sTypes.ts[44-49]
packages/n4s/src/eager/eagerTypes.ts[86-94]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`NextAsyncMode` becomes `boolean` (not a literal) when a rule return type is a union containing both PromiseLike and non-PromiseLike, which removes thenable typing from the eager chain.

### Issue Context
This is a type-level issue that impacts DX and correctness of TS signatures.

### Fix Focus Areas
- packages/n4s/src/n4sTypes.ts[44-71]

Suggested fix:
- Replace `IsPromiseLike&lt;T&gt; = T extends PromiseLike&lt;any&gt; ? true : false` with a non-widening union check, e.g.:
 - `type HasPromiseLike&lt;T&gt; = [Extract&lt;T, PromiseLike&lt;any&gt;&gt;] extends [never] ? false : true;`
 - and `type NextAsyncMode&lt;Cur extends boolean, R&gt; = Cur extends true ? true : HasPromiseLike&lt;R&gt;;`
This yields a literal `true` when the return type *may* be a promise.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. then/catch rule collision 🐞 Bug ⛯ Reliability
Description
The eager proxy now reserves then and catch and returns promise handlers before rule lookup,
making any custom rule named then or catch unreachable. This is a breaking behavior change for
existing extensions using those matcher names.
Code

packages/n4s/src/eager.ts[R76-104]

+  const ensurePendingPromise = () => {
+    if (!pendingPromise) {
+      pendingPromise = Promise.resolve();
+    }
+
+    return pendingPromise;
+  };
+
+  const getReservedProperty = (key: string) => {
+    switch (key) {
+      case MESSAGE_KEY:
+        return setMessage;
+      case THEN_KEY:
+        return ensurePendingPromise().then.bind(ensurePendingPromise());
+      case CATCH_KEY:
+        return ensurePendingPromise().catch.bind(ensurePendingPromise());
+      default:
+        return undefined;
+    }
+  };
+
  const proxy: EagerReturn<T> = new Proxy(
    {},
    {
      get(_target: any, key: string) {
-        if (key === MESSAGE_KEY) return setMessage;
+        const reservedProperty = getReservedProperty(key);
+        if (reservedProperty) {
+          return reservedProperty;
+        }
Evidence
The proxy get trap checks getReservedProperty first, and getReservedProperty returns handlers
for then and catch, so getRule/getSchemaRule is never consulted for those property names.

packages/n4s/src/eager.ts[76-120]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Custom rules named `then` or `catch` can no longer be called because those keys are reserved for thenable behavior.

### Issue Context
This is a breaking change; it should fail loudly at extension time rather than silently shadowing the user’s rule.

### Fix Focus Areas
- packages/n4s/src/eager.ts[76-120]
- packages/n4s/src/eager/ruleRegistry.ts[12-25]
- packages/n4s/src/extendLogic.ts[42-64]

Suggested approach:
- Add a reserved-name guard in `extendEager` and/or `extendEnforce` that throws if `rules` contains `then`, `catch` (and likely `message`).
- Update documentation/changelog accordingly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the enforce(value) validation library by introducing robust support for asynchronous rules. It allows developers to seamlessly integrate custom Promise-returning validation logic into existing eager validation chains, ensuring that the system correctly handles both synchronous and asynchronous operations while maintaining its fail-fast behavior for synchronous rules. The changes also include comprehensive type updates to provide accurate TypeScript inference for awaitable validation chains.

Highlights

  • Asynchronous Rule Support: Enabled support for Promise-returning custom rules within the eager enforce(value) API, allowing for asynchronous validation logic.
  • Awaitable Enforce Chains: Introduced the ability to await enforce chains, making mixed synchronous and asynchronous validation flows more ergonomic and intuitive.
  • Type System Enhancements: Updated TypeScript types to accurately reflect when an enforce chain becomes awaitable due to the presence of asynchronous rules, improving type safety and developer experience.
  • Internal Promise Tracking: Implemented internal pendingPromise tracking in eager.ts to manage the sequential execution of asynchronous rules within a chain.
  • Rule Result Processing: Modified ruleCallGenerator.ts to detect Promise results from rules, queue subsequent rule processing, and unify synchronous and asynchronous result handling.
  • Comprehensive Testing: Added extensive new tests and updated existing ones to thoroughly cover the new asynchronous behavior and type changes, ensuring reliability.
Changelog
  • packages/n4s/src/tests/asyncEnforce.types.test.ts
    • Added a new test file to verify type-level support for awaiting enforce chains after an async rule.
  • packages/n4s/src/tests/extend.test.ts
    • Modified an existing test to correctly handle custom rules that return promises, changing from throwing an error to resolving.
  • packages/n4s/src/tests/extendAsync.test.ts
    • Added a new test file to verify the registration and execution of async custom rules.
  • packages/n4s/src/eager.ts
    • Updated imports to use type keyword for Maybe.
    • Added THEN_KEY and CATCH_KEY constants for reserved promise methods.
    • Introduced pendingPromise tracking to manage asynchronous operations.
    • Implemented getReservedProperty to expose then and catch handlers for awaitable chains.
    • Updated createRuleCall configuration to include getPendingPromise and setPendingPromise.
  • packages/n4s/src/eager/eagerTypes.ts
    • Modified type definitions to introduce ThenableEnforce and AsyncMode generics.
    • Updated EnforceEagerReturn type to reflect when a chain becomes awaitable.
  • packages/n4s/src/eager/ruleCallGenerator.ts
    • Imported isPromise utility from vest-utils.
    • Refactored rule result processing into a new processRuleResult function.
    • Updated createRuleCall to handle promises by queuing subsequent rules and updating pendingPromise.
  • packages/n4s/src/n4sTypes.ts
    • Added IsPromiseLike and NextAsyncMode types for advanced type inference.
    • Updated EnforceCustomMatcher to allow custom matchers to return Promises.
    • Extended TCustomRules to propagate async mode through types, reflecting the awaitability of chains.
  • plans/async_enforcements_summary.md
    • Added a new markdown file documenting the architectural shift, behavior guarantees, and type updates for async enforcements.
Activity
  • The vitest test suite was executed, and all tests passed successfully.
  • New tests were specifically added to verify awaiting chains, the execution and rejection of async custom rules, and updated behavior in existing tests.
  • No automated test failures were observed after the changes were applied.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces asynchronous rule support to the eager enforce(value) API, a significant enhancement. The changes are well-implemented across the board, including runtime logic for promise-based rule execution, corresponding TypeScript type propagation to make chains awaitable, and comprehensive tests for the new async behaviors. My review found the code to be of high quality. I have one suggestion in ruleCallGenerator.ts to simplify a promise chain for better readability and maintainability.

Comment on lines +55 to +59
pendingPromise.then(() => {
return Promise.resolve(runRule()).then(ruleResult => {
processRuleResult(ruleResult, ruleName, value, customMessage, args);
});
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The nested .then() and Promise.resolve() call are unnecessary here. The .then() method of a promise automatically handles promise unwrapping (or 'thenable assimilation'), so you can chain the calls directly. This simplifies the code and reduces nesting, improving readability.

        pendingPromise.then(() => runRule()).then(ruleResult => {
          processRuleResult(ruleResult, ruleName, value, customMessage, args);
        }),

Comment on lines +19 to +50
function processRuleResult(
result: any,
ruleName: string,
value: any,
customMessage: string | undefined,
args: any[],
): void {
const transformedResult = transformResult(result, ruleName, value, ...args);

invariant(
transformedResult.pass,
enforceMessage(ruleName, transformedResult, value, customMessage),
);
}

export function createRuleCall(config: RuleCallConfig) {
const { target, rule, ruleName, value, customMessage, clearMessage } = config;
const {
target,
rule,
ruleName,
value,
customMessage,
clearMessage,
getPendingPromise,
setPendingPromise,
} = config;

return function ruleCall(...args: any[]): any {
const transformedResult = ctx.run({ value }, () =>
transformResult(
const runRule = () =>
ctx.run({ value }, () =>
(rule as (...args: any[]) => any)(value, ...args),
ruleName,
value,
...args,
),
);
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Context lost during messages 🐞 Bug ✓ Correctness

transformResult() now runs outside ctx.run(), so rule-returned message functions that rely on
enforce.context() will see no context and can produce incorrect/missing messages. This affects
both sync rules and queued async rules because the context implementation resets immediately after
the ctx.run callback returns.
Agent Prompt
### Issue description
`transformResult()` is executed outside `ctx.run()`, so message functions (`result.message`) evaluated via `dynamicValue` cannot read `enforce.context()` (the context is reset immediately after `ctx.run` returns).

### Issue Context
`packages/context/src/context.ts` shows `ctx.run` restores the parent context right after the callback returns, so any result/message processing must happen inside the callback if it needs context.

### Fix Focus Areas
- packages/n4s/src/eager/ruleCallGenerator.ts[19-95]

Suggested approach:
- Move `processRuleResult(...)` invocation inside `ctx.run({ value }, ...)`.
- For async rules, when the promise resolves, wrap the `processRuleResult(resolvedResult, ...)` call in a fresh `ctx.run({ value }, ...)` so message functions can still access context at evaluation time.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +44 to 53
type IsPromiseLike<T> = T extends PromiseLike<any> ? true : false;
type NextAsyncMode<
CurrentAsync extends boolean,
RuleReturn,
> = CurrentAsync extends true ? true : IsPromiseLike<RuleReturn>;

export type EnforceCustomMatcher<F extends CB> = (
...args: CustomMatcherArgs<F extends keyof n4s.EnforceMatchers ? F : never>
) => boolean | RuleRunReturn<any>;
) => boolean | RuleRunReturn<any> | Promise<boolean | RuleRunReturn<any>>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Async rules break lazy api 🐞 Bug ✓ Correctness

Promise-returning custom rules registered via enforce.extend(...) are also registered into the
lazy RuleInstance API, but lazy execution wraps results with RuleRunReturn.create, which treats
a Promise as an invalid object and returns pass: false. This makes
enforce.myAsyncRule().run(value) silently fail even when the async rule would pass in eager mode.
Agent Prompt
### Issue description
Async (Promise-returning) custom rules are now allowed/encouraged for eager enforcement, but `enforce.extend` also wires those rules into the lazy `RuleInstance` API where Promises are not supported, leading to silent incorrect failures.

### Issue Context
`extendEnforce` registers rules into both eager and lazy APIs. Lazy `RuleRunReturn.create` cannot interpret a Promise result.

### Fix Focus Areas
- packages/n4s/src/extendLogic.ts[48-64]
- packages/n4s/src/utils/RuleRunReturn.ts[44-65]
- packages/n4s/src/n4sTypes.ts[50-53]

Suggested approach options (pick one):
1) **Guard/throw in lazy wrapper**: In `ruleWrapper`, if `res` is promise-like, throw an explicit error (e.g., "Async custom rules are not supported in lazy RuleInstance API; use eager enforce(value) and await the chain").
2) **Do not register lazy rules** for promise-returning matchers (hard to detect reliably without calling the function; you can at least block reserved `async` declarations in user typing or provide a separate `extendAsync` that only extends eager).
3) Implement an **AsyncRuleInstance** (larger change) so lazy `.run/.test` can return promises.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/n4s/src/eager.ts (1)

127-128: Setting proxy.pass = true unconditionally may be misleading.

Setting pass = true at initialization implies the validation has passed before any rules have run. While this may be intentional (empty chains pass), it could cause confusion if users check .pass before calling any validation rules. Consider adding a comment explaining this design decision.

📝 Add clarifying comment
+  // An empty chain (no rules called) is considered passing
   proxy.pass = true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/n4s/src/eager.ts` around lines 127 - 128, The assignment proxy.pass
= true sets the validation as "passed" at initialization which can be
misleading; update the code around the proxy.pass assignment to include a
concise comment explaining the design decision (e.g., that empty/absent rule
chains are considered passing and therefore .pass defaults true) and warn
consumers not to rely on .pass before any rules execute; reference the
proxy.pass initialization in the eager.ts file (the proxy.pass assignment) so
future readers understand why the default is true.
packages/n4s/src/eager/ruleCallGenerator.ts (1)

52-65: Consider clarifying the error propagation behavior in async chains.

When processRuleResult throws inside the promise chain (line 57), the error propagates as a rejection. However, target.pass = true is set unconditionally on line 63 before the promise settles. This is correct for the async model (callers must await to see failures), but could be confusing.

A brief comment would help clarify that pass = true is a placeholder until the promise resolves/rejects.

📝 Add clarifying comment
+      // pass is set optimistically; actual result is determined when the promise settles
       clearMessage();
       target.pass = true;
       return target;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/n4s/src/eager/ruleCallGenerator.ts` around lines 52 - 65, Add a
short clarifying comment near the async branch that handles pending promises
(around getPendingPromise / setPendingPromise) explaining that target.pass is
set to true immediately as a placeholder for the asynchronous result and that
any error thrown inside the chained promise (from runRule or processRuleResult)
will propagate as a rejection which callers must await to observe; mention the
specific symbols getPendingPromise, setPendingPromise, runRule,
processRuleResult and target.pass so reviewers can find the spot.
packages/n4s/src/__tests__/extendAsync.test.ts (1)

6-17: Consider moving enforce.extend to beforeAll for test isolation.

Other async test files in this PR (e.g., asyncEnforce.test.ts) use beforeAll for extending enforce. Moving the extension outside the test case ensures consistent setup patterns across the test suite and avoids potential issues if more tests are added to this file later.

♻️ Suggested refactor
 describe('enforce.extend with async rules', () => {
+  beforeAll(() => {
+    enforce.extend({
+      myCustomAsyncRule: async (val: number) => {
+        return { pass: val > 5 };
+      },
+    });
+  });
+
   it('should register and execute an async custom rule properly', async () => {
-    enforce.extend({
-      myCustomAsyncRule: async (val: number) => {
-        return { pass: val > 5 };
-      },
-    });
-
     // `@ts-expect-error` - testing dynamic rule
     await expect(enforce(10).myCustomAsyncRule()).resolves.toBeUndefined();
     // `@ts-expect-error` - testing dynamic rule
     await expect(enforce(2).myCustomAsyncRule()).rejects.toThrow();
   });
 });

You'll also need to add beforeAll to the import on line 1.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/n4s/src/__tests__/extendAsync.test.ts` around lines 6 - 17, Move the
enforce.extend call out of the test body into a beforeAll block so the async
rule registration runs once per suite; add beforeAll to the test imports (as
suggested) and place the existing enforce.extend({ myCustomAsyncRule: async
(val: number) => ({ pass: val > 5 }) }) inside that beforeAll, then remove the
extend call from the 'should register and execute an async custom rule properly'
test so the test simply invokes enforce(10).myCustomAsyncRule() and
enforce(2).myCustomAsyncRule() as before.
packages/n4s/src/eager/eagerTypes.ts (1)

14-16: Consider adding finally for full Promise ergonomics.

The ThenableEnforce type omits finally, which is commonly used for cleanup in async flows (e.g., await enforce(x).asyncRule().finally(() => cleanup())). While not required for basic await support, adding it would provide a more complete Promise-like experience.

♻️ Suggested enhancement
 type ThenableEnforce = PromiseLike<void> & {
   catch: (onrejected?: ((reason: unknown) => unknown) | null) => Promise<void>;
+  finally: (onfinally?: (() => void) | null) => Promise<void>;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/n4s/src/eager/eagerTypes.ts` around lines 14 - 16, The
ThenableEnforce type currently models PromiseLike<void> with a custom catch but
omits finally; update the ThenableEnforce declaration (the type named
ThenableEnforce) to include a finally method signature (e.g.,
finally(onfinally?: (() => void) | null): Promise<void>) so it exposes
Promise-style cleanup chaining alongside the existing catch, preserving
PromiseLike<void> behavior and return type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/n4s/src/eager.ts`:
- Around line 84-95: The switch in getReservedProperty calls
ensurePendingPromise() twice for THEN_KEY and CATCH_KEY which can create
confusing/fragile promise references and eagerly create a pending promise;
change each branch to call ensurePendingPromise() exactly once, store it in a
local const (e.g., const p = ensurePendingPromise()) and then return
p.then.bind(p) for THEN_KEY and p.catch.bind(p) for CATCH_KEY so the same
promise reference is used and the call site is clearer (refer to
getReservedProperty, THEN_KEY, CATCH_KEY, and ensurePendingPromise).

---

Nitpick comments:
In `@packages/n4s/src/__tests__/extendAsync.test.ts`:
- Around line 6-17: Move the enforce.extend call out of the test body into a
beforeAll block so the async rule registration runs once per suite; add
beforeAll to the test imports (as suggested) and place the existing
enforce.extend({ myCustomAsyncRule: async (val: number) => ({ pass: val > 5 })
}) inside that beforeAll, then remove the extend call from the 'should register
and execute an async custom rule properly' test so the test simply invokes
enforce(10).myCustomAsyncRule() and enforce(2).myCustomAsyncRule() as before.

In `@packages/n4s/src/eager.ts`:
- Around line 127-128: The assignment proxy.pass = true sets the validation as
"passed" at initialization which can be misleading; update the code around the
proxy.pass assignment to include a concise comment explaining the design
decision (e.g., that empty/absent rule chains are considered passing and
therefore .pass defaults true) and warn consumers not to rely on .pass before
any rules execute; reference the proxy.pass initialization in the eager.ts file
(the proxy.pass assignment) so future readers understand why the default is
true.

In `@packages/n4s/src/eager/eagerTypes.ts`:
- Around line 14-16: The ThenableEnforce type currently models PromiseLike<void>
with a custom catch but omits finally; update the ThenableEnforce declaration
(the type named ThenableEnforce) to include a finally method signature (e.g.,
finally(onfinally?: (() => void) | null): Promise<void>) so it exposes
Promise-style cleanup chaining alongside the existing catch, preserving
PromiseLike<void> behavior and return type.

In `@packages/n4s/src/eager/ruleCallGenerator.ts`:
- Around line 52-65: Add a short clarifying comment near the async branch that
handles pending promises (around getPendingPromise / setPendingPromise)
explaining that target.pass is set to true immediately as a placeholder for the
asynchronous result and that any error thrown inside the chained promise (from
runRule or processRuleResult) will propagate as a rejection which callers must
await to observe; mention the specific symbols getPendingPromise,
setPendingPromise, runRule, processRuleResult and target.pass so reviewers can
find the spot.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fbc5bdb4-f56d-45e1-972e-ab4d02c11256

📥 Commits

Reviewing files that changed from the base of the PR and between d8c22eb and 11123c2.

📒 Files selected for processing (9)
  • packages/n4s/src/__tests__/asyncEnforce.types.test.ts
  • packages/n4s/src/__tests__/extend.test.ts
  • packages/n4s/src/__tests__/extendAsync.test.ts
  • packages/n4s/src/eager.ts
  • packages/n4s/src/eager/eagerTypes.ts
  • packages/n4s/src/eager/ruleCallGenerator.ts
  • packages/n4s/src/n4sTypes.ts
  • packages/n4s/src/rules/__tests__/asyncEnforce.test.ts
  • plans/async_enforcements_summary.md

Comment on lines +84 to +95
const getReservedProperty = (key: string) => {
switch (key) {
case MESSAGE_KEY:
return setMessage;
case THEN_KEY:
return ensurePendingPromise().then.bind(ensurePendingPromise());
case CATCH_KEY:
return ensurePendingPromise().catch.bind(ensurePendingPromise());
default:
return undefined;
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential bug: ensurePendingPromise() is called twice, potentially creating inconsistent promise references.

In the THEN_KEY and CATCH_KEY cases, ensurePendingPromise() is invoked twice per access — once to get .then/.catch and again inside .bind(). Since ensurePendingPromise may create a new promise on the first call if pendingPromise is null, the second call will return the same reference, but this pattern is confusing and could break if the implementation changes.

More importantly, the current pattern evaluates ensurePendingPromise() eagerly when .then or .catch is accessed, which creates a resolved promise even if no async rule has run. This might cause unexpected behavior where a purely synchronous chain becomes thenable.

🔧 Suggested fix to clarify intent
   const getReservedProperty = (key: string) => {
+    const p = ensurePendingPromise();
     switch (key) {
       case MESSAGE_KEY:
         return setMessage;
       case THEN_KEY:
-        return ensurePendingPromise().then.bind(ensurePendingPromise());
+        return p.then.bind(p);
       case CATCH_KEY:
-        return ensurePendingPromise().catch.bind(ensurePendingPromise());
+        return p.catch.bind(p);
       default:
         return undefined;
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/n4s/src/eager.ts` around lines 84 - 95, The switch in
getReservedProperty calls ensurePendingPromise() twice for THEN_KEY and
CATCH_KEY which can create confusing/fragile promise references and eagerly
create a pending promise; change each branch to call ensurePendingPromise()
exactly once, store it in a local const (e.g., const p = ensurePendingPromise())
and then return p.then.bind(p) for THEN_KEY and p.catch.bind(p) for CATCH_KEY so
the same promise reference is used and the call site is clearer (refer to
getReservedProperty, THEN_KEY, CATCH_KEY, and ensurePendingPromise).

@github-actions
Copy link
Copy Markdown

🚀 Benchmark Results

Suite Benchmark Ops/sec (Hz) P99 (ms) Margin of Error Diff (Abs) Diff (%)
Complex Data Validation Enforce Huge String 357.02 8.2252 6.89% 0 0.00%
State Management Serialize Large 284.11 5.1019 2.15% 0 0.00%
Integration & Edge Cases Callback Overhead 4.101 245.14 0.35% 0 0.00%
Complex Feature Mix full run with feature flags 132.88 15.2077 5.11% 0 0.00%
Complex Feature Mix focused/conditional run 234.75 8.5851 3.31% 0 0.00%
Conditional isolates skip even indices 371.42 10.6055 19.50% -114.86 -23.62% ⚠️
Conditional isolates omit multiples of 4 507.05 4.6253 9.10% 0 0.00%
Dynamic each and groups longer list 256.52 6.426 11.50% 0 0.00%
Reconciler & History Diffing Reconciler (Stable List) 3.95 280 2.97% 0 0.00%
Reconciler & History Diffing Reconciler (Full Invalidation) 4.046 251.24 0.72% 0 0.00%
Reconciler & History Diffing Reconciler (Prepend Item) 4.052 256.01 0.98% 0 0.00%
Reconciler & History Diffing Reconciler (Append Item) 4.037 249.31 0.36% 0 0.00%
Reconciler & History Diffing Reconciler (Interleaved) 4.028 255.14 0.76% 0 0.00%
Reconciler & History Diffing Isolate Reordering (Reverse) 4.036 249.88 0.32% 0 0.00%
Reconciler & History Diffing Isolate Reordering (Shuffle) 3.993 270.18 2.07% 0 0.00%
Reconciler & History Diffing Orphan GC Pressure 7.855 129.12 0.51% 0 0.00%
Result Selectors & Reporting hasErrors (Volume) 869.89 1.9498 1.41% 0 0.00%
Result Selectors & Reporting getErrors (Group Lookup) 472.59 2.5007 0.65% 0 0.00%
Result Selectors & Reporting Summary Generation (Large) 3.507 287.82 0.49% 0 0.00%
Async & Concurrency Stress Pending Storm (Memory) 4.003 251.89 0.67% 0 0.00%
Async & Concurrency Stress Resolve Storm (Throughput) 4.003 256.82 0.77% 0 0.00%
Async & Concurrency Stress Reject Storm 4.022 251.51 0.36% 0 0.00%
Async & Concurrency Stress Async Race 166.72 10.3055 3.55% 0 0.00%
Control Flow & Hooks Internals test.memo (Thrashing) 163.08 7.5101 2.24% 0 0.00%
Control Flow & Hooks Internals test.memo (Stagnation) 629.13 2.8488 1.72% 0 0.00%
Control Flow & Hooks Internals skipWhen (Active) 8.389 120.7 0.37% 0 0.00%
Control Flow & Hooks Internals only Starvation (Early) 7.203 142.71 1.04% 0 0.00%
Control Flow & Hooks Internals only Starvation (Late) 7.34 138.25 0.99% 0 0.00%
VestBus & Internals Bus Scaling 193.44 7.7309 2.36% 0 0.00%
VestBus & Internals State Refill 123.15 12.7454 3.86% 0 0.00%
Memory & Object Lifecycle Test Object Allocator 8.665 117.46 0.64% 0 0.00%
Memory & Object Lifecycle Garbage Collection Friendly 8.44 131.99 3.76% 0 0.00%
Serialization Serialize (Large) 122.45 9.5934 2.09% -13.74 -10.09% ⚠️
Serialization Deserialize (Large) 93.879 13.0836 1.85% 0 0.00%
Edge Cases & Integration Broad Group 4.208 240.99 0.73% 0 0.00%
Edge Cases & Integration Namespace Collision 4.185 253.34 1.66% 0 0.00%
Edge Cases & Integration Large Field Names 196.48 6.5423 2.30% 0 0.00%
Edge Cases & Integration Large Failure Messages 339.29 5.1646 3.27% 0 0.00%
Feature Coverage Matrix enforce matrix (small payload) 375.92 6.7459 7.13% 0 0.00%
Feature Coverage Matrix enforce matrix (larger payload) 590.39 5.2992 9.36% 0 0.00%
Feature Coverage Matrix flow control eager mode 278.51 7.4848 8.26% -29.97 -9.72% ⚠️
Feature Coverage Matrix flow control one mode 288.61 8.9055 7.80% 0 0.00%
Reordering & Reconciliation each (Reorder - Reverse) 105.11 15.5374 5.73% 0 0.00%
Reordering & Reconciliation each (Reorder - Insert Middle) 100.27 13.1974 4.10% 0 0.00%
Reordering & Reconciliation each (Reorder - Delete Middle) 107.86 12.3513 3.99% 0 0.00%
Reordering & Reconciliation each (Key Thrashing) 273.73 7.0221 4.87% +15.17 +5.87% 🎉
State Mutation & Reset suite.remove() (Many Fields) 171.6 7.8449 1.58% 0 0.00%
State Mutation & Reset suite.reset() (Memory Reclamation) 8.551 118.94 0.81% 0 0.00%
Concurrency & Events Bus Stress 4.288 234.84 0.40% 0 0.00%
Nested Fields with Hooks depth 3 with 40 fields per level 11.728 96.617 10.97% 0 0.00%
Nested Fields with Hooks depth 4 with 60 fields per level 6.332 184.2 36.54% 0 0.00%
Nested Fields with Hooks depth 5 with 80 fields per level 6.223 162.09 10.95% 0 0.00%
Field Volume Stress 10 fields 332.29 9.5057 7.13% -26.35 -7.35% ⚠️
Field Volume Stress 500 fields 4.767 232.03 2.73% 0 0.00%
Field Volume Stress 1000 fields 2.123 479.26 0.89% 0 0.00%
Deep Nesting Stress depth 10 75.662 38.0116 7.43% 0 0.00%
Deep Nesting Stress depth 50 30.939 37.8557 2.02% 0 0.00%
Deep Nesting Stress depth 100 19.409 53.4001 0.89% 0 0.00%
Complex Combinations & Edge Cases High Frequency test Creation 185.75 10.1727 3.39% 0 0.00%
Core Test Functionality test (High Volume, Same Name) 4.185 242.57 0.63% 0 0.00%
Core Test Functionality test (High Volume, Unique Names) 4.186 241.52 0.31% 0 0.00%
Raw Output
See CI logs for full output

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant