skills/oh-arkruntime-interop-promise/SKILL.md
ETS-JavaScript interop Promise bridging system in ArkCompiler. Use this skill when working on cross-language Promise conversion between ETS (ArkTS) and JavaScript, including JSConvertPromise Wrap/Unwrap, EtsPromise proxy creation, EtsPromiseRef bridging, CreatePromiseLink, OnJsPromiseCompleted callbacks, connectPromise, SettleJsPromise, PromiseInteropResolve/Reject, EtsAwaitPromise/AwaitProxyPromise, callback queue management, or any code under js_convert.h (Promise section), js_job_queue, ets_promise, ets_promise_ref, std_core_Promise.cpp, or PromiseInterop.ets. Also use when debugging cross-VM Promise state synchronization, coroutine suspension/resumption during await, or napi_deferred lifecycle issues.
npx skillsauth add openharmonyinsight/openharmony-skills interop-promiseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Guide for understanding, developing, and debugging the Promise interop system that bridges ETS (ArkTS) Promises with JavaScript Promises in the ArkCompiler hybrid runtime.
The interop Promise system enables transparent bidirectional Promise conversion between the static ETS VM and dynamic JS VM. When ETS calls a JS async function (or vice versa), the system creates proxy objects and registers callbacks so that resolve/reject events propagate correctly across the language boundary.
ETS VM Side JS VM Side
────────────── ──────────────
EtsPromise JS Promise
│ │
├── interopObject_ → EtsPromiseRef ────────┤
├── linkedPromise_ → EtsPromiseRef │
├── event_ (await suspend/resume) │
├── mutex_ (thread safety) │
└── callbackQueue_ (.then handlers) │
│
SharedReferenceStorage ────────────────┘
(EtsPromiseRef ↔ JS Promise mapping)
JS→ETS path: JS Promise → JSCONVERT_UNWRAP(Promise) → EtsPromise proxy
ETS→JS path: EtsPromise → JSCONVERT_WRAP(Promise) → JS Promise
| Component | Path |
|-----------|------|
| JSConvertPromise (Wrap/Unwrap) | plugins/ets/runtime/interop_js/js_convert.h |
| EtsPromise class | plugins/ets/runtime/types/ets_promise.h/.cpp |
| EtsPromiseRef bridge | plugins/ets/runtime/types/ets_promise_ref.h |
| JsJobQueue / CreatePromiseLink | plugins/ets/runtime/interop_js/js_job_queue.h/.cpp |
| Promise intrinsics | plugins/ets/runtime/intrinsics/std_core_Promise.cpp |
| SettleJsPromise / PromiseInterop | plugins/ets/runtime/interop_js/intrinsics_api_impl.cpp |
| PromiseInterop.ets | plugins/ets/stdlib/std/interop/js/PromiseInterop.ets |
| ETS Promise.ets | plugins/ets/stdlib/std/core/Promise.ets |
| CallJSHandler (ETS→JS calls) | plugins/ets/runtime/interop_js/call/call_js.cpp |
| CallETSHandler (JS→ETS calls) | plugins/ets/runtime/interop_js/call/call_ets.cpp |
| Type routing (ConvertArgToEts/JS) | plugins/ets/runtime/interop_js/call/arg_convertors.h |
Triggered when ETS calls a JS function that returns a Promise.
Call chain:
CallJSHandler::Handle()
→ ConvertRetval() → ConvertArgToEts() → ConvertRefArgToEts()
→ JSConvertPromise::UnwrapImpl() [JSCONVERT_UNWRAP(Promise)]
UnwrapImpl steps:
SharedReferenceStorage::GetReference(env, jsVal) — reuse existing proxy if JS Promise was already wrappedEtsPromise::Create(coro) — STATE_PENDINGSharedReferenceStorage::CreateJSObjectRef(ctx, ref, jsVal) — bidirectional EtsPromiseRef ↔ JS Promisehpromise->SetLinkedPromise(coro, href) — enables IsProxy() check in awaitEtsPromise::CreateLink() → JsJobQueue::CreatePromiseLink() — registers C++ callbacks on JS Promise's .then()Triggered when JS calls an ETS function that returns a Promise.
Call chain:
CallETSHandler::HandleImpl()
→ ConvertArgToJS() → ConvertRefArgToJS()
→ JSConvertPromise::WrapImpl() [JSCONVERT_WRAP(Promise)]
WrapImpl has two paths:
Fast path (EtsPromise already settled when Wrap is called):
GetInteropObject() + HasReference()napi_create_promise(env, &deferred, &jsPromise) — create pending JS PromiseLock() → check !IsPending() && !IsLinked() → fast pathJSRefConvertResolvenapi_resolve_deferred() or napi_reject_deferred() — immediately settle JS PromiseSlow path (EtsPromise still pending):
1-2. Same as fast path
3. Lock() → IsPending() || IsLinked() → slow path
4. Unlock() → call PromiseInterop.connectPromise(promise, deferred) via Invoke
5. connectPromise registers .then() callbacks that call PromiseInteropResolve/Reject native methods
6. When EtsPromise resolves later: OnPromiseCompletion() → LaunchCallback() → PromiseInteropResolve() → SettleJsPromise() → napi_resolve_deferred()
7. Create EtsPromiseRef + register in SharedReferenceStorage
ets_promise.h)States: STATE_PENDING(0), STATE_RESOLVED(1), STATE_REJECTED(2), STATE_LINKED(3)
| Method | Description |
|--------|-------------|
| Create(coro) | Create PENDING promise with mutex + event |
| Resolve(coro, value) | Set value, transition to RESOLVED, call OnPromiseCompletion |
| Reject(coro, error) | Set error, transition to REJECTED, call OnPromiseCompletion |
| Wait() | Block coroutine via EtsEvent::Wait() |
| IsProxy() | linkedPromise_ != nullptr — true for JS Promise proxies |
| IsPending/Resolved/Rejected/Linked() | State checkers |
| SubmitCallback(cb, workerDomain) | Add .then handler to callbackQueue |
| CreateLink(source, target) | Delegate to JobQueue::CreateLink() |
| OnPromiseCompletion(coro) | Fire event, launch queued callbacks, handle unhandled rejection |
| LaunchCallback(coro, cb, groupId) | Execute callback in new coroutine (PROMISE_CALLBACK priority) |
| ChangeStateToPendingFromLinked() | LINKED → PENDING state transition |
| GetInteropObject() / SetInteropObject() | EtsPromiseRef bridge object |
| GetLinkedPromise() / SetLinkedPromise() | For proxy detection |
| Lock() / Unlock() / IsLocked() | Thread-safe mutex via MarkWord |
Member variables: value_, mutex_, event_, callbackQueue_, workerDomainQueue_, interopObject_, linkedPromise_, queueSize_, state_
ets_promise_ref.h)Minimal bridge object to avoid MarkWord conflict between SharedReferenceStorage (interop hash) and EtsPromise (Lock).
class EtsPromiseRef : public EtsObject {
EtsObject *target_ {}; // Points to the actual EtsPromise
// MarkWord used by SharedReferenceStorage for interop hash index
};
js_job_queue.h/.cpp)Extends JobQueue with JS-specific callback and promise linking.
| Method | Description |
|--------|-------------|
| CreatePromiseLink(jsObject, etsPromise) | Register C++ then/catch callbacks on JS Promise |
| Post(fn, data) | Post callback to JS job queue via JS Promise |
Global C++ callbacks registered on JS Promise:
OnJsPromiseResolved(env, info) → delegates to OnJsPromiseCompleted(env, info, true)OnJsPromiseRejected(env, info) → delegates to OnJsPromiseCompleted(env, info, false)OnJsPromiseCompleted(env, info, isResolved): Converts JS value to ETS, calls EtsPromiseResolve or EtsPromiseRejectETS: await p;
→ EtsAwaitPromise(p)
→ IsProxy()?
→ YES: AwaitProxyPromise()
→ promise->Wait() // EtsEvent::Wait() — coroutine suspends
→ [JS resolves → OnJsPromiseCompleted → EtsPromiseResolve → Resolve → OnPromiseCompletion → Fire()]
→ Wait() returns
→ IsResolved()? return GetValue() : throw exception
→ NO: promise->Wait() // Direct ETS Promise await
Key: EtsAwaitPromise first yields CPU via coro->GetManager()->Schedule() to allow other coroutines (including JS microtasks) to execute before checking proxy status.
Create()
│
▼
STATE_PENDING
/ │ \
resolve() / CreateLink() \ reject()
/ │ \
▼ ▼ ▼
STATE_RESOLVED STATE_LINKED STATE_REJECTED
│
resolve() │ (from subscribeOnAnotherPromise)
▼
STATE_RESOLVED
.then() registration flow:
p.then(onResolve, onReject)
→ Promise.ets: thenImpl()
→ [native] EtsPromiseSubmitCallback(promise, callback, workerDomain)
→ SubmitCallback(): if settled → execute immediately; else → add to callbackQueue_
Execution on completion:
OnPromiseCompletion(coro)
→ Fire() // Wake awaiters
→ for each callback in queue:
→ LaunchCallback(coro, callback, groupId)
→ Create CompletionEvent
→ coroManager->Launch(event, method, args, groupId, PROMISE_CALLBACK)
Queue capacity management: Dynamic resizing with EnsureCapacity() — growth strategy is 2 * oldSize + 1.
The final step that completes a JS Promise from ETS:
void SettleJsPromise(EtsObject *value, napi_deferred deferred, EtsInt state)
{
// Must run on main worker thread
INTEROP_CODE_SCOPE_ETS_TO_JS(executionCtx);
// Convert ETS value to JS value
completionValue = refconv->Wrap(ctx, value);
// Complete the JS Promise
napi_resolve_deferred(env, deferred, completionValue); // or napi_reject_deferred
}
Called from PromiseInteropResolve() / PromiseInteropReject() which are native methods invoked by PromiseInterop.ets callbacks.
final class PromiseInterop {
static connectPromise<T>(p: Promise<T>, deferred: long): void {
p.then<void, void>(
(value: T): void => { PromiseInterop.resolve<T>(value, deferred); },
(error: Any): void => { PromiseInterop.reject(error, deferred); }
);
}
private static native resolve<T>(value: T, deferred: long): void;
private static native reject(error: Any, deferred: long): void;
}
This ETS code is invoked via PlatformTypes()->interopPromiseInteropConnectPromise->GetPandaMethod()->Invoke() from C++ during JSCONVERT_WRAP slow path.
Promise conversion is not triggered by runtime type detection (e.g., napi_is_promise). Instead, it's driven by compile-time type signatures from .d.ets files:
.d.ets: export declare function jsAsync(): Promise<string>;
↓ (compiler generates ProtoReader type info)
ProtoReader return type = EtsPromise class
↓ (runtime type routing)
ConvertRefArgToEts → JSRefConvertResolve(ctx, EtsPromise.RuntimeClass)
↓ (finds JSConvertPromise converter)
JSConvertPromise::UnwrapImpl() or WrapImpl()
If .d.ets declares Promise<T> but JS returns non-Promise, ASSERT(isPromise) fails in Debug mode.
| Direction | Entry Point | Router | Converter |
|-----------|-------------|--------|-----------|
| JS→ETS (return) | CallJSHandler::ConvertRetval() | ConvertArgToEts() → ConvertRefArgToEts() | JSConvertPromise::UnwrapImpl() |
| ETS→JS (return) | CallETSHandler::ConvertArgToJS() | ConvertRefArgToJS() | JSConvertPromise::WrapImpl() |
| JS→ETS (param) | CallETSHandler::ConvertArgs() | ConvertArgToEts() | JSConvertPromise::UnwrapImpl() |
| ETS→JS (param) | CallJSHandler::ConvertArgsAndCall() | ConvertArgToJS() | JSConvertPromise::WrapImpl() |
Three verification patterns exist for interop Promise testing:
| Pattern | ETS Promise State at Return | JSCONVERT_WRAP Path | JS Verification |
|---------|----------------------------|---------------------|-----------------|
| A: ETS internal verify | PENDING | Slow path | Poll ETS global state |
| B: JS verify resolved | RESOLVED | Fast path | JS .then() on returned Promise |
| C: JS verify pending | PENDING | Slow path | JS .then() + setTimeout trigger resolve |
ASSERT(IsMainWorker()))PROMISE_CALLBACK priorityEtsAwaitPromise yields CPU before suspending to allow JS microtask processingJSConvert<YourType>::Wrap() and UnwrapImpl() in js_convert.hJSRefConvertResolve lookup chainOnJsPromiseCompleted and SettleJsPromise (already generic via JSRefConvertResolve)EtsPromise::IsProxy() — is it a JS proxy?CreatePromiseLink registered callbacks — check JS Promise's .then() was calledOnJsPromiseCompleted was invoked — JS microtask queue running?EtsEvent::Fire() was called — OnPromiseCompletion executed?Schedule() yields properly?.d.ets type signature matches actual JS return typeProtoReader type info at runtimeASSERT(isPromise) in UnwrapImpl catches mismatchJSRefConvertResolve finds the correct converter for the value typetesting
--- name: ohos-req-value-decision description: Use after review meeting to record decision and route to next step. Triggers: 评审决策纪要, 评审结论回流, value decision, 评审接纳, 评审不接纳, 评审退回, 下次重新上会. Do NOT use for feature baseline (ohos-req-feature-baseline), review gate checks (ohos-req-review-gate), or IR generation (ohos-req-feature-to-ir). metadata: author: openharmony scope: common stage: requirements capability: value-decision version: 0.3.0 status: draft tags: - sdd - requirements
development
Use when converting an OpenHarmony requirement document, spec, or design proposal into an OpenHarmony review slide deck (需求评审 / 需求变更评审 / 设计评审 PPTX) — produces the fixed OpenHarmony-branded review-deck structure (OH logo on every page) with architecture/flow diagrams and field tables. Triggers on "需求评审PPT", "需求变更评审", "把需求文档转成评审PPT", "spec转评审PPT", "requirement/spec to review deck". NOT for arbitrary or generic slide decks unrelated to OpenHarmony requirement/design review.
testing
Use when performing the Phase 0 Step 0.5 Review Ready Gate on a 04-feature.md, especially when the user says "evaluate gate", "review readiness", "feature ready?", "should we generate IR", or when the ohos-req-intake-orchestration main session needs a structured Ready / Conditional Ready / Not Ready judgment instead of doing the check inline. Reads 01-04, runs seven fixed checks plus a conditional-items check, and returns a machine-readable JSON summary plus a human-readable table that the main session can route on. Do NOT use for feature baseline generation (ohos-req-feature-baseline), value decision recording (ohos-req-value-decision), or IR generation (ohos-req-feature-to-ir).
testing
--- name: ohos-req-requirement-intake description: Use when importing an OHOS requirement into Phase 0.1, especially for 01-requirement.md, requirement intake, background, user value, scenarios, scope, FR/NFR, affected modules, or priority. Triggers: 需求导入, 01-requirement, 需求基线, RR单号. Do NOT use for feasibility analysis (ohos-req-feasibility-analysis), architecture decision (ohos-req-arch-decision), or feature baseline (ohos-req-feature-baseline). metadata: author: openharmony scope: common