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
633652d6
Commit
633652d6
authored
Feb 08, 2026
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
missing
parent
bfd03193
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
236 additions
and
0 deletions
+236
-0
src/app-context/promise-utils.ts
src/app-context/promise-utils.ts
+67
-0
tests/app-context.spec.ts
tests/app-context.spec.ts
+169
-0
No files found.
src/app-context/promise-utils.ts
0 → 100644
View file @
633652d6
import
{
dualizeAny
,
throwDualPending
}
from
'
../dual-object
'
;
type
PromiseState
<
T
=
any
>
=
|
{
status
:
'
pending
'
;
value
?:
undefined
;
error
?:
undefined
}
|
{
status
:
'
fulfilled
'
;
value
:
T
;
error
?:
undefined
}
|
{
status
:
'
rejected
'
;
value
?:
undefined
;
error
:
any
};
const
promiseStates
=
new
WeakMap
<
Promise
<
any
>
,
PromiseState
>
();
export
const
isPromiseLike
=
(
value
:
any
):
value
is
Promise
<
any
>
=>
!!
value
&&
typeof
value
.
then
===
'
function
'
;
export
const
trackPromise
=
<
T
>
(
promise
:
Promise
<
T
>
):
PromiseState
<
T
>
=>
{
const
existing
=
promiseStates
.
get
(
promise
);
if
(
existing
)
return
existing
as
PromiseState
<
T
>
;
const
state
=
{
status
:
'
pending
'
}
as
PromiseState
<
T
>
;
promiseStates
.
set
(
promise
,
state
);
promise
.
then
(
(
value
)
=>
{
(
state
as
any
).
status
=
'
fulfilled
'
;
(
state
as
any
).
value
=
value
;
},
(
error
)
=>
{
(
state
as
any
).
status
=
'
rejected
'
;
(
state
as
any
).
error
=
error
;
},
);
return
state
;
};
export
const
wrapMaybePromise
=
<
T
>
(
value
:
T
,
options
?:
{
methodKeys
?:
Iterable
<
PropertyKey
>
},
):
T
=>
{
if
(
!
isPromiseLike
(
value
))
return
value
;
const
promise
=
Promise
.
resolve
(
value
);
const
state
=
trackPromise
(
promise
);
if
(
state
.
status
===
'
fulfilled
'
)
return
state
.
value
;
if
(
state
.
status
===
'
rejected
'
)
throw
state
.
error
;
return
dualizeAny
<
T
>
(
()
=>
{
const
current
=
trackPromise
(
promise
);
if
(
current
.
status
===
'
fulfilled
'
)
return
current
.
value
;
if
(
current
.
status
===
'
rejected
'
)
throw
current
.
error
;
throwDualPending
();
},
()
=>
promise
,
{
// Intentionally hide strict method return type here.
asyncMethods
:
Array
.
from
(
options
?.
methodKeys
??
[])
as
any
,
},
);
};
export
const
createAsyncMethod
=
(
inst
:
any
,
key
:
PropertyKey
)
=>
(...
args
:
any
[])
=>
Promise
.
resolve
(
inst
).
then
((
resolved
)
=>
{
const
fn
=
resolved
?.[
key
];
if
(
typeof
fn
!==
'
function
'
)
{
throw
new
TypeError
(
'
Target method is not a function
'
);
}
return
fn
.
apply
(
resolved
,
args
);
});
tests/app-context.spec.ts
0 → 100644
View file @
633652d6
import
{
createAppContext
,
AppContext
}
from
'
../src/app-context
'
;
const
delay
=
(
ms
:
number
)
=>
new
Promise
<
void
>
((
r
)
=>
setTimeout
(
r
,
ms
));
type
Equal
<
A
,
B
>
=
(
<
T
>
()
=>
T
extends
A
?
1
:
2
)
extends
<
T
>
()
=>
T
extends
B
?
1
:
2
?
true
:
false
;
type
Expect
<
T
extends
true
>
=
T
;
class
CounterService
{
constructor
(
public
ctx
:
AppContext
,
public
value
=
1
,
)
{}
inc
()
{
this
.
value
+=
1
;
return
this
.
value
;
}
async
ping
(
add
:
number
)
{
return
this
.
value
+
add
;
}
}
class
AsyncMutableService
{
constructor
(
public
ctx
:
AppContext
)
{}
count
=
0
;
}
class
InitLogService
{
constructor
(
public
ctx
:
AppContext
,
private
name
:
string
,
private
logs
:
string
[],
)
{}
async
init
()
{
this
.
logs
.
push
(
`init:
${
this
.
name
}
`
);
}
}
describe
(
'
app-context runtime
'
,
()
=>
{
test
(
'
provide + merge(method) binds this correctly
'
,
()
=>
{
const
ctx
=
createAppContext
()
.
provide
(
CounterService
,
1
,
{
merge
:
[
'
inc
'
]
})
.
define
();
const
inc
=
ctx
.
inc
;
expect
(
inc
()).
toBe
(
2
);
expect
(
inc
()).
toBe
(
3
);
});
test
(
'
provide getter on pending service: method call works and normal field throws
'
,
async
()
=>
{
const
ctx
=
createAppContext
()
.
provide
(
CounterService
,
5
,
{
provide
:
'
counter
'
,
useFactory
:
async
(
self
,
...
args
:
unknown
[])
=>
{
const
initial
=
args
[
args
.
length
-
1
];
await
delay
(
20
);
return
new
CounterService
(
self
,
initial
as
number
);
},
})
.
define
();
const
p
=
ctx
.
counter
.
ping
(
3
);
expect
(()
=>
ctx
.
counter
.
value
).
toThrow
(
new
TypeError
(
'
Value is not ready yet. Please await it first.
'
),
);
await
expect
(
p
).
resolves
.
toBe
(
8
);
});
test
(
'
merge(property) set is queued before resolve and flushed after getAsync
'
,
async
()
=>
{
const
ctx
=
createAppContext
()
.
provide
(
AsyncMutableService
,
{
merge
:
[
'
count
'
],
useFactory
:
async
(
self
)
=>
{
await
delay
(
20
);
return
new
AsyncMutableService
(
self
);
},
})
.
define
();
ctx
.
count
=
42
;
await
expect
(
ctx
.
getAsync
(
AsyncMutableService
)).
resolves
.
toMatchObject
({
count
:
42
,
});
expect
(
ctx
.
count
).
toBe
(
42
);
});
test
(
'
use replays object definition steps and merges registry
'
,
async
()
=>
{
const
ctx1
=
createAppContext
()
.
provide
(
CounterService
,
7
,
{
provide
:
'
counter
'
,
merge
:
[
'
inc
'
]
})
.
define
();
const
root
=
createAppContext
().
use
(
ctx1
).
define
();
expect
(
root
.
counter
.
inc
()).
toBe
(
8
);
await
expect
(
root
.
getAsync
(
CounterService
)).
resolves
.
toMatchObject
({
value
:
8
,
});
});
test
(
'
start resolves async provides and runs init in registration order
'
,
async
()
=>
{
const
logs
:
string
[]
=
[];
const
ctx1
=
createAppContext
()
.
provide
(
InitLogService
,
'
A
'
,
logs
,
{
useFactory
:
async
(
self
,
...
args
:
unknown
[])
=>
{
const
[
name
,
output
]
=
args
.
length
>=
2
?
args
:
[
args
[
0
],
logs
];
const
resolvedName
=
String
(
name
);
const
resolvedOutput
=
output
as
string
[];
await
delay
(
20
);
resolvedOutput
.
push
(
`resolve:
${
resolvedName
}
`
);
return
new
InitLogService
(
self
,
resolvedName
,
resolvedOutput
);
},
})
.
define
();
const
ctx2
=
createAppContext
()
.
provide
(
InitLogService
,
'
B
'
,
logs
,
{
useFactory
:
async
(
self
,
...
args
:
unknown
[])
=>
{
const
[
name
,
output
]
=
args
.
length
>=
2
?
args
:
[
args
[
0
],
logs
];
const
resolvedName
=
String
(
name
);
const
resolvedOutput
=
output
as
string
[];
await
delay
(
5
);
resolvedOutput
.
push
(
`resolve:
${
resolvedName
}
`
);
return
new
InitLogService
(
self
,
resolvedName
,
resolvedOutput
);
},
})
.
define
();
await
createAppContext
().
use
(
ctx1
).
use
(
ctx2
).
define
().
start
();
expect
(
logs
).
toContain
(
'
init:A
'
);
expect
(
logs
).
toContain
(
'
init:B
'
);
expect
(
logs
.
indexOf
(
'
init:A
'
)).
toBeLessThan
(
logs
.
indexOf
(
'
init:B
'
));
});
});
describe
(
'
app-context type checks
'
,
()
=>
{
test
(
'
compile-time type assertions
'
,
()
=>
{
const
ctx
=
createAppContext
()
.
provide
(
CounterService
,
1
,
{
provide
:
'
counter
'
,
merge
:
[
'
inc
'
]
})
.
define
();
const
n
:
number
=
ctx
.
counter
.
inc
();
expect
(
n
).
toBe
(
2
);
type
_counter
=
Expect
<
Equal
<
typeof
ctx
.
counter
,
CounterService
>>
;
type
_inc
=
Expect
<
Equal
<
typeof
ctx
.
inc
,
()
=>
number
>>
;
const
ok
:
_counter
|
_inc
=
true
;
expect
(
ok
).
toBe
(
true
);
});
test
(
'
start return type is never when requirement not fulfilled
'
,
()
=>
{
const
reqCtx
=
createAppContext
<
{
foo
:
number
}
>
().
define
();
type
Ret
=
Awaited
<
ReturnType
<
typeof
reqCtx
.
start
>>
;
type
_ret
=
Expect
<
Equal
<
Ret
,
never
>>
;
const
ok
:
_ret
=
true
;
expect
(
ok
).
toBe
(
true
);
});
test
(
'
use merges context types
'
,
()
=>
{
const
a
=
createAppContext
()
.
provide
(
CounterService
,
1
,
{
provide
:
'
counter
'
})
.
define
();
const
b
=
createAppContext
().
use
(
a
).
define
();
const
v
:
number
=
b
.
counter
.
value
;
expect
(
v
).
toBe
(
1
);
});
});
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