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 typedevelopment
Run local code quality checks covering a subset of OpenHarmony gate CI (copyright, CodeArts C/C++) plus additional local checks (pylint/flake8, shellcheck/bashate, gn format). Use before committing to reduce gate failures. Triggers on: /oh-precommit-codecheck, "门禁检查", "门禁预检", "检查代码", "run codecheck", "check code quality", "lint my code", "代码检查", or after completing code implementation. WHEN to use: before git commit, before creating PR, after modifying C/C++/Python/Shell/GN files, when gate CI fails with codecheck defects, or when you want to preview what gate will flag.
development
OpenHarmony PR full lifecycle workflow. Five modes: - Commit: standardized commit with DCO sign-off and Issue linking - Create PR: commit + push to fork + create Issue + create PR on upstream - Fix Codecheck: fetch gate CI codecheck defects from a PR and auto-fix them - Review PR: fetch a PR's changes to local for code review - Fix Review: fetch unresolved review comments from a PR and auto-fix them Triggers on: /oh-pr-workflow, "提交代码", "创建PR", "提个PR", "commit", "修复告警", "修复门禁", "修复codecheck", "fix codecheck", "review pr", "review这个pr", "看下这个pr", "检视pr", "修复review", "修复检视意见", "fix review", or a GitCode PR URL with fix/review intent.
testing
分析 HM Desktop PRD 文档,提取需求信息、验证完整性、检查章节顺序(需求来源→需求背景→需求价值分析→竞品分析→需求描述)、检查 KEP 定义、检测需求冲突并生成结构化分析报告。适用于用户请求:(1) 分析或审查 PRD 文档, (2) 从需求中提取 KEP 列表, (3) 检查 PRD 完整性或一致性, (4) 将需求映射到模块架构, (5) 验证 PRD 格式合规性, (6) 验证竞品分析章节完整性。关键词:PRD分析, requirement extraction, KEP验证, completeness check, chapter order validation, 竞品分析检查, analyze PRD, 需求提取, 完整性检查, 章节顺序验证
development
基于 PRD 文档自动生成鸿蒙系统设计文档,包括架构设计文档和功能设计文档。生成前会分析 OpenHarmony 存量代码结构,确保与现有架构兼容。架构设计文档第2章必须为竞品方案分析,位于需求背景之后。适用于用户请求:(1) 生成架构设计文档, (2) 生成功能设计文档, (3) 从 PRD 生成设计文档, (4) 创建系统架构设计, (5) 编写功能规格说明, (6) 分析 OH 代码结构。关键词:architecture design, functional design, design doc, 竞品方案分析, OpenHarmony code analysis, 架构设计, 功能设计, 设计文档生成, OH代码分析, analyze codebase, competitor analysis