Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
N
nfkit
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nanahira
nfkit
Commits
924883d2
Commit
924883d2
authored
Oct 24, 2025
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
dual object
parent
977ad6f8
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
638 additions
and
0 deletions
+638
-0
src/dual-object.ts
src/dual-object.ts
+184
-0
src/workflow.ts
src/workflow.ts
+11
-0
tests/dual-object-workflow.spec.ts
tests/dual-object-workflow.spec.ts
+39
-0
tests/dual-object.spec.ts
tests/dual-object.spec.ts
+287
-0
tests/workflow.spec.ts
tests/workflow.spec.ts
+117
-0
No files found.
src/dual-object.ts
0 → 100644
View file @
924883d2
// dual-unified.ts
export
type
Dual
<
T
>
=
T
&
PromiseLike
<
T
>
;
type
ThenKey
=
'
then
'
|
'
catch
'
|
'
finally
'
;
const
isThenKey
=
(
k
:
PropertyKey
):
k
is
ThenKey
=>
k
===
'
then
'
||
k
===
'
catch
'
||
k
===
'
finally
'
;
type
State
=
'
undecided
'
|
'
pending
'
|
'
fulfilled
'
|
'
rejected
'
;
/** 仅允许填入 “返回 Promise 的方法名” */
export
type
AsyncMethodKeys
<
T
>
=
{
[
K
in
keyof
T
]
-
?:
T
[
K
]
extends
(...
args
:
any
[])
=>
Promise
<
any
>
?
K
:
never
;
}[
keyof
T
];
export
interface
DualizeOptions
<
T
>
{
/** 这些方法在 undecided/pending 时会返回一个延迟执行函数,等待对象 Promise 完成后再调用 */
asyncMethods
?:
readonly
AsyncMethodKeys
<
T
>
[];
}
export
function
dualizeAny
<
T
>
(
sync
:
()
=>
T
,
// 同步构造;若抛错则视为 rejected
asyncFn
:
()
=>
Promise
<
T
>
,
// 异步构造
options
?:
DualizeOptions
<
T
>
,
):
Dual
<
T
>
{
let
state
:
State
=
'
undecided
'
;
let
value
!
:
T
;
// fulfilled 时的值(含来自 sync 或 async)
let
reason
:
any
;
// rejected 的错误
let
p
!
:
Promise
<
T
>
;
// 缓存 Promise(resolved/rejected/进行中)
const
asyncMethodSet
=
new
Set
<
PropertyKey
>
(
(
options
?.
asyncMethods
??
[])
as
readonly
PropertyKey
[],
);
const
startAsync
=
()
=>
{
if
(
!
p
||
state
===
'
undecided
'
)
{
state
=
'
pending
'
;
p
=
Promise
.
resolve
()
.
then
(
asyncFn
)
.
then
(
(
v
)
=>
{
value
=
v
;
state
=
'
fulfilled
'
;
return
v
;
},
(
e
)
=>
{
reason
=
e
;
state
=
'
rejected
'
;
throw
e
;
},
);
}
return
p
;
};
const
ensureSync
=
()
=>
{
if
(
state
===
'
undecided
'
)
{
try
{
value
=
sync
();
state
=
'
fulfilled
'
;
}
catch
(
e
)
{
reason
=
e
;
state
=
'
rejected
'
;
}
}
};
/** 在“对象可用”后调用某个异步方法(由 asyncMethods 声明) */
const
makeDeferredAsyncMethod
=
(
prop
:
PropertyKey
)
=>
(...
args
:
any
[])
=>
startAsync
().
then
((
obj
)
=>
{
const
fn
=
(
obj
as
any
)[
prop
];
return
fn
.
apply
(
obj
,
args
);
});
// 从某个值上取属性(原始值会装箱),并绑定 this
const
getFrom
=
(
v
:
unknown
,
prop
:
PropertyKey
)
=>
{
if
(
prop
===
Symbol
.
toPrimitive
)
{
return
(
hint
:
'
default
'
|
'
number
'
|
'
string
'
)
=>
{
const
x
:
any
=
v
;
if
(
hint
===
'
number
'
)
return
Number
(
x
);
if
(
hint
===
'
string
'
)
return
String
(
x
);
if
(
typeof
x
===
'
string
'
)
return
x
;
const
n
=
Number
(
x
);
return
Number
.
isNaN
(
n
)
?
String
(
x
)
:
n
;
};
}
if
(
prop
===
'
valueOf
'
)
return
()
=>
v
as
any
;
if
(
prop
===
'
toString
'
)
return
()
=>
String
(
v
);
const
boxed
:
any
=
v
!==
null
&&
(
typeof
v
===
'
object
'
||
typeof
v
===
'
function
'
)
?
v
:
Object
(
v
as
any
);
const
out
=
boxed
[
prop
];
return
typeof
out
===
'
function
'
?
out
.
bind
(
boxed
)
:
out
;
};
const
proxy
=
new
Proxy
(
Object
.
create
(
null
)
as
any
,
{
get
(
_t
,
prop
)
{
// then/catch/finally:走 Promise 通道
if
(
isThenKey
(
prop
))
{
if
(
state
===
'
undecided
'
)
{
startAsync
();
}
else
if
(
state
===
'
fulfilled
'
)
{
// 若已 fulfilled(来自 sync 或 async),补一个已完成的 Promise
p
||=
Promise
.
resolve
(
value
);
}
else
if
(
state
===
'
rejected
'
)
{
p
||=
Promise
.
reject
(
reason
);
}
else
{
// pending:已有 p
startAsync
();
}
const
anyP
:
any
=
p
;
const
m
=
anyP
[
prop
];
return
typeof
m
===
'
function
'
?
m
.
bind
(
anyP
)
:
m
;
}
// 声明为异步方法的键:在 undecided/pending 时返回“延迟函数”
if
(
asyncMethodSet
.
has
(
prop
))
{
if
(
state
===
'
undecided
'
||
state
===
'
pending
'
)
{
startAsync
();
return
makeDeferredAsyncMethod
(
prop
);
}
if
(
state
===
'
fulfilled
'
)
{
return
getFrom
(
value
,
prop
);
// 同步可直接取到方法(其本身返回 Promise)
}
if
(
state
===
'
rejected
'
)
{
// 访问即抛;也可以选择返回 () => Promise.reject(reason)
throw
reason
;
}
}
// 其它属性访问:遵循状态机
switch
(
state
)
{
case
'
undecided
'
:
{
ensureSync
();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if
(
state
===
'
fulfilled
'
)
return
getFrom
(
value
,
prop
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if
(
state
===
'
rejected
'
)
throw
reason
;
// 理论上不会到这里
throw
new
TypeError
(
'
Invalid state transition
'
);
}
case
'
pending
'
:
{
// 非 asyncMethods 的属性在 pending 时不可同步读取
throw
new
TypeError
(
'
Value is not ready yet. Please await it first.
'
);
}
case
'
fulfilled
'
:
{
return
getFrom
(
value
,
prop
);
}
case
'
rejected
'
:
{
throw
reason
;
}
}
},
has
(
_t
,
key
)
{
if
(
state
===
'
undecided
'
)
{
ensureSync
();
}
if
(
state
===
'
fulfilled
'
)
return
key
in
Object
(
value
as
any
);
return
false
;
// pending/rejected:保守处理
},
ownKeys
()
{
if
(
state
===
'
undecided
'
)
ensureSync
();
if
(
state
===
'
fulfilled
'
)
return
Reflect
.
ownKeys
(
Object
(
value
as
any
));
return
[];
},
getOwnPropertyDescriptor
(
_t
,
key
)
{
if
(
state
===
'
undecided
'
)
ensureSync
();
if
(
state
===
'
fulfilled
'
)
return
Object
.
getOwnPropertyDescriptor
(
Object
(
value
as
any
),
key
);
return
undefined
;
},
});
return
proxy
as
Dual
<
T
>
;
}
src/workflow.ts
View file @
924883d2
...
@@ -47,8 +47,17 @@ class Node {
...
@@ -47,8 +47,17 @@ class Node {
}
}
}
}
export
const
WF_NODE
=
Symbol
(
'
@@workflow/node
'
);
function
isWorkflowChain
(
x
:
any
):
x
is
Chain
<
any
>
{
return
!!
x
&&
typeof
x
===
'
function
'
&&
x
[
WF_NODE
]
instanceof
Node
;
}
// ========== 对外 API:workflow ==========
// ========== 对外 API:workflow ==========
export
function
workflow
<
T
>
(
source
:
T
|
Promise
<
T
>
):
Chain
<
T
>
{
export
function
workflow
<
T
>
(
source
:
T
|
Promise
<
T
>
):
Chain
<
T
>
{
if
(
isWorkflowChain
(
source
))
{
return
source
as
unknown
as
Chain
<
T
>
;
}
const
root
=
new
Node
(
source
,
null
,
null
);
const
root
=
new
Node
(
source
,
null
,
null
);
return
makeProxy
<
T
>
(
root
)
as
any
;
return
makeProxy
<
T
>
(
root
)
as
any
;
}
}
...
@@ -59,6 +68,7 @@ function makeProxy<T>(node: Node): Chain<T> {
...
@@ -59,6 +68,7 @@ function makeProxy<T>(node: Node): Chain<T> {
const
rootHandler
:
ProxyHandler
<
any
>
=
{
const
rootHandler
:
ProxyHandler
<
any
>
=
{
get
(
_t
,
prop
)
{
get
(
_t
,
prop
)
{
if
(
prop
===
WF_NODE
)
return
node
;
// 结束信号:所有 then/catch/finally 复用同一个 Promise
// 结束信号:所有 then/catch/finally 复用同一个 Promise
if
(
prop
===
'
then
'
)
if
(
prop
===
'
then
'
)
return
(
res
:
any
,
rej
?:
any
)
=>
runOnce
().
then
(
res
,
rej
);
return
(
res
:
any
,
rej
?:
any
)
=>
runOnce
().
then
(
res
,
rej
);
...
@@ -94,6 +104,7 @@ function makeProxy<T>(node: Node): Chain<T> {
...
@@ -94,6 +104,7 @@ function makeProxy<T>(node: Node): Chain<T> {
},
},
// 把 “.bar” 记录为 Get;继续深入时在这个 Get 的结果上再处理
// 把 “.bar” 记录为 Get;继续深入时在这个 Get 的结果上再处理
get
(
_t
,
next
)
{
get
(
_t
,
next
)
{
if
(
next
===
WF_NODE
)
return
node
;
if
(
next
===
'
then
'
)
if
(
next
===
'
then
'
)
return
(
r
:
any
,
j
?:
any
)
=>
return
(
r
:
any
,
j
?:
any
)
=>
node
node
...
...
tests/dual-object-workflow.spec.ts
0 → 100644
View file @
924883d2
import
{
workflow
}
from
'
../src/workflow
'
;
import
{
dualizeAny
}
from
'
../src/dual-object
'
;
describe
(
'
dualizeAny + workflow
'
,
()
=>
{
test
(
'
workflow over dualizeAny works correctly
'
,
async
()
=>
{
const
obj
=
dualizeAny
(
()
=>
{
return
{
id
:
'
local
'
,
name
:
()
=>
'
cached
'
,
ping
:
async
()
=>
1
,
};
},
async
()
=>
{
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
30
));
return
{
id
:
'
remote
'
,
name
:
()
=>
'
fresh
'
,
ping
:
async
()
=>
42
,
};
},
{
asyncMethods
:
[
'
ping
'
]
as
const
},
);
const
wf
=
workflow
(
obj
);
// Should be async property access
const
name1
=
await
wf
.
name
();
expect
(
name1
).
toBe
(
'
fresh
'
);
// Now call async method
const
pingResult
=
await
wf
.
ping
();
expect
(
pingResult
).
toBe
(
42
);
// Finally, await the whole object
const
finalObj
=
await
wf
;
expect
(
finalObj
.
id
).
toBe
(
'
remote
'
);
});
});
tests/dual-object.spec.ts
0 → 100644
View file @
924883d2
// tests/dual-object.spec.ts
import
{
dualizeAny
,
AsyncMethodKeys
}
from
'
../src/dual-object
'
;
type
Client
=
{
id
:
string
;
name
():
string
;
// 同步方法
ping
():
Promise
<
number
>
;
// 异步方法
};
const
delay
=
(
ms
:
number
)
=>
new
Promise
<
void
>
((
r
)
=>
setTimeout
(
r
,
ms
));
describe
(
'
dualizeAny basic state machine
'
,
()
=>
{
test
(
'
sync-first property access => fulfilled (from sync), asyncFn not called
'
,
async
()
=>
{
let
syncCalled
=
0
;
let
asyncCalled
=
0
;
const
obj
=
dualizeAny
<
Client
>
(
()
=>
{
syncCalled
++
;
return
{
id
:
'
local
'
,
name
:
()
=>
'
cached
'
,
ping
:
async
()
=>
1
,
};
},
async
()
=>
{
asyncCalled
++
;
await
delay
(
30
);
return
{
id
:
'
remote
'
,
name
:
()
=>
'
fresh
'
,
ping
:
async
()
=>
42
,
};
},
{
asyncMethods
:
[
'
ping
'
]
satisfies
readonly
AsyncMethodKeys
<
Client
>
[]
},
);
// undecided 下先读同步属性
expect
(
obj
.
id
).
toBe
(
'
local
'
);
expect
(
obj
.
name
()).
toBe
(
'
cached
'
);
expect
(
syncCalled
).
toBe
(
1
);
// 现在 await 只会 Promise.resolve(已有值),不会触发 asyncFn
const
v
=
await
obj
;
expect
(
v
.
id
).
toBe
(
'
local
'
);
expect
(
asyncCalled
).
toBe
(
0
);
});
test
(
'
await-first => pending→fulfilled (from async), sync not called
'
,
async
()
=>
{
let
syncCalled
=
0
;
let
asyncCalled
=
0
;
const
obj
=
dualizeAny
<
Client
>
(
()
=>
{
syncCalled
++
;
return
{
id
:
'
local
'
,
name
:
()
=>
'
cached
'
,
ping
:
async
()
=>
1
,
};
},
async
()
=>
{
asyncCalled
++
;
await
delay
(
10
);
return
{
id
:
'
remote
'
,
name
:
()
=>
'
fresh
'
,
ping
:
async
()
=>
42
,
};
},
{
asyncMethods
:
[
'
ping
'
]
as
const
},
);
// 先 await(或 obj.then),应走 asyncFn
const
v
=
await
obj
;
expect
(
v
.
id
).
toBe
(
'
remote
'
);
expect
(
syncCalled
).
toBe
(
0
);
expect
(
asyncCalled
).
toBe
(
1
);
// fulfilled 后再取属性为同步
expect
(
obj
.
name
()).
toBe
(
'
fresh
'
);
});
test
(
'
pending: accessing non-async property throws TypeError
'
,
async
()
=>
{
const
obj
=
dualizeAny
<
Client
>
(
()
=>
({
id
:
'
local
'
,
name
:
()
=>
'
cached
'
,
ping
:
async
()
=>
1
,
}),
async
()
=>
{
await
delay
(
50
);
return
{
id
:
'
remote
'
,
name
:
()
=>
'
fresh
'
,
ping
:
async
()
=>
42
,
};
},
{
asyncMethods
:
[
'
ping
'
]
as
const
},
);
// 触发 pending(但不等待完成)
// 通过 .then 或 await 的方式均可;这里用 .then 触发
const
p
=
(
obj
as
unknown
as
Promise
<
Client
>
).
then
(()
=>
{
/* noop */
});
// pending 状态下访问非 asyncMethods 的键应抛错
expect
(()
=>
{
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
(
obj
as
Client
).
id
;
}).
toThrow
(
new
TypeError
(
'
Value is not ready yet. Please await it first.
'
));
await
p
;
// 清理 pending
});
test
(
'
asyncMethods in undecided: returns deferred function that waits object promise
'
,
async
()
=>
{
const
obj
=
dualizeAny
<
Client
>
(
()
=>
({
id
:
'
local
'
,
name
:
()
=>
'
cached
'
,
ping
:
async
()
=>
1
,
}),
async
()
=>
{
await
delay
(
20
);
return
{
id
:
'
remote
'
,
name
:
()
=>
'
fresh
'
,
ping
:
async
()
=>
42
,
};
},
{
asyncMethods
:
[
'
ping
'
]
as
const
},
);
// undecided 下先访问 async 方法:应进入 pending,并返回一个 Promise<number> 的函数结果
const
r
=
await
obj
.
ping
();
expect
(
r
).
toBe
(
42
);
// 之后对象应已 fulfilled(来自 async)
expect
(
obj
.
name
()).
toBe
(
'
fresh
'
);
});
test
(
'
asyncMethods in pending: returns deferred function (no throw)
'
,
async
()
=>
{
const
obj
=
dualizeAny
<
Client
>
(
()
=>
({
id
:
'
local
'
,
name
:
()
=>
'
cached
'
,
ping
:
async
()
=>
1
,
}),
async
()
=>
{
await
delay
(
30
);
return
{
id
:
'
remote
'
,
name
:
()
=>
'
fresh
'
,
ping
:
async
()
=>
42
,
};
},
{
asyncMethods
:
[
'
ping
'
]
as
const
},
);
// 触发 pending
const
start
=
(
obj
as
unknown
as
Promise
<
Client
>
).
then
(()
=>
{});
// pending 下访问 async 方法不抛错,而是返回延迟函数的结果
const
r
=
await
obj
.
ping
();
expect
(
r
).
toBe
(
42
);
await
start
;
});
test
(
'
rejected from sync(): sync() throws => state rejected; any access throws; await rejects
'
,
async
()
=>
{
const
err
=
new
Error
(
'
boom-sync
'
);
const
obj
=
dualizeAny
<
Client
>
(
()
=>
{
throw
err
;
},
async
()
=>
{
await
delay
(
10
);
return
{
id
:
'
remote
'
,
name
:
()
=>
'
fresh
'
,
ping
:
async
()
=>
42
};
},
{
asyncMethods
:
[
'
ping
'
]
as
const
},
);
// undecided 下访问普通属性会触发 ensureSync → 抛错
expect
(()
=>
(
obj
as
Client
).
name
()).
toThrow
(
err
);
// then/await 也应得到同样的 rejection
await
expect
(
obj
).
rejects
.
toThrow
(
err
);
});
test
(
'
rejected from async(): await-first triggers async then rejects; further access throws same
'
,
async
()
=>
{
const
err
=
new
Error
(
'
boom-async
'
);
const
obj
=
dualizeAny
<
Client
>
(
()
=>
({
id
:
'
local
'
,
name
:
()
=>
'
cached
'
,
ping
:
async
()
=>
1
,
}),
async
()
=>
{
await
delay
(
10
);
throw
err
;
},
{
asyncMethods
:
[
'
ping
'
]
as
const
},
);
await
expect
(
obj
).
rejects
.
toThrow
(
err
);
// 之后访问任何键也应抛相同错误
expect
(()
=>
(
obj
as
Client
).
name
()).
toThrow
(
err
);
expect
(()
=>
(
obj
as
Client
).
id
).
toThrow
(
err
);
});
});
describe
(
'
primitives & coercion
'
,
()
=>
{
test
(
'
number primitive: arithmetic and toString/valueOf work in fulfilled (sync)
'
,
()
=>
{
const
x
=
dualizeAny
<
number
>
(
()
=>
5
,
async
()
=>
99
,
);
// 首次发生隐式转换,走 sync → fulfilled
expect
(
x
+
1
).
toBe
(
6
);
expect
(
String
(
x
)).
toBe
(
'
5
'
);
// 原型方法(通过装箱):
expect
((
x
as
any
).
toFixed
(
1
)).
toBe
(
'
5.0
'
);
});
test
(
'
string primitive: template literal, slice
'
,
()
=>
{
const
s
=
dualizeAny
<
string
>
(
()
=>
'
hello
'
,
async
()
=>
'
world
'
,
);
expect
(
`
${
s
}
world`
).
toBe
(
'
hello world
'
);
expect
((
s
as
any
).
slice
(
0
,
3
)).
toBe
(
'
hel
'
);
});
});
describe
(
'
then/catch/finally semantics
'
,
()
=>
{
test
(
'
then/catch/finally chaining (fulfilled)
'
,
async
()
=>
{
const
obj
=
dualizeAny
<
{
a
:
number
}
>
(
()
=>
({
a
:
1
}),
async
()
=>
({
a
:
2
}),
);
// 首次 await 触发 async
const
seen
:
number
[]
=
[];
await
(
obj
as
unknown
as
Promise
<
{
a
:
number
}
>
)
.
then
((
v
)
=>
{
seen
.
push
(
v
.
a
);
})
.
finally
(()
=>
{
seen
.
push
(
9
);
});
expect
(
seen
).
toEqual
([
2
,
9
]);
});
test
(
'
rejected path via catch (trigger sync first)
'
,
async
()
=>
{
const
err
=
new
Error
(
'
oops
'
);
const
obj
=
dualizeAny
<
{
a
:
number
}
>
(
()
=>
{
throw
err
;
},
async
()
=>
({
a
:
2
}),
);
// 触发同步路径 → 立刻进入 rejected
expect
(()
=>
(
obj
as
any
).
a
).
toThrow
(
err
);
// thenable 现在也应当 rejected
await
expect
(
obj
as
unknown
as
Promise
<
{
a
:
number
}
>
).
rejects
.
toBe
(
err
);
});
describe
(
'
reflect traps: has/ownKeys/getOwnPropertyDescriptor on fulfilled
'
,
()
=>
{
test
(
'
has / ownKeys reflect fulfilled value
'
,
()
=>
{
const
obj
=
dualizeAny
<
{
a
:
number
;
b
:
number
}
>
(
()
=>
({
a
:
1
,
b
:
2
}),
async
()
=>
({
a
:
10
,
b
:
20
}),
);
// 首次同步访问使其 fulfilled(from sync)
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
(
obj
as
any
).
a
;
expect
(
'
a
'
in
(
obj
as
any
)).
toBe
(
true
);
expect
(
Object
.
keys
(
obj
as
any
).
sort
()).
toEqual
([
'
a
'
,
'
b
'
]);
const
desc
=
Object
.
getOwnPropertyDescriptor
(
Object
(
obj
as
any
),
'
a
'
);
expect
(
desc
?.
enumerable
).
toBe
(
true
);
});
});
});
tests/workflow.spec.ts
View file @
924883d2
...
@@ -434,3 +434,120 @@ describe('workflow – Promise combinators interop', () => {
...
@@ -434,3 +434,120 @@ describe('workflow – Promise combinators interop', () => {
expect
(
c
.
counts
.
get
).
toBe
(
1
);
expect
(
c
.
counts
.
get
).
toBe
(
1
);
});
});
});
});
describe
(
'
workflow – nested workflow(workflow(x)) semantics
'
,
()
=>
{
class
Svc
{
count
=
0
;
async
inc
(
by
=
1
,
ms
=
2
)
{
await
sleep
(
ms
);
this
.
count
+=
by
;
return
this
;
}
val
()
{
return
this
.
count
;
}
}
it
(
'
is idempotent: wrapping an existing chain returns the same chain (no new steps)
'
,
async
()
=>
{
const
s
=
new
Svc
();
const
c1
=
workflow
(
s
).
inc
(
2
);
// 构建一条链
const
c2
=
workflow
(
c1
);
// 套娃:应当恒等
expect
(
c2
).
toBe
(
c1
);
// 引用相等(若你选择“同 Node 新代理”,可改为 not.toBe 但 Node 相等)
// 两次 then 也只执行一遍链
const
[
a
,
b
]
=
await
Promise
.
all
([
c1
.
then
((
x
)
=>
x
),
c2
.
then
((
x
)
=>
x
)]);
expect
(
a
).
toBe
(
b
);
expect
(
s
.
count
).
toBe
(
2
);
});
it
(
'
does not double-execute when awaited via inner and outer chains
'
,
async
()
=>
{
const
s
=
new
Svc
();
const
inner
=
workflow
(
s
).
inc
(
3
);
const
outer
=
workflow
(
inner
);
// 恒等
await
inner
;
await
outer
;
// 不应重复执行
expect
(
s
.
count
).
toBe
(
3
);
});
it
(
'
branches share the same instance; results are {6,8} and final count is 8
'
,
async
()
=>
{
const
s
=
new
Svc
();
// 前缀(未执行前只是定义步骤)
const
base
=
workflow
(
s
).
inc
(
5
,
1
);
// -> count: 5
const
wrapped
=
workflow
(
base
);
// 恒等返回
// 分支一:先 +1(较快完成)
const
b1
=
wrapped
.
inc
(
1
,
5
).
val
();
// 5 -> 6
// 分支二:后 +2(稍慢完成)
const
b2
=
wrapped
.
inc
(
2
,
10
).
val
();
// 6 -> 8 (若它先完成,则 5->7,再被另一条改成 8)
const
[
v1
,
v2
]
=
await
Promise
.
all
([
b1
,
b2
]);
// 两个返回值是 6 和 8(顺序不保证)
expect
(
new
Set
([
v1
,
v2
])).
toEqual
(
new
Set
([
6
,
8
]));
// 最终状态必然为 8
expect
(
s
.
count
).
toBe
(
8
);
});
it
(
'
interop with Promise combinators remains correct when nested
'
,
async
()
=>
{
const
s
=
new
Svc
();
const
chain
=
workflow
(
s
).
inc
(
1
);
// 前缀
const
nested
=
workflow
(
chain
);
// 恒等
const
[
all
,
race
,
any
]
=
await
Promise
.
all
([
Promise
.
all
([
nested
.
val
(),
nested
.
inc
(
1
).
val
()]),
// [1, 2]
Promise
.
race
([
nested
.
val
(),
nested
.
inc
(
1
).
val
()]),
// 先 settle 的可能是 val()(1)
Promise
.
any
([
nested
.
val
(),
nested
.
inc
(
1
).
val
()]),
// 第一个 fulfill
]);
expect
(
all
).
toEqual
([
1
,
2
]);
expect
([
race
===
1
,
race
===
2
].
some
(
Boolean
)).
toBe
(
true
);
expect
([
any
===
1
,
any
===
2
].
some
(
Boolean
)).
toBe
(
true
);
});
});
describe
(
'
workflow – wrapping a method chain property
'
,
()
=>
{
class
A
{
value
=
0
;
async
foo
()
{
await
sleep
(
2
);
this
.
value
+=
10
;
return
this
;
}
async
bar
()
{
await
sleep
(
2
);
this
.
value
+=
5
;
return
this
;
}
}
it
(
'
workflow(workflow(a).foo)() should behave like workflow(a).foo()
'
,
async
()
=>
{
const
a1
=
new
A
();
const
a2
=
new
A
();
// baseline:直接调用 workflow(a).foo()
await
workflow
(
a1
).
foo
();
const
baselineValue
=
a1
.
value
;
// wrapped:先拿出 workflow(a).foo 再 workflow 一次
const
wrapped
=
workflow
(
workflow
(
a2
).
foo
);
await
wrapped
();
// 调用它
const
wrappedValue
=
a2
.
value
;
// 两者行为应该一致
expect
(
wrappedValue
).
toBe
(
baselineValue
);
expect
(
wrappedValue
).
toBe
(
10
);
});
it
(
'
should also support chaining after such wrapped call
'
,
async
()
=>
{
const
a
=
new
A
();
// workflow(workflow(a).foo)() 之后还能继续链
const
result
=
await
workflow
(
workflow
(
a
).
foo
)().
bar
();
expect
(
result
.
value
).
toBe
(
15
);
// foo +10, bar +5
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment